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.
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 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> |
A transfer represents describes the receive and delivery of a resource. It contains:
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" /> |
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.
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.
Now lets add a form in which we will be able to see the resources details.
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.
Now a grid for transfers.
<?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> |
<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> |
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 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 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" /> |
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 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
warehouse.menu.warehouse = Warehouse warehouse.menu.warehouse.resources = Resources warehouse.menu.warehouse.warehouseTransfers = Transfers |
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 |
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 |
warehouse.resourcesList.window.mainTab.grid.header = Resources: warehouse.resourcesList.window.mainTab.grid.column.product = Product |
warehouse.transfersList.window.mainTab.grid.header = Transfers: warehouse.transfersList.window.mainTab.grid.column.resource = Resource |
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 |
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: |
warehouse.not.enought.resource.error = There is not enought resources in warehouse. |
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> |
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:
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 |
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.
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
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"/> |
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; } |
The next step is add one line for name of status correction
warehouse.transfer.type.value.03correction = correction |
We update the plugin in the same way as we install it: