Material Takeoff (MTO) Schemas Documentation¶
Last Updated: 2025-11-27
Database: lco-construction
Container: MaterialTakeoff
Partition Key: /projectId
Table of Contents¶
- Overview
- Discipline Types
- Common Fields (All Disciplines)
- Calculated Columns by Discipline
- Calculation Formulas
- Frontend Implementation
- Schema Consistency
Overview¶
The Material Takeoff (MTO) system tracks construction materials, equipment, and associated costs across 9 engineering disciplines. Each discipline has:
- Common fields shared across all disciplines
- Discipline-specific fields for technical details
- Calculated/computed columns with Excel-style formulas
Total Columns Across All Disciplines: 511 Total Calculated Columns: 50
Database Structure¶
Cosmos DB
└── lco-construction (database)
└── MaterialTakeoff (container)
├── Partition Key: /projectId
└── Documents by discipline type
Schema Versions¶
- Backend: Python Pydantic models (
/backend/app/schemas/mto.py) - Frontend: TypeScript interfaces (
/frontend/src/types/mto.ts) - Excel Source: 9 discipline sheet schemas (
/schemas/*.json)
Discipline Types¶
| Discipline | API Enum | Columns | Calculated | Complexity | Description |
|---|---|---|---|---|---|
| Electrical Equipment | electrical_equipment |
48 | 2 | ⭐ | Motors, MCCs, transformers, switchgear |
| Mechanical Equipment | mechanical_equipment |
50 | 2 | ⭐ | Pumps, tanks, heat exchangers, vessels |
| Bulk Electrical | bulk_electrical |
34 | 2 | ⭐ | Cables, conduits, cable trays |
| Instrumentation | instrumentation |
37 | 3 | ⭐⭐ | Sensors, control valves, transmitters |
| Piping | piping |
143 | 11 | ⭐⭐⭐⭐⭐ | Pipes, fittings, flanges, valves, bolts |
| Civil | civil |
52 | 8 | ⭐⭐⭐⭐ | Excavation, backfill, earthwork |
| Concrete | concrete |
46 | 9 | ⭐⭐⭐⭐ | Concrete, rebar, formwork |
| Structural | structural |
54 | 7 | ⭐⭐⭐ | Steel members, connections, coatings |
| Architectural | architectural |
47 | 6 | ⭐⭐⭐ | Walls, finishes, doors, windows |
Common Fields (All Disciplines)¶
Primary Keys & Discriminator¶
id(string) - Document IDmtoId(string) - MTO identifier (synced with id)discipline(DisciplineType) - Discipline discriminatorprojectId(string) - Partition keyserviceId(string, optional) - Reference to servicecreatedAt(string, datetime)updatedAt(string, datetime)
Common Identification Fields¶
sourceFileName- Original import file nameitemNo- Item numbermaturityLevel- Maturity level (IFC, IFR, etc.)revision- Revision numbertagNo- Tag number (Equipment/Piping)lineNo- Line number (Piping)pfdNumber- PFD numberdrawingNoOrPid- Drawing/P&ID numbersystemSubsystem- System/Subsystemfunction- Function
Description Fields¶
description- Main descriptionequipmentDescription- Equipment descriptiondescription1throughdescription5- Additional description fields
Quantity & Wastage Fields¶
quantity(number) - Base quantityuom(string) - Unit of measure (EA, M, M2, M3, etc.)wastePercent(number) - Waste percentagetotalQuantityInclWastage(number) - CALCULATED:quantity * (1 + wastePercent)
Cost Fields¶
manhourPerUnit(number) - CALCULATED (complex formula by discipline)materialUnitPrice(number) - Material unit price inputtotalMaterialUnitPrice(number) - CALCULATED (complex formula)tagEquipmentUnitPrice(number) - Equipment unit price inputtotalEquipmentUnitPrice(number) - CALCULATED (complex formula)subcontractorCostAllInRate(number) - Subcontractor all-in ratetotalEstimationUnitPrice(number) - CALCULATED Final estimation price (see Labor Cost Calculations)
Installation - Labour Fields (NEW)¶
laborProdFactor(number) - Labor productivity factor from discipline settingstotalManhours(number) - CALCULATED:quantity * manhourPerUnit * laborProdFactorhourlyCost(number) - Hourly labor cost from crew blended ratelaborUnitCost(number) - CALCULATED:manhourPerUnit * laborProdFactor * hourlyCosttotalLaborCost(number) - CALCULATED:quantity * laborUnitCost
Subcontractor Assignment Fields¶
subcontractorCode(string) - Reference to subcontractor codesubcontractorName(string) - Subcontractor name for display
Logistics Fields¶
shippingResponsibility- Shipping responsibilityshippingWeight(number) - Shipping weightshippingVolume(number) - Shipping volumedeliveryLeadTime(number) - Delivery lead time (weeks)countryOfOrigin- Country of originmaterialFreight(number) - Material freight costmaterialTransportation(number) - Material transportation cost
Manufacturer Fields¶
manufacturer- ManufacturermanufacturerSupplier- Manufacturer/SuppliermodelNumber- Model numbermodel- ModelserialNumber- Serial numberdatasheetSpecificationReference- Datasheet/Spec reference
Installation/Testing Fields¶
testingCommissioningRequirements- Testing/Commissioning requirementstestingCalibrationRequirements- Testing/Calibration requirementsspecialToolsRequired- Special tools requiredspecialInstallationRequirements- Special installation requirements
Other Fields¶
crew- Assigned crewcomments- CommentsbasisOfEstimateAssumptions- Basis of estimate assumptions
Calculated Columns by Discipline¶
Electrical Equipment (2 calculated columns)¶
- Quantity - Base quantity calculation
- Total Quantity incl. Wastage -
=quantity * (1 + wastePercent)
Mechanical Equipment (2 calculated columns)¶
- Quantity - Base quantity calculation
- Total Quantity incl. Wastage -
=quantity * (1 + wastePercent)
Bulk Electrical (2 calculated columns)¶
- Quantity - Base quantity calculation
- Total Quantity incl. Wastage -
=quantity * (1 + wastePercent)
Instrumentation (3 calculated columns)¶
- Quantity - Base quantity calculation
- Total Quantity incl. Wastage -
=quantity * (1 + wastePercent) - (1 additional - needs verification)
Piping (11 calculated columns) ⚠️ MOST COMPLEX¶
Fabrication Calculations¶
-
Number of Cuts (
Calculates cuts needed based on total length divided by pipe lengthnumberOfCuts) -
Number of Bevels (
numberOfBevels) -
Weld Quantity (
weldQuantity) -
Heat Tracing (
heatTracing) -
Insulated Length/Area (
insulatedLengthArea)
Quantity Calculation¶
-
Quantity (
quantity) - Multi-row formula based on item type: -
Total Quantity incl. Wastage (
totalQuantityInclWastage)
Cost Calculations¶
-
Manhour per Unit (
manhourPerUnit) - 35+ dependencies:=( handlingErection * quantity + manhourPerCut * numberOfCuts + manhourPerBevel * numberOfBevels + preheatingEa * weldQuantity + manhoursPerWeld * weldQuantity + manhoursPerThread * threadQuantity + manhoursPerBolt * boltQuantity + manhourPerPressFit * pressFitQuantity + manhoursPerMechanicalJoint * mechanicalJointQuantity + stressRelievingEa * weldQuantity + manhoursPerValve * valveQuantity + manhoursPerFitting * fittingQuantity + manhoursPerSpecialtyItem * specialtyItemQuantity + manhoursPerPipeSupport * supportQuantity + inspectionTestingNdtRt * weldQuantity + manhoursPerUnitCoating * coatedLengthArea + manhoursPerUnitInsulation * insulatedLengthArea + manhoursPerUnitSandblastPaint * coatedLengthArea + manhoursPerUnitHeatTracing * heatTracing + inspectionTestingPipesHydrostatic * quantity ) / quantity -
Total Material Unit Price (
totalMaterialUnitPrice) - 13 components:=( pipeUnitPrice * totalQuantityInclWastage + fittingUnitPrice * fittingQuantity + flangeUnitPrice * flangeQuantity + boltUnitPrice * boltQuantity + valveUnitPrice * valveQuantity + specialtyItemUnitPrice * specialtyItemQuantity + coatingUnitPrice * coatedLengthArea + paintingUnitPrice * coatedLengthArea + insulationUnitPrice * insulatedLengthArea + pipeSupportUnitPrice * supportQuantity + heatTracingUnitPrice * heatTracing + materialTransportation * totalQuantityInclWastage + materialFreight * totalQuantityInclWastage ) / quantity -
Total Tag Equipment Unit Price (
totalEquipmentUnitPrice)
Civil (8 calculated columns)¶
-
Volume (
volume) -
Surface Area (
area) -
Quantity with Swell/Shrink (
quantityWSwellShrink) -
Quantity (
quantity) - Based on calculation type -
Total Quantity incl. Wastage (
totalQuantityInclWastage) -
Manhour per Unit (
manhourPerUnit) - Multi-component formula -
Total Material Unit Price (
totalMaterialUnitPrice) -
Total Tag Equipment Unit Price (
totalEquipmentUnitPrice)
Concrete (9 calculated columns)¶
-
Volume (m³) (
volumeM3) -
Area (m²) (
areaM2) -
Formwork Area (
formworkArea) - Based on geometry -
Total Weight (
totalWeight) -
Quantity (
quantity) - Can be volume, area, or weight based -
Total Quantity incl. Wastage (
totalQuantityInclWastage) -
Manhour per Unit (
manhourPerUnit) -
Total Material Unit Price (
totalMaterialUnitPrice) -
Total Tag Equipment Unit Price (
totalEquipmentUnitPrice)
Structural (7 calculated columns)¶
-
Total Weight (
totalWeight) -
Surface Area (
surfaceArea) -
Quantity (
quantity) -
Total Quantity incl. Wastage (
totalQuantityInclWastage) -
Manhour per Unit (
manhourPerUnit) -
Total Material Unit Price (
totalMaterialUnitPrice) -
Total Tag Equipment Unit Price (
totalEquipmentUnitPrice)
Architectural (6 calculated columns)¶
-
Area (m²) (
areaM2) -
Volume (m³) (
volumeM3) -
Quantity (
This intelligently calculates quantity based on the unit of measure!quantity) - Conditional on UoM: -
Total Quantity incl. Wastage (
totalQuantityInclWastage) -
Total Material Unit Price (
totalMaterialUnitPrice) -
Total Tag Equipment Unit Price (
totalEquipmentUnitPrice)
Calculation Formulas¶
Standard Formula Patterns¶
All disciplines use these consistent patterns:
1. Wastage Calculation¶
Example: - quantity = 100 m - wastePercent = 0.05 (5%) - totalQuantityInclWastage = 100 * (1 + 0.05) = 105 m
2. Material Unit Price¶
totalMaterialUnitPrice = (
sum of (component_price * component_quantity) +
freight_costs +
transportation_costs
) / base_quantity
Pattern: Sum all material costs, then divide by quantity to get unit price
3. Equipment Unit Price¶
totalEquipmentUnitPrice = (
equipment_price * quantity +
freight +
spare_parts +
vendors_rep
) / base_quantity
Pattern: Equipment cost + one-time fees, divided by quantity
4. Manhour Calculation¶
Pattern: Sum all labor operations, divide by quantity
5. Labor Cost Calculations (NEW)¶
These fields support the estimation workflow with crew-based labor costing:
// Input fields (set from discipline settings and crew assignment)
laborProdFactor: number; // Productivity factor (default: 1.0)
hourlyCost: number; // From crew blended rate
// Calculated fields
totalManhours = quantity * manhourPerUnit * laborProdFactor;
laborUnitCost = manhourPerUnit * laborProdFactor * hourlyCost;
totalLaborCost = quantity * laborUnitCost;
Field Descriptions:
| Field | Source | Formula |
|---|---|---|
laborProdFactor |
Discipline settings | Manual input or inherited from project |
hourlyCost |
ServiceCrew blended rate | Retrieved from assigned crew |
totalManhours |
Calculated | quantity * manhourPerUnit * laborProdFactor |
laborUnitCost |
Calculated | manhourPerUnit * laborProdFactor * hourlyCost |
totalLaborCost |
Calculated | quantity * laborUnitCost |
6. Total Estimation Unit Price (NEW)¶
The final calculated price for estimation purposes:
// If subcontractor is assigned with rate > 0, use their all-in rate
if (subcontractorCostAllInRate !== null && subcontractorCostAllInRate > 0) {
totalEstimationUnitPrice = subcontractorCostAllInRate;
} else {
// Otherwise, calculate from components
totalEstimationUnitPrice = laborUnitCost + totalMaterialUnitPrice + totalEquipmentUnitPrice;
}
Logic:
1. Subcontractor Override: If subcontractorCostAllInRate > 0, it represents a complete price from the subcontractor and overrides all other calculations.
2. Component Calculation: Otherwise, sum the three cost components:
- laborUnitCost - Labor cost per unit
- totalMaterialUnitPrice - Material cost per unit
- totalEquipmentUnitPrice - Equipment cost per unit
Frontend Implementation¶
Implementation Strategy¶
Phase 1: Calculation Engine (Utility Functions)¶
Create calculation utilities in /frontend/src/utils/mtoCalculations.ts:
// Calculation engine for MTO formulas
export const mtoCalculations = {
// Common calculations (all disciplines)
calculateWastage(quantity: number, wastePercent: number): number {
return quantity * (1 + wastePercent);
},
// Geometric calculations
calculateArea(length: number, width: number): number {
return length * width;
},
calculateVolume(length: number, width: number, height: number): number {
return length * width * height;
},
// Piping-specific calculations
piping: {
calculateNumberOfCuts(totalLength: number, lengthPerUnit: number): number {
return Math.ceil(totalLength / lengthPerUnit);
},
calculateNumberOfBevels(numberOfCuts: number, bevelFactor: number = 2): number {
return numberOfCuts * bevelFactor;
},
calculateManhourPerUnit(params: PipingManhourParams): number {
const {
handlingErection,
quantity,
manhourPerCut,
numberOfCuts,
// ... all 20+ parameters
} = params;
return (
handlingErection * quantity +
manhourPerCut * numberOfCuts +
// ... rest of formula
) / quantity;
},
calculateTotalMaterialUnitPrice(params: PipingMaterialPriceParams): number {
// Complex formula implementation
},
},
// Structural-specific calculations
structural: {
calculateTotalWeight(weightPerLength: number, length: number, number: number): number {
return weightPerLength * length * number;
},
calculateSurfaceArea(totalWeight: number, factor: number = 6): number {
return factor * totalWeight;
},
},
// Architectural-specific calculations
architectural: {
calculateQuantityByUoM(
uom: string,
volume: number,
area: number,
length: number,
numberEa: number
): number {
switch (uom) {
case 'm3':
case 'M3':
return volume * numberEa;
case 'm2':
case 'M2':
return area * numberEa;
case 'm':
case 'M':
return length * numberEa;
default:
return numberEa;
}
},
},
// Add other disciplines as needed...
};
Phase 2: React Hooks for Calculations¶
Create hooks in /frontend/src/hooks/useMTOCalculations.ts:
export function useMTOCalculations(discipline: DisciplineType) {
const calculateFields = useCallback((item: MaterialTakeoff) => {
const calculated = { ...item };
// Common calculations for all disciplines
if (item.quantity && item.wastePercent) {
calculated.totalQuantityInclWastage = mtoCalculations.calculateWastage(
item.quantity,
item.wastePercent
);
}
// Discipline-specific calculations
switch (discipline) {
case DisciplineType.PIPING:
return calculatePipingFields(calculated as PipingMTO);
case DisciplineType.STRUCTURAL:
return calculateStructuralFields(calculated as StructuralMTO);
case DisciplineType.ARCHITECTURAL:
return calculateArchitecturalFields(calculated as ArchitecturalMTO);
// ... other disciplines
default:
return calculated;
}
}, [discipline]);
return { calculateFields };
}
Phase 3: Real-time Calculation in Tables¶
Integrate into DataTable component:
// In DisciplineSheetPage.tsx
const { calculateFields } = useMTOCalculations(disciplineApiEnum);
// When user edits a cell
const handleCellEdit = (rowId: string, field: string, value: any) => {
setMtoItems(prev =>
prev.map(item => {
if (item.id === rowId) {
const updated = { ...item, [field]: value };
// Recalculate all dependent fields
return calculateFields(updated);
}
return item;
})
);
};
Phase 4: Save to Database¶
Only save when user clicks "Save":
const handleSave = async () => {
setIsSaving(true);
try {
// Save all items with calculated fields
await Promise.all(
mtoItems.map(item =>
mtoApi.updateMTO(projectId!, item.id, item)
)
);
toast({ title: 'Saved successfully' });
} catch (error) {
toast({ title: 'Save failed', variant: 'destructive' });
} finally {
setIsSaving(false);
}
};
Benefits of Frontend Calculations¶
- Instant Feedback - User sees results immediately when changing inputs
- No Backend Load - Calculations happen in browser
- Offline Capable - Works without network connection
- Better UX - No loading spinners for calculations
- Easy to Test - Pure functions are unit testable
- Flexible - Easy to add/modify formulas
Calculation Dependencies¶
Track which fields trigger recalculation:
const CALCULATION_DEPENDENCIES: Record<string, string[]> = {
// When these fields change, recalculate these fields
quantity: ['totalQuantityInclWastage', 'totalMaterialUnitPrice', 'manhourPerUnit'],
wastePercent: ['totalQuantityInclWastage'],
numberOfCuts: ['numberOfBevels', 'weldQuantity', 'manhourPerUnit'],
// ... etc
};
Schema Consistency¶
Backend → Frontend Mapping¶
All schemas are consistent between backend Python and frontend TypeScript:
| Backend (Python) | Frontend (TypeScript) | Database (Cosmos DB) |
|---|---|---|
DisciplineType.PIPING |
DisciplineType.PIPING |
"piping" |
quantity: float \| None |
quantity?: number |
"quantity": 100.5 |
tag_no: str \| None = Field(alias="tagNo") |
tagNo?: string |
"tagNo": "P-001" |
total_quantity_incl_wastage |
totalQuantityInclWastage |
"totalQuantityInclWastage": 105 |
Key Points:
- Backend uses snake_case with Pydantic alias for camelCase JSON
- Frontend uses camelCase directly
- Database stores in camelCase
- All three are kept in sync via type definitions
Adding New Calculated Columns¶
When adding a new calculated column:
- Update Excel schema (
/schemas/<Discipline>_schema.json) - Add to
calculated_columnsarray -
Include formula, dependencies, example cells
-
Update backend schema (
/backend/app/schemas/mto.py) - Add field to discipline-specific class
- Mark as optional with default None
-
Add Pydantic Field with alias
-
Update frontend types (
/frontend/src/types/mto.ts) - Add field to discipline-specific interface
- Use camelCase naming
-
Mark as optional
-
Add calculation logic (
/frontend/src/utils/mtoCalculations.ts) - Implement formula in calculation engine
-
Add to dependency tracking
-
Update documentation (this file)
- Add to calculated columns list
- Document formula and purpose
References¶
Source Files¶
- Excel Schemas:
/schemas/*_schema.json - Schema Summary:
/schemas/schemas_summary.json - Backend Repository:
/backend/app/repositories/mto.py - Backend Schemas:
/backend/app/schemas/mto.py - Frontend Types:
/frontend/src/types/mto.ts - Frontend Pages:
/frontend/src/pages/DisciplineSheetPage.tsx/frontend/src/pages/EstimationServicePage.tsx
Related Documentation¶
- Schema Verification Report:
/tmp/schema_verification_report.md - API Reference:
/docs/API_REFERENCE.md - Architecture:
/docs/ARCHITECTURE.md
End of Document