Skip to content

Commercial Margin

The current browser dashboard is a draft proof of concept for modelling. Its purpose is to iterate on revenue, cost and contribution assumptions and experiment with ways to interpret them. Figures are provisional. Exact financial presentation and exhaustive issue diagnosis are not acceptance criteria for this draft; useful modelling feedback and a reliable refresh/recalculate loop are.

The existing report combines invoice-based service contribution, recorded work, private staff-cost estimates and the Xero business accounts. This page retains operational references for interpreting its outputs. Dashboard and render experiments are not requirements for the commercial services design.

The browser separates Commercial services, Performance and Forecast. The service list is the landing page; each service opens as a contextual drill-down. Power BI retains its separate historical contribution and business-accounting views.

Sources and access

The browser route /commercial-margin/dashboard reads /commercial-margin/service-review-report for Commercial Services whose CRM Review_State is exactly Confirmed.

Pages and reporting basis

  • Commercial services shows a selected-period overview and one service list, with customer, Service Role, lifecycle, revenue, delivery cost, Actual contribution and margin. Project, Onboarding and Recurring agreement contributions include delivery provisions or labelled planned estimates; incurred costs remain visible. Search, sortable financial columns and Needs attention refine the list; its overview matches visible rows. Attention covers recorded-cost/reserve gaps, forecast configuration and labelled cost estimates, independently of the forecast salary switch. A flag identifies a review item, not necessarily a bad margin.
  • Performance shows monthly or cumulative recorded results, a revenue and actual-contribution trend, then revenue, labour, purchases, incurred cost, provision/planned adjustments, combined delivery cost and Actual contribution. Opening and closing reserve balances are in a separate disclosure.
  • Forecast groups recurring financial-year expectations separately from whole-contract one-off expectations. Each group shows revenue, delivery cost, contribution, margin, a Service Role cost breakdown, largest contributors, loss-making services and missing configuration. There is no combined annual headline across incompatible horizons.

One-off forecasts cover whole contracts. Recurring forecasts cover the current financial year through June, limited by applicable start/end dates. Current monthly prices and quantities form a forecast, not a reconstruction of historical configuration. Recorded-period controls do not change these horizons.

Include estimated salaried delivery cost appears in Forecast and the service Summary, defaults on and persists in the URL. External consultant fees and purchases remain included. Fixed payroll and overheads are outside the forecast. Missing configured direct costs stay unavailable; missing salary allocations only affect forecast totals including salaried delivery. Recorded results always include all modelled labour. There is no forecast input form. See Signed-work financial forecast.

Provisioned contribution

Project, Onboarding and Recurring agreement services include the cost of delivery still owed. Monthly allowances carry forward until delivered or explicitly closed, unless a reviewed expiry policy says otherwise. Projects retain their remaining budget. Known overruns reduce the margin. When a project's opening history is incomplete, a complete configured plan supplies a labelled planned-margin estimate on recorded revenue; missing budgets leave the result unavailable. Cost breakdowns and detail retain incurred costs separately. See calculation rules.

Service Role and shared filters

The category is the Commercial Service's Service Role (Service_Role). The primary controls are All, MSP, Project and Hardware. Additional saved roles, including Onboarding, Ad hoc, Home or custom values, appear as additional controls. A role classifies the entire service; Products and service lines still determine cost treatment. Neither Deal category, Business_Category, Plan presence nor recurrence supplies a missing role.

Role Purpose and Performance treatment
MSP Ongoing managed support. Uses incurred contribution; monthly allowances expire.
Project Delivery commitment. Includes the remaining delivery provision, or a labelled planned estimate when opening history is incomplete.
Onboarding Initial discovery, setup and onboarding. Uses the same delivery provision and planned-estimate rules as Project, while retaining its own reporting category.
Hardware Equipment sales and associated configured setup. Uses incurred contribution.
Ad hoc Occasional work grouped separately. Uses incurred contribution.
Home Home services grouped separately. Uses incurred contribution only where costs are modelled; deferred labour keeps a complete margin unavailable.
Recurring agreement Retained role for recurring commitments. Includes owed delivery; monthly allowances carry forward unless a reviewed expiry policy applies.
Unclassified or custom Blank/unconfirmed classifications or other saved roles. Remain visible and use incurred contribution where costs are modelled.

