Estimating Engine
The estimating engine turns quantities and catalog values into costs and a selling price. It is plain .NET code with no Orchard Core dependency, no I/O and no AI: the same input always gives the same result. Every amount uses decimal.
| Package | Namespace | What it provides |
|---|---|---|
CrestApps.Estimator.Units | CrestApps.Estimator.Units | Units of measure, exact conversions, quantities, quantity bases (E, C, M), waste and purchase rounding |
CrestApps.Estimator.Engine | CrestApps.Estimator.Engine.* | Labor, assembly expansion, line extensions, the recap, the estimate calculator and calculation traces |
CrestApps.Estimator.Drawing | CrestApps.Estimator.Drawing.* | Drawing sheets: see Drawing Analysis |
The engine does not read the catalog itself. The caller passes a snapshot of the items and assemblies, so later catalog changes never change a calculated revision.
Units
BuiltInUnits holds the units every estimate can use. Conversion factors are exact definitions, so conversions between imperial and metric units round-trip without loss.
| Code | Name | Dimension | Base unit |
|---|---|---|---|
EA | Each | Count | each |
LS | Lump sum | Lump sum | lump sum |
M, FT, LF, IN | Meter, foot, linear foot, inch | Length | meter |
M2, SF, SY | Square meter, square foot, square yard | Area | square meter |
M3, CF, CY | Cubic meter, cubic foot, cubic yard | Volume | cubic meter |
KG, LB, TON | Kilogram, pound, short ton | Mass | kilogram |
HR | Hour | Time | hour |
Quantity is a value and a unit. ConvertTo, + and - convert between units of the same dimension and throw InvalidOperationException for different dimensions.
UnitRegistry.CreateDefault() returns an IUnitRegistry with the built-in units. Industry packages register their own units with Register, for example the roofing square:
registry.Register(new UnitOfMeasure("SQ", "Roofing square", UnitDimension.Area, 9.290304m));
Quantity bases
Prices and labor units can be quoted per one (E), per hundred (C) or per thousand (M) units, as electrical wire and conduit are. QuantityBasis.Extend(quantity, rate) divides by the basis, and QuantityBasisExtensions.TryParse reads the E, C and M codes.
Waste and purchase rounding
PurchaseRounding.ApplyWaste adds a waste allowance and PurchaseRounding.RoundUpToPackage rounds up to whole packages, such as 500-foot reels. Both apply to material only: labor is always calculated from the installed quantity.
Labor
LaborCalculator.Calculate returns base hours, the combined factor, adjusted hours and the cost, rounded to cents:
adjusted hours = quantity × labor unit hours ÷ basis × Π factors
cost = adjusted hours × hourly rate
A LaborFactor has a name, a value greater than zero and a LaborFactorScope. Factors apply in scope order: Estimate, Breakdown, Category, Line. A Line factor must state its reason.
CrewRate.Composite calculates the count-weighted average wage of a crew mix (CrewMember) plus burden.
Rates and burden
LaborRateCalculator.LoadedRate(wage, burdens) adds each LaborBurden to a wage: a PercentOfWage burden adds that percentage of the wage, an AmountPerHour burden adds its amount. LaborRateCalculator.WithOvertime(rate, sharePercent, multiplier) blends a rate with overtime: rate × (1 + share / 100 × (multiplier − 1)), so 10% of the hours at 1.5 raises the rate by 5%.
Production mode
ProductionCalculator.Calculate(quantity, unitsPerCrewHour, crew, factors, equipment) is the second way to consume labor, for work estimated by crew production rather than labor units per item. Crew hours are quantity ÷ unitsPerCrewHour × the factor stack; each CrewMember works the crew hours times its count, at its loaded rate; each EquipmentResource runs the crew hours. The ProductionResult gives the crew hours, the combined factor, the labor hours and cost, the equipment cost, the total and the hours and cost of each classification. The Estimator modules do not use production mode yet; it is in the engine so that heavy civil packages need no engine change.
Assemblies
An AssemblyDefinition is a reusable bill of items with parameters and their defaults. Each AssemblyComponent is an item or a nested assembly, created with:
| Factory | Quantity |
|---|---|
AssemblyComponent.Item(code, perUnit) | Assembly quantity × perUnit |
AssemblyComponent.ItemByFormula(code, formula) | The value of formula |
AssemblyComponent.Assembly(code, perUnit) | Assembly quantity × perUnit, then expanded |
AssemblyComponent.AssemblyByFormula(code, formula) | The value of formula, then expanded |
Set Condition on a component to include it only when the condition formula is not zero, and WasteRate (a fraction, such as 0.1) to replace the waste of the item for this component.
AssemblyExpander.Expand(code, quantity, parameters) returns an ExpandedComponent per item, with its quantity, its assembly path (for example REC-2G/REC-20A/REC-DUP-20A) and the WasteRate of its component. ExpandTree returns the same items and also every assembly of the tree with its quantity and path (ExpandedAssembly), for assemblies that carry their own labor. Formulas read Quantity and the assembly parameters; values passed in parameters replace the defaults, and reach nested assemblies too. The expander throws InvalidOperationException for a cycle of assemblies or a negative formula result, and KeyNotFoundException for an undefined assembly.
The engine does not parse formulas. The caller supplies an IFormulaEvaluator; the Estimator Catalog module supplies one on the Orchard Core expression language (see Estimator Catalog module).
Lines
ItemSnapshot holds the frozen catalog values of an item: unit, price and price basis, waste rate, package size, labor unit and labor category. LineCalculator.Calculate extends an installed quantity:
- Purchase quantity = installed quantity plus waste, rounded up to packages.
- Material = purchase quantity × price ÷ price basis, rounded to cents.
- Labor from the installed quantity, through
LaborCalculator.
Each LineResult carries a Trace: one TraceEntry per step, with the step name, how it was computed and the value. This is what lets an estimate explain any number.
Recap
RecapCalculator.Calculate(costs, settings, laborHours) runs the markup waterfall over CostTotals (material, labor, equipment, subcontracts, quotes, direct job expenses and allowances):
| Step | Basis |
|---|---|
| Material tax | Material × MaterialTaxRate |
| Prime cost | Every direct cost plus material tax |
| Overhead | Prime cost × OverheadRate, plus labor hours × OverheadPerLaborHour |
| Profit | Markup: cost × rate. Margin: cost ÷ (1 − rate) − cost. Cost is prime cost plus overhead. With CostTypeMarkups (markup only), each cost type × its rate (material with its tax), plus the overhead × ProfitRate |
| Contingency | Cost plus profit × ContingencyRate |
| Bond | A BondSchedule of tiers, solved on the contract amount including the bond |
| Rounding | The selling price is rounded up to RoundTo |
Every amount is rounded to cents (Money.Round, midpoints away from zero). The prime cost plus every adjustment always equals the selling price.
RecapCalculator.SolveProfitRate(costs, settings, targetPrice, laborHours) solves the profit rate that reaches a target selling price, with the bond on the price. It solves one rate on the whole cost (no cost type markups) without rounding, by bisection on the rate (a markup grows until it passes the target; a margin stays below 1), and keeps the rate whose price is nearest the target. A PriceToTargetResult gives the rate, the settings with it, the recap and whether the target is reachable: a target below the price without profit gives a rate of 0 and Reachable false.
Estimates
EstimateCalculator ties the steps together. Create it with the item snapshots and an AssemblyExpander, then call Calculate(EstimateInput). Each EstimateLineInput names exactly one item or assembly. Estimate factors are added to each line's own factors. The result holds every line with its component extensions, the cost totals (lines plus DirectCosts), the labor hours and the recap.
Tests
The engine has its own test project, tests/CrestApps.Estimator.Engine.Tests:
- Unit tests for every calculation.
- Property tests (FsCheck) for invariants, such as exact unit round-trips and the recap adding up to the selling price.
- Golden estimates: each
Golden/Fixtures/*.input.jsonestimate is recalculated and compared with its.approved.jsonsnapshot. A difference fails the test and writes a.received.jsonfile. Review it, and replace the approved file only when the change is intended.
Pull request CI runs the engine tests under dotnet-coverage with tests/engine.coverage.xml and keeps the Cobertura report as the engine-coverage artifact.