How to create an independent module ?
Use case
The use case is quite simple - a warehouse plugin. We want to have a possibility to see how many items of a product we have in our warehouse. We also want to have the ability to track who and when received or delivered the products.
Model definition
All entities (database's table representation) used by a plugin are defined in XML files in the src/main/resources/warehouse/model/ directory. The structure of those files is described on Model Definition Overview page. The Warehouse module will define two entities: resource and the transfer. Let's start from the resource.
Resource
Resource represents a quantity of some product in the warehouse. It has the number, quantity and related product. We will define it in this file:
<?xml version="1.0" encoding="UTF-8" ?> <!-- remember that this 'name' attribute determines the name of the entity, not the file name --> <model name="resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/model" xsi:schemaLocation="http://schema.qcadoo.org/model http://schema.qcadoo.org/model.xsd"> <fields> <!-- a number to evidence a certain resource --> <string name="number" required="true" unique="true" /> <!-- an unidirectional many-to-one relation to the product entity from the 'basic' plugin --> <belongsTo name="product" required="true" model="product" plugin="basic" /> <!-- amount of resource in this evidence record; we don't accept negative values --> <decimal name="quantity" default="0" > <validatesRange from="0" /> </decimal> </fields> <hooks> </hooks> <identifier expression="#number" /> </model>
Transfer
A transfer represents describes the receive and delivery of a resource. It contains:
- resource,
- quantity,
- type (incoming or outgoing),
- status (planned or done),
- request date,
- the worker who filled the request
- confirmation date/worker.
Fields that are marked as readonly will be set using a custom save action. Lets define the transfer entity in the file:
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?> <!-- remember that this 'name' attribute determines the name of the entity, not the file name --> <model name="transfer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/model" xsi:schemaLocation="http://schema.qcadoo.org/model http://schema.qcadoo.org/model.xsd"> <fields> <!-- a number to evidence a certain resource --> <string name="number" required="true" unique="true" /> <!-- which from or to which resource to we transfer; the second end of the bidirectoral relation between a resource and transfers --> <belongsTo name="resource" model="resource" required="true" /> <!-- how much to we transport --> <decimal name="quantity" required="true"> <validatesRange from="0" /> </decimal> <!-- an incoming transfer will add resource and an outgoing one will remove resource from the warehouse --> <enum name="type" values="01incoming,02outgoing" required="true" /> <!-- a pending transfer did not modify the resources quantity yet, but it will when we close it; this field should not be set by the edit from but by a proper business method --> <enum name="status" values="01planned,02done" required="true" default="01planned"/> <!-- when we planned to close this transfer; we will have to use a custom validator do check if it is in the future --> <datetime name="plannedDate" /> <!-- the worker that requested the transfer to be done and the date when the request was made; this will be set automatically by a hook --> <string name="requestWorker" readonly="true" /> <datetime name="requestDate" readonly="true" /> <!-- the worker that confirmed that the transfer was dome and the date when it happened; this will be set automatically by a proper business method --> <string name="confirmWorker" readonly="true" /> <datetime name="confirmDate" readonly="true" /> </fields> <hooks> </hooks> </model>
We also have to add a hasMany field to the resource.xml.
<!-- a bidirectional many-to-one relation to the transfer entity; the transfer entity will have a resource field of type belongsTo, we will join by this field --> <hasMany name="transfers" model="transfer" joinField="resource" />
View definition
All pages used by a plugin are defined in the src/main/resources/warehouse/view directory. The structure of this file is described on View Definition Overview page. The Warehouse will define four views: a form and a for resources and for transfers. Let's start from the grid that shows resources.
Grid for resources
- grid will contains three columns
- number - which links to edit form
- product's name
- quantity
- it will have correspondingView that points to edit form (used by linkable columns)
- it will be paginable
- it will be searchable and orderable using number and product's name columns
- it will be scaled to fill all page
- window header will be disabled
- window will have fix height (vertical scrollbar won't appear)
We must create the resourcesList.xml file in the view directory:
<?xml version="1.0" encoding="UTF-8" ?> <!-- the 'name' attribute determines the the name of the view, not the file name; the modelName attribute describes on which entities fields do we concentrate here; the menuAccessible attribute indicates that this view will be available from the main menu --> <view name="resourcesList" modelName="resource" menuAccessible="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/view" xsi:schemaLocation="http://schema.qcadoo.org/view http://schema.qcadoo.org/view.xsd"> <!-- a window is always the most outer container for other components --> <component type="window" name="window"> <!-- a ribbon is the big horizontal menu at the top --> <ribbon> <!-- a group contains several buttons; you can use group templates to insert buttons which are hooked to default actions that do simple navigation and CRUD operations --> <group template="gridNewCopyAndRemoveAction" /> </ribbon> <!-- grid = table --> <component type="grid" name="grid" reference="grid"> <!-- you tell this grid which columns to show using multiple 'column' options; in each column you can tell which field to show from the entity pointed out by the modelName attribute --> <option type="column" name="number" fields="number" link="true" /> <option type="column" name="product" expression="#product['name']" link="true" /> <option type="column" name="quantity" fields="quantity" /> <!-- these options indicate to which view should we jump when we click to edit an entity from the table or to add a new one; in correspondingView we point out the view path: plugin_name/view_name and in the correspondingComponent we point out the components reference in the view to which we want to bind the selected entity --> <option type="correspondingView" value="warehouse/resourceDetails" /> <option type="correspondingComponent" value="form" /> <!-- this option points out which columns can be filtered --> <option type="searchable" value="number,product" /> <!-- this option points out which columns change the order in the grid --> <option type="orderable" value="number,product" /> <option type="fullScreen" value="true" /> <!-- this option indicates by which column should the grid be ordered by default --> <option type="order" column="number" direction="asc"/> </component> <option type="fixedHeight" value="true" /> <option type="header" value="false" /> </component> <hooks> </hooks> </view>
Ribbon defines buttons that will be displayed under the application menu. All grids should have two buttons - the first which points to the new entity form and the second which removes selected entity.
Form for resource
Now lets add a form in which we will be able to see the resources details.
- form will contains three inputs
- number
- product's lookup
- quantity - disabled, because it is set using custom action
- expression will be used to generate window header
- window header will be displayed
We create resourceDetails.xml file in the view directory:
<?xml version="1.0" encoding="UTF-8" ?> <view name="resourceDetails" modelName="resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/view" xsi:schemaLocation="http://schema.qcadoo.org/view http://schema.qcadoo.org/view.xsd"> <component type="window" name="window"> <ribbon> <group template="navigation" /> <!-- the template for form CRUD buttons is a little different from the list buttons --> <group template="formSaveCopyAndRemoveActions" /> </ribbon> <!-- typical form which holds fields --> <component type="form" name="form" reference="form"> <!-- input field for the number; the 'name' attribute holds the name of the component, the 'field' attribute points out the field of the entity from the modelName attribute --> <component type="input" name="number" field="number" /> <!-- a lookup field in which you can select the entity --> <component type="lookup" name="product" field="product"> <!-- standard grid option which will be used in the grid that appears after you click the loop icon --> <option type="column" name="number" fields="number" link="true" /> <option type="column" name="name" fields="name" link="true" /> <option type="searchable" value="name,category" /> <option type="orderable" value="name,category" /> <option type="fullScreen" value="true" /> <option type="expression" value="#name" /> <!-- the field code indicates which field will be used to shot in the lookup field after the selection was made --> <option type="fieldCode" value="number" /> </component> <!-- we must use the reference if we want to do something with this component in the Java code --> <component type="input" name="quantity" field="quantity" reference="resourceQuantity" /> <option type="expression" value="#quantity + ' x ' + #number + ' (' + #product['name'] + ')'" /> <!-- we don't want to show the forms header, the window will already have one --> <option type="header" value="false" /> </component> </component> <hooks> </hooks> </view>
All forms should have one navigation button which points to the grid view and four actions buttons in their ribbon: save, save and back, cancel and delete and back.
Grid for transfers
Now a grid for transfers.
- grid will contains four columns
- resource's number - which links to edit form
- quantity
- type
- status
- it will have correspondingView that points to edit form (used by linkable columns)
- it will be paginable
- it will be searchable and orderable using resource's number, type and status columns
- it will be scaled to fill all page
- window header will be disabled
- window will have fix height (vertical scrollbar won't appear)
<?xml version="1.0" encoding="UTF-8" ?> <view name="transfersList" modelName="transfer" menuAccessible="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/view" xsi:schemaLocation="http://schema.qcadoo.org/view http://schema.qcadoo.org/view.xsd"> <component type="window" name="window"> <ribbon> <group template="gridNewCopyAndRemoveAction" /> </ribbon> <component type="grid" name="grid" reference="grid"> <option type="column" name="number" fields="number" link="true" /> <option type="column" name="resource" expression="#resource['number']" link="true" /> <option type="column" name="type" fields="type" /> <option type="column" name="status" fields="status" /> <option type="column" name="quantity" fields="quantity" /> <option type="column" name="plannedDate" fields="plannedDate" /> <option type="correspondingView" value="warehouse/transferDetails" /> <option type="correspondingComponent" value="form" /> <option type="searchable" value="resource,type,status" /> <option type="orderable" value="resource,type,status" /> <option type="fullScreen" value="true" /> <option type="order" column="quantity" direction="asc"/> </component> <option type="fixedHeight" value="true" /> <option type="header" value="false" /> </component> </view>
Form for transfer
- form will contains eight inputs
- resource's lookup
- quantity
- type
- status
- requestWorked - disabled, because it is set using custom action
- requestDate - disabled, because it is set using custom action
- confirmWorker - disabled, because it is set using custom action
- confirmWorker - disabled, because it is set using custom action
- expression will be used to generate window header
- window header will be displayed
<view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/view" xsi:schemaLocation="http://schema.qcadoo.org/view http://schema.qcadoo.org/view.xsd" name="transferDetails" modelName="transfer"> <component type="window" name="window" reference="window"> <ribbon> <group template="navigation" /> <group template="formSaveCopyAndRemoveActions" /> </ribbon> <component type="form" name="form" reference="form"> <!-- a grid layout component gives us more control how the inputs will be positioned --> <component type="gridLayout" name="gridLayout" columns="2" rows="5" hasBorders="false"> <layoutElement column="1" row="1"> <component type="input" name="number" field="number" /> </layoutElement> <layoutElement column="1" row="2"> <component type="lookup" name="resource" field="resource"> <option type="column" name="number" fields="number" link="true" /> <option type="column" name="product" expression="#product['name']" link="true" /> <option type="searchable" value="number,name" /> <option type="orderable" value=" number,name" /> <option type="fullScreen" value="true" /> <option type="expression" value="#number" /> <option type="fieldCode" value="number" /> </component> </layoutElement> <layoutElement column="1" row="3"> <component type="input" name="quantity" field="quantity" /> </layoutElement> <layoutElement column="1" row="4"> <component type="select" name="type" field="type" reference="type"/> </layoutElement> <layoutElement column="1" row="5"> <component type="calendar" name="plannedDate" field="plannedDate"> <option type="withTimePicker" value="true" /> </component> </layoutElement> <layoutElement column="2" row="1"> <component type="input" name="requestWorker" field="requestWorker" /> </layoutElement> <layoutElement column="2" row="2"> <component type="calendar" name="requestDate" field="requestDate" > <option type="withTimePicker" value="true" /> </component> </layoutElement> <layoutElement column="2" row="3"> <component type="input" name="confirmWorker" field="confirmWorker" /> </layoutElement> <layoutElement column="2" row="4"> <component type="calendar" name="confirmDate" field="confirmDate" > <option type="withTimePicker" value="true" /> </component> </layoutElement> <layoutElement column="2" row="5"> <component type="select" name="status" field="status" reference="status" /> </layoutElement> </component> <option type="expression" value="#quantity + ' x ' + #resource['number']" /> <option type="header" value="false" /> </component> </component> <hooks> </hooks> </view>
The plugin descriptor
Now that we created our model and view XMLs we must point them out in the plugin descriptor in the file qcadoo-plugin.xml. The whole descriptor in this module will look like this:
<?xml version="1.0" encoding="UTF-8"?> <plugin plugin="warehouse" version="0.1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.qcadoo.org/plugin" xmlns:model="http://schema.qcadoo.org/modules/model" xmlns:view="http://schema.qcadoo.org/modules/view" xmlns:menu="http://schema.qcadoo.org/modules/menu" xmlns:localization="http://schema.qcadoo.org/modules/localization" xsi:schemaLocation=" http://schema.qcadoo.org/plugin http://schema.qcadoo.org/plugin.xsd http://schema.qcadoo.org/modules/model http://schema.qcadoo.org/modules/model.xsd http://schema.qcadoo.org/modules/view http://schema.qcadoo.org/modules/view.xsd http://schema.qcadoo.org/modules/menu http://schema.qcadoo.org/modules/menu.xsd http://schema.qcadoo.org/modules/localization http://schema.qcadoo.org/modules/localization.xsd"> <information> <name>Warehouse Module</name> <vendor> <name>Warehouse Corp</name> <url>www.warehousecorp.com</url> </vendor> </information> <dependencies> <dependency> <plugin>basic</plugin> </dependency> </dependencies> <modules> <localization:translation path="locales" /> <model:model model="resource" resource="model/resource.xml" /> <model:model model="transfer" resource="model/transfer.xml" /> <menu:menu-category name="warehouse" /> <menu:menu-item name="resources" category="warehouse" view="resourcesList" /> <menu:menu-item name="warehouseTransfers" category="warehouse" view="transfersList" /> <view:view resource="view/resourcesList.xml" /> <view:view resource="view/resourceDetails.xml" /> <view:view resource="view/transfersList.xml"/> <view:view resource="view/transferDetails.xml" /> <view:resource uri="public/**/*" /> </modules> </plugin>
Custom action while saving forms
Custom actions add possibility to add some business logic to our entities. Let's prepare a Java class called WarehouseService for them:
package com.warehousecorporation.warehouse; // imports @Service public class WarehouseService { // custom action definitions }
Our first action will automatically set the transfers workers and dates. Workers will be set using the current logged user - the one who create request will be put into requestWorker, Afterwards the one who changes the status to done will be put into confirmWorker. If transfer has not been saved (id is null) we will set requests worker and date, if transfer's status is done we will set confirmation worker and date.
This action will also change quantity of the related resources, when the transfer is done.
package com.warehousecorporation.warehouse; import java.math.BigDecimal; import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.qcadoo.model.api.DataDefinition; import com.qcadoo.model.api.DataDefinitionService; import com.qcadoo.model.api.Entity; import com.qcadoo.security.api.SecurityService; import com.qcadoo.view.api.ComponentState; import com.qcadoo.view.api.ViewDefinitionState; @Service public class WarehouseService { @Autowired private DataDefinitionService dataDefinitionService; @Autowired private SecurityService securityService; public void setWorkersDatesAndResourceQuantity(final DataDefinition dataDefinition, final Entity transfer) { if (transfer.getId() == null) { transfer.setField("requestWorker", securityService.getCurrentUserName()); transfer.setField("requestDate", new Date()); } if ("02done".equals(transfer.getField("status"))) { transfer.setField("confirmWorker", securityService.getCurrentUserName()); transfer.setField("confirmDate", new Date()); DataDefinition resourceDataDefinition = dataDefinitionService.get("warehouse", "resource"); Entity resource = transfer.getBelongsToField("resource"); BigDecimal currentQuantity = (BigDecimal) resource.getField("quantity"); BigDecimal transferQuantity = (BigDecimal) transfer.getField("quantity"); BigDecimal newQuantity; if ("01incoming".equals(transfer.getField("type"))) { newQuantity = new BigDecimal(currentQuantity.doubleValue() - transferQuantity.doubleValue()); } else { newQuantity = new BigDecimal(currentQuantity.doubleValue() + transferQuantity.doubleValue()); } if (newQuantity.doubleValue() >= 0) { resource.setField("quantity", newQuantity); resourceDataDefinition.save(resource); } } } }
The custom action has to be registered in src/main/resources/warehouse/model/transfer.xml in transfer model. To do this we will add the below code to the hooks node.
<onSave class="com.warehousecorporation.warehouse.WarehouseService" method="setWorkersDatesAndResourceQuantity" />
Custom validator
Custom validators add the possibility to create our own validation rules. We want to check if a resource has enough quantity for delivery transfer. Let's create validator in WarehouseService.
public boolean checkIfHasEnoughtQuantity(final DataDefinition dataDefinition, final Entity transfer) { if ("02done".equals(transfer.getField("status")) && "02outgoing".equals(transfer.getField("type"))) { Entity resource = transfer.getBelongsToField("resource"); BigDecimal currentQuantity = (BigDecimal) resource.getField("quantity"); BigDecimal transferQuantity = (BigDecimal) transfer.getField("quantity"); if (transferQuantity.compareTo(currentQuantity) > 0) { transfer.addError(dataDefinition.getField("quantity"), "warehouse.not.enought.resource.error"); return false; } } return true; }
This validator also has to be registered in src/main/resources/warehouse/model/transfer.xml:
<validatesWith class="com.warehousecorporation.warehouse.WarehouseService" method="checkIfHasEnoughtQuantity" />
Custom view hook
Lets add a hook which will set the initial quantity of resources (this is readonly field, so we must initial value). Lets add the following method to WarehouseService:
public void setResourceInitialQuantity(final ViewDefinitionState state) { ComponentState quantity = (ComponentState) state.getComponentByReference("quantity"); if(quantity.getFieldValue() == null) { quantity.setFieldValue(0); } }
This action has to be registered in the view/resourceList.xml. So we have to add the below code to the end of the hooks node:
<beforeRender class="com.warehousecorporation.warehouse.WarehouseService" method="setResourceInitialQuantity" />
Localization
Localization messages are defined in src/main/resources/locales/warehouse_xx.properties files, where xx is an i18n two letter country code. Let's edit the English localization in src/main/resources/locales/warehouse_en.properties
Menu labels for the module
warehouse.menu.warehouse = Warehouse warehouse.menu.warehouse.resources = Resources warehouse.menu.warehouse.warehouseTransfers = Transfers
Messages for resource's entity
warehouse.resource.product.label = Product warehouse.resource.product.label.focus = Type product's number warehouse.resource.name.label = Name warehouse.resource.number.label = Number warehouse.resource.quantity.label = Quantity warehouse.resource.lookupCodeVisible = Number
Messages for transfer's entity
warehouse.transfer.number.label = Number warehouse.transfer.resource.label = Resource warehouse.transfer.resource.label.focus = Type resource's number warehouse.transfer.quantity.label = Quantity warehouse.transfer.type.label = Type warehouse.transfer.type.value.01incoming = incoming warehouse.transfer.type.value.02outgoing = outgoing warehouse.transfer.status.label = Status warehouse.transfer.status.value.01planned = planned warehouse.transfer.status.value.02done = done warehouse.transfer.requestWorker.label = Request worker warehouse.transfer.confirmWorker.label = Confirm worker warehouse.transfer.plannedDate.label = Planned date warehouse.transfer.requestDate.label = Request date warehouse.transfer.confirmDate.label = Confirm date
Messages for resource's grid
warehouse.resourcesList.window.mainTab.grid.header = Resources: warehouse.resourcesList.window.mainTab.grid.column.product = Product
Messages for transfer's grid
warehouse.transfersList.window.mainTab.grid.header = Transfers: warehouse.transfersList.window.mainTab.grid.column.resource = Resource
Messages for transfer's form
warehouse.transferDetails.window.mainTab.form.headerNew = New transfer: warehouse.transferDetails.window.mainTab.form.headerEdit = Transfer: warehouse.transferDetails.window.mainTab.form.resource.lookup.window.grid.header = Select resource: warehouse.transferDetails.window.mainTab.form.resource.lookup.window.grid.column.product = Product
Messages for resource's form
warehouse.resourceDetails.window.mainTab.form.headerNew = New resource: warehouse.resourceDetails.window.mainTab.form.headerEdit = Resource: warehouse.resourceDetails.window.mainTab.form.product.lookup.window.grid.header = Select product:
Messages for custom validator
warehouse.not.enought.resource.error = There is not enought resources in warehouse.
Change the plug-in version
We have to changes the plugin version in the mes/mes-plugin/pom.xml file:
<properties> <qcadoo.version>1.1.7-SNAPSHOT</qcadoo.version> </properties>
We also have to add dependency in mes/mes-application/pom.xml file:
<dependencies> <dependency> <groupId>com.qcadoo.mes</groupId> <artifactId>warehouse</artifactId> <version>1.1.7-SNAPSHOT</version> </dependency> . . . </dependencies>
Installation
To add our plugin to the qcadoo MES application we have to first compile the sources. Please open the terminal, go to the plugins main directory with the pom.xml file and type:
mvn clean install
The plugin jar file will be generated under target directory. In our case the file is called warehouse-0.4.5.jar.
The next step is to install the plugin in the application. You can do it in two ways:
- The normal way is to open the qcadoo MES application in the browser, log in as the administrator and go the Administration (gears icon on the right) > Plug-ins page. Click Download button, choose the warehouse-0.4.3.jar file and upload it. Choose the Warehouse plugin on the plugin's grid and click Switch on. The server will be restarted and the plugin activated.
- The fast way is to install the plugin by copying the jar in to the directory qcadoo-bin/webapps/ROOT/WEB-INF/lib while the server is shutdown. This way is preferred for developers.
Go to the Warehouse > Resources and use your first plugin. Congratulations!
All sources for this tutorial can be found on github:
https://github.com/qcadoo/qcadoo-incubator/tree/0.4.5/warehouse
Debuging
If you installed the plugin and qcadoo MES didn't startup, you got an internal error or see a 404 page then don't panic !
You probably just made a small mistake in the modules source code. To checkout what went wrong just go to the logs directory in qcadoo-bin/logs and open the file warn.log.
You should have a Java stack-trace there with all the information you need to fix the problem.
You can also attach a debugger for a more deeper analysis. Like in any Tomcat based application just start qcadoo MES using the following command:
./catalina.sh jpda start
Just remember that on Windows you have to use the catalina.bat script. The default debuggers port will be 8000.
Add a new scenario
Lets add some new business logic. We like that every time you change the quantity of any resource, then a new type of transfer will be created. This transfer will have the status "correction".
Let's go
Change models file
To add a new status of the transfer we have to make changes to the file transfer.xml
<enum name="type" values="01incoming,02outgoing,03correction" required="true"/>
and we must add the a hook to be carried out when resource resource is changed. To do this we add the following code to the hooks node of the resource.xml file in the model directory:
<onUpdate method="createCorrectionTransfer" class="com.warehousecorporation.warehouse.WarehouseService"/>
Custom action while update forms
And now we must a the createCorrectionTransfer method to WarehouseService that implements the hook:
public void createCorrectionTransfer(final DataDefinition dataDefinition, final Entity resource){ DataDefinition transferDataDefinition = dataDefinitionService.get("warehouse", "transfer"); Entity correction = transferDataDefinition.create(); correction.setField("resource", resource); correction.setField("quantity", resource.getField("quantity")); correction.setField("type", "correction"); correction.setField("status", "closed"); transferDataDefinition.save(correction); }
We must also change the setWorkersDatesAndResourceQuantity method. This is very important ! if we don't do this we will end up infinite loop there an onSave resource hook modifies a transfer and an onSave transfer hook modifies a resource.
To prevent this we have to add this code at the beginning of the setWorkersDatesAndResourceQuantity method:
if("03correction".equals(transfer.getField("type"))){ transfer.setField("requestWorker", securityService.getCurrentUserName()); transfer.setField("requestDate", new Date()); transfer.setField("confirmWorker", securityService.getCurrentUserName()); transfer.setField("confirmDate", new Date()); return; }
Localization
The next step is add one line for name of status correction
warehouse.transfer.type.value.03correction = correction
Update the Warehouse plugin
We update the plugin in the same way as we install it:
- Compile the plugin using maven
- In the fast way you just have to shutdown qcadoo MES, overwrite the plugins jar in qcadoo MES with the new one you got in the plugins directory, startup qcadoo.
Exercise
- Add new model with name purchase:
- product - product from basic plugin,
- quantity - quantity of product,
- price - price of product,
- date - date with time.
- Add validator which would check if product price is > 0.
- Add validator which would check if product quantity is > 0.
- Add validator which would check if there is no product with the same name and price.
- Add views for that model.
- Add hooks which would set default system currency in field next to price (locale chosen in framework).
- Add hooks which would set product unit in field next to quantity - when product changes, this field also should be updated.