Keeping the numbers right
Stock is a ledger, not a number
The most common design mistake is a quantity column that code adds to and subtracts from. It works until two requests arrive together or someone needs to know why the figure is what it is. We record every receipt, issue, transfer and adjustment as a row in a movements table. Quantity on hand is the sum of those rows, cached in a balance table for speed and checked against the ledger by a scheduled job. Nothing is ever edited in place. A mistake is corrected with a reversing movement, which is exactly how your accountant thinks.
Transactions and locks where money and stock change
Confirming an order touches several tables: the order, its lines, reservations, perhaps a credit limit. All of it runs inside one database transaction, so it either fully happens or does not happen at all. When two users compete for the same stock, a row lock on the balance makes the second request wait and then see the true remaining quantity. Document numbers come from a locked sequence table so invoices are gapless. Events that trigger emails or integrations are dispatched only after the transaction commits, which prevents a customer receiving a confirmation for an order that was rolled back.
Documents as state machines
Each document type has an explicit list of states and allowed transitions. A posted invoice cannot be edited, only credited. A delivered order cannot return to draft. The rules live in one class per document, and the user interface reads from it to decide which buttons to show. Approval steps sit between states and are driven by rules you can configure: by amount, by department, by supplier.
Talking to accounting
We rarely rebuild a general ledger. Your accountants already have a package they trust, and statutory reporting belongs there. The ERP owns operations and posts summarized or per-document entries to accounting through its API. Posting jobs are idempotent and keep the remote ID, so a retry never creates a duplicate invoice. A reconciliation screen lists anything that failed to sync and why.
Rolling out in phases
Big-bang ERP launches fail in familiar ways. We go module by module, usually starting where the pain is sharpest, and run the new module alongside the old process for a short overlap. Opening balances are loaded and counted. Each phase ends with users signing off real transactions before the next begins. Teams that also need sales pipeline tracking often pair this with Laravel CRM development on the same customer records.