SO Consolidation
Overview
SO Consolidation is the batch biller. Where a customer has accumulated many finalised sales orders over a period — a month of deliveries, a week of counter orders — this applet gathers the order lines and issues one consolidated internal sales invoice per customer, instead of one invoice per order.
It has three objects. A Template is a saved filter set you can reuse. An Event is a dated (and optionally repeating) instruction to consolidate. A Run is what an event produces, or what you create directly: a named, date-bounded batch whose lines you can inspect and whose resulting invoices you can see.
The backend subsystem behind it (genDocConsolidation) also carries a parallel Job Sheet variant
— Job Sheet to Sales Invoice — which has no registry applet of its own.
FAILED with the message “There is no Job Sheet under company: … branch: … customer: …
salesman: …” and throws. The run lines and the consolidated invoices may still exist. See
Troubleshooting; the equivalent line is present in the Job Sheet variant this class was copied
from.Where it fits
| Direction | Applet | Why |
|---|---|---|
| Upstream | Sales Order (Internal) | Only sales-order lines whose header is INTERNAL_SALES_ORDER and posting status FINAL are eligible |
| Downstream | Sales Invoice (Internal) | The consolidated invoice is an ordinary internal sales invoice created through GenericDocumentService, with the ordinary DCO — nothing about it is special once it exists |
| Beside | Job Sheet (Internal) | The same consolidation subsystem has a Job Sheet to Sales Invoice path, with its own tables and processors |
| Beside | Scheduler | The event and run processors are queue handlers; a repeating event only produces runs when the queue is being driven |
Screens and menus
Three screens, all linked, plus the standard Settings and Personalization sections.
| Menu item | Route | What it is |
|---|---|---|
| SO Consolidation Run | so-consolidation-run | The main screen. A listing of runs (Run Name, Created Date, Updated Date, Status), a Create form, and a detail view |
| SO Consolidation Event | so-consolidation-event | Dated instructions to consolidate, with optional recurrence |
| SO Consolidation Template | so-consolidation-template | Reusable filter sets that an event or run can be seeded from |
The Run detail view has eight tabs: Details, Branches, Companies, Customers, Sales Agent, Export, Sales Orders, Sales Invoices. The four filter tabs each have an Add sub-screen that picks the entities for that run; the Sales Orders tab lists the candidate order lines (Branch, Customer, Sales Agent, Item Code, Qty) and is where selection happens — selection is at line level, so part of a sales order can be consolidated and the rest left open; the Sales Invoices tab shows what the run produced.
Configuration
Before you can use it
| Prerequisite | Where | Why |
|---|---|---|
Sales orders at posting status FINAL | Sales Order (Internal) | Draft and non-final orders are invisible to the query |
| A sales agent on the order header | Sales Order header (sales_entity_hdr_guid) | The salesman filter is mandatory and the grouping key includes it; orders with no sales agent can never be matched |
| At least one company, one branch, one customer and one sales agent selected on the run | This applet | All four are hard-required — see below |
Applet settings
This applet has no working configuration. All three settings screens are non-functional, each in a different way, and no setting key is read anywhere in the repo.
| Screen | State |
|---|---|
| Settings → Field Settings | The unbound stub: the component declares no form and no save method, the template’s toggles (Unit Discount, SST/VAT/GST, WHT, Blanket Order, Department Settings…) have no formControl binding, and the SAVE button has no click handler at all. The labels are copied from a sales-document settings screen and describe nothing in this applet |
| Settings → Default Selection | A directly-routed component carrying @Input() and @Output(). Angular binds nothing on a component named in a component: route entry, so the input is never supplied and save.emit() reaches nobody |
| Personalization → Default Selection | The same shape, the same result |
DEFAULT_BRANCH, DEFAULT_LOCATION, DEFAULT_COMPANY and DEFAULT_TIMEZONE appear only in those
screens and in the settings model interface; a plain grep for each key across the applet finds no
consumer.
Two batch behaviours that would normally be settings are instead processor event properties, set on the queue event rather than anywhere a user can reach:
| Property | What it controls |
|---|---|
sales_order_get_limit_rows_to_process | The page size the run-header processor uses when reading sales-order lines and creating run lines |
execution_strategy | RUN_NOW processes the one named event or run; anything else sweeps every unprocessed one |
Feature visibility / permissions
bl_applet_client_side_perm_dfn holds zero rows for applet code SO Consolidation, so there are
no client-side visibility permissions.
Server-side, TntErpPermissionsV2 carries a full six-verb set (_OWNER, _ADMIN, _CREATE,
_UPDATE, _DELETE, _READ) for each of Consolidation Template Hdr, Consolidation Event
Hdr, Consolidation Run Hdr and Consolidation Run Line, all prefixed
TNT_DM_ERP_SALES_ORDER_TO_SALES_INVOICE_CONSOLIDATION_….
Note that the run-line processor reads the candidate sales orders with
getByCriteriaSnapshotWithoutPerm — the consolidation itself is not filtered by the operator’s
document permissions once the run lines exist.
Fields
Run — Create
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Run Name | The batch’s name | Yes | Validators.required |
| SO Consolidation Template | Seed the filters from a saved template | No | |
| Companies / Branches / Customers / Sales Agents | The four filter sets | Yes, in effect | Stored as company_json, branch_json, customer_json, salesman_json. Each is validated only at run time, and an empty one fails the run |
| Current Run Start Date / End Date | The period the run covers | No | |
| Previous Run · Name · Start Date · End Date | The preceding run, for continuity | Read-only | Populated from the selected previous run |
| Description | Free text | No | |
| Status | ACTIVE by default | — |
Event — Create
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Event Code | Yes | Validators.required | |
| Event Name | Yes | Validators.required | |
| Cycle Start Date / Cycle End Date | The window the event covers | No | |
| Is Recurring / RRULE | Repetition | No | An iCalendar RRULE string, the same recurrence model Stock Replenishment uses — editing or deleting a recurring event asks for a recurring_update_type (this occurrence, this and future, all), reusing bl_inv_stock_replenishment_event_hdr.RecurringUpdateEnum |
| SO Consolidation Template | Seed the filters | No | |
| Companies / Branches / Customers / Sales Agents | The four filter sets | Yes, in effect | Copied onto every run the event creates |
| Description, Status | No |
Lifecycle and effects
Posting proof
| Server document type | None of its own. bl_fi_slsodr_to_slsinv_consol_event_hdr, _run_hdr, _run_line, _run_hdr_slsinv_link and _template_hdr are not financial documents |
| Amount signum / quantity signum | Not applicable. The consolidation tables have no signums; the document it creates is an ordinary INTERNAL_SALES_INVOICE and carries that document type’s signums |
| Dr/Cr equation | None here. The consolidated invoice posts exactly as any internal sales invoice does, through its own DCO and JournalPostingService |
| GL precedence | Not applicable here; inherited from the sales invoice |
| Stock processor | None here; inherited from the sales invoice |
| What VOID reverses | There is no void on a run. Reversing means voiding the consolidated sales invoice in its own applet |
| What it does write | Run lines, one per eligible sales-order line; one consolidated sales invoice per group; bl_fi_generic_doc_link rows tying the orders to the invoice; and a permanent delete of the matching bl_fi_generic_doc_line_open_queue rows |
The pipeline
flowchart TD
T["Template<br/>saved filter set"] -.seeds.-> E
E["Event<br/>date + optional RRULE + filters"] -->|EVENT_HDR_PROCESSOR<br/>date_txn <= today| R["Run header<br/>filters copied from the event"]
RC["Run created directly<br/>in the applet"] --> R
R -->|RUN_HDR_PROCESSOR| RL["Run lines<br/>one per eligible FINAL sales-order line"]
RL -->|RUN_LINE_PROCESSOR| G["Group by company + branch<br/>+ customer + sales agent"]
G --> SI["One INTERNAL_SALES_INVOICE per group"]
SI --> L["bl_fi_generic_doc_link<br/>sales order to sales invoice"]
SI --> Q["Open-queue rows for SO to SI<br/>permanently deleted"]
Event → Run. SALES_ORDER_TO_SALES_INVOICE_CONSOLIDATION_EVENT_HDR_PROCESSOR picks up every
event whose date_txn is at or before now and whose processor_posting_run_hdr_guid is still
null, creates a run from it — copying the four filter JSON columns, the dates, the name and the
template link — stamps the run’s GUID back onto the event, and immediately enqueues the run
processor. An event that already has a run GUID is never processed again.
Run → Run lines. …_RUN_HDR_PROCESSOR picks up runs whose processor_posting_status is null.
It requires all four filter sets to be non-empty and fails the run with a specific message if any is
missing. It then loops company × branch, counting eligible generic-document lines — header type
INTERNAL_SALES_ORDER, header posting status FINAL, document entity in the customer set, sales
entity in the salesman set — and creates run lines in pages of
sales_order_get_limit_rows_to_process. If a previous attempt created some but not all of the
run lines for a company/branch pair, it deletes them and starts that pair again.
Run lines → Invoice. …_RUN_LINE_PROCESSOR takes the distinct (company, branch, customer,
sales agent) tuples among the selected run lines and, for each, reads back exactly the selected
order lines and hands them to SalesOrderToSalesInvoiceConverter.mapSalesInvoiceGenDocContainer.
That produces one INTERNAL_SALES_INVOICE per tuple, created through the ordinary
GenericDocumentService.createGenericDoc with the internal-sales-invoices DCO, then immediately
re-read and updated so the running number and derived fields are populated. The run lines are then
stamped with the invoice link and a shared consol_slsinv_group_guid, and the sales-order-to-sales-
invoice open-queue rows are permanently deleted so the orders no longer offer themselves for
conversion.
Run-line status values
bl_fi_slsodr_to_slsinv_consol_run_line.processor_posting_status is a free-text column, not an enum.
The values the processor writes are:
| Value | Meaning |
|---|---|
POSTED | The whole chain succeeded and at least one open-queue row was found and deleted |
FAILED_TO_CREATE_CONSOLIDATED_SALES_INVOICE | createGenericDoc rejected the consolidated invoice; the message column carries the reason |
FAILED_TO_UPDATE_CONSOLIDATED_SALES_INVOICE | The invoice was created but the follow-up update failed |
FAILED_TO_UPDATE_SALES_INVOICE_DATA_IN_RUN_LINE | The invoice is correct; writing its link back onto the run lines failed |
FAILED_TO_DELETE_GENDOC_LINE_OPEN_QUEUE | Everything succeeded except clearing the open queue |
| (unchanged) | No open-queue rows were found. The POSTED write sits inside the branch that only runs when the open-queue list is non-empty, so a successful consolidation of orders with no open-queue rows leaves the run line’s status untouched |
Related applets
- Sales Order (Internal) — the source documents, and where the sales agent that forms part of the grouping key is set.
- Sales Invoice (Internal) — what the run produces; everything about posting, tax and stock is decided there.
- Job Sheet (Internal) — the sibling consolidation path in the same backend subsystem.
- Scheduler — the queue machinery that drives the three processors.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Every run ends FAILED with “There is no Job Sheet under company: … branch: … customer: … salesman: …”, even when run lines and invoices were created | A copy-paste defect. SalesOrderToSalesInvoiceConsolidationRunHdrProcessorService declares total_jobsht_count and never adds to it; the Job Sheet class it was copied from does so at its line 81. The zero check then fires unconditionally, sets FAILED and throws | Check the run lines and the Sales Invoices tab rather than the run’s status. A fix in the platform is needed — see the product findings register |
A run marked FAILED is never retried | The run processor selects only runs whose processor_posting_status is null. Once anything has been written there, including FAILED, that run is permanently skipped | Create a new run |
| The run fails immediately with “There is no company/branch/customer/salesman guid to process” | One of the four filter sets is empty. All four are mandatory — there is no “all customers” option | Populate all four tabs before running |
| A finalised sales order is never picked up | The query requires header type INTERNAL_SALES_ORDER and header posting status FINAL, the document entity in the customer set and the header sales entity in the salesman set. An order with no sales agent matches nothing | Set the sales agent on the order header, or widen the salesman filter |
| Orders from several customers end up on one invoice, or one customer’s orders are split across several | The grouping key is the full tuple (company, branch, customer, sales agent) — not the customer alone. Two sales agents on the same customer’s orders produce two invoices; two branches produce two more | Expected. Narrow the run if you need one invoice per customer |
| Run lines stay blank in the status column after an apparently successful run | POSTED is written only inside the branch that runs when open-queue rows were found. Where the company has no knock-off flow configured for sales order → sales invoice, there are none, and the status is left as it was | Check the Sales Invoices tab; see the company’s Knock Off Configuration |
| Re-running a partially failed run deletes run lines belonging to another run | The recovery DELETE FROM bl_fi_slsodr_to_slsinv_consol_run_line WHERE guid_comp = … AND guid_branch = … filters on company and branch only, with no run-header predicate | Do not run two overlapping runs for the same company and branch concurrently |
| The consolidated invoice exists but the sales orders still offer themselves for conversion elsewhere | Open-queue deletion runs in a CompletableFuture after the invoice is committed; if it fails the run line records FAILED_TO_DELETE_GENDOC_LINE_OPEN_QUEUE | Read the message column on the run line |
| Settings screens do nothing | None of the three works; see Applet settings | — |
Related documentation
- Sales & POS module — where this sits in the product.