Project Invoice Reconciliation
Use this process when a project, onboarding, audit, or other one-off invoice was created directly in Xero and Commercial Margin needs to reconcile that posted revenue to the CRM Deal and its delivery labour.
Recurring managed-service invoices follow the MSP invoicing process instead.
Record Ownership
Project / onboarding
Xero Invoice -> CRM Invoice -> Commercial Deal <- Delivery Model <- Timelog
Recurring MSP
Xero Invoice -> CRM Invoice -> MSP Plan <- Plan timesheet scope <- Timelog
- Xero owns the posted financial invoice and its actual line revenue.
- CRM Invoice mirrors the Xero identity and carries one commercial attribution.
- Deal is the stable project/onboarding container.
- Delivery Models define scoped expected labour and map actual timelogs back to that Deal.
- MSP Plan is the recurring-service container and is not used for a one-off project invoice.
The CRM Invoice does not link to a Delivery Model. A Deal can have several Delivery Models as work is split into audit, onboarding, or later delivery budgets, but the invoice still belongs to the same commercial project.
Before You Start
Confirm:
- The Xero invoice is an exclusive-tax sales invoice in
AUTHORISEDorPAIDstatus. - The target CRM Deal represents the same signed project or onboarding scope.
- The Deal has the correct Company.
- The Company has the Xero Contact ID used by the invoice.
- Each Xero invoice line has an Item Code that exists as a CRM Product Code.
Do not proceed when one invoice covers several Deals. That requires manual review until line-level allocation is implemented.
Preview
Run without --apply first:
python -m xero_sync.project_invoice_mirror \
--invoice-id <xero-invoice-uuid> \
--deal-id <crm-deal-id>
Review the invoice number, Deal, Company, line count, and subtotal. A failed validation makes no changes.
Apply
After the preview is correct:
python -m xero_sync.project_invoice_mirror \
--invoice-id <xero-invoice-uuid> \
--deal-id <crm-deal-id> \
--apply
The command creates a CRM Invoice when the Xero ID is new. If a CRM Invoice
already has that Xero_Invoice_ID, it links the existing record or reports it
unchanged. It refuses to overwrite another Deal link and refuses an Invoice
already attributed to an MSP Plan.
The new CRM Invoice contains:
- Company and
Commercial Deallookups; - Xero invoice ID and number;
- invoice and due dates;
- mirrored descriptions, quantities, unit prices, discounts, and tax;
Xero Sync StatusSynced;update_sourcexero_project_invoice_mirror.
The command never creates, updates, approves, or sends anything in Xero.
Verify Reporting
After applying and refreshing Power BI:
- Drill through from the client to Client Profile.
- Check the service-to-date outcome row for the Deal. It answers whether the project made or lost money across the current model window.
- Check the monthly service table to see when revenue and labour cost occurred.
- Check the Xero invoice-line detail and actual labour detail for traceability.
- Check Data Quality for
Incompleteattribution conflicts.
The service-to-date measures ignore a month selection but remain bounded by 1 January 2025 through the report refresh date.
Exceptions
- Both
Commercial DealandMSP Planpopulated: clear the incorrect lookup. Reporting retains both IDs and marks the linesIncompleteuntil resolved. - No CRM Product for a Xero Item Code: create or map the correct shared Product; do not substitute an unrelated Product just to make the import pass.
- Xero contact differs from the Deal Company: correct the Account/Deal mapping before importing.
- One invoice spans several Deals: leave it for manual review until a line-level allocation structure exists.
- The invoice uses inclusive line amounts: leave it for manual review until the CRM mirror explicitly supports that tax presentation.
References
- Architecture and calculation:
docs/reports/Margin Calculation Approach.md - Technical implementation:
docs/dev/xero_crm_invoice_sync.md - Code:
xero_sync/project_invoice_mirror.py - Tests:
tests/test_project_invoice_mirror.py