Onboarding keeps its fixed delivery budget until consumed or explicitly closed. If configured with a monthly allowance, it follows Project carry-forward rules. Roles do not set prices, hours, labour tiers or invoice schedules. Forecast uses the configured Product and line budgets for every role.

Hardware sale is displayed as Hardware without changing CRM. Recurring agreement remains an available role; blank or Unconfirmed roles appear as Unclassified and stay in All. Role values do not generate review comments or Needs attention flags. The operator-run migration changes Recurring agreement to MSP only when the Commercial Service links to an existing Managed Service Plan through Legacy_MSP_Plan, across all review states. It preserves unlinked recurring agreements and other roles. Refresh source data after CRM role edits. The schema specification includes MSP, Project, Hardware, Onboarding, Ad hoc and Home, retaining legacy choices for compatibility. The migration adds MSP to the live picklist when needed, preserving existing choices. Editing the specification or navigating the dashboard does not change CRM. See the migration process.

Service Role and customer filters persist across tabs, drill-downs, reloads and browser navigation. A selected role/customer with no services shows an explicit empty result. Changing either filter from a detail returns to its originating report, without silently replacing the selected service.

Periods and drill-downs

Recorded activity defaults to Financial YTD from July through the latest complete saved month. If none is complete, the partial month is shown. Single month, Custom period and Available history use saved monthly rows. Financial or calendar YTD requires history covering its beginning. Cumulative starts at the selected range's beginning; its columns are running balances without a summed footer. Monthly mode includes a period total and compares the last two complete months. Rates use total contribution divided by total revenue; rate changes are percentage points.

Click a Performance month heading to open the service list for that month (or running period in Cumulative). Role/customer filters remain; search and attention refinements clear so they do not hide contributors. Open a service from the list for Activity, or from Forecast for Summary. The heading identifies the service, customer, role and lifecycle. Its ID stays in the URL. A missing service is unavailable rather than replaced. The return link preserves the originating page, period, filters, sorting and scroll position.

Detail has Summary, Activity and Lines & evidence sections. Summary shows the configured forecast and cost breakdown. Activity has monthly cost bars, monthly/cumulative results and independent dates initially inherited from the originating report. Lines & evidence holds configured lines, budget positions at the snapshot cutoff, selected-activity Product components and invoice items. Changing activity dates does not re-evaluate full-scope budgets. The all-services activity link carries the detail's dates into the service list.

Monthly results charge only reserve movements; balances are not summed. Historical reserve gaps do not block the configured forecast. See delivery reserve and service forecast.

Missing costs propagate into complete totals and suppress the affected period's margin rate. Revenue without recorded work is valid and does not itself raise a warning: zero incurred costs with positive revenue gives 100% activity margin. Known amounts remain visible. Partial months are labelled, and zero revenue has no margin percentage. See monthly service activity for rounding and current-configuration limits. Revenue and work begin on 1 July 2026; changing the display range does not fetch earlier history.

Expected Seat hours are configured seats × the monthly level expectation: L1 = 15 minutes, L2 = 30 minutes, L3 = 1.5 hours per seat per month. L3 support is unlimited; 1.5 hours is an indicative planning and reporting expectation, not a support limit. MSP expected labour costs $50/hour, independently of the line's labour tier. Other non-labour costs use Product Cost_Price, including explicit zero; missing prices use 90% of net revenue. Explicit MSP-DISCOUNT and MSP-NFP-DISCOUNT Products reduce revenue with zero additional cost, including when their cost price is blank. See calculation rules for proration, credits and hourly costing assumptions.

