Add facility overhead (rent + utilities) to operating costs and pricing engine
Adds MonthlyRent, MonthlyUtilities, and MonthlyBillableHours to CompanyOperatingCosts so fixed shop occupancy costs are recovered on every quote. The pricing engine converts these into a per-hour rate and applies it as a transparent "Facility Overhead" line between oven batch cost and shop supplies. UI added in Company Settings Operating Costs tab and Setup Wizard Step 3; migration AddFacilityOverheadFields applied. Help docs and AI knowledge base updated to cover the new fields and the revised quote pricing calculation order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,14 @@ namespace PowderCoating.Application.DTOs.Company
|
||||
public decimal DerivedBlastRateSqFtPerHour { get; set; }
|
||||
/// <summary>Derived coating rate — shown to the user as a sanity-check value.</summary>
|
||||
public decimal DerivedCoatingRateSqFtPerHour { get; set; }
|
||||
|
||||
// Facility Overhead
|
||||
public decimal MonthlyRent { get; set; }
|
||||
public decimal MonthlyUtilities { get; set; }
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
|
||||
/// <summary>Derived facility overhead rate = (MonthlyRent + MonthlyUtilities) / MonthlyBillableHours.</summary>
|
||||
public decimal FacilityOverheadRatePerHour { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,6 +236,19 @@ namespace PowderCoating.Application.DTOs.Company
|
||||
|
||||
[Range(0, 500)]
|
||||
public decimal ComplexityExtremePercent { get; set; } = 25m;
|
||||
|
||||
// Facility Overhead
|
||||
[Range(0, 1000000, ErrorMessage = "Monthly rent must be between 0 and 1,000,000")]
|
||||
[Display(Name = "Monthly Rent ($)")]
|
||||
public decimal MonthlyRent { get; set; } = 0m;
|
||||
|
||||
[Range(0, 1000000, ErrorMessage = "Monthly utilities must be between 0 and 1,000,000")]
|
||||
[Display(Name = "Monthly Utilities ($)")]
|
||||
public decimal MonthlyUtilities { get; set; } = 0m;
|
||||
|
||||
[Range(1, 10000, ErrorMessage = "Billable hours must be between 1 and 10,000")]
|
||||
[Display(Name = "Billable Hours/Month")]
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
}
|
||||
|
||||
/// <summary>DTO for updating the company AI profile text used for AI Photo Quote calibration.</summary>
|
||||
|
||||
@@ -606,6 +606,9 @@ public class QuotePricingBreakdownDto
|
||||
public int OvenBatches { get; set; }
|
||||
public int OvenCycleMinutes { get; set; }
|
||||
|
||||
public decimal FacilityOverheadCost { get; set; }
|
||||
public decimal FacilityOverheadRatePerHour { get; set; }
|
||||
|
||||
public decimal Total { get; set; }
|
||||
|
||||
// Cost Breakdown Details
|
||||
@@ -822,6 +825,10 @@ public class QuotePricingResult
|
||||
public int OvenBatches { get; set; }
|
||||
public int OvenCycleMinutes { get; set; }
|
||||
|
||||
// Facility overhead (rent + utilities apportioned by estimated job hours)
|
||||
public decimal FacilityOverheadCost { get; set; }
|
||||
public decimal FacilityOverheadRatePerHour { get; set; }
|
||||
|
||||
// Detailed breakdown for transparency
|
||||
public decimal MaterialCosts { get; set; }
|
||||
public decimal LaborCosts { get; set; }
|
||||
|
||||
@@ -112,6 +112,19 @@ public class WizardStep2Dto
|
||||
|
||||
[Display(Name = "Shop Capability Tier")]
|
||||
public ShopCapabilityTier ShopCapabilityTier { get; set; } = ShopCapabilityTier.Small;
|
||||
|
||||
// Facility Overhead
|
||||
[Range(0, 1000000)]
|
||||
[Display(Name = "Monthly Rent ($)")]
|
||||
public decimal MonthlyRent { get; set; } = 0m;
|
||||
|
||||
[Range(0, 1000000)]
|
||||
[Display(Name = "Monthly Utilities ($)")]
|
||||
public decimal MonthlyUtilities { get; set; } = 0m;
|
||||
|
||||
[Range(1, 10000)]
|
||||
[Display(Name = "Billable Hours/Month")]
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
}
|
||||
|
||||
// ─── Step 3: Branding & Numbering ───────────────────────────────────────────
|
||||
|
||||
@@ -37,7 +37,11 @@ public class CompanyProfile : Profile
|
||||
.ForMember(dest => dest.DerivedBlastRateSqFtPerHour,
|
||||
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetBlastRateSqFtPerHour(src)))
|
||||
.ForMember(dest => dest.DerivedCoatingRateSqFtPerHour,
|
||||
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetCoatingRateSqFtPerHour(src)));
|
||||
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetCoatingRateSqFtPerHour(src)))
|
||||
.ForMember(dest => dest.FacilityOverheadRatePerHour,
|
||||
opt => opt.MapFrom(src => src.MonthlyBillableHours > 0
|
||||
? (src.MonthlyRent + src.MonthlyUtilities) / src.MonthlyBillableHours
|
||||
: 0m));
|
||||
CreateMap<UpdateOperatingCostsDto, CompanyOperatingCosts>();
|
||||
CreateMap<UpdateBlastProfileDto, CompanyOperatingCosts>();
|
||||
|
||||
|
||||
@@ -542,6 +542,8 @@ public class PricingCalculationService : IPricingCalculationService
|
||||
ItemsSubtotal = 0,
|
||||
ShopSuppliesAmount = 0,
|
||||
ShopSuppliesPercent = 0,
|
||||
FacilityOverheadCost = 0,
|
||||
FacilityOverheadRatePerHour = 0,
|
||||
OverheadCosts = 0,
|
||||
OverheadPercent = 0,
|
||||
ProfitMargin = 0,
|
||||
@@ -683,14 +685,27 @@ public class PricingCalculationService : IPricingCalculationService
|
||||
|
||||
var itemsAndOvenSubtotal = itemsSubtotal + ovenBatchCost;
|
||||
|
||||
// 5. SHOP SUPPLIES (percentage of items + oven subtotal)
|
||||
// 5. FACILITY OVERHEAD (rent + utilities apportioned by estimated job hours)
|
||||
var facilityOverheadRatePerHour = 0m;
|
||||
var facilityOverheadCost = 0m;
|
||||
if (costs.MonthlyBillableHours > 0 && (costs.MonthlyRent + costs.MonthlyUtilities) > 0)
|
||||
{
|
||||
facilityOverheadRatePerHour = (costs.MonthlyRent + costs.MonthlyUtilities) / costs.MonthlyBillableHours;
|
||||
var totalEstimatedMinutes = items.Sum(i => (decimal)i.EstimatedMinutes * i.Quantity);
|
||||
facilityOverheadCost = facilityOverheadRatePerHour * (totalEstimatedMinutes / 60m);
|
||||
_logger.LogInformation(
|
||||
"Facility overhead: ${Rate:F2}/hr × {Min:F0} min = ${Cost:F2}",
|
||||
facilityOverheadRatePerHour, totalEstimatedMinutes, facilityOverheadCost);
|
||||
}
|
||||
|
||||
// 6. SHOP SUPPLIES (percentage of items + oven subtotal — does not include facility overhead)
|
||||
var shopSuppliesPercent = costs.ShopSuppliesRate;
|
||||
var shopSuppliesAmount = itemsAndOvenSubtotal * (shopSuppliesPercent / 100m);
|
||||
|
||||
// 6. SUBTOTAL BEFORE DISCOUNT (items + oven + shop supplies)
|
||||
var subtotalBeforeDiscount = itemsAndOvenSubtotal + shopSuppliesAmount;
|
||||
// 7. SUBTOTAL BEFORE DISCOUNT (items + oven + facility overhead + shop supplies)
|
||||
var subtotalBeforeDiscount = itemsAndOvenSubtotal + facilityOverheadCost + shopSuppliesAmount;
|
||||
|
||||
// 7. CUSTOMER PRICING TIER DISCOUNT (if applicable)
|
||||
// 8. CUSTOMER PRICING TIER DISCOUNT (if applicable)
|
||||
var pricingTierDiscountPercent = 0m;
|
||||
var pricingTierDiscountAmount = 0m;
|
||||
|
||||
@@ -782,6 +797,8 @@ public class PricingCalculationService : IPricingCalculationService
|
||||
OvenBatchCost = Math.Round(ovenBatchCost, 2),
|
||||
OvenBatches = effectiveBatches,
|
||||
OvenCycleMinutes = effectiveCycleMinutes,
|
||||
FacilityOverheadCost = Math.Round(facilityOverheadCost, 2),
|
||||
FacilityOverheadRatePerHour = Math.Round(facilityOverheadRatePerHour, 4),
|
||||
ShopSuppliesAmount = Math.Round(shopSuppliesAmount, 2),
|
||||
ShopSuppliesPercent = Math.Round(shopSuppliesPercent, 2),
|
||||
OverheadCosts = Math.Round(overheadCosts, 2),
|
||||
|
||||
@@ -127,5 +127,24 @@ namespace PowderCoating.Core.Entities
|
||||
/// </summary>
|
||||
[Range(0, 5000)]
|
||||
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
|
||||
|
||||
// ── Facility Overhead ─────────────────────────────────────────────────────
|
||||
// Monthly fixed costs divided by billable hours to derive a per-hour overhead
|
||||
// rate, which is then applied to each quote based on estimated labor time.
|
||||
|
||||
/// <summary>Monthly shop lease / rent payment.</summary>
|
||||
[Range(0, 1000000)]
|
||||
public decimal MonthlyRent { get; set; } = 0m;
|
||||
|
||||
/// <summary>Monthly utilities combined (electricity, gas, water, internet).</summary>
|
||||
[Range(0, 1000000)]
|
||||
public decimal MonthlyUtilities { get; set; } = 0m;
|
||||
|
||||
/// <summary>
|
||||
/// Estimated billable shop hours per month used to amortise facility overhead.
|
||||
/// Defaults to 160 (4 weeks × 40 hrs). Must be at least 1 to avoid division by zero.
|
||||
/// </summary>
|
||||
[Range(1, 10000)]
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+9191
File diff suppressed because it is too large
Load Diff
+94
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFacilityOverheadFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MonthlyBillableHours",
|
||||
table: "CompanyOperatingCosts",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 160);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MonthlyRent",
|
||||
table: "CompanyOperatingCosts",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MonthlyUtilities",
|
||||
table: "CompanyOperatingCosts",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 4, 24, 23, 28, 22, 104, DateTimeKind.Utc).AddTicks(7155));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 4, 24, 23, 28, 22, 104, DateTimeKind.Utc).AddTicks(7162));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 4, 24, 23, 28, 22, 104, DateTimeKind.Utc).AddTicks(7164));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MonthlyBillableHours",
|
||||
table: "CompanyOperatingCosts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MonthlyRent",
|
||||
table: "CompanyOperatingCosts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MonthlyUtilities",
|
||||
table: "CompanyOperatingCosts");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 4, 23, 1, 54, 43, 181, DateTimeKind.Utc).AddTicks(5272));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 4, 23, 1, 54, 43, 181, DateTimeKind.Utc).AddTicks(5281));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 4, 23, 1, 54, 43, 181, DateTimeKind.Utc).AddTicks(5283));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1770,6 +1770,15 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("MonthlyBillableHours")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MonthlyRent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MonthlyUtilities")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("OvenOperatingCostPerHour")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
@@ -5767,7 +5776,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 1,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 4, 23, 1, 54, 43, 181, DateTimeKind.Utc).AddTicks(5272),
|
||||
CreatedAt = new DateTime(2026, 4, 24, 23, 28, 22, 104, DateTimeKind.Utc).AddTicks(7155),
|
||||
Description = "Standard pricing for regular customers",
|
||||
DiscountPercent = 0m,
|
||||
IsActive = true,
|
||||
@@ -5778,7 +5787,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 2,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 4, 23, 1, 54, 43, 181, DateTimeKind.Utc).AddTicks(5281),
|
||||
CreatedAt = new DateTime(2026, 4, 24, 23, 28, 22, 104, DateTimeKind.Utc).AddTicks(7162),
|
||||
Description = "5% discount for preferred customers",
|
||||
DiscountPercent = 5m,
|
||||
IsActive = true,
|
||||
@@ -5789,7 +5798,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 3,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 4, 23, 1, 54, 43, 181, DateTimeKind.Utc).AddTicks(5283),
|
||||
CreatedAt = new DateTime(2026, 4, 24, 23, 28, 22, 104, DateTimeKind.Utc).AddTicks(7164),
|
||||
Description = "10% discount for premium customers",
|
||||
DiscountPercent = 10m,
|
||||
IsActive = true,
|
||||
|
||||
@@ -600,6 +600,7 @@ public static class HelpKnowledgeBase
|
||||
- Standard Labor Rate ($/hr)
|
||||
- Equipment hourly costs (oven, sandblaster, coating booth)
|
||||
- Powder cost per sq ft
|
||||
- **Facility Overhead** — Monthly Rent ($), Monthly Utilities ($), and Monthly Billable Hours. These three fields let the system recover your fixed shop costs on every quote by adding a per-job overhead charge based on estimated labor hours.
|
||||
- General markup %
|
||||
- Tax %
|
||||
- Shop minimum charge
|
||||
@@ -683,6 +684,20 @@ public static class HelpKnowledgeBase
|
||||
A percentage added to cover consumables: masking tape, hooks, racking supplies, touch-up powder, rags, etc. Applied as a percentage of the total material + labor cost.
|
||||
- Typical: 3–8%
|
||||
|
||||
### Facility Overhead (Rent & Utilities)
|
||||
|
||||
**Monthly Rent ($)**
|
||||
Your monthly shop lease or mortgage payment for the production space. Enter 0 if you own outright and have no occupancy cost.
|
||||
|
||||
**Monthly Utilities ($)**
|
||||
Gas, electricity, and water for the shop per month. Do NOT include utility costs already captured in your oven or equipment hourly rates — that would double-count.
|
||||
|
||||
**Monthly Billable Hours (default: 160)**
|
||||
The number of productive hours the shop operates per month. Default of 160 represents one full-time worker-equivalent (4 weeks × 40 hours). If you run two full-time workers, use 320. The system converts rent + utilities into a per-hour rate using: (Monthly Rent + Monthly Utilities) ÷ Monthly Billable Hours.
|
||||
- Example: $2,000 rent + $800 utilities ÷ 160 hours = $17.50/hr overhead rate
|
||||
- That rate × estimated job hours is added as "Facility Overhead" in the quote breakdown
|
||||
- If both rent and utilities are $0, no overhead line is added to quotes
|
||||
|
||||
### Overhead & Profit
|
||||
|
||||
**General Markup % (your profit margin)**
|
||||
@@ -722,15 +737,17 @@ public static class HelpKnowledgeBase
|
||||
1. **Surface area** — calculated from the dimensions you enter (length × width × quantity, adjusted for shape), or entered manually
|
||||
2. **Powder material cost** — if an inventory item is selected: (item cost per lb ÷ (coverage rate × efficiency)) × surface area. If no item selected, uses the fallback Powder Cost Per Sq Ft from Operating Costs
|
||||
3. **Additional coats** — each additional coat adds more material cost + Additional Coat Labor % of base labor
|
||||
4. **Shop supplies** — Shop Supplies Rate % × (material cost + labor cost)
|
||||
5. **Labor cost** — Standard Labor Rate × estimated hours. Prep service modifiers: Sandblasting = 1.5× rate, Masking = 0.5× rate
|
||||
6. **Equipment costs** — Oven, Sandblaster, Coating Booth each contribute their $/hr rate × estimated time
|
||||
7. **Complexity adjustment** — the complexity multiplier % is added to the item subtotal
|
||||
8. **General Markup** — applied as a multiplier on the running total (profit margin)
|
||||
9. **Rush charge** — added if the job/quote priority is Rush or Urgent
|
||||
10. **Customer tier discount** — if the customer has a pricing tier, their discount % is subtracted
|
||||
11. **Tax** — Tax % applied to the subtotal (skipped for tax-exempt customers)
|
||||
12. **Shop minimum** — if the total is below the Shop Minimum Charge, the minimum is used instead
|
||||
4. **Labor cost** — Standard Labor Rate × estimated hours. Prep service modifiers: Sandblasting = 1.5× rate, Masking = 0.5× rate
|
||||
5. **Equipment costs** — Oven, Sandblaster, Coating Booth each contribute their $/hr rate × estimated time
|
||||
6. **Complexity adjustment** — the complexity multiplier % is added to the item subtotal
|
||||
7. **Oven batch cost** — if a named oven is assigned to the quote, the batch cost is added based on the oven's hourly rate and cure cycle
|
||||
8. **Facility overhead** — (Monthly Rent + Monthly Utilities) ÷ Monthly Billable Hours × total estimated job hours. Shows as a separate "Facility Overhead" line in the breakdown. Skipped if rent and utilities are both $0.
|
||||
9. **Shop supplies** — Shop Supplies Rate % × (items subtotal + oven batch cost). Applied before facility overhead so supplies don't compound overhead.
|
||||
10. **General Markup** — applied as a multiplier on the running total (profit margin)
|
||||
11. **Customer tier discount** — if the customer has a pricing tier, their discount % is subtracted
|
||||
12. **Rush charge** — added if the job/quote priority is Rush or Urgent
|
||||
13. **Tax** — Tax % applied to the subtotal (skipped for tax-exempt customers)
|
||||
14. **Shop minimum** — if the total is below the Shop Minimum Charge, the minimum is used instead
|
||||
|
||||
**To get accurate quotes, you need:**
|
||||
- Operating Costs all filled in (Steps 3–4 of the wizard)
|
||||
@@ -780,7 +797,7 @@ public static class HelpKnowledgeBase
|
||||
- Footer note (quotes and invoices only)
|
||||
|
||||
**Operating Costs tab**
|
||||
All pricing rates. See the "Operating Costs Field Guide" section above for full details.
|
||||
All pricing rates including labor rates, equipment hourly costs, material costs, facility overhead (rent + utilities + billable hours), markup, and minimums. See the "Operating Costs Field Guide" section above for full details.
|
||||
|
||||
**Named Ovens tab**
|
||||
Configure physical ovens for the Oven Scheduler. Each oven has a name, cost per hour, max load capacity (sq ft), and default cure cycle (minutes). The first oven's cost also updates the Oven Operating Cost rate used in quote pricing.
|
||||
|
||||
@@ -361,6 +361,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Facility Overhead -->
|
||||
<h6 class="border-bottom pb-2 mb-3 mt-3">Facility Overhead
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right"
|
||||
data-bs-title="Facility Overhead"
|
||||
data-bs-content="Enter your monthly shop rent and combined utility costs. The system divides these by your estimated billable hours to derive a per-hour overhead rate, which is then added to every quote proportionally to the estimated job time. This ensures fixed facility costs are recovered across all jobs rather than absorbed into your markup.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</h6>
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="monthlyRent" class="form-label">Monthly Rent</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" step="0.01" class="form-control facility-overhead-input" id="monthlyRent" name="MonthlyRent" value="@(Model.OperatingCosts?.MonthlyRent ?? 0)" min="0" max="1000000">
|
||||
<span class="input-group-text">/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="monthlyUtilities" class="form-label">Monthly Utilities</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" step="0.01" class="form-control facility-overhead-input" id="monthlyUtilities" name="MonthlyUtilities" value="@(Model.OperatingCosts?.MonthlyUtilities ?? 0)" min="0" max="1000000">
|
||||
<span class="input-group-text">/mo</span>
|
||||
</div>
|
||||
<small class="text-muted">Electricity, gas, water, internet</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="monthlyBillableHours" class="form-label">Billable Hours/Month</label>
|
||||
<input type="number" step="1" class="form-control facility-overhead-input" id="monthlyBillableHours" name="MonthlyBillableHours" value="@(Model.OperatingCosts?.MonthlyBillableHours ?? 160)" min="1" max="10000">
|
||||
<small class="text-muted">Typical: 160 hrs (4 wks × 40 hrs)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Calculated Rate</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-calculator"></i></span>
|
||||
<input type="text" class="form-control bg-light" id="facilityOverheadRateDisplay" readonly
|
||||
value="@((Model.OperatingCosts?.FacilityOverheadRatePerHour ?? 0).ToString("C2")) / hr">
|
||||
</div>
|
||||
<small class="text-muted">Added to quotes per estimated labor hour</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Operating Costs -->
|
||||
<h6 class="border-bottom pb-2 mb-3 mt-3">Equipment Operating Costs
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
@@ -2131,6 +2182,16 @@
|
||||
});
|
||||
|
||||
// Operating Costs Form Submit
|
||||
// Live facility overhead rate preview
|
||||
function updateFacilityOverheadRate() {
|
||||
var rent = parseFloat($('#monthlyRent').val()) || 0;
|
||||
var utilities = parseFloat($('#monthlyUtilities').val()) || 0;
|
||||
var hours = parseInt($('#monthlyBillableHours').val()) || 1;
|
||||
var rate = hours > 0 ? (rent + utilities) / hours : 0;
|
||||
$('#facilityOverheadRateDisplay').val('$' + rate.toFixed(2) + ' / hr');
|
||||
}
|
||||
$('.facility-overhead-input').on('input', updateFacilityOverheadRate);
|
||||
|
||||
$('#operatingCostsForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -2153,7 +2214,10 @@
|
||||
ComplexitySimplePercent: parseFloat($('#complexitySimplePercent').val()) || 0,
|
||||
ComplexityModeratePercent: parseFloat($('#complexityModeratePercent').val()) || 5,
|
||||
ComplexityComplexPercent: parseFloat($('#complexityComplexPercent').val()) || 15,
|
||||
ComplexityExtremePercent: parseFloat($('#complexityExtremePercent').val()) || 25
|
||||
ComplexityExtremePercent: parseFloat($('#complexityExtremePercent').val()) || 25,
|
||||
MonthlyRent: parseFloat($('#monthlyRent').val()) || 0,
|
||||
MonthlyUtilities: parseFloat($('#monthlyUtilities').val()) || 0,
|
||||
MonthlyBillableHours: parseInt($('#monthlyBillableHours').val()) || 160
|
||||
};
|
||||
|
||||
const btn = $('#btnSaveOperatingCosts');
|
||||
|
||||
@@ -101,6 +101,33 @@
|
||||
<li class="mb-1"><strong>Shop Supplies Rate (%)</strong> — a percentage applied to material and labor costs to cover miscellaneous shop supplies (abrasives, tape, fasteners, etc.) that are not tracked per-job.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="h6 fw-semibold mt-3 mb-2" id="facility-overhead">Facility Overhead (Rent & Utilities)</h3>
|
||||
<p>
|
||||
Facility overhead lets you recover your shop's fixed occupancy costs on every quote automatically.
|
||||
Rather than burying rent and utilities in your General Markup, entering them here makes the cost
|
||||
transparent in the pricing breakdown and ensures accurate job costing.
|
||||
</p>
|
||||
<ul class="mb-3">
|
||||
<li class="mb-1"><strong>Monthly Rent ($)</strong> — your monthly shop lease or mortgage payment for the production facility.</li>
|
||||
<li class="mb-1"><strong>Monthly Utilities ($)</strong> — monthly gas, electricity, and water costs for the shop. Do not include costs already captured in your oven/equipment hourly rates.</li>
|
||||
<li class="mb-1"><strong>Monthly Billable Hours</strong> — the number of productive labor hours your shop operates per month (default: 160 — roughly one full-time worker for a month). This is used to convert the combined rent + utilities into a per-hour overhead rate.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>How it is applied:</strong> The system calculates a per-hour overhead rate as
|
||||
<code>(Monthly Rent + Monthly Utilities) ÷ Monthly Billable Hours</code>. For each quote, it
|
||||
multiplies that rate by the total estimated labor hours across all line items and adds the result
|
||||
as a separate line in the pricing breakdown. If rent and utilities are both $0, no overhead charge
|
||||
is added.
|
||||
</p>
|
||||
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
|
||||
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
|
||||
<div>
|
||||
<strong>Example:</strong> $2,000/month rent + $800/month utilities ÷ 160 billable hours =
|
||||
<strong>$17.50/hr overhead rate</strong>. A quote with 4 total estimated hours would add
|
||||
$70 to the price as "Facility Overhead."
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="h6 fw-semibold mt-3 mb-2">Pricing Mode, Markup, Minimums & Rush Charges</h3>
|
||||
<ul class="mb-3">
|
||||
<li class="mb-1">
|
||||
|
||||
@@ -903,6 +903,14 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.PricingBreakdown.FacilityOverheadCost > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span><i class="bi bi-building me-1"></i>Facility Overhead (@Model.PricingBreakdown.FacilityOverheadRatePerHour.ToString("C2")/hr):</span>
|
||||
<strong>@Model.PricingBreakdown.FacilityOverheadCost.ToString("C")</strong>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.PricingBreakdown.ShopSuppliesAmount > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
@@ -1043,7 +1051,7 @@
|
||||
</div>
|
||||
|
||||
@* ── SECTION 2: Quote-Level Additions ───────────────────── *@
|
||||
@if (pb.OvenBatchCost > 0 || pb.ShopSuppliesAmount > 0 || pb.OverheadCosts > 0)
|
||||
@if (pb.OvenBatchCost > 0 || pb.FacilityOverheadCost > 0 || pb.ShopSuppliesAmount > 0 || pb.OverheadCosts > 0)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<div class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.05em;">
|
||||
@@ -1056,6 +1064,13 @@
|
||||
<span>@pb.OvenBatchCost.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
@if (pb.FacilityOverheadCost > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between small mb-1">
|
||||
<span class="text-muted">Facility overhead (@pb.FacilityOverheadRatePerHour.ToString("C2")/hr × estimated hours)</span>
|
||||
<span>@pb.FacilityOverheadCost.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
@if (pb.ShopSuppliesAmount > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between small mb-1">
|
||||
@@ -1112,7 +1127,7 @@
|
||||
<span>@pb.Total.ToString("C")</span>
|
||||
</div>
|
||||
@{
|
||||
var totalDirectCost = pb.MaterialCosts + pb.LaborCosts + pb.EquipmentCosts + pb.OvenBatchCost + pb.ShopSuppliesAmount;
|
||||
var totalDirectCost = pb.MaterialCosts + pb.LaborCosts + pb.EquipmentCosts + pb.OvenBatchCost + pb.FacilityOverheadCost + pb.ShopSuppliesAmount;
|
||||
var grossProfit = pb.Total - totalDirectCost;
|
||||
var effectiveMargin = pb.Total > 0 ? (grossProfit / pb.Total * 100m) : 0m;
|
||||
var pricingModeLabel = dbgCosts?.PricingMode == PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost ? "margin" : "markup";
|
||||
|
||||
@@ -59,6 +59,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Facility Overhead -->
|
||||
<div class="wizard-card">
|
||||
<h5 class="wizard-card-title mb-3">Facility Overhead</h5>
|
||||
<p class="text-secondary small mb-3">
|
||||
Enter your monthly shop rent and utilities so the system can recover those costs proportionally
|
||||
across every job. Leave at zero if you work from home or your facility costs are already factored
|
||||
into your markup.
|
||||
</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label asp-for="MonthlyRent" class="form-label fw-semibold"></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input asp-for="MonthlyRent" class="form-control wz-overhead" step="0.01" type="number" min="0" />
|
||||
<span class="input-group-text">/mo</span>
|
||||
</div>
|
||||
<div class="form-text">Your monthly lease or rent payment for the shop space.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="MonthlyUtilities" class="form-label fw-semibold"></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input asp-for="MonthlyUtilities" class="form-control wz-overhead" step="0.01" type="number" min="0" />
|
||||
<span class="input-group-text">/mo</span>
|
||||
</div>
|
||||
<div class="form-text">Combined electricity, gas, water, and internet.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="MonthlyBillableHours" class="form-label fw-semibold"></label>
|
||||
<input asp-for="MonthlyBillableHours" class="form-control wz-overhead" step="1" type="number" min="1" />
|
||||
<div class="form-text">Hours per month the shop is actively producing work. Default: 160 (4 wks × 40 hrs).</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Calculated overhead rate</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-calculator"></i></span>
|
||||
<input type="text" id="wzOverheadRate" class="form-control bg-light" readonly value="$0.00 / hr">
|
||||
</div>
|
||||
<div class="form-text">This amount is added to quotes per estimated labor hour.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Costs -->
|
||||
<div class="wizard-card">
|
||||
<h5 class="wizard-card-title mb-3">Equipment Costs (per hour) <button type="button" class="btn btn-link btn-sm p-0 text-primary fw-normal ms-1" data-bs-toggle="modal" data-bs-target="#equipCalcModal"><i class="bi bi-calculator me-1"></i>Help me calculate</button></h5>
|
||||
@@ -336,6 +379,19 @@
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// ── Facility Overhead Rate Preview ────────────────────────
|
||||
function updateWzOverheadRate() {
|
||||
var rent = parseFloat(document.querySelector('[name="MonthlyRent"]').value) || 0;
|
||||
var utils = parseFloat(document.querySelector('[name="MonthlyUtilities"]').value) || 0;
|
||||
var hours = parseInt(document.querySelector('[name="MonthlyBillableHours"]').value) || 1;
|
||||
var rate = hours > 0 ? (rent + utils) / hours : 0;
|
||||
document.getElementById('wzOverheadRate').value = '$' + rate.toFixed(2) + ' / hr';
|
||||
}
|
||||
document.querySelectorAll('.wz-overhead').forEach(function(el) {
|
||||
el.addEventListener('input', updateWzOverheadRate);
|
||||
});
|
||||
updateWzOverheadRate();
|
||||
|
||||
// ── Labor Calculator ──────────────────────────────────────
|
||||
function laborCalc() {
|
||||
var employees = parseFloat(document.getElementById('lc_employees').value) || 0;
|
||||
|
||||
Reference in New Issue
Block a user