⚙️ Document commands and subtypes
Updated
How a user posts a document, why "there is a Posted subtype" does not yet mean the document can be posted from the card, and what happens to the postings when the state changes. How the contours run — the process map.
A command is the only transition button#
The subtype is the document's current state (Draft, Confirmed, Posted…).
There is no subtype field on the card: a document is born already in the
initial subtype (IsInitial). If there is no initial subtype, or several are
marked, Create in the journal first asks which subtype to start in. After
that only a command changes the subtype: picking "Posted" from a list is not
possible.
A document can only be posted from the UI by a command. The command is
bound to the source subtype (for example, only to Draft). When pressed it:
- Runs the checks through the domain services, without copying their
formulas into the command: the balance (
IStockAvailabilityService,ISalesFulfillmentService), the cell assignment (IStoreCellService), the quantity in the base unit (IItemQuantityConverter), the tax rate and amount (ITaxService), the period and the accounts (IGeneralLedgerService). If a check fails, the client gets a message (ClientAction.Message), execution stops, and the subtype does not change. - Sets the target subtype (
document.Subtype = SalesOrder.Subtypes.Confirmed) and saves. - The engine posts the document in its new state: the
OnBeforePost/OnAfterPostevents run, and so do the target subtype's transaction scripts — the postings into the registers, the reservation, the ledger.
A transition command does not call the services' mutating methods
(CreateCalculationAsync, InvoiceOrderAsync, CompleteTripAsync,
CreateAccrualAsync…): OnAfterPost already runs them. From a command —
reading and validation only.
Without such a command the document is a draft forever as far as the card is
concerned, no matter how many scripts are attached to "Confirmed". How to make the button —
the zuloone-new-command skill. How subtypes and postings work —
zuloone-new-document.
The programmatic path (document.Subtype = …, SetSubtypeAsync, the
/subtype API) is for services, events and tests, when the checks have already
been made higher up. It is not a substitute for the user's button.
A command that changes no subtype (filling in prices, expanding a BOM) does not post the document.
Postings do not accumulate: Mix semantics (replacement, not a sum)#
Yes: on a transition the engine removes every movement the document has already written and writes afresh what the scripts of the target subtype only produce. In the ZuloOne code this is called MIQS replace semantics — that is how Mix worked.
In Mix, on every save DocumentManager took the current SubtypeID's
scripts (GetTransactionScripts), computed the new set of postings and handed
it to TransactionProcessor.SaveDocumentTransactions: registers no longer in
the set went to DeleteDocumentTransactions, the rest to
MergeDocumentTransactions (old rows absent from the new set are deleted).
Changing the subtype did not "add a layer" — it substituted the document's
postings with what the new subtype knows how to write. Commands there were
bound to a subtype as well (DocumentCommandToSubtype): the button is visible
only on its own state and, once the checks pass, sets the new subtype + Save.
The typical Draft → Confirmed path feels like "just post the document": a draft has no
scripts, there is nothing to remove, and the confirmation's postings are
written.
But if the states form a chain and each has its own set of registers:
Realized → the script writes the realization
Done → the script writes something else, no realization in it
then moving Realized → Done will remove the realization's postings. Only what the "Done" subtype wrote stays in the registers. A rollback to draft (a subtype with no scripts) removes the postings entirely — the document is "empty" again.
That is why "pay" must not be made a subtype of the same sales realization: an
Issued → Paid transition would erase the stock, the revenue and the
receivables along with the "paid" flag. A payment is a document of
its own, see obligations and the ledger.
If a later state has to keep an earlier one's effect, the later subtype's scripts write both sets (the realization and "done"), or the second effect lives in another document. The current state's movements = what has to be true now, not the sum of the transition history.
The same transaction script can be attached to several subtypes (in Mix —
DocumentSubtypeTransactionScript on different SubtypeIDs). The later state
then keeps no "layer" of the earlier one but recomputes the same postings.
If the target subtype has no binding, the register rows are deleted.
Transition commands#
One command, one edge. The binding is to the source subtype. The
programmatic document.Subtype = … in tests and services is still legitimate;
from the card a button is needed.
Stores#
| Document | Command | Transition | What happens |
|---|---|---|---|
PutAwayTask |
Confirm put-away (ConfirmPutAway) |
Draft → Confirmed |
store: receiving → storage |
PickTask |
Confirm pick (ConfirmPick) |
Draft → Confirmed |
store: storage → picking |
StockAdjustment |
Post adjustment (PostStockAdjustment) |
Draft → Posted |
write-off / write-on |
StockTransfer |
Post transfer (PostStockTransfer) |
Draft → Posted |
cell ↔ cell |
StockCount |
Post stock count (PostStockCount) |
Draft → Posted |
discrepancies |
GoodsIssue |
Post goods issue (PostGoodsIssue) |
Draft → Posted |
an issue out of the store |
Sales and purchasing#
| Document | Command | Transition | What happens |
|---|---|---|---|
SalesLead |
Qualify lead (QualifyLead) |
New → Qualified |
interest, no postings |
SalesLead |
Create quotation (ConvertLead) |
Qualified → Quoted |
a Draft SalesQuotation, key SourceLead |
SalesLead |
Mark lost (LoseLead) |
New / Qualified → Lost |
no quotation is created |
SalesQuotation |
Issue quotation (IssueQuotation) |
Draft → Issued |
a commercial offer, no postings |
SalesQuotation |
Create sales order (ConvertQuotation) |
Issued → Converted |
a Draft SalesOrder, key SourceQuotation |
SalesQuotation |
Cancel quotation (CancelQuotation) |
Draft / Issued → Cancelled |
no order is created |
PurchaseOrder |
Place order (PlaceOrder) |
Draft → Ordered |
an obligation towards the supplier, no stock yet. With PurchasingSettings.AutoReceiveOnOrder — immediately Ordered and then Received, as with Receive order; receiving is checked while still in Draft, and a refusal leaves the draft |
PurchaseOrder |
Receive order (ReceiveOrder) |
Ordered → Received |
stock+, payables, tax, a put-away task |
VendorPayment |
Pay vendor (PayVendor) |
Draft → Paid |
settles payables |
PurchaseReturn |
Post purchase return (PostPurchaseReturn) |
Draft → Posted |
stock−, payables− (net); another document's cell reserve is not written off; input VAT is not reversed |
PurchaseCreditNote |
Post purchase credit note (PostPurchaseCreditNote) |
Draft → Posted |
reverses input VAT and the tax part of the payable; the order stays Received |
SalesOrder |
Submit order (SubmitSalesOrder) |
Draft → Submitted |
a commercial request, no postings. With SalesSettings.ConfirmOrderOnSubmit — Draft → Confirmed straight away and a sales realization, as with Approve order |
SalesOrder |
Approve order (ApproveSalesOrder) |
Submitted → Confirmed |
creates the realization in Reserved (does not write the reservation itself); the Manager phone: ApproveOrder |
SalesOrder |
Reject order (RejectSalesOrder) |
Submitted → Draft |
RejectReason is mandatory; the agent corrects it and submits again. The Manager phone: RejectOrder |
SalesOrder |
Cancel order (CancelSalesOrder) |
Submitted / Confirmed → Cancelled |
an unclosed realization is removed |
SalesRealization |
Start picking (StartPicking) |
Reserved → Picking |
the same reservation |
SalesRealization |
Mark packed (MarkPacked) |
Picking → Packing |
the same reservation |
SalesRealization |
Mark shipped (MarkShipped) |
Packing → Shipped |
the same reservation |
SalesRealization |
Release realization (ReleaseRealization) |
Shipped → Issued |
stock−, debt+, revenue+, tax, loyalty; the reservation is released |
SalesRealization |
Issue realization (IssueInvoice) |
Draft → Issued |
POS / without an order: the same Issued set straight away |
SalesReturn |
Post sales return (PostSalesReturn) |
Draft → Posted |
stock+, debt− (net), revenue− |
SalesCreditNote |
Post sales credit note (PostSalesCreditNote) |
Draft → Posted |
reverses the VAT and the tax part of the debt; the original realization stays Issued |
SalesDebitNote |
Post sales debit note (PostSalesDebitNote) |
Draft → Posted |
additional VAT and the tax part of the debt; the lines are not copied from the original realization; that realization stays Issued |
CustomerPayment |
Receive payment (ReceiveCustomerPayment) |
Draft → Paid |
settles receivables |
Issued in the UI is Released.
Issued → Paid on a realization does not exist: payment is
made by CustomerPayment. There is no Mark delivered on a realization.
DeliveryTrip stays an option for consolidated delivery, not the axis:
FillTripFromRoute (collects the Confirmed orders of the route's stops),
DispatchTrip (Draft → Dispatched, checks the crew and the capacity),
CompleteTrip (closes the orders: a refusal → Cancelled, otherwise
Delivered). The global PlanDeliveryWave creates the drafts from the day's
schedule. The global PlanPickWave (Gather pick wave, warehouse ribbon) folds
draft PickTasks for several orders from one storage cell into one task.
An order becomes Delivered when the realization is Released, or when a trip completes. The usual path is order →
realization, with no trip. The process —
sales and purchasing.
Production, accounting, HR, taxes, loyalty#
| Document | Command | Transition | What happens |
|---|---|---|---|
ProductionOrder |
Release to shop (ReleaseProduction) |
Draft → Released |
the components are written off |
ProductionOrder |
Finish production (FinishProduction) |
Released → Finished |
the write-off + the output |
JournalEntry |
Post journal entry (PostJournalEntry) |
Draft → Posted |
Dr=Cr balance, the period on the date, a closed month refuses |
TimeSheet |
Approve timesheet (ApproveTimeSheet) |
Draft → Approved |
otherwise "Accrue payroll" is not visible |
PayrollAccrual |
Post payroll accrual (PostPayrollAccrual) |
Draft → Posted |
an accrual created by hand; from a timesheet the same is done by AccruePayroll |
PayrollAccrual |
Void payroll accrual (VoidPayrollAccrual) |
Posted → Voided |
Mix removes the payroll, the liability and the GL; the linked contributions go first; a net already paid refuses |
PayrollPayment |
Pay payroll (PayPayroll) |
Draft → Paid |
the payout |
PayrollPayment |
Void payroll payment (VoidPayrollPayment) |
Paid → Voided |
Mix brings the liability back |
SocialInsuranceAccrual |
Post social insurance (PostSocialInsuranceAccrual) |
Draft → Posted |
the Posted transition: SocialInsuranceAccrualTx; usually spawned from an accrual |
SocialInsuranceAccrual |
Void social insurance accrual (VoidSocialInsuranceAccrual) |
Posted → Voided |
Mix removes the register and the withholding; amounts already paid to the fund refuse |
SocialInsurancePayment |
Pay social insurance (PaySocialInsurance) |
Draft → Paid |
the Paid transition: SocialInsurancePaymentTx |
SocialInsurancePayment |
Void social insurance payment (VoidSocialInsurancePayment) |
Paid → Voided |
Mix brings SocialInsurance back; after that the contribution accrual can be voided |
TaxReturn |
File tax return (FileTaxReturn) |
Draft → Filed |
the filing + a TaxSubmission row |
TaxPayment |
Pay tax (PayTax) |
Draft → Paid |
the Paid transition: TaxPaymentTx |
TaxCalculation |
Finalize tax calculation (FinalizeTaxCalculation) |
Draft → Finalized |
usually set by the tax service |
LoyaltyRedemption |
Redeem points (RedeemLoyalty) |
Draft → Redeemed |
the points are written off |
Commands without a subtype change#
| Document | Command | What for |
|---|---|---|
TimeSheet |
Fill standard week (FillTimeSheet) on Draft |
Mon–Fri at the 40h norm, the gaps out of TimeOff, manual ticks are not overwritten |
TimeSheet |
Accrue payroll (AccruePayroll) on Approved |
creates a PayrollAccrual and sets it to Posted straight away; the salary, or hours × rate |
SalesRealization / PurchaseOrder |
Fill prices | fills in the lines' prices |
ProductionOrder |
Expand bill of materials (ExpandBom) |
fills the components from the BOM |
ProductionOrder |
Schedule operations (ScheduleProduction) on Draft and Released |
sets the day of each operation by sequence and remaining capacity |
Rolling back (posted → draft) the engine can do by itself. No document has a separate "Back to draft" command.
How not to confuse the three mechanisms#
| What | What for |
|---|---|
| The command | the button: the checks + the subtype change. The only path from the UI |
The transition map (subtypeTransitions) |
which edges are permitted at all. An empty map = everything is permitted |
| The transaction scripts | what to write into the registers once the subtype has already changed. They are attached to a subtype, not to the document as a whole |
The events (OnBeforePost / OnAfterPost) |
validation and side effects of any transition, including one made from the API |
A map with no command gives no button. Scripts with no command do not run from the card. Events with no command only fire if the subtype was changed programmatically.
What's not there yet#
- There is no subtype selector on a saved card; a visible command is the only user transition.
- There is no generic Back to draft command for posted documents.
- A sales realization has no paid state; payment is a separate customer payment.