Browse the docs

Imported from laraerp/docs/rag/MODULE.mdEdit it in the module repository to change this page

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: stub by default; supported values are stub, fatturapa, aruba
  • ERP_EINVOICE_ARUBA_BASE_URL: Aruba API base URL, for example the contracted demo/production web-service host
  • ERP_EINVOICE_ARUBA_AUTH_BASE_URL: optional Aruba auth base URL when using username/password token acquisition
  • ERP_EINVOICE_ARUBA_UPLOAD_PATH: upload endpoint path, default /services/invoice/upload
  • ERP_EINVOICE_ARUBA_NOTIFICATIONS_PATH: notifications polling endpoint path, default /api/v2/invoices-out/notifications
  • ERP_EINVOICE_ARUBA_TOKEN: bearer token used by the Aruba adapter; if missing, username/password auth is attempted
  • ERP_EINVOICE_ARUBA_USERNAME / ERP_EINVOICE_ARUBA_PASSWORD: optional credentials for Aruba /auth/signin
  • ERP_EINVOICE_ARUBA_CALLBACK_API_KEY: optional static API key expected on Aruba callback requests
  • ERP_PAYMENT_REQUEST_DRIVER: payment-request provider binding (stub currently)
  • ERP_PAYMENT_REQUEST_STUB_CALLBACK_API_KEY: enables and authenticates stub provider callbacks; empty means disabled
  • ERP_EINVOICE_ARUBA_SIGNATURE_CREDENTIAL / ERP_EINVOICE_ARUBA_SIGNATURE_DOMAIN: optional Aruba signature parameters
  • ERP_EINVOICE_ARUBA_SENDER_PIVA: optional sender VAT override; defaults to company fiscal country + tax id
  • ERP_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:

Module metadata

  • Priority: 999 in module.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

  • companies table with functional currency
  • BelongsToCompany trait + global scope for tenant isolation
  • Dual-currency columns via ERPMigrateUtils::moneyColumns() (amount_doc / currency_doc / amount_local / fx_rate)
  • CurrencyConverter service (no-op in M0, pluggable for live FX)

M0 — Versioning

  • HasVersions trait with VersionStrategy::DIFF on accounting models
  • Immutable version strategy on contabile models (cannot be overridden via settings)
  • Core ForcedVersionStrategySettings removes stale version_strategy_erp_* controls from Filament for class-forced DIFF models without deleting their rows

M1 — Accounting Backbone

  • Chart of Accounts (accounts table, Account model, ChartOfAccountsProvider interface, Italian PDC default)
  • Journal Entries (JournalEntry / JournalEntryLine, JournalPostingService::post/reverse, immutability after posting)
  • JournalEntry, JournalEntryLine, VatRegisterEntry, StockCostLayer, StockLevel and StockMovement implement Core RestrictsCrudWrites (via DeniesGenericCrudWrites): generic CRUD writes on them answer 403; they change only through their owning services
  • Fiscal Calendar (FiscalYear, FiscalPeriod, FiscalPeriodCloser with re-open audit)
  • Document Sequences (DocumentSequence, DocumentNumberAllocator, DocumentType enum with gap policy)