The review consumes billing configuration and Product metadata from the same read-only source refresh as the invoice facts. Native item configured-charge lookups determine the component even when an older invoice has a different Product name. Source collection does not require SERVICE_REVIEW_PLANS_FILE. Whole-service forecasts use the existing service-line quantities, prices and cost budgets. Local management overrides do not change them. The separate planning estimator remains available for other scenarios. MSP Seat uses a standard $50/hour estimate for planned and recorded labour, labelled in the Activity detail. Private salary configuration and source-snapshot fallback rates do not change this browser estimate. Private staff costs retain their separate role in the business contribution export and staff reconciliation. Internal Consulting uses its line tier for planned and recorded hours without staff costs. External Consulting uses its positive line-owned External Hourly Cost; all hours over budget remain chargeable.

To change an internal Consulting line's costing tier, edit Expected Labour Tier (Expected_Labour_Tier) on its MSP Invoice Lines record, then use Refresh source data. Tier 1 = $50/hour, Tier 2 = $80/hour, Tier 3 = $95/hour and Tier 4 = $110/hour, all in AUD. Blank uses Tier 1. The optional picklist stores only the tier; commercial_services/labour_tiers.py maintains the cost mapping. An unrecognised Consulting tier blocks refresh. MSP ignores this field.

Link each line to its Product and Commercial Service. A Consulting Product (003, internal, or CONSULTING-EXTERNAL) line can coexist with purchased Products in the same service. To price a fixed fee independently of work, select Labour Budget Type = Fixed budget, enter sold units in Quantity, the fee per unit in Unit Price Override, and planned work in Expected Labour Hours. Hours can remain blank while pending; the known purchased margin stays visible and complete service margin remains unavailable. Recurring allowance uses Expected Labour Hours per calendar month, independently of service Arrangement.

For legacy Consulting lines with blank Labour Budget Type, enter planned hours in Quantity and the hourly selling price in Unit Price Override. Expected labour uses those planned hours at the selected internal tier or explicit external supplier rate. Recorded work uses the same line rate, regardless of staff rates or saved labour costs. The component includes a link to edit the service line. For a service with Arrangement = Recurring, Quantity means hours per calendar month; otherwise it is the full one-off budget. Recurring allowances are prorated by calendar days within the report and line dates, capped by an ended service's End Date, and do not roll over. Tier changes affect both planned and recorded-hour cost estimates without changing selling prices. The legacy currency field is not read. Changing the code mapping also affects recalculated historical periods. The detail shows the full-month allowance and cost as well as the selected-period allowance. Actual costing continues if monthly planned hours are missing. Separate days/amount fields and non-monthly allowance intervals remain future extensions.

Open one-off Consulting holds the full labour budget in Projected margin until recorded hours exceed the budget. The detail shows incurred cost and remaining or overrun hours. To close that work for this pilot, set the Commercial Service Lifecycle to Ended and set its End Date, then refresh source data. At a report cutoff on or after that date, Final margin uses incurred cost and releases unused hours. Monthly Consulting and Seat allowances expire at month end; an open month reserves the unused full-month allowance. See the budget rules for date and coverage limits.

The page retains the last successful snapshot during refresh or refresh failure, showing its actual timestamp and status. This review has a separate cache and validates complete documents for the selected customers; unrelated customers’ capture discrepancies do not block the selected services. A changed historical source fingerprint is a non-blocking draft warning: the dashboard uses current Xero values with the retained service attribution. Missing selected-service history, customer/ownership conflicts, native invoice mismatches and invalid document totals still fail validation. Power BI keeps its existing full-business validation, fresh-only behavior and contribution calculations.

Power BI uses pbi/commercial-margin/CommercialMargin.pbip and consumes /commercial-margin/contribution-report with the existing report authentication.

The browser displays saved figures immediately. The Data & refresh → Source collection scope selector restricts both the saved report and its refresh scope. This selector is hidden on the locked service page, which labels the retained refresh scope. Refresh source data checks CRM confirmation, then refreshes all confirmed services or the service selected in the dropdown. Recalculate saved data runs the calculations against saved source inputs without contacting CRM, Xero or Projects, including a completed collection whose calculation failed. Its source timestamp stays unchanged. Refresh progress and failures are shown while the previous successful figures remain visible, including across date changes and server restarts.

