The costing module in src/cluster_erp/modules/costing/ costs a BOM version. A costing analysis reads the BOM explosion, applies labour and overhead rates, adds an optional nesting cost, and stores a cost breakdown, a total, a suggested price for a target margin, and a low-margin flag. An analysis then goes through submission, approval, or rejection, and an approved analysis linked to a quotation reports its price back. Labour rates and overhead rates are the module's own reference records.
At a glance¶
| Item | Value | Source |
|---|---|---|
| Package | src/cluster_erp/modules/costing/ |
folder |
| Module class | Module |
src/cluster_erp/modules/costing/module.py:15 |
name / version |
costing / 0.1.0 |
src/cluster_erp/modules/costing/module.py:16-22 |
| API prefix | /api/v1/costing (ModuleBase.api_prefix) |
src/cluster_erp/core/module_base.py:37-38 |
| Sub-modules | 3: costing_analyses, labor_rates, overhead_rates |
folder listing |
| Tables | 5: costing_costing_analyses, costing_cost_breakdown_items, costing_rate_snapshots, costing_labor_rates, costing_overhead_rates |
src/cluster_erp/modules/costing/costing_analyses/models.py:24, src/cluster_erp/modules/costing/costing_analyses/models.py:193, src/cluster_erp/modules/costing/costing_analyses/models.py:294, src/cluster_erp/modules/costing/labor_rates/models.py:15, src/cluster_erp/modules/costing/overhead_rates/models.py:13 |
| Entities in metadata | 3: costing_analysis, labor_rate, overhead_rate |
src/cluster_erp/modules/costing/module.py:303-307 |
| Routes | 30 route decorators in the package | grep for @router. |
| Web app presence | Catalogue app costing |
src/cluster_erp/modules/app_store/catalog.json (entry costing) |
| Desktop presence | Generic widgets only; no folder in src/cluster_erp/desktop/modules/ |
folder |
What the module does¶
- Records a costing analysis for one BOM and BOM version, optionally linked to a quotation and a nesting job, with a target margin and a currency (
src/cluster_erp/modules/costing/costing_analyses/models.py:23-189). - Calculates the cost breakdown in tiers and stores one breakdown row per cost line (
src/cluster_erp/modules/costing/costing_analyses/services.py:256-708). - Stores a snapshot of every rate used, so a later rate change does not change the explanation of an old result (
src/cluster_erp/modules/costing/costing_analyses/services.py:627-662). - Lets a user set a final price, submit, approve, reject, return to draft, and supersede (
src/cluster_erp/modules/costing/costing_analyses/services.py:710-896). - Compares two to five analyses and clones an analysis as a what-if scenario with cost multipliers (
src/cluster_erp/modules/costing/costing_analyses/services.py:957-1159). - Keeps labour rates per work centre and operation type, and overhead rates with a basis and an apply order (
src/cluster_erp/modules/costing/labor_rates/models.py:14-68,src/cluster_erp/modules/costing/overhead_rates/models.py:12-54).
Sub-modules¶
| Sub-module | Purpose | Main entities | Router | Source |
|---|---|---|---|---|
costing_analyses |
Analyses, breakdown, snapshots, lifecycle | CostingAnalysis, CostBreakdownItem, CostingRateSnapshot |
/api/v1/costing/analyses |
src/cluster_erp/modules/costing/costing_analyses/routes.py:31 |
labor_rates |
Labour rates | LaborRate |
/api/v1/costing/labor-rates |
src/cluster_erp/modules/costing/labor_rates/routes.py:22 |
overhead_rates |
Overhead rates | OverheadRate |
/api/v1/costing/overhead-rates |
src/cluster_erp/modules/costing/overhead_rates/routes.py:22 |
Data model¶
| Entity | Table | Key fields | Notes | Source |
|---|---|---|---|---|
CostingAnalysis |
costing_costing_analyses |
analysis_no, bom_id, bom_version_id, quote_id, nesting_job_id, status, is_scenario, parent_analysis_id, superseded_by_id, seven cost totals, target_margin_pct, actual_margin_pct, markup_pct, suggested_price, final_price, currency, low_margin_flag, calculation_version, approval stamps |
money Numeric(19, 4), percentages Numeric(5, 2) |
src/cluster_erp/modules/costing/costing_analyses/models.py:23-189 |
CostBreakdownItem |
costing_cost_breakdown_items |
analysis_id, cost_type, bom_item_id, part_id, work_center_id, operation_type, overhead_category, description, quantity, rate, scrap_factor, base_cost, scrap_cost, total_cost, currency, rate_snapshot_id |
one row per cost line | src/cluster_erp/modules/costing/costing_analyses/models.py:192-290 |
CostingRateSnapshot |
costing_rate_snapshots |
analysis_id, rate_type, source_rate_id, snapshot_data |
JSONB copy of a rate | src/cluster_erp/modules/costing/costing_analyses/models.py:293-307 |
LaborRate |
costing_labor_rates |
work_center_id, operation_type, rate_type, rate_per_hour, rate_per_unit, pricing_unit_id, currency, valid_from, valid_to, is_active |
one open rate per work centre and operation type | src/cluster_erp/modules/costing/labor_rates/models.py:14-68 |
OverheadRate |
costing_overhead_rates |
name, rate_type, basis, rate_value, currency, valid_from, valid_to, is_active, apply_order |
one open rate per rate_type |
src/cluster_erp/modules/costing/overhead_rates/models.py:12-54 |
"Open" means valid_to is empty and the row is live; partial unique indexes enforce it (src/cluster_erp/modules/costing/labor_rates/models.py:16-24, src/cluster_erp/modules/costing/overhead_rates/models.py:14-21). Money is Numeric and Decimal; see money and Decimal.
Screens and metadata¶
get_metadata declares a sidebar group Costing Management (Turkish Maliyet Yönetimi) with three lists: Costing Analyses, Labor Rates, and Overhead Rates (src/cluster_erp/modules/costing/module.py:253-308). The analysis list shows number, status, total cost, final price, actual margin, currency, low-margin flag, and calculation time, with a status filter (src/cluster_erp/modules/costing/costing_analyses/metadata.py:125-200).
Status and actions¶
Statuses and allowed transitions are fixed in the service (src/cluster_erp/modules/costing/costing_analyses/services.py:71-80): draft to calculated; calculated to calculated again or to pending_approval; pending_approval to approved or rejected; approved to superseded; rejected back to draft. approved and superseded analyses cannot be changed.
| Action | Label | Condition | Endpoint | Permission | Source |
|---|---|---|---|---|---|
calculate |
Calculate | status is draft |
POST /analyses/{id}/calculate |
costing_analysis.calculate |
src/cluster_erp/modules/costing/costing_analyses/metadata.py:41-51, src/cluster_erp/modules/costing/costing_analyses/routes.py:129-141 |
recalculate |
Recalculate | status is calculated |
POST /analyses/{id}/recalculate |
costing_analysis.calculate |
src/cluster_erp/modules/costing/costing_analyses/metadata.py:52-65, src/cluster_erp/modules/costing/costing_analyses/routes.py:144-156 |
submit_approval |
Submit for Approval | status is calculated |
POST /analyses/{id}/submit-for-approval |
costing_analysis.submit |
src/cluster_erp/modules/costing/costing_analyses/metadata.py:66-79, src/cluster_erp/modules/costing/costing_analyses/routes.py:175-187 |
approve |
Approve | status is pending_approval |
POST /analyses/{id}/approve |
costing_analysis.approve |
src/cluster_erp/modules/costing/costing_analyses/metadata.py:80-93, src/cluster_erp/modules/costing/costing_analyses/routes.py:190-203 |
reject |
Reject | status is pending_approval |
POST /analyses/{id}/reject |
costing_analysis.reject |
src/cluster_erp/modules/costing/costing_analyses/metadata.py:94-107, src/cluster_erp/modules/costing/costing_analyses/routes.py:206-219 |
return_to_draft |
Return to Draft | status is rejected |
POST /analyses/{id}/return-to-draft |
costing_analysis.update |
src/cluster_erp/modules/costing/costing_analyses/metadata.py:108-121, src/cluster_erp/modules/costing/costing_analyses/routes.py:222-234 |
Two lifecycle routes have no ActionMeta: POST /analyses/{id}/set-final-price and POST /analyses/{id}/supersede (src/cluster_erp/modules/costing/costing_analyses/routes.py:159-172, src/cluster_erp/modules/costing/costing_analyses/routes.py:237-250).
Business rules¶
The costing roll-up¶
The diagram shows the order of the tiers in calculate.
flowchart TD E["GetBomExplosion (bom)"] --> M["material tier: effective_qty x material_cost_per_unit, plus scrap"] E --> L["labour tier: minutes x hourly rate, or qty x unit rate, plus scrap"] N["GetNestingJobById (nesting)"] --> NT["nesting tier: net_material_cost"] M --> S["subtotal"] L --> S NT --> S S --> O["overhead tier, in apply_order"] O --> T["total_cost = material + labour + overhead + scrap + nesting"] T --> P["suggested_price = total / (1 - target margin)"] P --> F["low_margin_flag if margin below 15"]
In prose, step by step (src/cluster_erp/modules/costing/costing_analyses/services.py:256-708):
- The analysis must be allowed to move to
calculated(src/cluster_erp/modules/costing/costing_analyses/services.py:261-262). - The module asks the
bommodule for the explosion of the BOM version and fails if there is none or it has no items (src/cluster_erp/modules/costing/costing_analyses/services.py:264-290). - It loads the active labour and overhead rates and refuses any rate whose currency differs from the analysis currency, with
COSTING_CURRENCY_MISMATCH(src/cluster_erp/modules/costing/costing_analyses/services.py:292-313). - It removes the previous breakdown rows and snapshots (
src/cluster_erp/modules/costing/costing_analyses/services.py:315-329). - Material tier: for each item with a material cost, base cost is effective quantity times material cost per unit, and scrap cost is base cost times the scrap factor (
src/cluster_erp/modules/costing/costing_analyses/services.py:346-404). - Labour tier: for each item with labour minutes, the rate is looked up by work centre and operation type, then work centre only, then operation type only, then the
generaloperation type, then a rate with neither. A unit-price rate multiplies the quantity; an hourly rate multiplies the hours. Without a rate, the item's own labour cost per unit is used. Labour scrap is the labour cost times the scrap factor (src/cluster_erp/modules/costing/costing_analyses/services.py:406-504). - Nesting tier: when the analysis names a nesting job, its
net_material_costbecomes one breakdown line (src/cluster_erp/modules/costing/costing_analyses/services.py:506-534). - Overhead tier: each active overhead rate, in
apply_order, takes a percentage of labour, material, the total so far, or the running subtotal, or a fixed amount per unit of quantity; each result is added to the running subtotal (src/cluster_erp/modules/costing/costing_analyses/services.py:536-587). - The total is material plus labour plus overhead plus scrap plus nesting (
src/cluster_erp/modules/costing/costing_analyses/services.py:589-600). - With a target margin, the suggested price is the total divided by one minus the margin, and the markup is derived from it (
src/cluster_erp/modules/costing/costing_analyses/services.py:602-619). - The low-margin flag is set when the margin is below 15 percent (
src/cluster_erp/modules/costing/costing_analyses/services.py:60,src/cluster_erp/modules/costing/costing_analyses/services.py:621-625). - Rate snapshots are stored, the status becomes
calculated,calculation_versiongoes up by one, andCostingCalculatedis emitted, plusLowMarginAlertwhen flagged (src/cluster_erp/modules/costing/costing_analyses/services.py:627-706).
Each money result is rounded with ROUND_HALF_EVEN to four places (src/cluster_erp/modules/costing/costing_analyses/services.py:359-372, src/cluster_erp/modules/costing/costing_analyses/services.py:606-609).
Other rules¶
- The final price can be set only on a
calculatedanalysis; the actual margin becomes (price minus cost) divided by price, and the markup (price minus cost) divided by cost (src/cluster_erp/modules/costing/costing_analyses/services.py:710-739). - Submission needs a final or a suggested price; without a final price, the suggested price is copied into it (
src/cluster_erp/modules/costing/costing_analyses/services.py:742-759). - Approval of an analysis with a quotation and a final price also emits
QuoteCostingCompleted(src/cluster_erp/modules/costing/costing_analyses/services.py:768-808). - Supersede needs a new analysis of the same BOM (
src/cluster_erp/modules/costing/costing_analyses/services.py:857-880). - Only a draft analysis can be deleted one by one (
src/cluster_erp/modules/costing/costing_analyses/services.py:233-244). - A scenario can be cloned from a
calculatedorapprovedanalysis; material and labour lines are scaled by the given multipliers (src/cluster_erp/modules/costing/costing_analyses/services.py:1031-1104). - A labour or overhead rate's
valid_tomay not be before itsvalid_from(src/cluster_erp/modules/costing/labor_rates/services.py:159-163,src/cluster_erp/modules/costing/overhead_rates/services.py:107-111). A labour rate type ishourlyorunit_price(src/cluster_erp/modules/costing/labor_rates/schemas.py:9). analysis_nocomes from the yearly sequencecosting_analysis(src/cluster_erp/modules/costing/costing_analyses/services.py:183).
Events and queries¶
The module consumes 7 contracts and produces 10, counted as on the parties module page.
| Direction | Contract class | Handler or emitter | Source |
|---|---|---|---|
| emits | CostingCreated |
create |
src/cluster_erp/modules/costing/costing_analyses/services.py:200-209 |
| emits | CostingCalculated, LowMarginAlert |
calculate |
src/cluster_erp/modules/costing/costing_analyses/services.py:660-692 |
| emits | CostingApproved, QuoteCostingCompleted, CostingRejected, CostingSuperseded |
lifecycle methods | src/cluster_erp/modules/costing/costing_analyses/services.py:786-808, src/cluster_erp/modules/costing/costing_analyses/services.py:828-836, src/cluster_erp/modules/costing/costing_analyses/services.py:886-894 |
| emits | CostingRateUpdated |
rate updates | src/cluster_erp/modules/costing/labor_rates/services.py:218, src/cluster_erp/modules/costing/overhead_rates/services.py:157 |
| queries | GetBomExplosion, GetNestingJobById |
calculate |
src/cluster_erp/modules/costing/costing_analyses/services.py:262-267, src/cluster_erp/modules/costing/costing_analyses/services.py:508-513 |
| answers query | GetCostingById, GetCostingByBomId |
handlers | src/cluster_erp/modules/costing/module.py:66-70, src/cluster_erp/modules/costing/module.py:89-139 |
| answers query | GetActiveLaborRate, GetActiveOverheadRates, GetCostBreakdown |
handlers | src/cluster_erp/modules/costing/module.py:71-82, src/cluster_erp/modules/costing/module.py:141-223 |
| subscribes | BomActivated, NestingCostCalculated |
_on_bom_activated, _on_nesting_cost_calculated |
src/cluster_erp/modules/costing/module.py:84-85, src/cluster_erp/modules/costing/module.py:227-231 |
The two subscribed handlers have an empty body at this commit (src/cluster_erp/modules/costing/module.py:227-231). The full list is on the contracts reference.
Permissions¶
| Permission key | Used by | Source |
|---|---|---|
costing_analysis.read, .create, .update, .delete, .bulk_delete |
analysis routes, breakdown, compare, snapshots, scenario | src/cluster_erp/modules/costing/costing_analyses/routes.py:43-295 |
costing_analysis.calculate, .submit, .approve, .reject, .supersede |
lifecycle routes | src/cluster_erp/modules/costing/costing_analyses/routes.py:135-244 |
labor_rate.read, .create, .update, .delete, .bulk_delete |
labour rate routes | src/cluster_erp/modules/costing/labor_rates/routes.py:34-113 |
overhead_rate.read, .create, .update, .delete, .bulk_delete |
overhead rate routes | src/cluster_erp/modules/costing/overhead_rates/routes.py:32-109 |
Privacy, retention, and history¶
The module stores user ids of submitters and approvers and no personal fields of its own; it has no privacy_handlers.py. Analyses and rates are soft-deleted. Changes are captured by change history.
Analytics facts¶
This module has no analytics_facts.py.
Settings and reference data¶
Labour and overhead rates are the module's reference data; there is no settings section and no seed (src/cluster_erp/modules/costing/module.py:28-30). The low-margin threshold is the module constant _LOW_MARGIN_THRESHOLD (src/cluster_erp/modules/costing/costing_analyses/services.py:60).
Tests¶
Found by a grep for modules.costing in tests/ and by file name in web-client/.
| Kind | File | Covers |
|---|---|---|
| e2e | web-client/_e2e_costing_app.mjs |
the costing app's three metadata lists and its store card |
| e2e | web-client/_e2e_quote_line_bom.mjs, web-client/_e2e_order_shipment_chain.mjs |
chains that call costing routes |
No pytest file imports the module.
Discrepancies¶
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| costing analysis | CostingAnalysis |
A costed BOM version with totals, price, margin, and a lifecycle. | Maliyet analizi | src/cluster_erp/modules/costing/costing_analyses/models.py:23 |
| cost breakdown item | CostBreakdownItem |
One material, labour, nesting, or overhead line of an analysis. | Maliyet kırılım kalemi | src/cluster_erp/modules/costing/costing_analyses/models.py:192 |
| rate snapshot | CostingRateSnapshot |
A frozen copy of a rate used by a calculation. | Oran anlık görüntüsü | src/cluster_erp/modules/costing/costing_analyses/models.py:293 |
| labour rate | LaborRate |
An hourly or unit price for a work centre and operation type. | İşçilik oranı | src/cluster_erp/modules/costing/labor_rates/models.py:14 |
| overhead rate | OverheadRate |
A percentage or per-unit overhead with a basis and an apply order. | Genel gider oranı | src/cluster_erp/modules/costing/overhead_rates/models.py:12 |
| scenario | is_scenario |
A cloned analysis with cost multipliers for what-if comparison. | Senaryo | src/cluster_erp/modules/costing/costing_analyses/models.py:61 |
Related pages¶
- Modules index
- BOM module
- Nesting module
- Quotations module
- Money and Decimal
- API reference for this module
Verified against commit 92f8051 on 2026-11-13.