Skip to content

Stock that can't be oversold

Declare consumes on a line, and the balance it uses moves with it; a take past zero refuses the write.

work_order_part: {
consumes: { column: 'part_id.on_hand', by: 'quantity', refuse: 'Only {part_id.on_hand} of {part_id.name} left' },
},

Each write of a work_order_part line moves part.on_hand:

The write The balance
Insert Subtracts by (one per line without it), or refuses with 422 consumes_refused and writes nothing
Delete Gives it back
Update Moves the difference; a line moved to another part gives back to one and takes from the other
The reference set to null Gives back

refuse is filled from the part as it stands, with only what the reader may read.

Use it for a stored balance that must never go below zero. It applies on every door: Save, a list cell, an import, the App API, MCP and a command’s own writes. Writing the line is the whole grant, so no role needs write access to on_hand.

For a figure you only read, such as units used so far, use a rollup (sum('work_order_part', 'quantity')) and store nothing.

  • floor: false. Lets the balance go below zero, for backorders.
  • by. The line’s amount column; leave it out for one per line.
  • refuse. The sentence a refusal says, reading {ref.column} of the part.

Receiving stock is a line too: restocks is consumes the other way round, so Receive stock is the receipt’s New form, with no command.

stock_receipt: {
restocks: { column: 'part_id.on_hand', by: 'quantity', refuse: 'Only {part_id.on_hand} of {part_id.name} are still here' },
},

An insert adds by, an update moves the difference, and a delete takes it back out, refused where those parts were used since. It takes the same keys and is held on the same doors.

A restocks refused (a delete or an update that would take back stock already used) answers with the same consumes_refused as consumes, since both refusals are the balance’s.

  • There is no when. A cancel action zeroes its lines’ quantity or deletes them, and the stock comes back.
  • The part’s history does not record the move; the line’s does.
  • consumes-path: column starts with something that is not a reference of the line. Write part_id.on_hand.
  • consumes-balance: the balance is not a number column of the record the reference names.
  • consumes-by: by is not a number column of the line.
  • consumes-decimal: a whole-number balance moved by an amount that may be a fraction. Give both the same numeric type.
  • consumes-written: a command writes a line and the balance too, so the line would count twice. Leave it to the rule. A command that writes only the balance (a stock count) is not named.