M2 — Tax & Invoice Foundation

  • Tax Codes (TaxCode, TaxKind enum, immutable fiscal snapshot, TaxLineCalculator, TaxCodeSupersessionService)
  • ItalianTaxCodesSeeder baseline
  • 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 / SalesOrderLine with lock-chain progression
  • SalesOrderEvasionService (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_layers tables
  • StockMovementService with 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 / DeliveryNoteLine with SO line linking
  • DeliveryNoteInventoryService (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, nullable warehouse_id, source_type + source_id (opaque owning document), quantity (decimal 15,4, DB check keeps it positive), state (soft / hard / consumed / released, StockReservationState), nullable expires_at, timestamps with soft delete. Indexes: (company_id, item_id, state) for the availability sum, (source_type, source_id) for lifecycle lookups.
  • StockReservation is company-scoped (BelongsToCompany), not versioned, and implements RestrictsCrudWrites (generic CRUD writes answer 403): it changes only through StockReservationService. source_type / source_id are 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) and mes.production_order_material (MES, see the MES module doc).
  • soft and hard are the live states; consumed (already reflected in the on-hand drop) and released are 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: mode is soft or hard; expiresAt applies only to soft (null means it does not lapse), a hard row never expires. Throws InsufficientStockException when the quantity exceeds availability, ValidationException for a non-positive quantity or an item/warehouse of another company, LockTimeoutException when the item lock cannot be taken.
  • promoteToHard(sourceType, sourceId, companyId): turns the source's live soft rows into hard and clears expires_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's hard rows only (the part consume() can close).
  • release(sourceType, sourceId): gives back every soft/hard row of the source; idempotent.
  • consume(sourceType, sourceId, quantity): closes quantity of the source's hard rows oldest first, splitting the last row when it is only partly consumed; ValidationException above 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, consume and reservedQuantity rely 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) runs ReserveStockForConfirmedSalesOrder, 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-reserves min(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 from reserve() 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 SalesOrderCancelled event (fired when the status changes to cancelled) runs ReleaseStockForCancelledSalesOrder, which releases every soft/hard row of each line. Consumed rows are terminal.
  • Evasion. Posting a delivery note records the outbound StockMovements and then SalesOrderEvasionService::registerDelivery, in the same transaction. For each item-backed line it consumes min(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 stays hard. 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 whose expires_at has passed, in one guarded UPDATE that 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 (env ERP_STOCK_RESERVATION_SOFT_TTL, minutes, default 1440) 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 - reservations only: 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

  • InvoicePostingService with full journal post / unpost cycle (observer-driven via posted_at)
  • DocumentNumberAllocator integration 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_line for flexible Invoice↔DDT linking, mapped by Models\Pivot\InvoiceLineHasDeliveryNoteLine with 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_at editing)
  • Posting is refused with PostingToClosedFiscalPeriodException when the posting date falls in a closed fiscal period: InvoicePostingService resolves the covering period of the company and JournalPostingService rejects it. Reopen the period (Filament Reopen action or the reopen domain action) before posting
  • force_post is 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 as MatchStatus::Forced
  • See docs/ERP_GUIDA_SEMPLICE.md §4.7 and docs/rag/MODULE.md § Invoice posting workflow for end-to-end flows

M3.6 — Purchasing Cycle

  • Unified Party entity (replaces Customer) with is_customer / is_supplier boolean flags
  • Party type validation on all models (saving callback) + filtered Filament dropdowns
  • PurchaseOrder / PurchaseOrderLine, GoodsReceipt / GoodsReceiptLine
  • ThreeWayMatchService with configurable price / quantity tolerances (wired in InvoicePostingService on purchase posting)
  • MatchStatus enum (matched, tolerance, forced, unmatched) persisted on invoice_lines with match_discrepancy JSON
  • Filament Force three-way match checkbox on purchase invoice Post action
  • Per-company settings via ErpCompanySettings on companies.settings JSON (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 $fillable minus LOCKED_COMMERCIAL_FIELDS so 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

  • PaymentTerm model (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, and PaymentRunLine for supplier payment runs
  • PaymentRunBuilderService builds draft outbound supplier payment batches from open purchase schedule lines
  • SepaPain001Exporter exports approved supplier payment runs as auditable SEPA SCT pain.001 XML files
  • Payment execution is file generation only: no direct bank/API submission, no CBI/Ri.Ba/SDD in v1

M5.2 — Credit & Debit Notes

  • InvoiceType enum (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)

  • VatRegisterEntry with progressive protocol numbers per register / year
  • VatRegisterService (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

  • EInvoiceProvider contract (prepare, submit, validateXml, remoteStatus)
  • EInvoiceSubmission model + EInvoiceSubmissionStatus enum
  • 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 local fatturapa driver validation
  • aruba driver: production-oriented adapter for Aruba v2-style upload (dataFile), notifications polling, optional auth signin, provider callbacks, and conservation availability metadata in response_payload
  • POST /api/v1/erp/einvoice/aruba/callbacks applies signed provider callbacks when ERP_EINVOICE_ARUBA_CALLBACK_API_KEY is configured
  • erp:einvoice:refresh-statuses polls open submissions for the configured provider and supports --company, --provider, --limit, and --dry-run
  • erp:health-check --format=table|json performs read-only installation and operational checks for the default company, chart of accounts, fiscal calendar, domain permissions, current-year sequences, and e-invoice configuration
  • erp:sequences:audit --company=ID --year=YYYY --format=table|json cross-checks persisted invoice/order references against sequence formatters, counters, duplicates, and gap policy without repairing data
  • erp:bank-statements:import --bank-account=ID --path=FILE_OR_DIR --format=auto --dry-run validates or imports CSV, CAMT.053, and MT940 files idempotently by SHA-256 checksum; --archive-path explicitly enables post-import file moves
  • erp:vat-settlements:compute --company=ID --year=YYYY --period=YYYY-N --dry-run previews 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 stub driver remains default; fatturapa generates 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, and BankStatementLine models
  • 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
  • BankDifferenceJournalService posts balanced journals against bank_cash and the selected difference account
  • BankStatementLine.difference_journal_entry_id keeps audit linkage to the accounting adjustment

M6.2 — Returns Management

  • ReturnOrder for customer returns and SupplierReturn for 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_complete creates NC/ND drafts during return completion
  • Customer return credit notes use ReturnOrderLine.invoice_line_id and the source sales InvoiceLine.unit_price; ReturnOrderLine.unit_price is an optional manual override.
  • Supplier return debit notes use SupplierReturnLine.invoice_line_id and the source purchase InvoiceLine.unit_price; purchase_order_line_id and goods_receipt_line_id remain 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 / PriceListItem with validity windows and Filament resource
  • Each price-list row targets exactly one direct Item or one Activity taxonomy; direct item prices take precedence and taxonomy prices are the fallback
  • PartyPriceRule with percent, fixed, and override discounts
  • Party::price_rules() relation and Party Filament relation manager
  • PriceResolverService and InvoiceLinePricingService apply commercial pricing to quotation / sales order / invoice lines
  • Activity is the concrete ERP taxonomy used for activity-based price rules; do not point UI directly at abstract Core Taxonomy.

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, and erp.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

  • Movement is an adapter over double-entry journals with positive amount, date, document/local currency snapshots, income/expense/contribution/withdrawal type, explicit economic counterparty, and unique posted_journal_entry_id.
  • MovementPostingService locks and posts idempotently. Income: debit bank_cash, credit Revenue. Expense: debit Expense, credit bank_cash. Contribution: debit bank_cash, credit Liability. Withdrawal: debit Liability, credit bank_cash. Counterparty company/kind/activity are validated.
  • CashBalanceService sums posted bank_cash journal 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.
  • MovementResource exposes index/create/view only. CreateMovement wraps row creation and MovementPostingService::post() in one transaction; no edit route or parallel balance write exists.

Partner Pools and Settle-up

  • PartnerPool is company/currency scoped and relates Core users through first-class pivot PartnerPoolHasUser.
  • MovementAllocation stores exact owed/paid amounts. Both aggregate totals must equal the expense movement amount.
  • PartnerPoolSettlementService owns 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:import accepts only the ERP BulkImporterInterface; 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.
  • ExternalCashMovementImportService and ExternalExpenseAllocationService use Core RecordOriginRegistry: stable source identity plus normalized SHA-256 fingerprint makes reruns idempotent. Changed unposted data may update; changed posted data throws PostedImportConflict with 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

  • PaymentRequest targets 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; StubPaymentRequestProvider is deterministic and performs no I/O.
  • PaymentRequestService locks 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, or PoolTransaction records.
  • Both provider callback routes (this one and erp/einvoice/{provider}/callbacks) sit behind Core's expose_api switch (403 while it is off, the default) and are marked SelfAuthenticatedApiRoute: 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-address api rate limit (core.api.rate_limit_per_minute). See Modules/Core/docs/rag/API_ACCESS_CONTROL.md.

Task Calendar Export

  • TaskIcsExporter emits 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 canonical Site::place() address/postcode/city/province/country.
  • Task casts validity columns to immutable datetimes; nullable project_id/site_id are declared before FK constraints in the original migration for cross-driver correctness.
  • TaskResource exposes CRUD and a streamed text/calendar download action. No calendar provider API or two-way synchronization is implemented.

Quotation Revisions and Project Locks

  • QuotationRevisionService locks the source, rejects editable drafts/branches/version overflow, creates a draft successor through unique revises_quotation_id, increments version, and snapshots all quotation items transactionally.
  • Quotation::revised_from() / revision() expose the linear chain. Filament create_revision uses the service and opens the successor draft.
  • Operational sales-order states lock both linked quotation and project. Project uses HasLocks, ORM/Filament guards, and MySQL/MariaDB/PostgreSQL trigger reinforcement. The chain triggers write locked_at and leave locked_user_id null, 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's RECORD_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 ERPModelPolicy guards domain actions on top of Core CRUD permissions.
  • Domain abilities seeded in ERPDatabaseSeeder include 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.001 and 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.
  • PaymentRunBuilderService selects open/partial purchase invoice schedule lines and creates a draft payment run.
  • PaymentRunLine stores a beneficiary snapshot: name, IBAN, BIC, amount, due date, and remittance text.
  • Payment runs move through draft -> approved -> exported; cancelled is 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.001 and CBI bonifici text export; bank submission remains outside ERP.

Filament Admin UI

  • SiteResource manages company/name/validity and selects the canonical Core Place; ERP does not duplicate postal/geographic fields.

  • Site uses Core HasValidity, exposes place(): 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 stub workflow. The optional fatturapa driver generates and XSD-validates ordinary FPR12 XML locally, but it does not deliver to SDI.
  • The optional aruba driver 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 in EInvoiceSubmission.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, FatturaPaAnagraphicMapper maps those fields into a FatturaPA-shaped neutral payload, and FatturaPaXmlBuilder generates 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.001 and CBI bonifici files. Ri.Ba and SDD CORE generators exist for customer receivable schedule lines, with SDD mandate validation on PartyBankAccount. 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, and FxRevaluationService for 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 Money value object built on Decimal; analytic accounting has company dimensions, dimension values, and a journal-line pivot with allocation percentage.
  • Domain actions are exposed on the internal /app surface through Core's generic POST /app/crud/{action}/{module}/{entity} route. External /api/v1 actions 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.
  • Money exists 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:

  1. Fork the repository.
  2. Create a new branch for your feature or fix.
  3. 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 /app dispatcher with ERP policy authorization
  • External /api/v1 action/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
100%