Saved review snapshots are due for source refresh after 24 hours or when a new report end date is requested. Opening a due report starts a background refresh; opening a current report makes no remote calls. A changed calculation configuration can be recalculated from current saved sources. Reconfirmation and CRM edits are discovered on source refresh, not by recalculating cached inputs. Zero confirmed services produces an empty report, never a whole-business fetch. An unchanged failure stays stopped until an explicit retry or a model/configuration revision; opening or polling the page does not repeatedly restart it. The error shows the stage that failed. Calculation revisions reuse recent collected inputs for the same date and scope; ownership/mapping changes require a source refresh.

If refresh fails during calculation, use Recalculate saved data after fixing the model. If it fails during source collection, use Refresh source data after resolving the source problem. Progress polling reads job status while the worker is busy; it does not rebuild the report every two seconds. The browser allows 30 seconds for response headers and a further 90 seconds to download the report. A timeout, dropped connection or gateway timeout triggers up to two attempts to read the saved result again, without repeating the source refresh or calculation request. If those attempts fail, the controls are enabled and the page asks for a reload. A connection failure does not establish whether the worker succeeded; an explicit calculation/source error stops polling immediately.

COMMERCIAL_MARGIN_CACHE_DIR selects the private cache directory; the review's default is .local/commercial-margin under the application working directory. Keep it on persistent storage in deployment. Cached source files contain customer data, use owner-only permissions and must remain outside Git. A process lock coalesces duplicate refreshes, and source collection runs one job at a time with at most four concurrent API reads. COMMERCIAL_MARGIN_CONFIG can select another reviewed mapping file.

The collector reads lightweight CRM indexes and reuses unchanged invoice details. CRM index updates use modified-since timestamps, with a full reconciliation daily to discover deletions. It reads complete invoices and credits for selected customers, known linked documents, and tasks/time for their projects. Shared customers and projects are fetched once. Company-wide P&Ls and historical-position calculations do not run for this browser review. Each source refresh rechecks all months in the selected work period, so edited or removed older time is reflected. The small five-second source cache coalesces overlapping reads.

The same worker is available for a future daily server job:

python -m flask --app server commercial-margin-refresh
python -m flask --app server commercial-margin-refresh --service-id CRM_SERVICE_ID
python -m flask --app server commercial-margin-refresh --recalculate

--end-date YYYY-MM-DD selects the reporting cutoff. The command returns success only after validation and publication; a running job returns a busy error rather than claiming completion. Use the same cache directory and costing configuration as the server. No daily schedule is installed by this change. Xero document and project-history incremental checkpoints remain a later optimization; current refreshes reread that history only for selected customers/projects. The separate full-business contribution endpoint retains its original 15-minute cache.

For local review with the existing private staff costing file:

SERVICE_REVIEW_STAFF_COST_FILE=.local/staff-costing-private/staff-costs.json \
  python -m flask --app server --debug run --host=127.0.0.1 -p 5050

Open http://127.0.0.1:5050/commercial-margin/dashboard for that server. The separate development preview on port 5051 proxies the backend on port 5052. The debug reloader can interrupt background refreshes when watched Python files change. For a stable collection session, add --no-reload to flask run, or use the synchronous refresh command above. An interrupted worker releases its process lock; the next read can restart collection. Completed collected inputs survive restarts on the configured persistent volume.

config/commercial_services.json enables retained Commercial Service identities and native-item attribution. config/commercial_service_attribution.json holds immutable historical ownership and source fingerprints after the exploratory module cleanup. The original-baseline and historical-position extension is retired from live collection. See the cleanup record for retained records and verified module removal. Existing MSP billing continues through its operational Plan, Seat and configured-charge records.

Interpretation and maintenance references

Source code remains in app/commercial_margin/, with browser routes in app/blueprints/report_exports.py. This documentation cleanup does not deploy, remove or change those implementations.