Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generate material procurement lists from CWICR data. Calculate quantities with waste factors, group by supplier categories, and create purchase orders.
.claude/skills/datadrivenconstruction-cwicr-material-procurement/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -13% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 170% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 128% | 0% |
Material procurement needs accurate quantity lists:
Generate procurement lists from CWICR material data with waste factors, delivery scheduling, and supplier grouping.
pythonimport pandas as pd import numpy as np from typing import Dict, Any, List, Optional, Tuple from dataclasses import dataclass, field from datetime import datetime, timedelta from enum import Enum from collections import defaultdict class MaterialCategory(Enum): """Material categories for procurement.""" CONCRETE = "concrete" STEEL = "steel" TIMBER = "timber" MASONRY = "masonry" FINISHES = "finishes" MEP = "mep" INSULATION = "insulation" ROOFING = "roofing" EARTHWORK = "earthwork" OTHER = "other" class ProcurementPriority(Enum): """Procurement priority levels.""" CRITICAL = 1 HIGH = 2 MEDIUM = 3 LOW = 4 @dataclass class MaterialItem: """Single material item for procurement.""" material_code: str description: str category: MaterialCategory unit: str net_quantity: float waste_factor: float gross_quantity: float unit_price: float total_cost: float lead_time_days: int required_date: datetime order_date: datetime supplier: str = "" work_item_codes: List[str] = field(default_factory=list) @dataclass class ProcurementList: """Complete procurement list.""" project_name: str generated_date: datetime total_items: int total_cost: float items: List[MaterialItem] by_category: Dict[str, float] by_supplier: Dict[str, List[MaterialItem]] # Standard waste factors by material type WASTE_FACTORS = { 'concrete': 0.05, # 5% 'reinforcement': 0.03, # 3% 'formwork': 0.10, # 10% 'masonry': 0.05, # 5% 'timber': 0.08, # 8% 'drywall': 0.10, # 10% 'tiles': 0.10, # 10% 'paint': 0.05, # 5% 'insulation': 0.05, # 5% 'pipes': 0.03, # 3% 'cables': 0.05, # 5% 'default': 0.05 # 5% } # Standard lead times by category (days) LEAD_TIMES = { 'concrete': 1, # Ready-mix 'reinforcement': 7, # Steel delivery 'formwork': 3, # Standard forms 'masonry': 5, # Block delivery 'timber': 5, # Lumber 'structural_steel': 21, # Fabrication 'windows': 28, # Manufacturing 'doors': 14, # Standard doors 'mep': 14, # MEP equipment 'finishes': 7, # Standard finishes 'default': 7 } class CWICRMaterialProcurement: """Generate procurement lists from CWICR data.""" def __init__(self, cwicr_data: pd.DataFrame, resources_data: pd.DataFrame = None): self.work_items = cwicr_data self.resources = resources_data self._index_data() def _index_data(self): """Index data for fast lookup.""" if 'work_item_code' in self.work_items.columns: self._work_index = self.work_items.set_index('work_item_code') else: self._work_index = None def get_waste_factor(self, material_type: str) -> float: """Get waste factor for material type.""" material_lower = str(material_type).lower() for key, factor in WASTE_FACTORS.items(): if key in material_lower: return factor return WASTE_FACTORS['default'] def get_lead_time(self, material_type: str) -> int: """Get lead time for material type.""" material_lower = str(material_type).lower() for key, days in LEAD_TIMES.items(): if key in material_lower: return days return LEAD_TIMES['default'] def get_category(self, material_type: str) -> MaterialCategory: """Determine material category.""" material_lower = str(material_type).lower() category_mapping = { 'concrete': MaterialCategory.CONCRETE, 'cement': MaterialCategory.CONCRETE, 'steel': MaterialCategory.STEEL, 'rebar': MaterialCategory.STEEL, 'reinforcement': MaterialCategory.STEEL, 'timber': MaterialCategory.TIMBER, 'wood': MaterialCategory.TIMBER, 'lumber': MaterialCategory.TIMBER, 'masonry': MaterialCategory.MASONRY, 'block': MaterialCategory.MASONRY, 'brick': MaterialCategory.MASONRY, 'paint': MaterialCategory.FINISHES, 'tile': MaterialCategory.FINISHES, 'floor': MaterialCategory.FINISHES, 'electrical': MaterialCategory.MEP, 'plumbing': MaterialCategory.MEP, 'hvac': MaterialCategory.MEP, 'insulation': MaterialCategory.INSULATION, 'roof': MaterialCategory.ROOFING } for key, cat in category_mapping.items(): if key in material_lower: return cat return MaterialCategory.OTHER def extract_materials(self, items: List[Dict[str, Any]], schedule: Dict[str, datetime] = None) -> List[MaterialItem]: """Extract material requirements from work items.""" materials = defaultdict(lambda: { 'net_quantity': 0, 'work_items': [], 'required_date': None }) for item in items: code = item.get('work_item_code', item.get('code')) qty = item.get('quantity', 0) required_date = item.get('required_date') if self._work_index is not None and code in self._work_index.index: work_item = self._work_index.loc[code] # Get material info from work item material_desc = str(work_item.get('material_description', work_item.get('description', ''))) material_unit = str(work_item.get('material_unit', work_item.get('unit', ''))) material_norm = float(work_item.get('material_norm', 1) or 1) material_cost = float(work_item.get('material_cost', 0) or 0) # Calculate material quantity material_qty = qty * material_norm # Aggregate by material description mat_key = f"{material_desc}|{material_unit}" materials[mat_key]['net_quantity'] += material_qty materials[mat_key]['work_items'].append(code) materials[mat_key]['description'] = material_desc materials[mat_key]['unit'] = material_unit materials[mat_key]['unit_price'] = material_cost / material_norm if material_norm > 0 else 0 if required_date: if materials[mat_key]['required_date'] is None: materials[mat_key]['required_date'] = required_date else: materials[mat_key]['required_date'] = min( materials[mat_key]['required_date'], required_date ) # Convert to MaterialItem list result = [] for mat_key, data in materials.items(): description = data['description'] waste_factor = self.get_waste_factor(description) lead_time = self.get_lead_time(description) net_qty = data['net_quantity'] gross_qty = net_qty * (1 + waste_factor) unit_price = data.get('unit_price', 0) required_date = data['required_date'] or datetime.now() + timedelta(days=30) order_date = required_date - timedelta(days=lead_time) result.append(MaterialItem( material_code=mat_key.split('|')[0][:20], description=description, category=self.get_category(description), unit=data['unit'], net_quantity=round(net_qty, 2), waste_factor=waste_factor, gross_quantity=round(gross_qty, 2), unit_price=round(unit_price, 2), total_cost=round(gross_qty * unit_price, 2), lead_time_days=lead_time, required_date=required_date, order_date=order_date, work_item_codes=data['work_items'] )) return result def generate_procurement_list(self, items: List[Dict[str, Any]], project_name: str = "Project") -> ProcurementList: """Generate complete procurement list.""" materials = self.extract_materials(items) # Group by category by_category = defaultdict(float) for mat in materials: by_category[mat.category.value] += mat.total_cost # Group by supplier (placeholder - would use supplier mapping) by_supplier = defaultdict(list) for mat in materials: supplier = self._suggest_supplier(mat) mat.supplier = supplier by_supplier[supplier].append(mat) return ProcurementList( project_name=project_name, generated_date=datetime.now(), total_items=len(materials), total_cost=sum(m.total_cost for m in materials), items=materials, by_category=dict(by_category), by_supplier=dict(by_supplier) ) def _suggest_supplier(self, material: MaterialItem) -> str: """Suggest supplier based on material category.""" supplier_mapping = { MaterialCategory.CONCRETE: "Ready-Mix Supplier", MaterialCategory.STEEL: "Steel Fabricator", MaterialCategory.TIMBER: "Lumber Yard", MaterialCategory.MASONRY: "Masonry Supplier", MaterialCategory.MEP: "MEP Distributor", MaterialCategory.FINISHES: "Building Materials", MaterialCategory.INSULATION: "Insulation Supplier", MaterialCategory.ROOFING: "Roofing Supplier" } return supplier_mapping.get(material.category, "General Supplier") def create_purchase_order(self, materials: List[MaterialItem], supplier: str, po_number: str) -> Dict[str, Any]: """Create purchase order for supplier.""" po_items = [m for m in materials if m.supplier == supplier] return { 'po_number': po_number, 'supplier': supplier, 'date': datetime.now().isoformat(), 'delivery_date': min(m.required_date for m in po_items).isoformat() if po_items else None, 'items': [ { 'description': m.description, 'quantity': m.gross_quantity, 'unit': m.unit, 'unit_price': m.unit_price, 'total': m.total_cost } for m in po_items ], 'subtotal': sum(m.total_cost for m in po_items), 'item_count': len(po_items) } def export_to_excel(self, procurement_list: ProcurementList, output_path: str) -> str: """Export procurement list to Excel.""" with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # All materials items_df = pd.DataFrame([ { 'Description': m.description, 'Category': m.category.value, 'Unit': m.unit, 'Net Qty': m.net_quantity, 'Waste %': m.waste_factor * 100, 'Gross Qty': m.gross_quantity, 'Unit Price': m.unit_price, 'Total Cost': m.total_cost, 'Lead Time': m.lead_time_days, 'Order By': m.order_date.strftime('%Y-%m-%d'), 'Required': m.required_date.strftime('%Y-%m-%d'), 'Supplier': m.supplier } for m in procurement_list.items ]) items_df.to_excel(writer, sheet_name='Materials', index=False) # By category cat_df = pd.DataFrame([ {'Category': cat, 'Total Cost': cost} for cat, cost in procurement_list.by_category.items() ]) cat_df.to_excel(writer, sheet_name='By Category', index=False) # Summary summary_df = pd.DataFrame([{ 'Project': procurement_list.project_name, 'Generated': procurement_list.generated_date.strftime('%Y-%m-%d'), 'Total Items': procurement_list.total_items, 'Total Cost': procurement_list.total_cost }]) summary_df.to_excel(writer, sheet_name='Summary', index=False) return output_path def get_critical_orders(self, procurement_list: ProcurementList, days_ahead: int = 14) -> List[MaterialItem]: """Get materials that need to be ordered soon.""" cutoff = datetime.now() + timedelta(days=days_ahead) return [ m for m in procurement_list.items if m.order_date <= cutoff ] def aggregate_by_material(self, items: List[Dict[str, Any]]) -> pd.DataFrame: """Aggregate materials across multiple work items.""" materials = self.extract_materials(items) df = pd.DataFrame([ { 'Material': m.description, 'Category': m.category.value, 'Total Qty': m.gross_quantity, 'Unit': m.unit, 'Total Cost': m.total_cost, 'Work Items': len(m.work_item_codes) } for m in materials ]) return df.sort_values('Total Cost', ascending=False)
python# Load CWICR data cwicr = pd.read_parquet("TR_workitems_costs_resources_DDC_CWICR.parquet") # Initialize procurement generator procurement = CWICRMaterialProcurement(cwicr) # Define work items items = [ {'work_item_code': 'CONC-001', 'quantity': 150}, {'work_item_code': 'REBAR-002', 'quantity': 5000}, {'work_item_code': 'FORM-003', 'quantity': 300} ] # Generate procurement list proc_list = procurement.generate_procurement_list(items, "Building A") print(f"Total Items: {proc_list.total_items}") print(f"Total Cost: ${proc_list.total_cost:,.2f}")
pythoncritical = procurement.get_critical_orders(proc_list, days_ahead=7) print(f"Order immediately: {len(critical)} items")
pythonpo = procurement.create_purchase_order( proc_list.items, supplier="Steel Fabricator", po_number="PO-2024-001" )
pythonprocurement.export_to_excel(proc_list, "procurement_list.xlsx")
pythonmaterials_df = procurement.aggregate_by_material(items) print(materials_df.head(10))
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 19,070 | 72,491 | +280% | 1 | 1 | 0% | 4,564 | 5,052 | +11% | 0 | 0 | — |
case-02 | fail→pass | 14,884 | 11,640 | -22% | 1 | 1 | 0% | 3,140 | 6,811 | +117% | 0 | 0 | — |
case-03 | fail→pass | 32,668 | 10,032 | -69% | 1 | 1 | 0% | 7,485 | 6,479 | -13% | 0 | 0 | — |
case-04 | fail→pass | 14,788 | 7,092 | -52% | 1 | 1 | 0% | 2,888 | 5,662 | +96% | 0 | 0 | — |
case-05 | fail→pass | 11,663 | 9,020 | -23% | 1 | 1 | 0% | 2,253 | 6,080 | +170% | 0 | 0 | — |
case-06 | fail→pass | 12,245 | 3,608 | -71% | 1 | 1 | 0% | 2,126 | 4,849 | +128% | 0 | 0 | — |
case-07 | fail→pass | 10,492 | 3,578 | -66% | 1 | 1 | 0% | 1,804 | 4,802 | +166% | 0 | 0 | — |
case-08 | pass→pass | 7,721 | 5,444 | -29% | 1 | 1 | 0% | 1,440 | 5,260 | +265% | 0 | 0 | — |
case-09 | fail→pass | 6,380 | 4,202 | -34% | 1 | 1 | 0% | 1,047 | 5,116 | +389% | 0 | 0 | — |
case-10 | pass→pass | 9,909 | 4,751 | -52% | 1 | 1 | 0% | 1,689 | 5,230 | +210% | 0 | 0 | — |
case-11 | fail→pass | 10,653 | 5,726 | -46% | 1 | 1 | 0% | 1,795 | 5,356 | +198% | 0 | 0 | — |
case-12 | fail→pass | 8,769 | 5,163 | -41% | 1 | 1 | 0% | 1,574 | 5,210 | +231% | 0 | 0 | — |
case-13 | pass→pass | 8,282 | 4,584 | -45% | 1 | 1 | 0% | 1,613 | 5,136 | +218% | 0 | 0 | — |
case-14 | fail→pass | 8,777 | 3,076 | -65% | 1 | 1 | 0% | 1,402 | 4,753 | +239% | 0 | 0 | — |
case-15 | fail→pass | 5,530 | 2,518 | -54% | 1 | 1 | 0% | 893 | 4,622 | +418% | 0 | 0 | — |
case-16 | fail→pass | 16,204 | 2,779 | -83% | 1 | 1 | 0% | 995 | 4,665 | +369% | 0 | 0 | — |
case-17 | fail→fail | 15,344 | 3,417 | -78% | 1 | 1 | 0% | 2,687 | 4,907 | +83% | 0 | 0 | — |
case-18 | fail→pass | 11,800 | 4,652 | -61% | 1 | 1 | 0% | 2,055 | 5,051 | +146% | 0 | 0 | — |
case-19 | fail→pass | 8,208 | 3,449 | -58% | 1 | 1 | 0% | 1,336 | 4,818 | +261% | 0 | 0 | — |
case-20 | fail→pass | 3,636 | 6,095 | +68% | 1 | 1 | 0% | 432 | 5,444 | +1160% | 0 | 0 | — |
case-21 | fail→pass | 14,519 | 5,874 | -60% | 1 | 1 | 0% | 2,359 | 5,190 | +120% | 0 | 0 | — |
case-22 | fail→pass | 12,605 | 4,952 | -61% | 1 | 1 | 0% | 2,308 | 5,206 | +126% | 0 | 0 | — |
case-23 | pass→pass | 16,537 | 17,524 | +6% | 1 | 1 | 0% | 2,921 | 7,228 | +147% | 0 | 0 | — |
case-24 | pass→pass | 10,396 | 12,658 | +22% | 1 | 1 | 0% | 2,226 | 6,930 | +211% | 0 | 0 | — |
case-25 | pass→pass | 23,511 | 17,958 | -24% | 1 | 1 | 0% | 5,128 | 8,247 | +61% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted, and 24 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +68 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/22/2026 | +18% |
Other measured skills in the registry, with their headline benchmark lift.