Parsware
All articles

Parsware Platform

Checking stock on an order

The answer to "is there enough stock?" isn't on the order line. It's on the product. We saved three order lines against a product with 10 in stock and watched a plugin step read the product, take the stock and log the movement, or refuse the line, then read the script that does it.

A new order line for 50 WIDGET refused with the message "There is not enough WIDGET in stock." across the top of the form, next to the title "Checking stock on an order"

Every rule so far has looked at the record being saved. An expense limit reads the claim's amount; a purchase-order rule reads the claim's amount again. This one can't: whether an order line can be filled depends on another table, the product's stock.

That's a job a form rule can't do. The browser has the order line in front of it, not the product, and even if it looked the stock up, it couldn't stop two people ordering the last ten at the same moment. A plugin step can, because it runs on the server, inside the save.

We used the Order Stock sample, as in the last article. Import it from samples/solutions/business-rules-order-stock to follow along. It adds an Orders — Stock app with Products, Order lines and Stock movements.

An order that can be filled

We added one product, WIDGET, with 10 in stock. Then, in Order lines, we saved a line for 3:

The saved order line Window display: Product code WIDGET, Quantity 3

Nothing on the order line changed. The other two tables did. The product is down to 7:

The Products list: Widget, WIDGET, 7 in stock

And there's a new stock movement saying why:

The Stock movements list: Taken for Window display, WIDGET, 3

Two that can't

A line for 50, with 7 left:

A new order line, Spring catalogue launch, 50 WIDGET, refused with "There is not enough WIDGET in stock."

And a line for a product code that doesn't exist:

A new order line, Office move, 5 GADGET, refused with "No product has the code GADGET."

Both refused, both before anything was stored. The product still says 7.

We also sent the second line straight to the API, with no form involved:

POST /environments/{environment}/data/par_order_line
{ "par_name": "Warehouse top-up", "par_product_code": "WIDGET", "par_quantity": 50 }

400  { "title": "modelapps.record.validation_failed",
       "detail": "There is not enough WIDGET in stock." }

Same step, same sentence. An import or another app would get the same answer.

The script

In the maker portal: the sample's solution, Plugin steps, select Take the stock, or refuse the line, Edit code:

The step's script in the editor, 34 lines: getValue for the code and quantity, countRecords, queryRecords, a stock check, updateRecord and createRecord

Without its comments, it's short:

var code = getValue('par_product_code');
var wanted = getValue('par_quantity');

if (countRecords('par_product', 'par_code', code) == 0) {
  throwValidationError('No product has the code ' + code + '.');
}

var matches = queryRecords('par_product', 'par_code', code);
var product = matches[0];

if (product.par_in_stock < wanted) {
  throwValidationError('There is not enough ' + code + ' in stock.');
}

updateRecord('par_product', product.id, { par_in_stock: product.par_in_stock - wanted });
createRecord('par_stock_movement', {
  par_name: 'Taken for ' + getValue('par_name'),
  par_product_code: code,
  par_quantity: wanted
});

Four functions do the work on other tables:

Function What it does here
countRecords(table, field, value) How many products have this code. It asks the database to count, so it's cheap however many rows there are
queryRecords(table, field, value) The matching products, as a list. Each one is an object: product.par_in_stock, and product.id
updateRecord(table, id, fields) Writes the new stock level to that product. Pass every field you're changing in one object
createRecord(table, fields) Adds the stock movement, and gives back its id

Three things worth knowing

It all happens in one transaction. The step runs before the write, so the stock update, the movement and the order line are stored together or not at all. If the order line then failed to save for any other reason, the stock would come back. The last article showed this with a second step refusing after this one had taken the stock.

It runs as the person saving. The step reads and writes products with the permissions of whoever saved the order line. If they can't read products, queryRecords returns nothing for them. A script is never a way around a permission.

It never writes the table it watches. A script's writes go through the ordinary save, with its plugin steps. If this step updated an order line, that update could start another run of a step on order lines, and so on. The platform stops after eight levels, and depth() tells a script how deep it is, but the real fix is not to do it: write another table, as this step does, or check a condition first.

Limits

  • queryRecords matches one field against one value. For ranges, several conditions or a sort, use runQuery, which takes the same query the Advanced Find builder produces.
  • A query returns at most 200 rows. A question about every row is a report's job.
  • The check reads the product and then updates it. Two lines saved at the same instant can both read 7. Treat this step as the pattern, not as a warehouse system.

Try it

Add a second product and a line for each. Then add a plugin step on Stock movement, Create, that refuses a movement for more than 100, and save a line for 150 of a product you've stocked with 500. The order-line step's createRecord triggers the movement step, and the refusal undoes everything, stock included.

Next: a claim approved by the server: a step that doesn't refuse anything, but sets values on the record as it's saved.