ERPNext is the only system of record; we build tools on it
Deploy test site / deploy (push) Successful in 46s

User (October 2026): we only build tools and interfaces that interact
with ERPNext; invoices, stock and the rest are kept there, except
telemetry and other machine data.

digital-factory.md: standing decision, decisions 2, 3, 6, 7 and 10
rewritten (ERPNext holds every record of the business and of production,
including artist gates, metal-part genealogy as serial numbers with
parents, and quality inspections; the machine-data store keeps only
cycle and record rows, the hash chain and time series, keyed to ERPNext
names; ERPNext first, then custom fields or document types in cnx_erp,
then a tool, never a second business database). Systems table,
architecture labels, factory application section (modules as a table:
records in ERPNext, what we build, machine data outside; our only tables
are the machine-data ones), ownership table, integration rules 4 and 8,
phasing and estimates (cnx_erp app 2 to 3 person-months; total 3.5 to 6).
artist-platform.md: campaigns are an ERPNext document from launch, pledges
post as advance payment entries; traceability, README, facility security
(archive location on the serial number), production timeline (sets per
mother on the mother's serial number), crane-workshop spares store,
total-investment.

Factory console: KPI row no longer touches the postings card (user
screenshot); ERPNext page, order card, sidenote and ledger say every
record lives in ERPNext and only machine data stays outside; new
postings for artist gates, stamper metrology inspections, stamper set
serial numbers (grown, retired with play count). Checked headless:
Edge dark and light, 1440/1024/390, no overflow or page errors, rows
aligned, pw_fc, pw_wo, pw_feed and pw_erp pass.
This commit is contained in:
grabowski committed 2026-10-16 12:14:52 +07:00
1 parent a101c9f7db
commit 5c303dfc61
9 files changed
+188 -158

No files matched your search

+4 -3
View File
@@ -149,9 +149,10 @@ The detail lives in dedicated documents:
shipment, and the requirements this sets for the factory software
- [Digital factory systems plan](docs/digital-factory.md): every system
that has to exist for environment, machine, QC and genealogy data to land
in one database keyed to one record identity, with ERPNext as the
plant's ERP that every tool we build integrates with: the plant as one
more site in B4L's existing NixOS fleet (the cnx-network clan: mesh, DNS, fleet
in one place keyed to one record identity, with ERPNext as the plant's
only system of record (every tool we build reads and writes it; only
machine data stays outside): the plant as one more site in B4L's
existing NixOS fleet (the cnx-network clan: mesh, DNS, fleet
monitoring, site gateway module, public proxy), what changes in that
repo, VLAN zoning, Telegraf/MQTT acquisition source by source, QC
instrument integration, the factory application, alerting, backup,
+14 -10
View File
@@ -348,8 +348,8 @@ approves the test pressing; a photo or short video of it to the fans is
cheap marketing), the run, packaging, and **dropshipping one parcel per
fan** with tracking, which is fulfillment step 9 as already planned.
Fans follow it on the campaign page, fed from the factory application's
event log: the progress is the plant's own record, not a status someone
Fans follow it on the campaign page, fed from the order's records in
ERPNext and the plant's event log: the progress is the plant's own record, not a status someone
remembers to update. If a stage slips, the estimated ship month moves
and every fan is told.
@@ -421,20 +421,24 @@ No SaaS beyond the payment provider (Opn/Omise, 2C2P or a bank gateway,
to be chosen), in line with the plan's standing rule that every plant
service runs on the NixOS fleet.
**ERPNext** (decided October 2026: the plant runs ERPNext, and every tool
we build interacts with it; [digital factory](digital-factory.md#erpnext-the-plants-erp)).
For the platform that means:
**ERPNext** (decided October 2026: the plant runs ERPNext as its only
system of record, and what we build are tools and interfaces on it;
[digital factory](digital-factory.md#erpnext-the-plants-erp)). The
platform keeps no business records of its own. For the platform that
means:
- the **price list** behind the calculator is an ERPNext price list
(the demos' `R` list is its prototype), synced out to the public
process, so the calculator and the invoice never disagree;
- an artist or label account is linked to one ERPNext **customer**; a
direct order becomes a **sales order** when it is confirmed;
- a **campaign** stays in the public process while it runs; when it
succeeds it becomes a sales order, the pledges become **payment
entries** against it in the ring-fenced account, and fan shipments
become delivery notes; the refunds of a missed campaign are posted the
same way;
- a **campaign** is a custom Campaign document in ERPNext from launch;
each pledge is posted as an advance **payment entry** into the
ring-fenced account as it arrives, with the fan as a customer; when the
campaign succeeds it becomes a sales order the advances are allocated
to, and fan shipments become delivery notes; the refunds of a missed
campaign are posted as payment entries too. The public process holds
sessions and a cache of what it shows, nothing else;
- every posting goes through the plant's integration layer and its
outbox over the mesh; the public side never talks to ERPNext directly.
+1 -1
View File
@@ -183,7 +183,7 @@ the press hall.
|---|---|---|
| Service bay | Clear crane-served floor for one press-sized machine with about 1.5 m working space all round (a Newbilt press stands on under 1 m², an AD12 on about 3.25 m²); bunded drip-tray floor; 400 V three-phase and 230 V outlets; compressed air drop; task lighting | ~60 m² |
| Bench area | Workbenches and vices, pillar drill, hydraulic hose crimper (the press hydraulics run at 180 to 200 bar and hoses are a routine spare), closed-loop parts washer, mould and platen bench, optionally a small lathe and mill for brackets and one-off parts | ~40 m² |
| Spares store | Lockable, shelved; every part tracked in the factory application (stock, minimum level, which machine it fits) | ~20 – 30 m² |
| Spares store | Lockable, shelved; every part tracked in ERPNext (stock, reorder level, which machine it fits) | ~20 – 30 m² |
| Hot-work corner | Welding screen and fume extractor; hot-work permit, because paper, cartons and hydraulic oil share the building | ~10 m² |
| **Total** | | **~100 – 150 m²** |
+144 -126
View File
@@ -21,13 +21,19 @@ Nothing is built twice. Vendor instrument PCs (Keyence, vision controllers,
PLC HMIs) are appliances, not servers: they export into the NixOS estate
and never host a service of ours.
**Standing decision (October 2026): the whole plant runs ERPNext**,
self-hosted on a NixOS machine in the same clan. It is the master for
customers, prices, orders, money, items, stock and assets, and every tool
this plan builds (the factory application with its stations and customer
portal, the artist platform, the instrument integrations) reads from and
writes to it through one integration layer; none keeps a second master of
what ERPNext owns ([ERPNext](#erpnext-the-plants-erp)).
**Standing decision (October 2026): the whole plant runs ERPNext, and
it is the only system of record.** Self-hosted on a NixOS machine in the
same clan, it keeps every record of the business and of production:
customers, prices, orders and the artist gates, invoices and payments,
items, BOMs, stock and batches, work orders and job cards, quality
inspections, metal parts and their genealogy, deliveries, assets and the
accounts. What we build are **tools and interfaces that read and write
ERPNext** (station kiosks, the customer portal, the artist platform,
instrument adapters, reports); none keeps business records of its own.
The one exception is machine data: time series, per-cycle and per-record
rows, instrument raw files and images, audio captures. That stays in the
machine-data store on the plant core, keyed to ERPNext's document names
([ERPNext](#erpnext-the-plants-erp)).
## What the fleet already provides
@@ -66,26 +72,32 @@ TODO list or small extensions to existing modules):
already there. A failed box is replaced from a fresh disk with
`clan machines install`; a change is `clan machines update`. Nothing is
configured by hand on a running machine.
2. **Two stores, by shape of the data.** Everything with an identity
(orders, gates, genealogy, lots, press runs, per-cycle rows, QC
verdicts, events) lives in PostgreSQL on the plant core. Everything
that is a time series (1 Hz machine tags, zone environment, energy,
water) lives in a plant-local VictoriaMetrics, the same engine and
modules the fleet already runs on `control`, so dashboards, PromQL and
vmalert rules carry over unchanged. Per-cycle rows carry a snapshot of
2. **ERPNext for records, a machine-data store for machine data.**
Every record of the business and of production (orders, gates,
genealogy, lots, press runs, inspections, deliveries, money) lives in
ERPNext (decision 10). What machines produce at machine speed lives
in the machine-data store on the plant core, in two parts. Rows with
an identity that are too many for an ERP (press cycles keyed by press
and counter, per-record optical results, audio sample scores,
per-unit packaging scans, the hash-chained event log) go to
PostgreSQL, each carrying the ERPNext job card, batch and serial
numbers it belongs to. Time series (1 Hz machine tags, zone
environment, energy, water) go to a plant-local VictoriaMetrics, the
same engine and modules the fleet already runs on `control`, so
dashboards, PromQL and vmalert rules carry over unchanged. Per-cycle rows carry a snapshot of
the zone environment and the key press values, so the relational join
"this record, that cycle, those conditions" never needs the time-series
store; the curve behind the snapshot is one PromQL range query away.
(TimescaleDB in PostgreSQL is the fallback if that split proves
awkward; it is not the starting point.) Business documents (sales
orders, stock, invoices) are ERPNext's, decision 10.
awkward; it is not the starting point.)
3. **MQTT is the plant bus, Telegraf is the universal collector.** Every
machine, meter and sensor is read by a Telegraf instance on the edge
collector nearest it (Modbus, OPC UA, Siemens S7 and MQTT inputs are
stock plugins) and published to Mosquitto on the core. On the core,
Telegraf writes the stream to VictoriaMetrics (InfluxDB line protocol,
native to VM) and the factory application subscribes to the event
topics (cycle complete, alarm, instrument result) and writes rows.
topics (cycle complete, alarm, instrument result), writes rows to the
machine-data store and posts the totals and verdicts to ERPNext.
Custom code exists only where no plugin does (vision results,
instrument CSV ingestion, the audio QC pipeline).
4. **Read-only toward machines.** The data platform never writes to a PLC
@@ -96,14 +108,18 @@ TODO list or small extensions to existing modules):
machine's own cycle counter; every instrument result carries the part
serial it was read from. Wall-clock time is recorded (UTC, chrony from
the fleet baseline) but is never what two systems are joined on.
6. **Append-only, hash-chained.** Process events, gate approvals and QC
verdicts are written once and never updated; corrections are new events
that reference the old. Each event carries the hash of the previous one
so the log's integrity can be checked, which is what lets the customer
portal be trusted.
7. **The customer portal is a view, not a copy.** It reads the same
database through the same application with a customer role; there is
no exported report to keep in sync.
6. **Append-only, hash-chained.** Machine events are written once and
never updated; corrections are new events that reference the old. Each
event carries the hash of the previous one so the log's integrity can
be checked, and records the ERPNext document it led to. In ERPNext,
gate approvals, quality inspections, job cards and stock entries are
submitted documents: a correction is a cancel and amend that keeps
both, with ERPNext's version history. Together that is what lets the
customer portal be trusted.
7. **The customer portal is a view, not a copy.** It reads the
customer's records in ERPNext and the machine data of the same orders
through one portal tool scoped to the ERPNext customer; there is no
exported report to keep in sync.
8. **The plant runs without the internet.** Everything that pressing,
QC and traceability need is on site. A WAN outage stops only the
customer portal, off-site backup, fleet monitoring and remote support,
@@ -116,28 +132,33 @@ TODO list or small extensions to existing modules):
live feedback loops that the staff use to improve production, rather
than only for management reporting, decide whether a plant succeeds
([exhibitor briefing](suppliers/making-vinyl-europe-2026-exhibitors.md#after-the-show)).
10. **ERPNext owns the business, the factory application owns the floor.**
ERPNext holds every business document (customer, sales order, work
order, job card, batch, quality inspection, stock entry, delivery
note, invoice, asset); the factory application holds what happens at
machine speed (cycles, per-record inspection, metrology, genealogy,
gates, the hash-chained log) and posts results to ERPNext in
business-sized pieces. ERPNext document names are the keys both sides
share ([ERPNext](#erpnext-the-plants-erp)).
10. **ERPNext is the system of record; we build tools and interfaces.**
(October 2026.) Invoices, stock and every other record of the
business and of production live in ERPNext: customer, sales order,
artist gate, work order, job card, batch, quality inspection, serial
number per metal part with its parents, stock entry, delivery note,
invoice, asset. What we build reads and writes those records; the only
data kept outside is machine data (decision 2), which ERPNext receives
as totals and verdicts per job card, run, batch and shipment. A new
need is met with a standard ERPNext document type first, then with
custom fields or a custom document type in our `cnx_erp` app, and only
then with a tool of ours that still keeps its records in ERPNext;
never with a second database of business records. ERPNext document
names are the keys everything shares ([ERPNext](#erpnext-the-plants-erp)).
## The systems on one page
| System | What it does | Runs on | Phase |
|---|---|---|---|
| Site gateway | PPPoE WAN, VLAN zoning, DHCP/DNS, NTP for the site, internal TLS proxy, CrowdSec | `gw-<site>-1` (existing `cnx.router` module) | Build |
| Plant core | PostgreSQL, Mosquitto, Telegraf persister, VictoriaMetrics + vmalert + Grafana, Alertmanager, factory application, file datasets on ZFS | `vinyl-core` | Build |
| Factory application | Orders, gates, genealogy, materials, press runs, QC verdicts, packaging, fulfillment, customer portal, audit log; the ERPNext integration layer | `vinyl-core` | Build (v1), Launch (v2) |
| ERPNext | Accounting with Thai VAT and withholding tax, customers, prices, sales and purchase orders, items, BOMs, work orders and job cards, stock and batches, incoming quality inspections, assets and maintenance, payroll | `vinyl-erp` | from the first capex invoice; manufacturing at commissioning |
| Plant core | Machine-data store (PostgreSQL for cycle and record rows, VictoriaMetrics + vmalert + Grafana for time series), Mosquitto, Telegraf persister, Alertmanager, factory application, file datasets on ZFS | `vinyl-core` | Build |
| ERPNext | The system of record: accounting with Thai VAT and withholding tax, customers, prices, sales and purchase orders, artist gates, items, BOMs, work orders and job cards, stock and batches, quality inspections, metal parts and their genealogy, deliveries, assets and maintenance, payroll | `vinyl-erp` | from the first capex invoice; production records at commissioning |
| Factory application | The tools and interfaces we build on ERPNext: station kiosks working the job cards, customer portal, machine-data service (cycles and per-record results in, totals and verdicts to ERPNext), instrument adapters, reports; the ERPNext integration layer. No business records of its own | `vinyl-core` | Build (v1), Launch (v2) |
| Edge collectors | Telegraf per machine zone, disk-backed store-and-forward, NTP to the OT segment, vision result receiver | `vinyl-edge-press`, `vinyl-edge-util`, `vinyl-edge-galv` | Build, Expand |
| Environmental monitoring | Zone temperature, humidity, PM, differential pressure; alarms; correlation with production | RS485 loops into the edge collectors | Build |
| Machine telemetry | Presses, extruders, hydraulics, boiler, chiller, compressor, heat pump, HVAC, PV, BESS, energy and water meters | edge collectors | Build |
| QC instrument integration | Optical inspection results and images, metrology CSV ingestion, PVC lab entries, audio QC scores, packaging scans | stations plus `vinyl-edge-press` | Launch, Expand |
| Station kiosks | Badge check-in and scanning at each manual step; kiosk browser on the factory application | `vinyl-st-*` | Build |
| Station kiosks | Badge check-in and scanning at each manual step against the station's ERPNext job card; kiosk browser on the factory application | `vinyl-st-*` | Build |
| Process alerting | Environment, utilities, reject rate, stamper life, PV/BESS, fire and BESS detection contacts, UPS, data silence; delivered to phones | `vinyl-core` (vmalert + Alertmanager) | Build |
| Fleet monitoring | Host metrics, journals, host alerts for every plant machine | `control` (existing) | Build |
| Backup and DR | ZFS snapshots and replication, Borg to the plant backup server and off site, PostgreSQL point-in-time recovery, restore drills | `vinyl-backup`, off-site destination | Build |
@@ -169,8 +190,8 @@ flowchart LR
subgraph L2[Plant core, NixOS, servers VLAN]
MQ[Mosquitto]
VM[(VictoriaMetrics<br/>vmalert, Grafana)]
DB[(PostgreSQL)]
APP[Factory application]
DB[(PostgreSQL<br/>machine data)]
APP[Factory application<br/>tools on ERPNext]
AM[Alertmanager]
ERP[(ERPNext<br/>vinyl-erp)]
end
@@ -507,12 +528,17 @@ same watcher pattern as the metrology exports below. See
## The factory application
Working name `cnx-factory`, its own repository. Server-rendered web
application in Rust (axum, SQLx, minijinja templates, HTMX) on PostgreSQL,
the stack B4L already uses for its other in-house tooling, with the same
append-only ledger and hash-chained audit log pattern. It subscribes to
the MQTT event topics (cycle complete, machine alarm, instrument result)
and owns every write to the identity tables; Telegraf owns the writes to
Working name `cnx-factory`, its own repository: **the tools and
interfaces we build on ERPNext**, not a second system of record.
Server-rendered web application in Rust (axum, SQLx, minijinja templates,
HTMX), the stack B4L already uses for its other in-house tooling. Every
business and production record it shows or changes is read from and
written to ERPNext through the integration layer
([ERPNext](#erpnext-the-plants-erp)). The only data it owns is the
machine-data store: it subscribes to the MQTT event topics (cycle
complete, machine alarm, instrument result), writes cycle and record rows
to PostgreSQL with the append-only, hash-chained event log, and posts
totals and verdicts to ERPNext; Telegraf owns the writes to
VictoriaMetrics. Packaged as a flake so the clan repo pins it as an input.
**Demo.** [Factory console and order status](factory-console.html) (test
@@ -530,91 +556,75 @@ gates, copies pressed so far, the lab certificate of the compound lot,
listening checks). All orders, values and IDs are simulated; limits are
illustrative until the acceptance specifications are written.
Modules, in build order:
Modules, in build order. Each keeps its records in ERPNext; the last
column is the machine data it keeps outside.
1. **Orders and releases**: customer, order, release, sides, formats, run
quantity, bill of materials (record, inner sleeve, jacket, inserts,
shrink), due dates.
2. **Audio intake and the client vault**: upload via portal or SFTP with
checksums, versions, handler, technical QA form; files in an encrypted,
access-logged dataset separate from everything else.
3. **The three artist gates**: master plan, master, test pressing; each is
an event with who approved, when, from which portal session or on whose
verbal instruction, and nothing downstream is schedulable without it.
4. **Mastering and cutting sessions**: engineer, chain settings, lacquer
IDs, dead-wax scribe text and fiducial applied, microscope check result.
5. **Metal genealogy**: lacquer to father to mother to stamper, or bridged
supplier plus batch; per-part metrology history; stamper lifecycle
(mounted on which press, play count, retired why), archive location
(box and shelf position in the mobile shelving,
[facility security](facility-security.md#archive-room-mobile-shelving)),
stamper sets grown per mother.
6. **Materials**: PVC lots with the lab gate and CoA, regrind batches,
label lots with print inspection and oven pre-dry, carton and sleeve
lots with incoming QC.
7. **Press runs and cycles**: a run binds order, stamper pair, press,
operator, shift, PVC lot in the hopper, regrind batch, label lot; each
cycle event from the PLC becomes a row keyed by press + counter with
the key press values and the zone environment snapshot; the record key
is derived from it per side.
8. **QC verdicts**: optical per record, audio per sampled record, lab per
lot, packaging per unit; reject reasons; test-pressing hold state.
9. **Finishing, packaging, fulfillment**: assembly checks, consignment
against the BOM, carrier and tracking per shipment or dropship end
customer.
10. **Customer portal**: per-order timeline of everything above, scoped so
one customer sees only its own orders; PDF export of the trace. The
public front of order intake (price calculator, uploads with
automatic checks, fan-funded campaigns) is planned in
[artist platform](artist-platform.md), including why its public
side should not depend on the plant's WAN link.
11. **Reports and signals**: yield per stamper, reject rate drift, audio
score trend versus play count (the automatic retirement signal),
energy per record, environment excursions overlaid on runs (the
curves fetched from VictoriaMetrics for the run's window).
| # | Module | Records in ERPNext | What we build | Machine data, outside ERPNext |
|---|---|---|---|---|
| 1 | Orders and releases | Customer, Sales Order, an Item and BOM per release and format (record, inner sleeve, jacket, inserts, shrink), custom Release and Side documents, due dates | order intake on the artist platform, order views | none |
| 2 | Audio intake and the client vault | custom Audio Asset document per file version: checksum, handler, technical QA form | upload via portal or SFTP with automatic checks | the audio files, in an encrypted, access-logged dataset separate from everything else |
| 3 | The three artist gates | custom Artist Gate document per gate (master plan, master, test pressing), submitted on approval: who, when, from which portal session or on whose verbal instruction; the next job card cannot start without it | approve and report-a-problem in the portal | none |
| 4 | Mastering and cutting sessions | Job Card with custom fields: engineer, chain settings, lacquer serial numbers, dead-wax scribe text and fiducial, microscope check | mastering and cutting-room kiosks | lathe logs, microscope images |
| 5 | Metal genealogy | Serial No per lacquer, father, mother and stamper with its parent (or the bridged supplier and batch), its warehouse (archive with box and shelf position in the mobile shelving, [facility security](facility-security.md#archive-room-mobile-shelving); rack; press), play count and retirement reason, stamper sets grown per mother; a Quality Inspection per measured part | plating, stamper-prep and archive scanning | Keyence and XRF raw files |
| 6 | Materials | Batch per PVC lot, regrind batch, label lot, carton and sleeve lot; Purchase Receipt; Quality Inspection for the lab gate (CoA attached), label print inspection and incoming QC; Stock Entry into the hopper | lab station, goods-in scanning | instrument raw files |
| 7 | Press runs | Job Card per run: press (workstation), stamper serial numbers, operator, shift, batches consumed, good and rejected quantities; Downtime Entry per stop; Stock Entry (manufacture) at the end | press-side kiosk, the machine-data service | every cycle as a row keyed by press + counter with the key press values and the zone environment snapshot; the record key is derived from it per side |
| 8 | QC verdicts | Quality Inspection per run, batch and test pressing with reject reasons; flagged listening checks with the engineer's verdict; the test-pressing hold | QC views, listening station | optical result and image per record, audio captures and scores per sampled record, packaging scan per unit |
| 9 | Finishing, packaging, fulfillment | packing Job Card, Delivery Note, Shipment with carrier and tracking per consignment or dropshipped end customer, Sales Invoice | packing and dispatch kiosks | per-unit assembly scans (dead-wax code against jacket barcode) |
| 10 | Customer portal | reads the customer's records above, scoped to the ERPNext customer; PDF export of the trace | the portal | reads the machine data of the customer's own orders |
| 11 | Reports and signals | ERPNext reports for money, stock and work orders | yield per stamper, reject-rate drift, audio score versus play count (the automatic retirement signal), energy per record, environment excursions over runs | the curves from VictoriaMetrics for the run's window |
Core entities: `customer`, `order`, `release`, `side`, `audio_asset`,
`gate_approval`, `session`, `lacquer`, `father`, `mother`, `stamper`,
`stamper_measurement`, `press`, `press_run`, `press_cycle`, `record`,
`optical_result`, `audio_result`, `pvc_lot`, `regrind_batch`, `label_lot`,
`carton_lot`, `lab_result`, `packaging_check`, `shipment`, `operator`,
`shift`, `event` (the hash chain).
The public front of order intake (price calculator, uploads with
automatic checks, fan-funded campaigns) is planned in
[artist platform](artist-platform.md), including why its public side
should not depend on the plant's WAN link.
Where a module touches a business document, the document is ERPNext's
and the module holds its production side: the order is an ERPNext sales
order, a run is a work order, each station step is a job card, a PVC lot
is a batch with its quality inspection, a shipment is a delivery note
([ERPNext](#erpnext-the-plants-erp)).
The machine-data store has the only tables we own: `press_cycle`,
`record`, `optical_result`, `audio_result`, `packaging_scan`,
`instrument_file`, `event` (the hash chain), each carrying the ERPNext
names it belongs to (job card, batch, serial number). In ERPNext the
`cnx_erp` app adds the custom document types Release, Side, Audio Asset
and Artist Gate, and custom fields on Job Card, Serial No, Batch and
Quality Inspection; everything else is a standard ERPNext document type.
Stations use the same application in kiosk mode: an operator taps a badge
(the same card the access-control system issues, so one ID) and scans
whatever the step asks for. No forms longer than one screen.
whatever the step asks for; each confirmation is a time log or a
completed quantity on the station's ERPNext job card. No forms longer
than one screen. The office works in ERPNext's own desk (purchasing,
accounts, stock counts, planning); we build an interface only where the
desk does not fit the work.
## ERPNext: the plant's ERP
Decided October 2026: **the whole plant runs ERPNext**, the open-source ERP
on the Frappe framework (GPLv3, no licence fee; version 15 is the current
stable line). It closes the item this plan had left for after launch, and
it sets a rule for everything we build: **every tool has to interact with
ERPNext**, through the integration described here.
it sets the rule for everything we build: **ERPNext is the system of
record, and what we build are tools and interfaces that interact with
it**, through the integration described here. Invoices, stock and every
other record of the business and of production are kept in ERPNext; only
machine data (telemetry and the other machine-speed data of decision 2)
is kept outside it.
### Who owns what
| Object | Master | The other side keeps |
|---|---|---|
| Customers, contacts, addresses | ERPNext | the factory application scopes the portal by the ERPNext customer; the artist platform links each account to one |
| Price list, item prices, pricing rules | ERPNext | the artist-platform calculator reads them; the demos' price list becomes an ERPNext price list |
| Quotations, sales orders, invoices, payments (fan pre-orders in the ring-fenced account included) | ERPNext | the artist platform creates them; the factory application reads the order |
| Items and BOMs: one item per release and format (record, sleeves, jacket, labels, compound per copy), raw materials and packaging | ERPNext | the factory application reads the BOM of the run |
| Work order per press run; job cards per operation (cutting, plating, stamper prep, test pressings, pressing, packing); workstations (presses, lathe, plating cells, Sibert line, packing) | ERPNext | the stations' work orders are these job cards: kiosk scans and times post back as time logs and completed quantity |
| Stock: compound lots as batches, label and carton lots, finished goods, warehouses including the archive | ERPNext | the factory application knows which lot sat in which hopper for every cycle and posts the consumption |
| Incoming lab gate per compound lot | Quality Inspection in ERPNext, with a template carrying the parameters and limits of [quality automation](quality-automation.md) | instrument files and the CoA scan in the factory application |
| Metal parts: lacquers, fathers, mothers, stampers | serial numbers in ERPNext for location and value (warehouse: archive, rack, press) | genealogy, metrology, play counts and stamper sets per mother in the factory application |
| Gates, per-cycle rows, per-record optical results, audio scores, telemetry, environment, the hash-chained event log, client audio | factory application and VictoriaMetrics only: thousands of rows a day are not ERP documents | ERPNext gets the totals: good, rejected and scrap to regrind per job card and run |
| Stops | Downtime Entry in ERPNext per stop (workstation, reason, from, to) | the PLC detail |
| Deliveries | delivery notes in ERPNext | carrier and tracking per consignment on both sides |
| Assets: presses, lathe, galvanics, Sibert line, utilities, PV and BESS, IT | ERPNext asset register, depreciation, maintenance schedules | telemetry and run hours |
| Purchasing, suppliers, accounting, Thai VAT and withholding tax, payroll | ERPNext, with the HR app for payroll | nothing |
| Object | In ERPNext | Outside ERPNext (machine data) | Our tools |
|---|---|---|---|
| Customers, contacts, addresses (fans who buy included) | Customer, Contact, Address | none | the portal is scoped to the ERPNext customer; the artist platform links each account to one |
| Price list, item prices, pricing rules | Price List, Item Price, Pricing Rule | none | the artist-platform calculator reads them; the demos' price list becomes an ERPNext price list |
| Quotations, sales orders, invoices, payments (fan pledges in the ring-fenced account included) | Quotation, Sales Order, Sales Invoice, Payment Entry | none | the artist platform creates them |
| Releases, audio versions, the three artist gates | custom Release, Side, Audio Asset and Artist Gate documents | the audio files, in the client vault | portal upload and approval |
| Items and BOMs: one item per release and format (record, sleeves, jacket, labels, compound per copy), raw materials, packaging, spares | Item, BOM | none | none |
| Work orders, job cards per operation (mastering, cutting, plating, stamper prep, test pressings, pressing, packing), workstations | Work Order, Job Card with time logs and quantities, Operation, Workstation | none | the station kiosks work the job cards |
| Stock: compound lots, regrind, label and carton lots, finished goods, spares, warehouses including the archive | Batch, Purchase Receipt, Stock Entry, Warehouse | which lot sat in which hopper for each cycle | lab and goods-in stations, the machine-data service posts consumption |
| Lab gates and inspections | Quality Inspection per batch, run, stamper and test pressing, with templates carrying the parameters and limits of [quality automation](quality-automation.md); CoA attached | instrument raw files and images | instrument adapters create the inspections |
| Metal parts: lacquers, fathers, mothers, stampers | Serial No with its parent, location (archive box and shelf, rack, press), play count, retirement, sets per mother | metrology raw files | plating, prep and archive kiosks |
| Press runs and stops | Job Card per run (good, rejected, scrap to regrind), Downtime Entry per stop (workstation, reason, from, to) | per-cycle rows, PLC telemetry | the machine-data service posts the totals |
| Per-record inspection, audio samples, packaging scans | totals and verdicts per run, the engineer's verdict on each flagged sample | every record's optical result and image, audio captures and scores, per-unit scans | QC views |
| Environment, energy, utilities | energy per record in reports, from totals | VictoriaMetrics time series | Grafana, alerting |
| Deliveries | Delivery Note, Shipment (carrier, tracking) | none | dispatch kiosk |
| Assets: presses, lathe, galvanics, Sibert line, utilities, PV and BESS, IT | Asset, depreciation, Asset Maintenance, maintenance schedules | run hours, alarms | none |
| Purchasing, suppliers, accounting, Thai VAT and withholding tax, payroll | ERPNext, with the HR app for payroll | none | none |
### How every tool interacts with it
@@ -634,9 +644,10 @@ ERPNext**, through the integration described here.
3. **Business-sized postings.** Per job card, per run, per lot, per
stop, per shift. Never per record or per cycle.
4. **ERPNext names are the shared keys** (sales order, work order, job
card, batch, serial number). The factory application keeps its own IDs
for records, cycles and parts and stores the ERPNext name beside them;
the record key on the sleeve never changes.
card, batch, serial number). The machine-data store has keys of its
own only for cycles and records (press + counter, and the record key
on the sleeve, which never changes) and stores the ERPNext names
beside them.
5. **Customisation as code.** Custom fields, document types, print
formats and webhooks ship as one Frappe app in our Gitea (working name
`cnx_erp`), installed and upgraded by the deployment, never clicked
@@ -650,8 +661,10 @@ ERPNext**, through the integration described here.
same outbox. No ERPNext endpoint is published.
8. **New tools follow the same rule.** A dashboard, a script or a kiosk
app built later uses the same client and an integration user of its
own. A tool that cannot read from or write to ERPNext this way is not
finished.
own, and keeps its records in ERPNext. A tool that cannot read from or
write to ERPNext this way is not finished; a tool with its own
database of business records is not allowed. Machine data is the only
exception.
### Hosting on NixOS
@@ -707,16 +720,21 @@ ERPNext**, through the integration described here.
build's cost entries in proplan (project CNX-01) are the source for
capitalising the plant at handover.
- **Commissioning and launch:** items, BOMs, workstations, inspection
templates, work orders and job cards; stations post job cards.
- **Expand:** serial numbers for in-house metal parts; payroll if it did
not start earlier.
templates, releases and artist gates, work orders and job cards, serial
numbers for every metal part (bought stampers as well as our own);
stations work the job cards.
- **Expand:** payroll if it did not start earlier.
Estimates: no licence; `vinyl-erp` 2,000 to 4,000 USD of hardware (none
if it runs as a VM); ERPNext configuration (chart of accounts, Thai tax,
items, BOMs, workstations, inspection templates, print formats) 1 to 2
person-months or a partner quote; the integration and the `cnx_erp` app
1.5 to 2.5 person-months; Nix packaging and the clan module 0.5 to 1
person-month.
(custom documents for releases, sides, audio assets and gates; custom
fields on job cards, serial numbers, batches and inspections) 2 to 3
person-months; Nix packaging and the clan module 0.5 to 1 person-month.
The factory application's own estimate shrinks by what ERPNext now holds
(orders, stock, genealogy, inspections, shipping); re-estimate both once
the dev ERPNext runs.
## Observability and alerting
@@ -917,7 +935,7 @@ options, which come back with the quotes. Fleet pieces already paid for
| off-site backup storage, per year | 100 to 500 |
| **Hardware total** | **~34,000 to 68,000** |
| `cnx-factory` to v1 (orders through shipments, stations, portal) | 4 to 6 person-months |
| ERPNext: configuration with Thai tax, the integration and the `cnx_erp` app, Nix packaging | 3 to 5.5 person-months, or a partner quote for the configuration |
| ERPNext: configuration with Thai tax, the integration and the `cnx_erp` app, Nix packaging | 3.5 to 6 person-months, or a partner quote for the configuration |
| Clan modules, router extensions, collectors, dashboards and rules to commissioning | 1 to 2 person-months |
| Vision, metrology and audio pipeline integration (Launch and Expand) | 3 to 5 person-months, plus integrator fees in the vision quote |
+2 -2
View File
@@ -121,8 +121,8 @@ would hold about 60 % of that.
sensor along the rails, lockable carriages behind the room's access
control.
- **Locations.** Every box carries a barcode and every position an
address (carriage, face, level, slot); the factory application records
each part's archive location there, so a repress order says which box
address (carriage, face, level, slot); each part's serial number in
ERPNext carries its archive location, so a repress order says which box
to pull.
**Open points.**
+14 -9
View File
@@ -164,6 +164,7 @@ document.documentElement.setAttribute("data-theme",t);})();
.er.hd { color: var(--faint); border-bottom: 1px solid var(--line); padding-top: 0; }
.er.hd > span { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--faint); }
.own td:first-child { font-weight: 650; white-space: nowrap; }
.kpis + .card { margin-top: 16px; }
.al { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); color: var(--ink); text-decoration: none; }
.al:last-child { border-bottom: 0; }
@@ -404,7 +405,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="fac" id="factory">
<aside class="side">
<nav id="fnav" aria-label="Factory console"></nav>
<div class="sidenote"><b>What this is.</b> The factory application from the digital factory plan (working name cnx-factory): every machine, instrument and operator check writes to one append-only log, and the customer portal is a scoped view of the same records.<br><br><b>Simulated:</b> all orders, values and IDs. <b>From the plan:</b> stages and the three artist gates, the inspection layers, Newbilt cycle times, two shifts 06:00 to 22:00. The demo runs one Duplex system and one Classic. Limits are illustrative: the acceptance specifications are still to be written.<br><br><b>ERPNext</b> runs the whole plant: orders, job cards, batches and invoices are posted there (see ERPNext).</div>
<div class="sidenote"><b>What this is.</b> The factory application from the digital factory plan (working name cnx-factory): tools and interfaces on ERPNext, which keeps every order, job card, batch, inspection, metal part and invoice. Machine data (cycles, per-record inspection, telemetry, audio) is the only thing kept outside it. The customer portal is a scoped view of the same records.<br><br><b>Simulated:</b> all orders, values and IDs. <b>From the plan:</b> stages and the three artist gates, the inspection layers, Newbilt cycle times, two shifts 06:00 to 22:00. The demo runs one Duplex system and one Classic. Limits are illustrative: the acceptance specifications are still to be written.</div>
</aside>
<main id="fmain"></main>
</div>
@@ -494,7 +495,7 @@ const pById=id=>S.presses.find(p=>p.id===id);
function log(t,k,txt,actor,oid){
S.ledN++;const prev=S.led.length?S.led[S.led.length-1].h:"0000000000000000";
const e={t,k,txt,actor,oid,p:prev};e.h=hx(prev+t+k+txt);S.led.push(e);
erpFrom(t,k,txt,oid);
erpFrom(t,k,txt,oid,actor);
if(oid&&S.byId[oid]){const o=S.byId[oid];o.ev.push(e);if(o.ev.length>60)o.ev.shift();}
if(!S.warm&&S.led.length>900)S.led.splice(0,S.led.length-800);
}
@@ -504,11 +505,11 @@ const jcName=key=>"PO-JOB"+p5(10000+fnv(key,2166136261)%89990);
const erpO=(o,k)=>({so:"SAL-ORD-",wo:"MFG-WO-",dn:"MAT-DN-",si:"ACC-SINV-"})[k]+yr(o.created)+"-"+p5(o.n);
const eNum=(k,base)=>(S.eseq[k]=(S.eseq[k]||base)+1);
let STF=null;
function ePost(t,dir,dt,name,txt,oid){S.erpN++;S.erp.push({t,dir,dt,name,txt,oid});if(S.erp.length>900)S.erp.splice(0,S.erp.length-600);}
function ePost(t,dir,dt,name,txt,oid){if(!S.erp)return;S.erpN++;S.erp.push({t,dir,dt,name,txt,oid});if(S.erp.length>900)S.erp.splice(0,S.erp.length-600);}
/* when a posting left the outbox: within a minute, or after the nightly ERPNext backup window 01:00 to 01:15 (hash-based, never the simulation's random stream) */
function eSent(e){if(e.dir==="in")return e.t;const d=new Date(e.t),lag=(15+fnv(e.name+e.t,2166136261)%45)*SEC;
if(d.getHours()===1&&d.getMinutes()<15){d.setMinutes(15,0,0);return d.getTime()+lag;}return e.t+lag;}
function erpFrom(t,k,txt,oid){
function erpFrom(t,k,txt,oid,actor){
if(!STF)STF=Object.fromEntries(STAGES.map(s=>[s.f,s.id]));
const o=oid&&S.byId[oid],Y=yr(t);let m;
if(o){
@@ -519,6 +520,10 @@ function erpFrom(t,k,txt,oid){
if(prev==="plate"&&o.mo&&!o.repress)ePost(t,"out","Serial No",o.mo[0].id,`fathers and mothers ${o.fa.map(x=>x.id).join(", ")}, ${o.mo.map(x=>x.id).join(", ")} into warehouse Archive`,o.id);
if(nid==="ship"){ePost(t,"out","Delivery Note",erpO(o,"dn"),`${fI(o.good||o.qty)} records, against ${erpO(o,"so")}`,o.id);ePost(t+2*SEC,"out","Sales Invoice",erpO(o,"si"),`from ${erpO(o,"dn")}`,o.id);}
return;}
if(k==="gate"&&(m=/ gate (\d) approved: (.+)$/.exec(txt))){ePost(t,"out","Artist Gate",`GATE-${yr(o.created)}-${p5(o.n)}-${m[1]}`,`submitted: ${m[2]} approved${actor?" by "+actor:""}`,o.id);return;}
if(k==="metal"&&(m=/: (\d+) stampers punched/.exec(txt))){ePost(t,"out","Quality Inspection","MAT-QA-"+Y+"-"+p5(eNum("qa",500)),`Accepted, ${m[1]} stampers: thickness, run-out, ridges; serial numbers to the rack`,o.id);return;}
if(k==="metal"&&(m=/extra stamper set (\d+) grown/.exec(txt))){const st=o.sets[+m[1]-1];ePost(t,"out","Serial No",st?st.st[0].id:"S-"+o.n,`stamper set ${m[1]} grown from mother ${o.mo?o.mo[0].id:""}, ${o.moUse||1} of about 10 sets`,o.id);return;}
if(k==="press"&&(m=/: stamper set (\d+) retired after ([\d,]+) plays/.exec(txt))){const st=o.sets[+m[1]-1];ePost(t,"out","Serial No",st?st.st[0].id:"S-"+o.n,`set ${m[1]} retired after ${m[2]} plays, play count closed`,o.id);return;}
if(k==="metal"&&/: lacquers /.test(txt)){ePost(t,"out","Serial No",o.lac[0].id,`lacquers ${o.lac.map(x=>x.id).join(", ")} registered, warehouse Cutting room`,o.id);return;}
if(k==="press"&&(m=/^(\S+) set up for /.exec(txt))){ePost(t,"out","Job Card",jcName(`WO-${o.n}-run`),`started on ${m[1]}, stamper set ${o.setIdx+1}`,o.id);return;}
if(k==="press"&&(m=/^(\S+) finished /.exec(txt))){ePost(t,"out","Job Card",jcName(`WO-${o.n}-run`),`completed on ${m[1]}: ${fI(o.good)} good, ${fI(o.rej)} rejected`,o.id);
@@ -538,7 +543,7 @@ function erpWO(w){
if(w.p)return `Asset Maintenance Log <span class="mono">ACC-AML-${yr(w.start||S.t)}-${p5(1000+fnv(w.id,2166136261)%8999)}</span>`;
return "-";}
function erpCard(o){const wos=orderWOs(o),dn=wos.filter(w=>w.status==="done").length;
return `<dl class="kv"><dt>Customer</dt><dd>${esc(o.artist)}</dd><dt>Sales order</dt><dd><span class="mono">${erpO(o,"so")}</span>, submitted ${fDT(o.created)}</dd><dt>Work order</dt><dd><span class="mono">${erpO(o,"wo")}</span>, ${fI(Math.min(o.good,o.qty))} of ${fI(o.qty)} produced</dd><dt>Job cards</dt><dd>${dn} of ${wos.length} completed</dd><dt>Batches</dt><dd class="mono">${o.lots.length?o.lots.join(", "):"-"}</dd><dt>Delivery</dt><dd>${o.si>=SI.ship?`<span class="mono">${erpO(o,"dn")}</span>, invoice <span class="mono">${erpO(o,"si")}</span>`:'<span class="faint">note and invoice when it ships</span>'}</dd></dl><p class="note"><a href="#erp">ERPNext link</a>: what this order posted. Document names are simulated.</p>`;}
return `<dl class="kv"><dt>Customer</dt><dd>${esc(o.artist)}</dd><dt>Sales order</dt><dd><span class="mono">${erpO(o,"so")}</span>, submitted ${fDT(o.created)}</dd><dt>Work order</dt><dd><span class="mono">${erpO(o,"wo")}</span>, ${fI(Math.min(o.good,o.qty))} of ${fI(o.qty)} produced</dd><dt>Artist gates</dt><dd>${Object.keys(o.gates).length?Object.keys(o.gates).map(g=>`<span class="mono">GATE-${yr(o.created)}-${p5(o.n)}-${g}</span>`).join(", "):(o.repress?"none, repress":'<span class="faint">submitted on approval</span>')}</dd><dt>Job cards</dt><dd>${dn} of ${wos.length} completed</dd><dt>Metal parts</dt><dd>${o.mo?`Serial No <span class="mono">${o.mo.map(x=>x.id).join(", ")}</span> in Archive, ${o.moUse||1} of about 10 sets`:'<span class="faint">after plating</span>'}</dd><dt>Batches</dt><dd class="mono">${o.lots.length?o.lots.join(", "):"-"}</dd><dt>Delivery</dt><dd>${o.si>=SI.ship?`<span class="mono">${erpO(o,"dn")}</span>, invoice <span class="mono">${erpO(o,"si")}</span>`:'<span class="faint">note and invoice when it ships</span>'}</dd></dl><p class="note"><a href="#erp">ERPNext link</a>: every record of this order lives in ERPNext; cycles, per-record results and telemetry stay in the machine-data store. Document names are simulated.</p>`;}
function rechain(){S.led.sort((a,b)=>a.t-b.t);if(S.led.length>800)S.led=S.led.slice(-800);let prev=hx("genesis "+(S.ledN-S.led.length));for(const e of S.led){e.p=prev;e.h=hx(prev+e.t+e.k+e.txt);prev=e.h;}}
/* ================= orders ================= */
@@ -1055,13 +1060,13 @@ PAGES.metal={
};
function ledgerTable(L,showOrder){return `<div class="tw"><table class="t"><tr><th>Time</th><th>Actor</th><th>Event</th><th>Hash</th></tr>${L.map(e=>`<tr><td class="mono" style="white-space:nowrap">${fDT(e.t)}</td><td class="mute" style="white-space:nowrap">${esc(e.actor)}</td><td>${esc(e.txt).replace(/CNX-\d+/,m=>`<a href="#order=${m}">${m}</a>`)}</td><td class="mono mute" title="previous ${e.p}">${e.h.slice(0,10)}</td></tr>`).join("")}</table></div>`;}
PAGES.ledger={
layout:()=>`<div class="ph"><div><div class="eyebrow">Factory console</div><h1>Event log</h1></div><p class="mute">Append-only; each entry carries the hash of the one before, so an edit anywhere breaks the chain</p></div><section class="card"><div id="gL"></div></section>`,
layout:()=>`<div class="ph"><div><div class="eyebrow">Factory console</div><h1>Event log</h1></div><p class="mute">Machine events, append-only; each entry carries the hash of the one before, so an edit anywhere breaks the chain. Business records are submitted ERPNext documents</p></div><section class="card"><div id="gL"></div></section>`,
blocks:()=>({gL:ledgerTable(S.led.slice(-60).reverse(),true)+`<p class="note">Showing the latest 60 of ${fI(S.ledN)} events since the simulation started. The demo uses a short hash; the factory application uses SHA-256.</p>`})
};
const OWN=[["Orders","Sales Order, Delivery Note, Sales Invoice","the three artist gates, the portal view"],["Production","Work Order, a Job Card per station, Downtime Entry","cycles, telemetry, kiosk scans, stamper changes"],["Materials","Batch, Purchase Receipt, Quality Inspection, Stock Entry","instrument files, CoA scans, which lot fed which cycle"],["Metal parts","Serial No in a warehouse: archive, rack, press","genealogy, metrology, play counts, sets per mother"],["Quality","inspection results per batch and run","every record's optical result, audio samples"],["Plant","Asset, maintenance schedule, accounts, payroll","run hours, alarms, the hash-chained log"]];
const OWN=[["Orders","Sales Order, Artist Gate, Delivery Note, Sales Invoice","none"],["Production","Work Order, a Job Card per station with its scans and times, Downtime Entry","every cycle, PLC telemetry"],["Materials","Batch, Purchase Receipt, Quality Inspection, Stock Entry","instrument raw files, which lot fed which cycle"],["Metal parts","Serial No with its parents, archive location, play count, sets per mother","metrology raw files"],["Quality","Quality Inspection per batch, run and flagged sample","every record's optical result, audio captures"],["Plant","Asset, maintenance, accounts, payroll","run hours, energy, environment, the hash-chained log"]];
PAGES.erp={
layout:()=>`<div class="ph"><div><div class="eyebrow">Factory console</div><h1>ERPNext link</h1></div><p class="mute" style="max-width:600px">The whole plant runs ERPNext. It owns orders, stock, inspections, deliveries and money; this application owns the floor and posts what happens there in business-sized pieces, through an outbox, so no press waits for the ERP.</p></div>
layout:()=>`<div class="ph"><div><div class="eyebrow">Factory console</div><h1>ERPNext link</h1></div><p class="mute" style="max-width:600px">The whole plant runs ERPNext, and every record lives there: orders, gates, job cards, stock, inspections, metal parts, deliveries, invoices. This console is one of the interfaces on it. Only machine data stays outside: cycles, per-record results, telemetry, audio. Writes from the floor go through an outbox, so no press waits for the ERP.</p></div>
<div class="kpis" id="eK"></div>
<section class="card"><div class="ch"><h3>Postings and webhooks</h3><span class="mute">latest 40, newest first</span></div><div id="eL"></div></section>
<div class="cols2"><section class="card"><div class="ch"><h3>Who owns what</h3></div><div class="tw" id="eO"></div></section>
@@ -1075,7 +1080,7 @@ PAGES.erp={
return {
eK:kpi("Posted today",fI(today.filter(e=>e.dir==="out").length),"documents created or updated")+kpi("In the outbox",fI(q.length),q.length?"sent within a minute":"held only in the nightly backup, 01:00 to 01:15",q.length>40?"warn":"")+kpi("Webhooks in today",fI(inT),"sales orders from the order platform")+kpi("Reconciled",fT(rc.getTime()),"work order and batch totals match","ok"),
eL:L.length?`<div class="feed"><div class="er hd"><span class="t">Time</span><span class="d">Document type</span><span class="n">Document</span><span class="e">Event</span><span class="s">Status</span></div>${L.slice(0,40).map(row).join("")}</div>`:'<p class="mute">Nothing posted yet.</p>',
eO:`<table class="t own"><tr><th></th><th>ERPNext</th><th>This application</th></tr>${OWN.map(([a,b,c])=>`<tr><td>${a}</td><td>${b}</td><td class="mute">${c}</td></tr>`).join("")}</table>`,
eO:`<table class="t own"><tr><th></th><th>In ERPNext</th><th>Machine data, outside</th></tr>${OWN.map(([a,b,c])=>`<tr><td>${a}</td><td>${b}</td><td class="mute">${c}</td></tr>`).join("")}</table>`,
eD:(BL.length?`<div class="pareto">${BL.map(([r,n])=>`<div><span>${esc(r)}</span>${bar(n/mx,"acc")}<span class="num">${fI(n)}</span></div>`).join("")}</div>`:'<p class="mute">Nothing posted today yet.</p>')+`<p class="note">Postings are per order, run, lot, stop and shipment, never per record or cycle. Document names follow ERPNext's default naming series and are simulated; the real link is the Frappe REST API with an integration user per tool, and a nightly reconciliation of totals. ${fI(S.erpN)} postings since the simulation started.</p>`
};
}
+1 -1
View File
@@ -50,7 +50,7 @@ building, software and stamper prep lines, and the contingency on top).
| Press fleet changed (October 2026) to two Newbilt Duplex systems (two presses each, EX 50 main extruder and EX 35 colour extruder) plus one Newbilt Classic, replacing the two blocks above (930k – 1,290k): Duplex system 1 as the first block (350k – 370k; the quote's second-extruder option becomes the EX 35), system 2 ~230k – 300k (two presses at the quoted ~115k per press and extruder set as the yardstick, plus hydraulic unit and EX 35, estimate; no EX 35 price on file), the Classic ~290k – 460k (the AD12 band per press as a placeholder; no Newbilt price on file) | −60k (low case) / −160k (high case) | −2.0M / −5.3M | estimate, [pressing line](../equipment/pressing-line.md); the Newbilt re-quote replaces it. The utilities for five presses are re-sized in the row below |
| Utilities re-sized for the five-press fleet (October 2026): boiler bank 2 × 500 kg/h, chiller bank 3 × 250 kW with plate heat exchanger and tanks, larger softener and loop treatment ([steam and cooling](../steam-cooling.html)) | +185k (low case) / +294k (high case) | +6.1M / +9.7M | estimate, [utilities CSV](utilities-investment.csv) re-based from 24.0M – 41.6M to 30.1M – 51.3M THB |
| Utility yard built for four boiler and four chiller modules (October 2026): yard ~379 m² against 178 m² ([scale](../scale.md#detached-utility-yard-parking-and-what-that-moves)) | 73k – 122k | 2.4M – 4.0M | estimate: the building rate above on the ~200 m² the yard grows by; the open chiller compound costs less than a building, so this is the cautious end. The valved branches and feeders for the empty positions are not priced yet |
| ERPNext for the whole plant (October 2026): configuration with Thai tax, integration with every tool, Nix packaging; `vinyl-erp` server | to quote | to quote | not in the sum: 3 to 5.5 person-months in-house or a partner quote (Ecosoft), plus 2k to 4k USD hardware ([digital factory](../digital-factory.md#erpnext-the-plants-erp)); no licence fee |
| ERPNext for the whole plant (October 2026): configuration with Thai tax, integration with every tool, Nix packaging; `vinyl-erp` server | to quote | to quote | not in the sum: 3.5 to 6 person-months in-house or a partner quote (Ecosoft), plus 2k to 4k USD hardware ([digital factory](../digital-factory.md#erpnext-the-plants-erp)); no licence fee |
| Archive mobile shelving for the father and mother archive (October 2026) | to quote | to quote | not in the sum: A-Z Solutions publishes no prices; quote it and a Thai supplier ([facility security](../facility-security.md#archive-room-mobile-shelving)) |
| **Added so far** | **~678k – 1,027k** | **~22.3M – 33.9M** | about 13 to 17% on the full-vision subtotal |
+1 -1
View File
@@ -211,7 +211,7 @@ Two commercial consequences worth acting on:
before a new lacquer. These are industry rules of thumb, not this
plant's figures: the galvanics vendors are asked for theirs
([galvanics](equipment/galvanics.md#open-questions-for-the-quotes)), and
the factory application counts stamper sets per mother.
ERPNext counts stamper sets on each mother's serial number.
- **The archive is an asset with a shelf life.** It needs the
storage conditions and the genealogy records that
[traceability.md](traceability.md) already specifies, and a stated
+7 -5
View File
@@ -45,11 +45,13 @@ For any order, the customer can follow the full chain:
mother, stamper, press run, box) gets an ID, and every ID knows its
parents. A customer complaint about one record resolves to a specific
stamper, press, shift, and compound lot.
- **One ERP, one set of keys**: business documents (orders, work orders
and job cards, lots, deliveries, invoices) live in ERPNext, which the
whole plant runs; the traceability detail lives in the factory
application and carries the same ERPNext document names, so a trace and
an invoice point at the same order
- **One system of record, one set of keys**: every record of the
business and of production (orders, gates, work orders and job cards,
batches, quality inspections, metal parts and their genealogy,
deliveries, invoices) lives in ERPNext, which the whole plant runs;
only machine data (per-cycle rows, per-record results, telemetry, audio
captures) lives outside it, carrying the same ERPNext document names,
so a trace and an invoice point at the same order
([digital factory](digital-factory.md#erpnext-the-plants-erp)).
- **Environment attached**: the whole-factory HVAC monitoring (temperature,
humidity, PM per zone) is logged on the same timeline, so any production