Project Invoice Reconciliation
Use this process to attribute July-2026-onward Xero invoices to their commercial services. MSP invoices already linked to an MSP Plan retain that attribution. A non-MSP invoice can belong to an existing project, recurring Deal or a reporting-only service. This process does not create Deals.
Commercial Services historical capture
The current browser dashboard is a draft proof of concept for iterating and experimenting with commercial modelling. Its figures and assumptions are provisional; precise presentation and exhaustive issue diagnosis are secondary to testing the model and keeping refresh/recalculation usable.
The browser forecast includes Commercial Services with Review_State = Confirmed.
Its Commercial services page shows the selected-period service overview.
Performance reports recorded results; Forecast reports signed revenue, delivery
costs and contribution, separating whole contracts for one-off work from the
current financial year through June for recurring services. An optional salaried-delivery allocation leaves external
consultant fees included in both views. Open Performance or a service Activity section for invoice,
work and delivery-reserve detail beginning July 2026. The forecast is before
fixed payroll and overheads.
Billing configuration determines component identity in preference to conflicting
agreement text or older invoice Product names linked through a configured charge.
For MSP, Seat holds all service labour. Expected monthly effort is configured seats × 15 minutes for L1, 30 minutes for L2 or an indicative 1.5 hours for L3 per seat per month, prorated by calendar days within charge dates. L3 support is unlimited; the indicative expectation is used for planning and reporting, not as a support limit. Planned and recorded hours cost an estimated $50/hour for every Seat line; Expected Labour Tier has no effect on MSP. Lines are priced individually before grouping by Product. Private staff rates and the source snapshot's labour costs do not change this standard estimate. The Activity detail states the $50/hour basis. The fixed Plan hours and separate website labour budget no longer drive this browser review. An MSP service can be confirmed before its first billing date. Dates outside its configured allowance add no expected hours, while any recorded work still costs $50/hour. The report retains the configured Seat identity in those periods.
Products other than Seat and hourly Consulting use their cost price, including explicit zero, for both
expected and actual cost on the same signed billed quantity. A missing cost price
uses 90% of signed net revenue for both columns, including Product credits.
The explicit MSP-DISCOUNT and MSP-NFP-DISCOUNT Products are price adjustments:
they reduce revenue once and have no cost, regardless of Product cost price.
Seat and Consulting labour create the cost difference. Recorded MSP Seat hours
are costed at $50 even if the source staff cost is missing.
Internal Consulting uses the line tier rate for planned and recorded hours;
External Consulting uses its explicit supplier hourly cost, including overruns.
Current configuration is a scenario over the selected period, not a verified
historical budget. The calculation is reusable for other Plans without bespoke
cost classifications; CRM confirmation determines which services enter the review.
The review validates complete documents for the selected customers. Missing
selected-service history, ownership conflicts, native invoice mismatches or invalid
document totals stop refresh. Changes to historical source fingerprints are
non-blocking warnings in this draft: current Xero values are used with retained
service attribution;
unrelated customers’ capture discrepancies do not block this scoped review. The
full business report keeps its existing reconciliation checks.
This presentation does not change source records or invoice reconciliation.
Implementation: app/commercial_margin/service_review.py; regression coverage:
tests/test_commercial_service_review.py.
Product-led lines and pending setup labour
A Commercial Service can combine purchased and labour Products on shared
Service Lines. The attached Products choose their cost treatment; a Plan is an
operational billing relationship. The browser
costs a purchased-only service when all configured and billed Products are
Purchased product/licence or explicit discount Products and no labour is
recorded against that scope. It uses the same signed billed-quantity cost rule
as MSP non-labour items. An unknown classification or recorded work keeps the
service's margin unavailable until reviewed.
ARACY's single Commercial Service owns the two laptops and the AUD 599 setup
fee on two lines. The laptop Product supplies its purchase cost. The Consulting
Product supplies the setup labour treatment. Selling quantity one at AUD 599
does not establish planned hours. Setup's Labour Budget Type is Fixed budget
and Expected Labour Hours records the agreed delivery budget. When that budget
is blank, margin including staff allocation stays unavailable; contribution
before staff allocation still includes the known laptop costs. Confirmation approves ownership and configuration; it does not turn a
pending line cost into zero or require splitting the service.
For hardware setup preparation, estimate hours separately for each setup-fee
line: divide its total selling amount excluding GST by AUD 200 and round up to
the next whole hour. Three setup fees sold together at AUD 299 give a line total
of AUD 897 and a five-hour budget. Keep that as one setup line with selling
quantity three; do not round each device separately. Save the result in
Expected Labour Hours. The report reads the saved budget; it does not derive
hours automatically from the invoice price. Callouts and project-day budgets
need their own treatment.
A hardware service with a configured Consulting Product keeps its attributed recorded work in the service margin. Hardware scopes without configured labour retain the historical deferred-work treatment; Home labour remains deferred. This avoids dropping actual setup time when a hardware service enters the Product-led model. Recorded overruns can therefore exceed the setup estimate.
A reviewed hardware price discount reduces the configured hardware selling amount without reducing the purchased quantity or supplier cost. Retain the original purchase and discount rows in Xero. A native discount item can reference the hardware Service Line while retaining an explicit discount Product with zero cost. This requires a negative discount amount, positive quantity, a purchased Product on the configured line, and exactly one positive purchase item on that line in the same invoice. Other Product mismatches remain blocked. This allows one discounted hardware line and one setup line without adding another configured line for the discount.
The optional line budget type also supports Recurring allowance, whose
Expected Labour Hours are per calendar month. This explicit rule takes
precedence over service Arrangement. Blank type retains the legacy Consulting
quantity convention. The Product-driven pilot supports one Consulting labour
line with purchased items or Seat lines with purchased items, independent of
Plan presence. Several labour components require explicit work allocation.
The commercial_services.service_line_labour.ensure_fields installer adds only
the two optional budget fields, validates their types and choices, and verifies
readback. It preserves existing records and rejects conflicting field
definitions. Existing lines retain their interpretation until a Labour Budget
Type is explicitly selected. Tests: tests/commercial_services/test_service_line_labour.py.
For new invoices, complete native item mappings identify the service and Service Line without a historical capture or an entry in the migration list. Every issued Xero line must map exactly once. The report validates source line IDs, customer, Product, charge ownership and issued quantities, prices, descriptions and amounts. New native invoice headers must also reconcile their dates, tax and totals to Xero. Multiple issued items can reference one configured package line without duplicating its labour budget.
For previously archived invoice ownership, native mappings can replace ownership while the archive's source fingerprint and Quote evidence remain in force. A gap in an archived document cannot be filled by treating it as new history. The explicitly migrated invoices retain their required reviewed coverage. Elsewhere, unknown service coverage stays blank; supplied dates must form an ordered pair, and invoice date still determines billed revenue. An invoice spanning several services leaves its Commercial Service header blank and assigns each item separately. Duplicate native invoices claiming the same Xero document on this attribution path block refresh. These reads and attribution edits do not issue invoices or change Xero.
Percentage-discounted historical items remain on the reviewed reporting-link
path while the native item reader does not support percentage discounts. An
approved setup can save the service and configured-line lookups on the CRM item,
preserving its original Product, quantity, price and discount. Leave its Xero
Line Item ID unset on this path, and use the exact invoice/item assignment in
config/commercial_services.json for reporting. That assignment validates the
service and source/target Product identities; the historical source fingerprint
continues to protect issued values. It does not add a new historical capture or
change Xero. A later native conversion requires its own adapter validation.
When correcting a Product link on an existing CRM invoice item, explicitly retain its issued description, quantity, unit price and discount in the update. CRM can otherwise replace descriptions and prices with catalogue defaults, even when the supplied Product ID is unchanged. Verify the saved invoice lines, tax and totals against the original issued values before confirming the service.
Implementation: app/commercial_margin/native_items.py, service_review.py,
price_adjustments.py, services.py.
Tests: tests/test_commercial_margin_native_items.py,
tests/test_purchased_service_review.py, tests/test_contribution_report.py.
External consultant product and migration
Use External Consulting (CONSULTING-EXTERNAL) for hourly supplier delivery.
Set External Hourly Cost on the Service Line in AUD excluding GST; both planned
and recorded hours use this rate. The budget is an hours estimate, never a cap on
the supplier fee. Overruns increase cost, and dated estimates include further
work still expected. Internal Consulting (003) retains its salaried allocation.
The forecast toggle only removes that internal allocation.
The scoped migration creates the external Product and optional hourly-cost field,
then switches only explicitly selected existing Consulting lines. It preserves
their selling quantities, selling prices, budget types/hours, dates, and ownership.
Initial rates preserve each line's existing tier cost. Cercle's recurring retainer
and Gidget Performance Reporting Phase 5 are the selected initial services.
Both CRM products keep the existing Xero account mapping; invoice generation
uses the shared Xero item 003 and invoice label Consulting. No Xero or historical
invoice records are changed by this migration.
Historical generic Consulting items follow their service's sole external line for reporting. Several internal/external lines require explicit work allocation. The migration keeps private before-images, uses conditional updates with optional workflows disabled, verifies readback and recognises already-completed switches. A changed Product, line or supplier rate stops the migration for review.
Implementation: commercial_services/external_consulting.py; tests:
tests/commercial_services/test_external_consulting.py.
Service Role migration
An operator runs commercial_services.service_role_migration to classify
Commercial Services whose Service_Role is exactly Recurring agreement and
whose Legacy_MSP_Plan lookup points to an existing Managed Service Plan.
The migration changes only their Service Role to MSP, regardless of Review
State or Lifecycle. Unlinked recurring agreements and other roles retain their
values. Linked Plans, service lines, invoices, pricing and Review State are not
changed.
The command captures a private preview and full before-images before applying. It adds the MSP picklist option when missing, preserving existing choices. Record writes suppress optional workflow triggers and use the current modification timestamp. Changed service fields or an unavailable Plan stop the batch. Readback verifies the saved role and unrelated business fields; a retry recognises already-migrated records. Private journals retain each write and its verification. A final scan checks roles, Plan links and Review States throughout the captured service scope. Refresh report sources after the migration.
python -m commercial_services.service_role_migration .local/service-role-migration
python -m commercial_services.service_role_migration .local/service-role-migration --apply
Implementation: commercial_services/service_role_migration.py; tests:
tests/commercial_services/test_service_role_migration.py.
Reading dashboard and service activity
Commercial services is the landing page, with a period overview and one searchable, sortable service list. Revenue follows the selected period. Delivery cost and Actual contribution include provisions for Project, Onboarding and Recurring agreement roles, or a labelled planned estimate where opening history is incomplete. Needs attention identifies missing costs/budgets and labelled cost estimates. Service Role values do not create review comments or attention flags. Its overview matches the visible list; missing amounts are not converted to zero.
Service Role on the Commercial Service determines browser categories.
Use MSP, Project or Hardware for those service types; Onboarding, Ad hoc, Home
and other saved roles can be separate filters. Products and service-line rules
still determine costs for mixed services. Hardware sale displays as Hardware.
Onboarding retains its own category and uses the same delivery provision,
planned-estimate fallback and allowance carry-forward rules as Project.
Recurring agreement remains available for services using that role.
Blank or Unconfirmed roles are Unclassified and remain in All. Change the role
on the service in CRM, then refresh source data. Dashboard navigation does not
edit CRM records or install picklist values. Business_Category retains its
separate existing uses and is not a fallback for the browser role.
Performance shows the recorded monthly/cumulative trend and financial table: revenue, labour, purchases, incurred costs, provision/planned adjustments, combined delivery cost and headline Actual contribution. Opening/closing reserves are separate boundary balances. Financial YTD defaults through the latest complete saved month; partial months are labelled. Single month, custom periods and available history use the same saved data beginning July 2026. YTD requires history covering its start. Monthly mode compares the last two complete months. Cumulative starts at the chosen range beginning; running balances are not summed. Rates divide total contribution by total revenue; rate changes are percentage points.
Forecast separates recurring FY expectations from one-off whole contracts, with role-level costs, largest contributors, losses and missing configuration. It uses configured quantities, selling prices and purchase/labour budgets, including unbilled work. Recurring lines project through June, limited by service and line dates. Recorded external-consultant overruns increase their fee. Include estimated salaried delivery cost is on by default in Forecast and service Summary. External fees and purchases remain included in either setting; recorded results and the attention review retain all cost types. Fixed payroll and overheads are outside the forecast. Missing history does not block configured expectations. See forecast calculation rules.
Role/customer filters persist across pages. Select a Performance month to open its service list with the same role/customer filters; local search and attention refinements clear to show the contributors. Click a service for Activity, or a forecast contributor for Summary. The service's name, customer, role and lifecycle remain visible. Summary shows the forecast and costs; Activity shows dated work; Lines & evidence shows configured lines, cutoff budgets, components and invoices. Where one configured line owns a Product component, its budget row uses that service-line name with the Product underneath. Explicitly reviewed historical invoice items can be attributed to that line for reporting when a Product was replaced. Support revenue then uses the line's labour costing once, without an additional purchased-Product estimate for the old name. Original invoice Products and billed quantities stay visible in the evidence. A conflicting service, Product or existing charge link requires review; the report does not guess from the generic Product name or change CRM/Xero records. The service identity stays fixed. Changing shared filters returns to the originating report instead of selecting another service. The return link restores report filters, dates, sorting and position; reload/browser navigation preserve URL state. Detail dates initially inherit the report and can then vary independently.
Invoice revenue follows document date and labour follows work date. Line budgets stay at the full snapshot cutoff when activity dates change. Monthly results charge reserve increases and release decreases without resetting effort. A month with revenue and no recorded work is valid. For Project, Onboarding and Recurring agreement services, owed delivery still reduces the headline contribution. Missing required costs leave complete margins unavailable. See monthly service activity.
The header displays the saved timestamp and refresh status. Data & refresh contains source collection scope, source refresh and saved-data recalculation; role/customer filtering changes only display selection. The previous successful snapshot remains visible during refresh and failure with its actual scope and source timestamp while its source mappings remain compatible. After a mapping change, the report waits for refreshed sources before rendering new figures.
Implementation: templates/commercial_margin.html,
commercial_margin_dashboard.js, commercial_margin_model.js,
commercial_margin_detail.html, commercial_services/service_roles.py.
Tests: tests/test_commercial_margin_dashboard.py,
tests/commercial_services/test_service_roles.py.
Delivery reserve and service forecast
For Project, Onboarding and Recurring agreement Service Roles, the browser's Actual contribution includes delivery still owed. Incurred facts remain available separately:
- Contribution before provision = issued revenue net of credits − incurred costs.
- Reserve movement = closing delivery reserve − opening delivery reserve.
- Headline contribution = revenue − incurred costs − reserve movement.
The list's delivery cost includes that movement, with incurred costs and the adjustment identified below it. Overview totals, sorting, Performance trends, monthly/cumulative tables and Activity use the same headline basis. Other roles retain incurred contribution. Forecast's salary toggle does not change activity.
If an open fixed project has incomplete opening history but a complete configured plan, the headline uses Planned margin · delivery position incomplete. The configured whole-contract cost/revenue ratio is applied to cumulative recorded revenue, floored at zero revenue; known cumulative incurred costs are the minimum cost. Monthly changes in that cost produce additive period figures. This estimates the planned margin on the billed revenue, without inserting a whole-contract margin beside period revenue or inventing opening hours/reserves. The adjustment absorbs later recorded costs until that allowance is exhausted; known overruns reduce contribution. Missing incurred costs, missing selling or cost budgets, or completed projects with unknown opening history remain unavailable. The opening-position warning stays visible on planned estimates.
An increase is a cost; a decrease releases cost already reserved. Period totals sum movements, while opening and closing reserves remain boundary balances. Selecting August or YTD carries the reserve from before that selection. A fixed budget is charged once, when it enters the available history, rather than in every month. A project already underway needs an opening balance; its original budget is not charged again in July. An unknown opening makes the affected movement and adjusted margin unavailable. A labelled planned estimate may supply the headline as described above; incurred figures remain available separately.
The supporting delivery-reserve calculation can use previously saved opening costs and dated remaining-work estimates. An open fixed project keeps at least its unused original labour budget. Recorded work and purchases consume remaining estimates; dated completion releases unused reserves. These local management records change no CRM or accounting records.
The dashboard does not expose those legacy inputs. They can support the provisioned activity headline, but do not set the configured forecast. Its forecast comes directly from Commercial Services and service lines. Historical reserve gaps do not block that configured expectation. The reserve is a management view and posts no accounting entries.
Confirming services and refreshing reports
Mark a Commercial Service Confirmed in Review State after its reporting
configuration and ownership have been checked. Other values do not enter the
browser report. Active/Ended lifecycle remains separate; an ended confirmed
service can still report its historical activity. Confirmation does not change
invoices, billing eligibility or financial source validation.
For historical revenue coverage, confirmation can retain an incomplete cost
configuration after customer, service and issued-invoice ownership have been
verified. Record the unresolved setup in Review Reason. A service without
configured lines reports its issued revenue while margin stays Not modelled
and its configured forecast stays unavailable. Confirmation does not approve
unverified commercial terms or turn missing budgets into zero. Missing or
unreadable work scopes must be resolved before confirming a service whose report
depends on them.
A Consulting Product (003) line links through Commercial Service and can
coexist with purchased Products. Explicit Labour Budget Type uses Expected
Labour Hours, separately from selling Quantity and Unit Price Override. A fixed
fee can therefore retain its price while labour hours are pending. For a legacy
one-off line with blank Labour Budget Type, its Quantity
supplies the full planned hours, independent of selling price and actual invoice quantity.
For 10 days of 7.6 hours, enter 76. Start/end dates must overlap the report period;
the one-off budget is retained when Active is unchecked. The report labels this
full budget alongside revenue and time for the displayed period. No Plan,
Delivery Model or Qwilr lookup is required on the line.
For a legacy line with blank Labour Budget Type and Arrangement = Recurring,
the single Consulting line's Quantity means
hours per calendar month. Enter 23 for a 23-hour monthly retainer; at
Tier 2 ($80/hour) its full-month expected labour cost is $1,840. Allowances are
prorated by calendar days within report and line dates, and stop at an ended
service's End Date. Active remains a billing flag and does not remove this
allowance. For Project, Onboarding and Recurring agreement Service Roles, each full
applicable monthly budget is provisioned, including an open month. Undelivered
hours carry forward within available history and are consumed by later delivery.
A line end stops new accrual but retains owed hours; dated service closeout
releases them. Missing budget leaves reserves unavailable, even at month-end.
Recorded overruns reduce contribution and do not consume future allowances.
External-fee forecasts use the same carry-forward policy when identifying
recorded overruns.
An explicit reviewed private allowance_policy = expire_monthly applies when
an agreement's unused hours expire. Other roles, including MSP, retain monthly
expiry: a closed month has zero reserve; an open month reserves its remaining
full applicable budget. Monthly/line expiry or dated completion releases it.
Expected activity hours remain prorated comparisons, separate from the carried
remaining-hours balance. The report does not reconstruct obligations before its
available history; a partial first month without an opening stays unavailable.
Recorded project work is counted once on that Consulting line and costed at its Expected Labour Tier rate, just like planned hours. Staff rates and saved labour costs are ignored for Consulting; blank tier uses Tier 1 ($50/hour). Consulting does not also incur a Product purchase cost. Other billed Products keep the existing Product-cost rule. Multiple labour lines need explicit work allocation and leave project costs unavailable rather than duplicating work.
Open one-off Consulting holds its full expected labour cost in the line Projected margin while recorded hours remain within budget. The detail shows incurred cost and remaining hours. Exceeding the hours budget switches the line margin position to incurred cost and shows overrun hours. For the pilot, close completed work by setting the Commercial Service Lifecycle = Ended and End Date, then refresh source data. When that date is on or before the report cutoff, Final margin uses incurred cost and releases unused hours. An ended service without an end date keeps its budget and requests the date. End Date alone, Cancelled, Review State and the line's Active flag do not close the budget. Linked Projects completion is not used. Seat allowance expiry or carry-forward follows the same Service Role and reviewed-policy rules described above.
For projects beginning before July, unknown opening delivery costs affect the supporting reserve movements. The configured forecast remains available.
The optional Expected Labour Tier (Expected_Labour_Tier) picklist applies
only to project Consulting lines. CRM stores Tier 1, Tier 2, Tier 3 or
Tier 4; code maintains their respective AUD hourly costs of 50, 80, 95 and 110.
Blank, including CRM's -None- option, uses Tier 1. An unrecognised Consulting
tier stops cost validation. MSP ignores the field and retains its $50/hour
planning rate and $80/hour fallback for missing staff rates.
Editing a Consulting tier changes both expected and actual labour costs after source refresh. Actual means recorded hours at that tier rate; it is not payroll cost. Invoices and selling prices remain independent. The report does not read the legacy currency field. Code mapping changes apply when periods are recalculated; these scenarios do not preserve dated historical tier costs. Separate day units and allowance intervals other than monthly remain outside this hourly pilot.
The field installer python -m commercial_services.service_line_costing --apply
creates only this optional picklist when absent and verifies it by reading it
back. It preserves existing values and rejects conflicting field definitions
or missing tier choices. It does not update service-line records or create invoices.
Tier costs are maintained in commercial_services/labour_tiers.py.
Open the report to see saved figures. Use Refresh source data after CRM, Xero or time-entry changes, choosing all confirmed services or one service in the dropdown. The collector selects that scope before reading invoice details and project work. A newly confirmed service enters on source refresh. Use Recalculate saved data while changing report calculations; it makes no external calls and retains the source timestamp.
The report shows progress and keeps the previous successful figures visible when their source mappings still match. If mappings change, it waits for a fresh collection instead of validating old invoice/configuration data against the new links. Failed source or cost validation preserves the previous snapshot on disk and displays an error; an incompatible snapshot is not served as a current report. A blank confirmed scope produces an empty report. No source refresh falls back to processing every Commercial Service. Saved reports are refreshed when due (after 24 hours or at a new reporting cutoff); a daily unattended schedule has not been enabled. Refresh remains read-only in business systems. Completed source inputs are retained even when the model calculation fails, so Recalculate saved data can retry after a correction without collecting again. An unchanged failure stays stopped until an explicit retry or a model/configuration revision. The dashboard shows the stage that failed, and keeps the saved figures and source timestamp visible when their mappings remain compatible.
Implementation: app/commercial_margin/review_refresh.py, scoped_sources.py.
Tests: tests/test_scoped_refresh.py, tests/test_commercial_service_review.py.
Shared service records and cleanup
Commercial Services groups the commitment. The existing MSP Invoice Lines module holds shared configured lines. Its optional Commercial Service lookup is populated on all 166 existing lines, and their Plan, Product, dates, quantities and prices are preserved. Plan is optional on the Standard layout; Product remains required. Lines without a Plan are excluded from MSP billing. A Tech Project line therefore needs no placeholder Plan.
Native Invoices have an optional Commercial Service lookup. The cleanup links 135 invoices where item, captured-source, Plan, Deal and customer evidence agrees. The other 161 captured invoices remain unassigned: 160 lack service evidence, and one has a source-customer mismatch. Its cleanup-added header was restored to blank; its existing Plan and financial values remain untouched. A single header link represents the whole invoice; mixed-service invoices require item attribution. Invoice IDs, all item rows, totals and Xero links are preserved. The backfill compares before-images, suppresses workflows, uses modification timestamps and reads each result back. An existing different relationship or changed business value blocks application; repeating a verified relationship makes no write.
Reporting uses the invoice header where available and rejects conflicts with item, Plan, Deal or reviewed ownership. It reads actual quantities, prices, discounts, taxes and amounts from Xero. The six migrated native MSP invoices continue to validate their native items against Xero.
The exploratory Service Invoice Lines, Service Terms, Service Labour Budgets and Service Source Links are removed. Record and metadata before-images remain in a private local archive. Immutable reporting mappings retain invoice/credit ownership, coverage, Product and Quote-row cost references, reviewed billing aliases, Deal links and dated work ownership. Financial fingerprints block changed or missing Xero evidence. These mappings are a historical migration bridge; new relationships belong in retained CRM records. They do not introduce Delivery Model or Qwilr fields on service lines.
The reader does not query the four modules. Historical writers reject creating those modules or writing their records. Original Baseline and its archived lookup dependency are removed from Commercial Services. The cleanup record records completed CRM removal and verification. No new Xero invoices or generation controls are enabled by this work.
Implementation: commercial_services/cleanup.py, retired.py,
app/commercial_margin/attribution.py, services.py and engine.py.
Tests: tests/test_commercial_cleanup.py, tests/test_contribution_report.py.
Service performance and original expectations
The exploratory original-baseline and cumulative historical-position extension
is retired from live report collection. Its 71 terms and 79 labour budgets were
all marked Needs Review; their values remain in the private archive. They are
not promoted into approved service-line budgets. Live reporting starts on
1 July 2026; selective pre-July evidence remains archived and the native pilot
keeps its identities without changing accounting dates.
The MSP browser review retains its configured Seat expectations and actual work-date costs described above. Tech Project labour expectations belong on shared service lines in the next reporting pilot. That pilot has no active invoice-generation workflow.
Existing service lifecycle values remain context: payment, exhausted budget or a closed shared Project does not establish that the commercial service is closed. Reviewed historical work-date ownership remains available to the July-onward report and each recorded work row is counted once.
Ownership and review
Xero owns the financial document. CRM owns the existing Deal/Plan identity and Product cost prices. Projects owns recorded delivery time. Reporting joins these identities without changing Xero.
A reviewer checks the invoice's customer and scope against an existing Deal. A matching customer or similar amount is insufficient when several scopes are possible. Clear standalone subscriptions/ad hoc services can use reporting-only identities. Those identities and reviewed contact aliases are stored in the report configuration; they do not change Company or Deal records.
The current report starts on 1 July 2026. Keep uncertain scope assignments in the reconciliation queue. Invoices with multiple scopes can be reviewed at line level in reporting, but a single CRM Invoice header cannot represent several Commercial Deals. Do not mirror a conflicting line split to one Deal.
Preview and apply
The reviewed mapping file is config/commercial_margin.json. Every mapping has
a stable source ID and a reason. Preview the existing-Deal invoice candidates:
python -m app.commercial_margin.reconcile --output /secure/local/path/preview.json
Review the invoice, Deal, Company, line count and subtotal. The batch preflights all candidates before applying valid ones; blocked candidates remain visible in the output. After reviewing the result:
python -m app.commercial_margin.reconcile --apply --output /secure/local/path/result.json
The write path is constrained to CRM Invoices and disables workflow triggers.
It can create an Invoice for a new Xero ID or add Commercial_Deal to an
existing unassigned Invoice. It will not overwrite a different Deal or an MSP
Plan link. An already-correct attribution is unchanged. Source changes after
preview block application. The result journal is updated during the batch;
the saved CRM service identity and newly-created invoice totals are read back
before the result is marked verified.
Financial lines preserve Xero quantities, unit prices and discounts. Standard Australian GST uses one nominal 10% rate. A bounded rounding adjustment preserves the source total where Xero rounds tax per line. Mixed/nonstandard tax requires review rather than stacking inferred rates in the CRM header. A reviewed product link can resolve a blank or generic Item Code without changing Xero. Product identity and costing remain separately auditable. Missing required product mappings block the CRM mirror; the reporting service link can still be used while the product issue is reviewed. A reviewed contact alias must match the invoice contact and the target Deal's Company; it does not overwrite the Company's existing Xero ID.
The existing single-invoice command remains available for manually reviewed cases:
python -m xero_sync.project_invoice_mirror --invoice-id <xero-id> --deal-id <existing-deal-id>
Its older unrestricted date behaviour is retained for existing users. The new reviewed batch is explicitly restricted to July 2026 onward.
What cannot change
No Xero invoice, credit note, payment or accounting record is modified. No Deal, Company, Product or Projects record is directly created or edited by the reviewed batch. CRM may perform its native inventory bookkeeping when an Invoice is created; disabling workflow triggers does not disable that platform behaviour. Reporting refresh itself performs read requests only.
Verify the outcome
Inspect the report for the selected period and service, then its invoice and recorded-work details. Check the product match, actual quantity and current cost price; a successful service link does not prove the product cost or timesheet coverage is complete.
Use Gaps & review for missing product prices, credits, annual billing, zero-hour revenue and delivery with no invoice revenue. Do not interpret a billing-period mismatch as a whole-contract profit or loss. Use Business profit or loss for Xero's actual result, with shared infrastructure, payroll and overhead charged once at business level.
References
- Calculation approach
app/commercial_margin/reconcile.pyxero_sync/project_invoice_mirror.pytests/test_project_invoice_mirror.py,tests/test_contribution_report.py