MODULE
⚠️ Caution: This package is a work in progress. Don't use this in production or use at your own risk—no guarantees are provided... or better yet, collaborate with me to create the definitive Laravel boilerplate; that's the right place to introduce your ideas. Let me know your ideas...
Table of Contents
Description
The ERP module provides Laraplate’s accounting and operations domain: multi-company, chart of accounts, journal entries, fiscal calendar, tax codes, commercial scaffolding (customers, quotations, projects), and related Filament admin resources. The module is optional and can be enabled or disabled via modules_statuses.json like other Laraplate modules.
The package evolves with product requirements; treat public APIs as unstable until a stable release is declared.
Point 0
The canonical status is Modules/ERP/docs/STATUS.md. Approved mandatory ERP work that does not require external /api/v1 exposure is implemented. External-source importers are a separate workstream and are excluded from this baseline.
Architecture and Extension Points
ERP is optional and depends on Core, never on other optional vertical modules. Business mutations remain in transactional services. Supported replaceable bindings are ChartOfAccountsProvider, CurrencyConverter, EInvoiceProvider, and Core OutboxPublisher. No container-tagged ERP plugin hooks currently exist. See Modules/ERP/docs/VISION.md for invariants, module boundaries, stable outbox event names, deferred API governance, and the implementation checklist.
Installation
If you want to add this module to your project, you can use the joshbrw/laravel-module-installer package.
In a full Laraplate application you typically depend on Core first, then add ERP. Add the repositories to your composer.json file (adjust URLs if you use forks or private registries):
"repositories": [
{
"type": "composer",
"url": "https://github.com/getlaraplate/laracore.git"
},
{
"type": "composer",
"url": "https://github.com/getlaraplate/laraerp.git"
}
]
composer require joshbrw/laravel-module-installer getlaraplate/laracore getlaraplate/laraerp
Then install and activate the modules:
php artisan module:install Core
php artisan module:install ERP
Ensure modules_statuses.json lists ERP as enabled when you want the module loaded.
Configuration
When the module is active, configuration is published under the erp key (file: Modules/ERP/config/config.php).
// Example
config('erp.name'); // "ERP"
config('erp.einvoice.driver'); // "stub", "fatturapa", or "aruba"
E-invoice provider configuration is read through Laravel config/env:
ERP_EINVOICE_DRIVER:stubby default; supported values arestub,fatturapa,arubaERP_EINVOICE_ARUBA_BASE_URL: Aruba API base URL, for example the contracted demo/production web-service hostERP_EINVOICE_ARUBA_AUTH_BASE_URL: optional Aruba auth base URL when using username/password token acquisitionERP_EINVOICE_ARUBA_UPLOAD_PATH: upload endpoint path, default/services/invoice/uploadERP_EINVOICE_ARUBA_NOTIFICATIONS_PATH: notifications polling endpoint path, default/api/v2/invoices-out/notificationsERP_EINVOICE_ARUBA_TOKEN: bearer token used by the Aruba adapter; if missing, username/password auth is attemptedERP_EINVOICE_ARUBA_USERNAME/ERP_EINVOICE_ARUBA_PASSWORD: optional credentials for Aruba/auth/signinERP_EINVOICE_ARUBA_CALLBACK_API_KEY: optional static API key expected on Aruba callback requestsERP_PAYMENT_REQUEST_DRIVER: payment-request provider binding (stubcurrently)ERP_PAYMENT_REQUEST_STUB_CALLBACK_API_KEY: enables and authenticates stub provider callbacks; empty means disabledERP_EINVOICE_ARUBA_SIGNATURE_CREDENTIAL/ERP_EINVOICE_ARUBA_SIGNATURE_DOMAIN: optional Aruba signature parametersERP_EINVOICE_ARUBA_SENDER_PIVA: optional sender VAT override; defaults to company fiscal country + tax idERP_EINVOICE_ARUBA_SKIP_EXTRA_SCHEMA/ERP_EINVOICE_ARUBA_DRY_RUN: optional upload flags
Per-company ERP settings are stored in companies.settings (JSON). Use ErpCompanySettings to read dotted keys (e.g. erp.three_way_match.price_tolerance_percent).
Features
Requirements
- PHP >= 8.5
- Laravel 12.0+
- Recommended: Laraplate Core for users, permissions, and shared infrastructure (typical production setup)
Installed Packages (development)
The ERP module aligns with the same quality toolchain as Cms and Core:
- pestphp/pest and Laravel / type-coverage plugins
- laravel/pint
- larastan/larastan
- rector/rector and driftingly/rector-laravel
- filament/filament (v5) for admin UI
- nunomaduro/phpinsights, peckphp/peck
Module metadata
- Priority:
999inmodule.json(load order consistent with other feature modules) - Namespace:
Modules\ERP\... - Composer package:
getlaraplate/laraerp - Web / API routes:
Modules/ERP/routes/web.php,Modules/ERP/routes/api.php
M0 — Multi-tenancy & Multi-currency Foundation
companiestable with functional currencyBelongsToCompanytrait + global scope for tenant isolation- Dual-currency columns via
ERPMigrateUtils::moneyColumns()(amount_doc / currency_doc / amount_local / fx_rate) CurrencyConverterservice (no-op in M0, pluggable for live FX)
M0 — Versioning
HasVersionstrait withVersionStrategy::DIFFon accounting models- Immutable version strategy on contabile models (cannot be overridden via settings)
- Core
ForcedVersionStrategySettingsremoves staleversion_strategy_erp_*controls from Filament for class-forced DIFF models without deleting their rows
M1 — Accounting Backbone
- Chart of Accounts (
accountstable,Accountmodel,ChartOfAccountsProviderinterface, Italian PDC default) - Journal Entries (
JournalEntry/JournalEntryLine,JournalPostingService::post/reverse, immutability after posting) JournalEntry,JournalEntryLine,VatRegisterEntry,StockCostLayer,StockLevelandStockMovementimplement CoreRestrictsCrudWrites(viaDeniesGenericCrudWrites): generic CRUD writes on them answer403; they change only through their owning services- Fiscal Calendar (
FiscalYear,FiscalPeriod,FiscalPeriodCloserwith re-open audit) - Document Sequences (
DocumentSequence,DocumentNumberAllocator,DocumentTypeenum with gap policy)
M2 — Tax & Invoice Foundation
- Tax Codes (
TaxCode,TaxKindenum, immutable fiscal snapshot,TaxLineCalculator,TaxCodeSupersessionService) ItalianTaxCodesSeederbaseline- Invoice / InvoiceLine stub with tax snapshot columns
M3.1 — CRM (moved)
Leads, opportunities, pipeline stages and contacts moved to the CRM module (Modules/CRM/docs/rag/MODULE.md). The ERP keeps QuotationObserver: an accepted quotation makes its Party a customer and closes the linked CRM opportunity as won.
M3.2 — Sales Order Processing
SalesOrder/SalesOrderLinewith lock-chain progressionSalesOrderEvasionService(delivery + invoice qty tracking, status management)SalesOrderAmendmentService(creates amendment SO from confirmed / partially evaded orders)- Lock propagation: confirming SO locks linked Quotation
M3.3 — Inventory Management
- Items, Warehouses, StockLevels
StockMovement/stock_cost_layerstablesStockMovementServicewith FIFO and weighted-average costing; an outbound FIFO movement locks and consumes open layers oldest first in keyset pages of 100 (lazyById), so any number of open layers is consumed in order- COGS calculation integrated with delivery posting
StockReservation/StockReservationService:available = on hand - live reservations(see Stock Reservation and Availability (ATP))
M3.4 — Delivery Notes
DeliveryNote/DeliveryNoteLinewith SO line linkingDeliveryNoteInventoryService(stock posting + rollback)DeliveryNoteCogsJournalService(COGS journal on posting, full reversal on unpost)- Automatic SO qty_delivered updates
Stock Reservation and Availability (ATP)
ERP owns a native stock reservation, so a confirmed but not yet shipped sales order reduces what can still be promised. The concept is available = on hand - live reservations, where on hand is the sum of the item's StockLevel quantity across all of the company's warehouses and a reservation is live when it is hard, or soft with expires_at null or in the future. StockLevel stays the on-hand truth and StockMovement the realized-movement ledger; a reservation never moves stock.
Table and model
erp_stock_reservations:company_id,item_id, nullablewarehouse_id,source_type+source_id(opaque owning document),quantity(decimal 15,4, DB check keeps it positive),state(soft/hard/consumed/released,StockReservationState), nullableexpires_at, timestamps with soft delete. Indexes:(company_id, item_id, state)for the availability sum,(source_type, source_id)for lifecycle lookups.StockReservationis company-scoped (BelongsToCompany), not versioned, and implementsRestrictsCrudWrites(generic CRUD writes answer403): it changes only throughStockReservationService.source_type/source_idare plain columns, not a morph relation: ERP never resolves the source document, so a source can live in a module ERP must not import. Current source aliases:erp.sales_order_line(ERP) andmes.production_order_material(MES, see the MES module doc).softandhardare the live states;consumed(already reflected in the on-hand drop) andreleasedare terminal and never count.StockReservation::active()is the scope for soft + hard by state alone.
StockReservationService contract (Modules\ERP\Services\Inventory, exact decimal strings of scale 4 through Decimal, no floats)
available(companyId, itemId): string: on hand minus live reservations, item-level and company-wide. An expired soft is ignored on read, whether or not the sweep has closed it.reserve(companyId, itemId, quantity, mode, sourceType, sourceId, ?warehouseId, ?expiresAt): StockReservation:modeissoftorhard;expiresAtapplies only tosoft(null means it does not lapse), ahardrow never expires. ThrowsInsufficientStockExceptionwhen the quantity exceeds availability,ValidationExceptionfor a non-positive quantity or an item/warehouse of another company,LockTimeoutExceptionwhen the item lock cannot be taken.promoteToHard(sourceType, sourceId, companyId): turns the source's live soft rows into hard and clearsexpires_at; a missing or already expired soft is left alone (the caller reserves a fresh hard one, which re-validates availability).reservedQuantity(sourceType, sourceId): string: the sum of the source'shardrows only (the partconsume()can close).release(sourceType, sourceId): gives back every soft/hard row of the source; idempotent.consume(sourceType, sourceId, quantity): closesquantityof the source's hard rows oldest first, splitting the last row when it is only partly consumed;ValidationExceptionabove the hard total. It does not move on-hand stock: the caller records the outbound movement in the same transaction, so availability never counts the same units twice.release,consumeandreservedQuantityrely on the model's company global scope: callers run in company context.- The service is a PHP contract only. No route, domain action or Filament resource exposes reservations.
Concurrency. reserve() is a critical section guarded twice. A per-(company, item) cache lock (erp:stock-reservation:{company}:{item}, held up to 10 s, waited up to 5 s) is a cheap first gate. Inside the transaction the item's StockLevel rows and its live reservation rows are row-locked (FOR UPDATE, in id order so concurrent reservers cannot deadlock) and availability is recomputed from those current reads; the row lock is the real guarantee, because the database keeps it until the actual COMMIT and so still serializes reservers when reserve() runs inside the caller's own outer transaction (the inner transaction is then only a savepoint). Overselling through a reservation is impossible: it never exceeds what is on hand.
Sales order lifecycle (source alias erp.sales_order_line, one source per SalesOrderLine, warehouse_id null)
- Confirm.
SalesOrderConfirmed(fired on create-as-confirmed and on the transition to confirmed) runsReserveStockForConfirmedSalesOrder, best effort and never blocking the confirm. For each item-backed line, in id order, it first promotes a live soft hold of the line to hard, then hard-reservesmin(quantity not yet hard-reserved, available). The unreserved remainder is backorder or make-to-order (MES plans production off the same event). The quantity is pre-clamped, and a lost race retries up to three times with fresh availability. No exception fromreserve()escapes: a contended lock, or three lost races in a row, ends as a WARNING log, and an item that is not the order company's as an ERROR log, and the line is left unreserved either way. Lines without an item reserve nothing. - Cancel. The
SalesOrderCancelledevent (fired when the status changes to cancelled) runsReleaseStockForCancelledSalesOrder, which releases every soft/hard row of each line. Consumed rows are terminal. - Evasion. Posting a delivery note records the outbound
StockMovements and thenSalesOrderEvasionService::registerDelivery, in the same transaction. For each item-backed line it consumesmin(quantity shipped by this posting, hard reserved quantity): only the reserved part is closed, so a partly backordered line never over-consumes, and on a partial evasion the remainder stayshard. Unposting the delivery note reverts the movement and the delivered quantity but does not restore consumed reservations. - Amend.
SalesOrderAmendmentService::amend()copies the remaining quantity (ordered minus the larger of delivered and invoiced) of each line into a draft amendment order and releases the source line's live hold in the same transaction. The amendment hard-reserves its own lines when it is confirmed (an amend-up is simply a larger reservation there). Consumed rows of the source are not touched. - Soft holds. A soft hold is caller-initiated (for example a storefront checkout holding stock during the payment window); nothing in the repository creates one yet, and ERP forces none on a manual draft. Promotion to hard happens through
promoteToHard()or at confirm as above.
Expiry and configuration
erp:stock-reservations:expire(scheduled hourly,onOneServer,withoutOverlapping) releases, across every company, the soft rows whoseexpires_athas passed, in one guardedUPDATEthat cannot clobber a soft promoted to hard in the meantime. It is housekeeping: availability already ignores an expired soft on read, so a missed run changes no figure.erp.stock_reservation.soft_ttl(envERP_STOCK_RESERVATION_SOFT_TTL, minutes, default1440) is the soft hold lifetime. It is read by whoever creates the soft hold (expires_at = now + soft_ttl), not by the sweep. Keep it well above the payment window.
Known limitations
- Availability is item-level and company-wide for sales: which warehouse ships is still decided at evasion. Reservations created by sales have a null warehouse; only a module that must pin a location (MES) sets
warehouse_id. - Best-effort confirm means a short order confirms anyway, with a backorder remainder. Refusing an order whose stock cannot be covered, or refunding the unfulfillable remainder, is a consumer policy (a Shop concern), not part of the ERP confirm.
- Availability is
on hand - reservationsonly: incoming stock (open purchase orders, planned production) is not promised against. - An abandoned amendment frees the source order's reservation: the hold is released when the draft amendment is created, and the source order stays confirmed with no hold until the amendment is confirmed.
M3.5 — Invoice Posting & DDT Integration
InvoicePostingServicewith full journal post / unpost cycle (observer-driven viaposted_at)DocumentNumberAllocatorintegration at posting time (reference assigned on post, cleared on unpost)- Tax snapshot on invoice lines (tax_code, tax_rate, tax_label frozen at posting)
- SO qty_invoiced tracking via
SalesOrderEvasionService - Pivot
erp_invoice_line_delivery_note_linefor flexible Invoice↔DDT linking, mapped byModels\Pivot\InvoiceLineHasDeliveryNoteLinewith covered quantity and timestamps InvoiceDeliveryNoteValidationService— optional DDT linkage rules at posting (posted DDT, qty caps, SO line consistency)InvoiceCompactionService(compact / expand invoice lines by item)- Filament Post / Unpost actions on invoice edit page and list (no manual
posted_atediting) - Posting is refused with
PostingToClosedFiscalPeriodExceptionwhen the posting date falls in a closed fiscal period:InvoicePostingServiceresolves the covering period of the company andJournalPostingServicerejects it. Reopen the period (Filament Reopen action or thereopendomain action) before posting force_postis a separate ability that applies only to unposted purchase invoices: it shows the Force three-way match checkbox on the Post action, which lets posting proceed past a price/quantity tolerance breach recorded asMatchStatus::Forced- See
docs/ERP_GUIDA_SEMPLICE.md§4.7 anddocs/rag/MODULE.md§ Invoice posting workflow for end-to-end flows
M3.6 — Purchasing Cycle
- Unified
Partyentity (replacesCustomer) withis_customer/is_supplierboolean flags - Party type validation on all models (saving callback) + filtered Filament dropdowns
PurchaseOrder/PurchaseOrderLine,GoodsReceipt/GoodsReceiptLineThreeWayMatchServicewith configurable price / quantity tolerances (wired inInvoicePostingServiceon purchase posting)MatchStatusenum (matched, tolerance, forced, unmatched) persisted oninvoice_lineswithmatch_discrepancyJSON- Filament Force three-way match checkbox on purchase invoice Post action
- Per-company settings via
ErpCompanySettingsoncompanies.settingsJSON (erp.three_way_match.*, default 0%)
M3+ — Database Lock Triggers
- MySQL/MariaDB and PostgreSQL triggers protect locked quotations, sales orders, and projects from raw updates/deletes
- Sales-order operational states lock quotation/project; linked DDT lines lock their source sales-order line
- The sales-order-line trigger is selective: commercial fields and delete are guarded, while operational counters/status remain writable.
SalesOrderLine::attributesWritableWhileLocked()states the same rule to the Eloquent guard, derived from$fillableminusLOCKED_COMMERCIAL_FIELDSso the two cannot drift, which is why fulfilment services write to frozen lines without any bypass - SQLite and Oracle use equivalent Eloquent guards for this rule
M5.1 — Payment Schedule & Receivables
PaymentTermmodel (rate_lines JSON with days / percent / payment_method)PaymentScheduleLine(auto-generated at invoice posting)Payment/PaymentAllocation(allocation to schedule lines)PaymentScheduleGeneratorService(integrated in InvoicePostingService)PaymentAllocationService(allocate / deallocate with status tracking)AgingReportService(AR / AP aging by 30 / 60 / 90 / 120+ day buckets)PartyBankAccount,PaymentRun, andPaymentRunLinefor supplier payment runsPaymentRunBuilderServicebuilds draft outbound supplier payment batches from open purchase schedule linesSepaPain001Exporterexports approved supplier payment runs as auditable SEPA SCTpain.001XML files- Payment execution is file generation only: no direct bank/API submission, no CBI/Ri.Ba/SDD in v1
M5.2 — Credit & Debit Notes
InvoiceTypeenum (invoice, credit_note, debit_note)- Separate document numbering (SalesCreditNote, PurchaseCreditNote, SalesDebitNote, PurchaseDebitNote)
CreditNoteService(create CN from posted invoice, line copying, total validation)- Inverted journal entries on credit note posting
- Filament "Create Credit Note" action on invoice page
- Credit/debit notes are fiscal corrections: natural prices come from source invoice lines, not from orders.
M5.3 — VAT Registers & Settlement (Italian Compliance)
VatRegisterEntrywith progressive protocol numbers per register / yearVatRegisterService(auto-registration at invoice posting, one entry per tax code)VatSettlement(IVA debito − IVA credito − credito precedente)VatSettlementService(compute + confirm with carry-forward)- Filament read-only register and settlement pages
M5.4 — Financial Statements
TrialBalanceService(debit / credit balance per account at date)BalanceSheetService(assets = liabilities + equity + net income)IncomeStatementService(revenue − expenses for date range)- Filament pages with Blade templates for tabular reports
E-Invoice Interface
EInvoiceProvidercontract (prepare, submit, validateXml, remoteStatus)EInvoiceSubmissionmodel +EInvoiceSubmissionStatusenum- Stub provider binding and deterministic local submission workflow
- Filament actions for posted sale invoice submit / status refresh
- Phase 2C FatturaPA readiness: fiscal schema fields on company/party/invoice,
FatturaPaAnagraphicMapper,FatturaPaXmlBuilder, vendored official FPR12 v1.2.3 XSD resources, and localfatturapadriver validation arubadriver: production-oriented adapter for Aruba v2-style upload (dataFile), notifications polling, optional auth signin, provider callbacks, and conservation availability metadata inresponse_payloadPOST /api/v1/erp/einvoice/aruba/callbacksapplies signed provider callbacks whenERP_EINVOICE_ARUBA_CALLBACK_API_KEYis configurederp:einvoice:refresh-statusespolls open submissions for the configured provider and supports--company,--provider,--limit, and--dry-runerp:health-check --format=table|jsonperforms read-only installation and operational checks for the default company, chart of accounts, fiscal calendar, domain permissions, current-year sequences, and e-invoice configurationerp:sequences:audit --company=ID --year=YYYY --format=table|jsoncross-checks persisted invoice/order references against sequence formatters, counters, duplicates, and gap policy without repairing dataerp:bank-statements:import --bank-account=ID --path=FILE_OR_DIR --format=auto --dry-runvalidates or imports CSV, CAMT.053, and MT940 files idempotently by SHA-256 checksum;--archive-pathexplicitly enables post-import file moveserp:vat-settlements:compute --company=ID --year=YYYY --period=YYYY-N --dry-runpreviews or computes draft VAT settlements for open fiscal periods; confirmed settlements and closed periods are never modified- Current runtime is still local by default: the
stubdriver remains default;fatturapagenerates and validates XML without delivery - Remaining go-live work: verify the configured Aruba tenant/contract, callback accreditation/IP allowlist, production credentials, and legal retention obligations with the provider
M6.1 — Bank Reconciliation
BankAccount,BankStatement, andBankStatementLinemodels- Bank statement import from CSV, CAMT.053, and a minimal MT940 transaction subset
- Manual reconciliation service with ranked payment suggestions
- Match-with-difference service and Filament workflow for fees, rounding, and write-offs
BankDifferenceJournalServiceposts balanced journals againstbank_cashand the selected difference accountBankStatementLine.difference_journal_entry_idkeeps audit linkage to the accounting adjustment
M6.2 — Returns Management
ReturnOrderfor customer returns andSupplierReturnfor supplier returns- Customer returns generate/link inbound DDTs for physical stock receipts
- Supplier returns generate/link outbound DDTs for physical stock shipments
- DDT lines remain quantity/source-link only; no prices or costs on bolle
- Return completion tracks returned quantities on source invoice / sales order / purchase order / goods receipt lines
- Manual follow-up actions create and link credit/debit note drafts; optional setting
erp.returns.auto_create_notes_on_completecreates NC/ND drafts during return completion - Customer return credit notes use
ReturnOrderLine.invoice_line_idand the source salesInvoiceLine.unit_price;ReturnOrderLine.unit_priceis an optional manual override. - Supplier return debit notes use
SupplierReturnLine.invoice_line_idand the source purchaseInvoiceLine.unit_price;purchase_order_line_idandgoods_receipt_line_idremain logistics references. - Supplier return debit-note creation is blocked when the source purchase invoice line is missing. Returning goods physically can happen from PO/GR, but fiscal correction must reference the purchase invoice.
M7.1 — Advanced Pricelists & Party Rules
PriceList/PriceListItemwith validity windows and Filament resource- Each price-list row targets exactly one direct
Itemor oneActivitytaxonomy; direct item prices take precedence and taxonomy prices are the fallback PartyPriceRulewith percent, fixed, and override discountsParty::price_rules()relation and Party Filament relation managerPriceResolverServiceandInvoiceLinePricingServiceapply commercial pricing to quotation / sales order / invoice linesActivityis the concrete ERP taxonomy used for activity-based price rules; do not point UI directly at abstract CoreTaxonomy.
Integration Outbox
- Posted invoices, exact/difference payment matches, completed customer returns, and completed supplier returns write durable Core outbox events in the same database transaction.
- Event types are
erp.invoice.posted,erp.payment.matched,erp.customer-return.completed, anderp.supplier-return.completed. - Core queues publication after commit. Its default publisher is a no-I/O stub; an application transport binding is required for external delivery.
Cash Movements and Journal
Movementis an adapter over double-entry journals with positive amount, date, document/local currency snapshots,income/expense/contribution/withdrawaltype, explicit economic counterparty, and uniqueposted_journal_entry_id.MovementPostingServicelocks and posts idempotently. Income: debitbank_cash, credit Revenue. Expense: debit Expense, creditbank_cash. Contribution: debitbank_cash, credit Liability. Withdrawal: debit Liability, creditbank_cash. Counterparty company/kind/activity are validated.CashBalanceServicesums postedbank_cashjournal lines; there is no parallel mutable balance.erp:migrate-movements-to-journal [--company=ID] [--dry-run]processes only unlinked movements and reports per-row failures.MovementResourceexposes index/create/view only.CreateMovementwraps row creation andMovementPostingService::post()in one transaction; no edit route or parallel balance write exists.
Partner Pools and Settle-up
PartnerPoolis company/currency scoped and relates Core users through first-class pivotPartnerPoolHasUser.MovementAllocationstores exact owed/paid amounts. Both aggregate totals must equal the expense movement amount.PartnerPoolSettlementServiceowns pessimistic locking, atomic split replacement, decimal balance derivation, settlement suggestions, and confirmed transfer writes.- The pool is an internal memorandum subledger. It does not duplicate the general ledger or
CashBalanceService.
External ERP imports
erp:importaccepts only the ERPBulkImporterInterface; it inherits Core's bootstrap, repeatable constructor args, limit, dry-run, and Scout controls.- Runtime plugins own source parsing and explicit company/user/account/pool/currency/category maps. ERP owns typed destination inputs and accounting invariants and has no Symfony, SPLID, or Tricount schema hooks.
ExternalCashMovementImportServiceandExternalExpenseAllocationServiceuse CoreRecordOriginRegistry: stable source identity plus normalized SHA-256 fingerprint makes reruns idempotent. Changed unposted data may update; changed posted data throwsPostedImportConflictwith source and local movement evidence.- Money crosses the boundary only as scale-4 decimal strings. Owed/paid expense shares are allocations; dated contributions, withdrawals, and reimbursements are separate movements.
Payment Requests
PaymentRequesttargets exactly one ERP Party or Core user and persists amount/currency, provider identity, checkout URL, remote ID, status, and callback metadata.PaymentRequestProvider::createCheckout()is the replaceable boundary;StubPaymentRequestProvideris deterministic and performs no I/O.PaymentRequestServicelocks drafts during send and callback updates, validates provider identity, and prevents terminal-state regression.- External callbacks use
POST /api/v1/erp/payment-requests/{provider}/callbacks, require a configured Bearer key, and only update request state. - Provider callbacks deliberately do not create
Payment,JournalEntry, orPoolTransactionrecords. - Both provider callback routes (this one and
erp/einvoice/{provider}/callbacks) sit behind Core'sexpose_apiswitch (403while it is off, the default) and are markedSelfAuthenticatedApiRoute: the provider's bearer secret is checked by the controller, not read as a Laraplate token, and the request runs as the anonymous user under the per-addressapirate limit (core.api.rate_limit_per_minute). SeeModules/Core/docs/rag/API_ACCESS_CONTROL.md.
Task Calendar Export
TaskIcsExporteremits RFC 5545 VCALENDAR/VEVENT content with stable task UID, UTC validity dates, text escaping, CRLF, and 75-byte physical-line folding.- Summary resolves from
Activity, then project/task fallback. Location is formatted from canonicalSite::place()address/postcode/city/province/country. Taskcasts validity columns to immutable datetimes; nullableproject_id/site_idare declared before FK constraints in the original migration for cross-driver correctness.TaskResourceexposes CRUD and a streamedtext/calendardownload action. No calendar provider API or two-way synchronization is implemented.
Quotation Revisions and Project Locks
QuotationRevisionServicelocks the source, rejects editable drafts/branches/version overflow, creates a draft successor through uniquerevises_quotation_id, incrementsversion, and snapshots all quotation items transactionally.Quotation::revised_from()/revision()expose the linear chain. Filamentcreate_revisionuses the service and opens the successor draft.- Operational sales-order states lock both linked quotation and project.
ProjectusesHasLocks, ORM/Filament guards, and MySQL/MariaDB/PostgreSQL trigger reinforcement. The chain triggers writelocked_atand leavelocked_user_idnull, so what ERP locks by itself is a freeze: ownerless, immutable at the database level. The triggers block frozen rows only, never a lease, otherwise these four documents would be the only lockable models on which a user could not edit the record they had taken charge of. See Core'sRECORD_LOCKING_DEVELOPER.md. - Creating or relinking a DDT line locks its source
SalesOrderLine. Once locked, its commercial identity, ordered quantity and price are immutable; operational counters and status remain service-writable.
Spec 2 Phase 2A/2B — Domain Actions & Commercial UX
- State-aware
ERPModelPolicyguards domain actions on top of Core CRUD permissions. - Domain abilities seeded in
ERPDatabaseSeederinclude posting/unposting, forced posting, e-invoice submit/refresh, quotation unlock, document-sequence reset/reserve, tax-code supersession, and company context switch. - Filament edit/list actions call services; they do not implement business mutations inline.
- Implemented Phase 2B items: Party price rules UI, PriceList resource, quotation unlock, document sequence reset, return line fiscal override contract, and optional auto NC/ND on return complete.
- Implemented Phase 2B items also include supplier payment runs with SEPA
pain.001and CBI bonifici exports. - Implemented Phase 2B banking items also include bank difference journals, match-with-difference UI, and CAMT.053 / MT940 statement import.
- Implemented Phase 2B reporting items also include CSV export actions and immutable CSV/PDF snapshot archives for trial balance, balance sheet, and income statement.
- Implemented Phase 2B operational dashboard items also include Stock Valuation warehouse filter/KPIs/CSV export (the Sales Pipeline report, with its filters, KPIs and CSV export, moved to the CRM module).
Supplier Payment Runs
- Supplier bank coordinates live on
PartyBankAccount, not on invoices or payment lines. PaymentRunBuilderServiceselects open/partial purchase invoice schedule lines and creates a draft payment run.PaymentRunLinestores a beneficiary snapshot: name, IBAN, BIC, amount, due date, and remittance text.- Payment runs move through
draft -> approved -> exported;cancelledis available before export. - Exported payment runs are immutable and store file name, export timestamp, and SHA-256 checksum.
- Supported supplier payment file formats are SEPA Credit Transfer
pain.001and CBI bonifici text export; bank submission remains outside ERP.
Filament Admin UI
-
SiteResourcemanages company/name/validity and selects the canonical CorePlace; ERP does not duplicate postal/geographic fields. -
Siteuses CoreHasValidity, exposesplace(): BelongsTo, and is the location source for task calendar exports. -
Resources: Party, Quotation, Project, SalesOrder, DeliveryNote, Invoice, PurchaseOrder, GoodsReceipt, ReturnOrder, SupplierReturn, PaymentTerm, Payment, PaymentRun, BankAccount, BankStatement, VatRegister (read-only), VatSettlement (read-only)
-
Core accounting: Company, Account, JournalEntry (with view page), FiscalYear, FiscalPeriod, DocumentSequence, TaxCode
-
Report pages: Trial Balance, Balance Sheet, Income Statement, Stock Valuation (the Sales Pipeline report lives in the CRM module). Financial and operational report pages expose CSV export actions; financial reports can be archived through immutable CSV/PDF snapshots.
Current ERP Status
| Area | Status | Notes |
|---|---|---|
| M3.6 Purchasing | Implemented / cleanup only | Purchase invoice posting and 3-way match are present; keep regression coverage focused. |
| M4 Permissions & reporting | Implemented v1 + CSV/PDF snapshots | Domain permissions, accounting/operational reports, CSV exports, immutable report snapshots, read-only pages, Filament actions, and registered internal HTTP actions are present. |
| M5.1 Payment execution | Implemented v1 + Italian bank exports | Supplier bank coordinates, payment runs, SEPA pain.001 XML export, CBI bonifici export, checksum metadata, and Filament resource are present. Ri.Ba/SDD generators cover customer receivable schedules; direct bank submission is a separate transport concern. |
| M6.1 Bank reconciliation | Implemented v1 + differences + bank formats | CSV, CAMT.053, and minimal MT940 import, manual match, suggestions, match-with-difference UI, and difference journal entries are present. |
| M6.2 Returns management | Implemented v1 + fiscal override + optional auto notes | Customer/supplier returns, DDT integration, returned-quantity tracking, manual NC/ND follow-up actions, optional auto NC/ND on completion, and invoice-line-based fiscal pricing are present. |
| M6.3 E-invoice stub | Implemented v1 + Phase 2C + Aruba operations | Provider binding, deterministic stub submission workflow, invoice actions, FatturaPA readiness fields, local XML/XSD validation, Aruba upload/polling/callback adapter, and polling command are present. |
| M7.1 Advanced pricelists | Implemented v1 + UI | Direct item and taxonomy fallback prices, validity windows, party rules, percent/fixed/override discounts, resolver, document-line integrations, PriceList resource, and Party price-rule UI are present. |
| Spec 2 Phase 2A | Implemented | Domain actions, state-aware policies, and Filament service-backed actions are present. |
| Spec 2 Phase 2B | Implemented | 2B-01/02/03/04/05/06/07/08/09/10/11/12/13 are done. |
| Spec 2 Phase 2C | Implemented | FatturaPA schema/readiness fields (2C-05), SDI/FatturaPA mapping (2C-02), FPR12 XML/XSD validation (2C-01), Aruba upload/polling/callback adapter (2C-03), polling command (6-03), and extended admin permissions (2C-04) are present. |
| Phase 3 internal actions | Implemented | Registered stateful ERP actions use Core's authenticated /app dispatcher and ERP policy authorization. External /api/v1 exposure is deferred. |
| Phase 4 commercial depth | Required scope implemented | Partner pools, Payment Requests, canonical Site/Place linkage, Task UI, and RFC 5545 ICS export are present. Gantt (4-08) and mobile API (4-13) are optional and unapproved. |
| Phase 5 enterprise depth | Implemented | Return reversal, e-invoice operations, Italian bank formats, report snapshots, FX/revaluation, Money, and analytic dimensions are present. |
| Phase 6 integration architecture | Implemented | Commands, outbox integration, extension contracts, and architecture documentation are present. |
Domain actions over HTTP
Filament is the administration and verification surface for developers and superadmins, not
the application UI. Real user interfaces consume the session-based /app surface, and the
operations that were previously reachable only as Filament actions are now registered as
domain actions on Core's generic route:
POST /app/crud/{action}/{module}/{entity} id and action payload in the body
ErpDomainActionRegistrar maps each action onto the service that already implements it, at
boot. Handlers stay thin: the transactional rules, locks and state guards stay in the
services and in ERPModelPolicy.
| Entity | Actions |
|---|---|
Invoice |
post, unpost, submitEInvoice, refreshEInvoice |
DeliveryNote |
post, unpost |
JournalEntry |
reverse — requires a reversal_reason in the payload |
FiscalPeriod |
close, reopen |
FiscalYear |
close |
SalesOrder |
amend |
Quotation |
create_revision |
DocumentSequence |
reset |
ReturnOrder |
approve*, complete, cancel, reverse_processed, create_credit_note |
SupplierReturn |
approve*, complete, cancel, reverse_processed, create_debit_note |
PaymentRequest |
send |
PaymentRun |
export_sepa, export_cbi_bonifici — stream a file |
Task |
export_ics — streams a file |
BankStatement |
import_file — consumes a multipart/form-data upload |
* approve overrides Core's generic verb, which votes on a pending Modification. Here it
advances the return from Draft to Approved. Both models declare the override, and neither may
ever take HasApprovals: the registry refuses that combination at boot.
Forcing the three-way match is not a separate action. It is a force_three_way_match
flag on post, carrying its own forcePost permission, mirroring the Filament form. A second
route would be a way to post that bypasses the normal path.
Not exposed: supersede, switch_context and reserve each have a seeded permission and
a policy method but no service behind them. A handler is not the place to invent the
behaviour, so they stay unregistered until the operation itself exists.
Posting is not a generic document state. post and unpost belong to Invoice and
DeliveryNote alone. A fiscal period opens and closes, a quotation and a sales order move
through their own status enum, a document sequence has no posting at all, and a journal entry
is posted as a consequence of posting the invoice that produced it. The pair used to be
declared on those five models too; the permissions were unreachable, because the dispatcher
resolves the handler before authorizing and an unregistered action is a 404, so they were
dropped rather than left on the role screen looking like a rule.
Every model listed above is governed by ERPModelPolicy. That is a precondition, not a side
effect: without a registered policy the Gate has nothing to consult and denies the action.
Point 0 Go-Live and Future Boundaries
- E-invoice defaults to the deterministic
stubworkflow. The optionalfatturapadriver generates and XSD-validates ordinary FPR12 XML locally, but it does not deliver to SDI. - The optional
arubadriver now follows the Aruba v2-style upload and notifications polling shape, supports optional auth signin, exposes a signed callback route, and records polling/callback/conservation metadata inEInvoiceSubmission.response_payload. It still needs verification against the contracted Aruba tenant before production go-live. - FatturaPA / SDI readiness fields now exist on company, party, and invoice records, sale e-invoice submit validates mandatory data,
FatturaPaAnagraphicMappermaps those fields into a FatturaPA-shaped neutral payload, andFatturaPaXmlBuildergenerates XML from it. Edge-case fiscal mappings still need dedicated fields and rules. - Legal retention is tracked only as provider metadata such as
pddAvailable; full conservazione governance, retrieval/esibizione workflows, and contract-specific retention checks remain go-live/backlog work. - Supplier payment execution exports SEPA SCT
pain.001and CBI bonifici files. Ri.Ba and SDD CORE generators exist for customer receivable schedule lines, with SDD mandate validation onPartyBankAccount. Direct bank/API submission and bank-specific certification remain outside ERP. - Bank import supports CSV, CAMT.053, and a minimal MT940 subset; it is not full bank API sync and it does not auto-confirm matches without the reconciliation workflow.
- Financial report snapshots store immutable payload, CSV, PDF content, hash, and parameters through
erp_report_snapshots; the built-in PDF renderer is intentionally simple and dependency-free. - Multi-currency now has
ExchangeRate,DatabaseCurrencyConverter, andFxRevaluationServicefor historical/inverse FX rates and unrealized revaluation journals on open foreign-currency schedules. External FX feed imports and realized FX settlement automation remain later enhancements. - Money math now has a
Moneyvalue object built onDecimal; analytic accounting has company dimensions, dimension values, and a journal-line pivot with allocation percentage. - Domain actions are exposed on the internal
/appsurface through Core's genericPOST /app/crud/{action}/{module}/{entity}route. External/api/v1actions and ERP-specific resources remain deferred until a concrete consumer and governance contract are approved. - Processed returns can be safely reversed before any linked credit/debit note exists: the generated DDT is unposted, returned quantities are restored, and the return goes back to approved. Linked fiscal notes must be handled explicitly first.
- DDT/bolle lines intentionally do not carry prices or costs. Fiscal corrections price from source invoice lines, not from DDTs, orders, goods receipts, or current price lists.
- Reports remain live-query in Filament, but financial report snapshots now archive immutable payload/CSV/simple-PDF rows. Rich paginated PDF design and operational report snapshot scheduling remain enhancements.
Moneyexists for decimal-safe amount/currency arithmetic, and journal lines support analytic dimensions. Full refactoring of all legacy money helpers and analytic reporting cubes remains future work.- Application lock-chain guards cover all supported databases. MySQL/MariaDB and PostgreSQL add DB triggers; SQLite and Oracle rely on application guards for this specific defense.
- MES remains a separate vertical. External-source importers are a separate workstream. Gantt and mobile API are optional; calendar/ICS export and partner-pool settlement are implemented. External payment execution remains separate.
Roadmap
- ERP Point 0: approved mandatory non-external-API scope is implemented; see
docs/STATUS.md. - A new implementation wave starts only after explicitly approving external API governance, optional Gantt/mobile scope, or another independent requirement.
Scripts
Development fixtures
Modules/ERP/database/factories/ holds twelve model factories (company, anagraphics, fiscal
frame, document chain), resolved through newFactory() on each model. They never write a document
reference: numbering belongs to DocumentNumberAllocator at posting time. ->for($company)
keeps a whole document chain inside one company, which the models require.
DevERPDatabaseSeeder seeds a demo company with both commercial flows
(sales order to delivery note to invoice; purchase order to goods receipt to invoice), on stable
identifiers (DEMO-ITEM-001..003, warehouse DEMO-WH) and idempotently.
The ERP module exposes the same Composer script conventions as Cms and Core:
The module has no runner of its own: its tests extend the application's TestCase
and it has no vendor/, so everything below runs from the application root.
Code quality and testing
php artisan test --compact Modules/ERP/tests # the whole module
php artisan test --compact Modules/ERP/tests/Unit # one suite of it
php artisan test --compact --testsuite=Unit # the fast suite, every module
composer test # Full test and quality pipeline
composer test:type-coverage # Type coverage (target: 100%)
composer test:typos # Peck typo check
composer test:lint # Pint + Rector dry-run
composer test:types # PHPStan
composer test:refactor # Rector
Document-number concurrency coverage is split between the normal 8-worker feature test and DocumentNumberConcurrencyStressTest, an opt-in 50-worker test. The stress harness forks with pcntl, synchronizes worker start, and uses an isolated temporary SQLite WAL database; it does not migrate or query the configured application database. Run it explicitly with RUN_ERP_STRESS_TESTS=1 php artisan test --group=stress. Without the flag or pcntl, it is skipped by design.
Maintenance
composer lint # Rector, Pint, IDE helpers
composer check # PHPStan
composer fix # PHPStan with fix
composer refactor # Rector
composer update:requirements # composer bump + npm-check-updates
Releases
Releases are cut by the maintainers with private tooling; do not bump versions or edit CHANGELOG.md by hand.
Repository rename (from laraplate-business)
If you still have a clone using the old remote:
git remote set-url origin git@github.com:getlaraplate/laraerp.git
# or HTTPS: https://github.com/getlaraplate/laraerp.git
In the parent Laraplate app, update .gitmodules to path = Modules/ERP and url / pushurl for laraerp, then run git submodule sync and composer update.
Contributing
If you want to contribute to this project, follow these steps:
- Fork the repository.
- Create a new branch for your feature or fix.
- Open a pull request.
License
ERP module is open-sourced software licensed under the GNU AGPL v3.
TODO
- M6.1 — Bank reconciliation and statement import
- M6.2 — Returns management (customer and supplier)
- M6.2/2B-07 — Invoice-line-based return credit/debit note pricing contract
- M6.2/2B-06 — Optional automatic NC/ND creation on return completion
- M6.3 — E-invoice stub workflow
- M7.1 — Advanced pricelists with party-specific pricing
- M4 — Policies, permissions, and reporting pages
- Spec 2 Phase 2A — State-aware policies and Filament domain actions
- Spec 2 Phase 2B — Party pricing UI, PriceList UI, quotation unlock, document sequence reset, return fiscal override contract, optional auto NC/ND, banking depth, financial CSV export, operational dashboard polish
- Spec 2 Phase 2C — FatturaPA / SDI schema, mapper, XML/XSD validation, configurable Aruba adapter, and extended admin permissions
- Phase 3 internal domain actions — authenticated Core
/appdispatcher with ERP policy authorization - External
/api/v1action/resources governance — deliberately deferred until a consumer contract is approved - Optional Gantt/mobile capabilities (
4-08,4-13) — not approved for implementation - Comprehensive accounting golden-master tests
- Export CSV for financial reports