Modules:OMS Engine/OMS Developers Guide
Contents |
OMS Developers Guide
About this document
This document is divided into two main sections. The first one shows the way to call the OMS Engine from external systems through a web service, describing the input and output formats expected by the engine.
The second one explains how to develop new algorithms that can be consumed by the OMS engine.
OMS Engine as Web Service
Here you can find the documentation of the OMS Web Service API: https://livebuilds.openbravo.com/retail_modules_pgsql_pi/api?urls.primaryName=oms1.1.0
URL
The OMS engine is exposed through a Web Service with the URL:
https://<host>:<port>/openbravo/ws/org.openbravo.oms.getOrdersProposal
Request format
Version 1.0.0
This Web Service receives a JSON request as input which describes the ticket from which we want to calculate the order proposal sourcing. The structure is a simplification of the current JSON object generated by the Openbravo POS software, and it’s a common format shared by other modules, like for example the Discounts engine.
Let’s see an example:
{ "ticket":{ "id":"XXXXXXX1", "organization":{ "id":"DC206C91AA6A4897B44DA897936E0EC3", "_identifier":"F&B España - Región Sur" }, "businessPartner":{ "id":"ABD91C9D3BC94175B876FBBE9CACA008", "_identifier":"VBS Customer" }, "date":"2019-07-30T11:54:21.018Z", "lines":[ { "id":"000000000000010", "product":{ "id":"DA7FC1BB3BA44EC48EC1AB9C74168CED", "_identifier":"Cerveza Ale 0,5L" }, "qty":10, "price":100, "uom":100 }, { "id":"000000000000020", "product":{ "id":"C0E3824CC5184B7F9746D195ACAC2CCF", "_identifier":"Cerveza Lager 0,5L" }, "qty":10, "price":100, "uom":100 }, { "id":"000000000000030", "product":{ "id":"7206FAA45A3842659D93F59CCA2B0613", "_identifier":"Cola 0,5L" }, "qty":10, "price":100, "uom":100 } ] } }
The mandatory fields for the OMS engine are:
- ticket.id: this is the identifier of the ticket in the external system. The OMS engine will store this information into the OMS Run’s search key field.
- lines.product.id: represents the Openbravo’s M_Product_ID.
- lines.<line>.qty: represents the quantity ordered in the line.
- lines.<line>.uom: represents the Openbravo’s C_UOM_ID.
All the above fields are the mandatory ones when calling to the OMS engine. Other fields like _identifier or price are not really required and they will be silently ignored by the engine.
Version 1.1.0
This version includes the following modifications:
- The components BusinessPartner, Organization and Product can be identified from the id (like in version 1.0.0) or by the searchKey (new in version 1.1.0). It's mandatory to specify at least any of them.
- There is no need to include uom in the input json. In this case the quantity (qty) is expressed in the product's UOM.
Let's see an example for a valid request for version 1.1.0:
{ "ticket":{ "id":"TestData_Input_1.1.0", "organization":{ "searchKey":"F&B España - Región Sur" }, "lines":[ { "product":{ "searchKey":"ES\/0001" }, "qty":1, "uom":100 }, { "product":{ "searchKey":"ES\/0002", "_identifier":"Cerveza Lager 0,5L" }, "qty":10 }, { "product":{ "id":"7206FAA45A3842659D93F59CCA2B0613", "searchKey":"ES\/0019", "_identifier":"Cola 0,5L" }, "qty":1 } ] } }
Response format
Version 1.0.0
The response is also formatted as JSON with the following structure:
{ "oms":{ "tickets":[ { "client":"23C59575B9CF467C9620760EB255B389", "organization":"DC206C91AA6A4897B44DA897936E0EC3", "lines":[ { "product":{ "id":"C0E3824CC5184B7F9746D195ACAC2CCF", "_identifier":"Cerveza Lager 0,5L" }, "qty":6, "uom":"100" } ], "warehouse":"5848641D712545C7AE0FE9634A163648" }, { "client":"23C59575B9CF467C9620760EB255B389", "organization":"E443A31992CB4635AFCAEABE7183CE85", "lines":[ { "product":{ "id":"7206FAA45A3842659D93F59CCA2B0613", "_identifier":"Cola 0,5L" }, "qty":10, "uom":"100" }, { "product":{ "id":"C0E3824CC5184B7F9746D195ACAC2CCF", "_identifier":"Cerveza Lager 0,5L" }, "qty":4, "uom":"100" }, { "product":{ "id":"DA7FC1BB3BA44EC48EC1AB9C74168CED", "_identifier":"Cerveza Ale 0,5L" }, "qty":10, "uom":"100" } ], "warehouse":"B2D40D8A5D644DD89E329DC297309055" } ] } }
Everything returned by the OMS engine is wrapped into the oms namespace. Inside it we can find the tickets array, which includes all the tickets proposals calculated by the OMS engine. It’s important to note that the tickets are grouped by organization and warehouse, so it is easy to consume them by external services.
In the example above the OMS engine has proposed 2 orders: the first one with only one line with 6 units of Cerveza Lager, and the rest of the products will be delivered with a second order by a different organization and warehouse.
Version 1.1.0
The response in version 1.1.0 includes the following modifications:
- The components Client, Organization and Warehouse are components identified by the id, _identifier and searchKey.
- Added obomsRunId in the response, so the consumer can use it to retrieve more information from this window.
Let's see an example of response for version 1.1.0:
{ "oms":{ "obomsRunId": "01948CAC5011452CB13A794C722B31AE", "tickets":[ { "client":{ "id":"23C59575B9CF467C9620760EB255B389", "searchKey":"F&B International Group", "_identifier":"F&B International Group" }, "organization":{ "id":"DC206C91AA6A4897B44DA897936E0EC3", "searchKey":"F&B España - Región Sur", "_identifier":"F&B España - Región Sur" }, "lines":[ { "product":{ "id":"DA7FC1BB3BA44EC48EC1AB9C74168CED", "searchKey":"ES\/0001", "_identifier":"Cerveza Ale 0,5L" }, "qty":1, "uom":"100" }, { "product":{ "id":"C0E3824CC5184B7F9746D195ACAC2CCF", "searchKey":"ES\/0002", "_identifier":"Cerveza Lager 0,5L" }, "qty":10, "uom":"100" }, { "product":{ "id":"7206FAA45A3842659D93F59CCA2B0613", "searchKey":"ES\/0019", "_identifier":"Cola 0,5L" }, "qty":1, "uom":"100" } ], "warehouse":{ "id":"5848641D712545C7AE0FE9634A163648", "searchKey":"RS", "_identifier":"España Región Sur" } } ] } }
How-to create new OMS algorithms
The OMS engine module includes several OMS algorithms. However the engine is prepared to easily work with new algorithms provided by external modules. In this section we will see how to develop new algorithms.
It’s assumed in this howto that you have already created a new module which will include the new algorithm. If you need more information about this step, please see How_To_Create_and_Package_a_Module
OMS Database Model
Before developing your first OMS algorithm, it’s very important to understand the database model behind the OMS engine.
When the OMS engine is run for a concrete ticket, the system automatically populates some tables with useful information that is later consumed by the OMS algorithms.
Let’s see the most important tables:
OBOMS_Run
This stores information about the concrete OMS run for a ticket. Every single table explained below will mandatory have a link to the OBOMS_Run_ID. It’s critical you make sure your algorithm always filters by the OBOMS_Run_ID
OBOMS_Run_Rule
This represents the execution of a concrete rule within a OBOMS_Run. Each OBOMS_Run might have many OBOMS_Run_Rule (depending on the OMS Configuration). It’s critical you make sure your algorithm always filters by the OBOMS_Run_Rule_ID.
OBOMS_Run_Rule_Criteria
This represents the execution of a concrete rule criteria within a OBOMS_Run_Rule (and OBOMS_Run). Each OBOMS_Run_Rule might have many OBOMS_Run_Rule_Criteria (depending on the OMS Configuration). It’s critical you make sure your algorithm always filters by the OBOMS_Run_Rule_Criteria_ID.
OBOMS_Run_OrderLine
This represents the ticket lines that are pending to find the warehouse for a concrete OBOMS_Run_Rule_ID. They are always linked to an OBOMS_Run_ID and OBOMS_Run_Rule_ID.
This table is very important for the OMS algorithms because it represents the products and quantities pending to deliver that the algorithm must try to resolve. Example:
OBOMS_RUN_ID | M_PRODUCT_ID | QTYORDERED | C_UOM_ID | OBOMS_RUN_RULE_ID |
1 | A | 10 | 100 | 5 |
1 | B | 15 | 100 | 5 |
1 | C | 7 | 100 | 5 |
OBOMS_Run_Stock_Proposed
This table stores the stock proposed by the different OMS steps in the engine. It is the input and output (based on the IsOutput column) for the algorithms. It’s linked to the OBOMS_Run_ID, OBOMS_Run_Rule_ID and OBOMS_Run_Rule_Criteria_ID. It’s critical to always filter by these entities in your algorithm.
This table is very important for the OMS algorithms because it holds the stock received as the algorithm’s input, i.e. the total available quantity in a warehouse for a product. Based on this dataset, the algorithm must build a new dataset filtering out or reordering it (using the Priority column), which will be the output.
An order usually has more than one line to deliver, and the OMS algorithms must work will all the lines at the same time. The way to group together the OMS algorithm’s input and output stock is by using the GroupId column. Example:
OBOMS_RUN_ID | OBOMS_RUN_RULE_CRITERIA_ID | M_WAREHOUSE_ID | M_PRODUCT_ID | QTYORDERED | C_UOM_ID | PRIORITY | ISOUTPUT | GROUPID | OBOMS_RUN_RULE_ID |
1 | 7 | 8 | A | 50 | 100 | 0 | N | 2 | 3 |
1 | 7 | 8 | B | 27 | 100 | 0 | N | 2 | 3 |
1 | 7 | 9 | A | 11 | 100 | 5 | N | 2 | 3 |
1 | 7 | 8 | B | 27 | 100 | 10 | Y | 3 | 3 |
1 | 7 | 9 | A | 11 | 100 | 15 | Y | 3 | 3 |
In the table above we can see that the OMS algorithm receives as input the GroupId = 2, containing three lines: two of them for Warehouse = 8 and one for Warehouse = 9. These lines represent the total quantity available in the warehouse for this product. It will always be greater or equal than the pending quantity line.
The OMS algorithm has generated two lines more with IsOutput = Y. Note that, in the example, the algorithm has done 2 different things:
- To exclude one of the proposed input stock lines
- To add some priority to the output records.
Please notice how the OBOMS_RUN_ID, OBOMS_RUN_RULE_CRITERIA_ID and OBOMS_RUN_RULE_ID are equal for all the records. The only thing that really changes is the GroupId (and the priority)
Declare the OMS algorithm
The very first thing we need to do is to declare into the Openbravo’s Application Dictionary the new OMS algorithm we want to develop.
So we log into the Openbravo backend as System Administrator and, inside the OMS Algorithm window, we enter a new record linked with our module with a detailed name and description.
The Final flag is important and, when set, it avoids the OMS engine to run further criterias within the rule after the one that is linked to a final algorithm. Or in other words, when the Final flag is set, no more criterias can be defined to be executed after this one within the rule.
In our example the final is unset, so more criterias could be executed afterwards if the user configures them.
Create the Java class
OMS Algorithms are written as Java classes which contain the algorithm itself, and that are executed in the server. The class must extend from OMSAlgorithm class, and it must mandatory add a @Qualifier annotation to point to the OBOMS_ALGORITHM_ID just created above. To get the ID you can just export the database and look at the new src-db/database/sourcedata/OBOMS_ALGORITHM.xml file in your module’s folder.
@Qualifier(algorithmId = "XXXXXXXXXXXXXXXXXXXXXXXX") public class WarehouseDoubleAvailability extends OMSAlgorithm { @Override public void run() { // TODO algorithm business logic goes here } }
As you can see we need to override the run() method. Let’s see the implementation for our algorithm (imports hidden for simplicity):
@Qualifier(algorithmId = "XXXXXXXXXXXXXXXXXXXXXXXX") public class WarehouseDoubleAvailability extends OMSAlgorithm { @Override public void run() { calculateWarehousesThatCouldIssueDoubleQtyOfIndividualProducts(); } private void calculateWarehousesThatCouldIssueDoubleQtyOfIndividualProducts() { // @formatter:off final String hql = "insert into OBOMS_Run_Stock_Proposed(" + "id, client, organization, " + "creationDate, createdBy, updated, updatedBy, " + "obomsRun, groupId, obomsRunRuleCriteria, " + "warehouse, product, orderedQuantity, uOM, " + "priority, isOutput, obomsRunRule) " + "select get_uuid(), sp.client, sp.organization, " + "now(), sp.createdBy, now(), sp.updatedBy, " + "sp.obomsRun, :outStockGroupId, sp.obomsRunRuleCriteria, " + "sp.warehouse, sp.product, sp.orderedQuantity, sp.uOM, " + "sp.priority, :isOutput, sp.obomsRunRule " + "from OBOMS_Run_Stock_Proposed sp " + "join OBOMS_Run_OrderLine ol on (sp.obomsRun.id = ol.obomsRun.id " + " and ol.obomsRunRule.id = :obomsRunRuleId " + " and ol.product.id = sp.product.id " + " and ol.uOM.id = sp.uOM.id " + " and sp.orderedQuantity >= 2 *(ol.orderedQuantity)) " + "where sp.groupId = :inStockGroupId " + "and sp.obomsRun.id = :obomsRunId "; // @formatter:on @SuppressWarnings("rawtypes") final Query insertIntoSelect = OBDal.getInstance().getSession().createQuery(hql); insertIntoSelect.setParameter("outStockGroupId", getOutputStockGroupId()); insertIntoSelect.setParameter("isOutput", true); insertIntoSelect.setParameter("inStockGroupId", getInputStockGroupId()); insertIntoSelect.setParameter("obomsRunId", getObomsRunId()); insertIntoSelect.setParameter("obomsRunRuleId", getObomsRunRuleId()); logNewLine("Inserted lines: " + insertIntoSelect.executeUpdate()); } }
As you can see the logic is fully implemented in HQL. This is possible thanks to the OMS database model explained above. Working directly with the database is very convenient as the RDBMS is optimized to work with high volumes in a simple way.
The same logic could have been developed using Java, but that would be slower to execute and more complex to develop.
Let’s see the details about the implementation:
- The algorithm inserts new records into the OBOMS_Run_Stock_Proposed table which, as you already know, it is the one that stores the stock input and output for an OMS algorithm.
- Please pay special attention to the HQL parameters :outStockGroupId and :isOutput.
- The first parameter is retrieved using the getOutputStockGroupId() method. The Output Stock Group Id parameter is automatically set by the OMS engine before calling the OMS algorithm, so you only need to make sure the new records you’re creating are linked to this Id.
- The isOutput parameter is set to true, because obviously we are generating an output for the OMS algorithm. You must always set this column to true (‘Y’) in the records inserted by your algorithm.
- The query searches for records inside the OBOMS_Run_Stock_Proposed (sp) filtering by the OBOMS_Run_ID and the GroupId returned by the getInputStockGroupId(). This is actually the input stock for the OMS algorithm, which was automatically created by the OMS engine before executing the OMS algorithm. You must always filter by these two columns when querying the input stock.
- The records we are inserting into the OBOMS_Run_Stock_Proposed table are also linked to the OBOMS_Run_ID, OBOMS_Run_Rule_Criteria_ID and OBOMS_Run_Rule. Note how we reuse the content of the current input stock proposed sp.* columns to create the new records in the OBOMS_Run_Stock_Proposed. You should follow a similar approach in your algorithm implementations.
- In our example the logic for this concrete algorithm needs to insert as output only those input stock proposed lines with an available quantity greater or equal than two times the pending order line. So the way to do it is by joining the input stock proposed (sp) with the order lines (ol) available for this OBOMS_Run_Rule_ID, gotten using the getObomsRunRuleId() method. The query joins by the OBOMS_Run_ID, M_Product_Id and C_UOM_ID dimensions, and gets only the sp lines that fulfill: sp.orderedQuantity >= 2 *(ol.orderedQuantity)
- The logNewLine() is used to add log the algorithm execution. This log is available to the end user inside the OMS Run | Rule Run | Criteria Run tab.
- In our example the output stock proposed priority is the same as the input, or in other words the algorithm doesn’t change the priority. However, in the case it’s needed, you would just need to modify the priority column value either at the same moment of inserting the output stock proposed or in a separate method within the same algorithm execution.
So, as you can see, the OMS Engine simplifies the OMS algorithm implementation because:
- It automatically calculates and saves everything you need to simplify the OMS algorithm implementation:
- the available input stock (OBOMS_RUN_STOCK_PROPOSED), which must be filtered by:
- The OBOMS_Run_ID returned by the getObomsRunId() method
- The groupId returned by the getInputStockGroupId() method
- The pending order lines (OBOMS_RUN_ORDERLINE), which must be filtered by:
- The OBOMS_Run_ID returned by the getObomsRunId() method
- The OBOMS_Run_Rule_ID returned by the getObomsRunRuleId() method.
- the available input stock (OBOMS_RUN_STOCK_PROPOSED), which must be filtered by:
- Based on both datasets, the implementation just need to create new records inside the OBOMS_RUN_STOCK_PROPOSED table with the same OBOMS_Run_ID, OBOMS_Run_Rule_ID and OBOMS_Run_Rule_Criteria_ID as the input stock, and with the groupId returned by the getOutputStockGroupId() method.
- The implementation can be directly developed using queries, which clearly improves performance and simplifies the development.