Automated Accounting Cascades

This is the most valuable — and hardest to rebuild — part of the pre-built foundation: automatic linkage between business actions and accounting.

When sales, warehouse, or HR staff complete routine work, the system generates and updates journal entries and bills in the background. Your application never needs to know how accounts are derived, how tax is computed, or how reconciliation is matched — one ctx.erp call runs the whole chain.

These engines treat every entry point equally: the cascade happens whether data arrived from a Custom App, the REST API, or the Dashboard.


1. Sales invoicing

Trigger: creating an invoice from a confirmed sales order SDK: ctx.erp.create_invoice(order_id)

Automatically:

  • Pulls order lines, to-invoice quantities, unit prices, and discounts
  • Looks up the product category to bring in the default income account and tax rate
  • Drafts a customer invoice

Merging multiple orders: order_id accepts a list. Passing a list consolidates all lines into one invoice, requiring the same customer and organization. This solves the common "one invoice per month for account customers" requirement outright.

Missing orders or no invoiceable lines return empty rather than producing half a record.


2. Purchase billing

Trigger: creating a bill from a purchase order SDK: ctx.erp.create_bill(order_id)

Automatically:

  • Computes received quantity minus already-invoiced quantity
  • Drafts a vendor bill for the received-but-unbilled difference only
  • Debits the expense account from the product category; credits accounts payable

That difference calculation is the key defense against duplicate payment — partial deliveries don't produce duplicate bills.

Multiple orders can likewise be merged into one bill (same supplier, same organization).


3. Real-time inventory valuation

Trigger: a stock move reaching completed state SDK: ctx.erp.validate_picking(id) cascades into it

If the product category is configured for real-time valuation, the system computes value from quantity × unit cost and produces a posted valuation journal entry:

DirectionEntry
Inbound (supplier / production → internal location)Dr. Inventory Cr. Accrued payable or expense
Outbound (internal location → customer / production)Dr. Cost of goods sold Cr. Inventory

The effect: cost of goods sold on the income statement and inventory value on the balance sheet update the moment stock moves, with no month-end count required.


4. Payroll

SDK: ctx.erp.confirm_payroll_run(id) / ctx.erp.cancel_payroll_run(id)

ActionWhat happens
ConfirmCreates payroll and social insurance accrual entries — base pay, allowances, and employer contributions posted to the corresponding expense and payable accounts
CancelGenerates reversing entries, backing out the original accrual and keeping the books correct

Cancellation reverses rather than deleting, preserving a complete accounting trail.

Both methods return information about the generated entries, which your application can display or act on.


5. Payment and invoice reconciliation

Trigger: a payment being posted SDK: ctx.erp.confirm_payment(id); use ctx.erp.reconcile_payment(id) to re-run

Automatically:

  1. Creates the payment journal entry. If debits and credits differ by a negligible amount (usually rounding), a balancing entry is generated and posted
  2. Reconciles first-in-first-out: searches the customer's or supplier's open invoices and bills and applies the payment against balances in FIFO order, producing partial or full reconciliation records
  3. Derives payment status: an invoice whose balance reaches zero is marked paid; otherwise partially paid

reconcile_payment handles catch-up — a posted payment whose reconciliation never completed can be fixed after the fact without reversing and re-entering.


Using these engines from an application

def execute(ctx):
    # Find this period's invoiceable orders for an account customer
    orders = ctx.db.query('sale_orders', filters=[
        {'column': 'customer_id', 'op': 'eq', 'value': ctx.params['customer_id']},
        {'column': 'state', 'op': 'eq', 'value': 'sale'},
        {'column': 'invoice_status', 'op': 'eq', 'value': 'to invoice'},
    ])

    if not orders:
        return ctx.response.json({'message': 'Nothing to invoice this period'})

    # Merge into a single invoice — accounts, tax, and lines all derived automatically
    result = ctx.erp.create_invoice([o['id'] for o in orders])

    ctx.response.json({
        'invoice_id': result['invoice_id'],
        'merged_orders': len(orders),
    })

Not one line of that code handles accounts or tax rates. The engine does.


Caveats

Account configuration is a prerequisite. The engines read default income and expense accounts from product categories. Categories without accounts configured will fail to invoice, or fall back to defaults. Complete product category account setup early in your rollout.

Real-time valuation must be enabled. Valuation entries only appear for product categories configured for real-time valuation. Periodic categories produce nothing automatically.

These methods are high-risk scopes. Each ctx.erp.* method maps to its own scope, all high-risk — granting them requires the owner to re-enter their password. That is deliberate: these methods directly affect the books.

Approvals intercept. If a related table has an approval workflow, these actions are intercepted and wait for sign-off before executing.