Add Custom Powder Order line item and fix CSV import FinalPrice crash

Custom powder/incoming powder material cost now flows into a separate
auto-generated 'Custom Powder Order' line item instead of rolling into
individual item prices, so users can add shipping charges before the
customer sees the total. A dashed yellow preview card in the wizard
shows the material cost and lets users edit the total (including shipping)
before saving. After first save the price is user-owned.

Also fixes a fatal CSV import crash when FinalPrice contains a non-numeric
value (e.g. 'false' from a spreadsheet formula): the job CSV importer now
streams rows one at a time with a lenient decimal converter, treating bad
values as $0 with a per-row warning instead of aborting the entire import.

Updated HelpKnowledgeBase.cs and Help articles (Jobs, Quotes) with
Custom Powder Order behavior and a new Data Import / Export section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:37:46 -04:00
parent e476b4744d
commit a7ad0e1de8
19 changed files with 721 additions and 78 deletions
+23 -1
View File
@@ -216,6 +216,28 @@
<p class="small">Click <strong>Add Item</strong> to get started.</p>
</div>
<div id="itemCardsContainer"></div>
<div id="customPowderOrderPreview" class="d-none mt-2">
<div class="quote-item-card border-warning" style="border-style:dashed!important;background:var(--bs-warning-bg-subtle,#fff8e1);">
<div class="d-flex align-items-start gap-2">
<div class="flex-grow-1">
<div class="d-flex align-items-center flex-wrap gap-1 mb-1">
<span class="badge bg-warning text-dark item-badge"><i class="bi bi-truck me-1"></i>Powder Order</span>
<span class="fw-semibold" id="customPowderOrderPreviewDesc">Custom Powder Order</span>
</div>
<div class="text-muted small"><span id="customPowderOrderPreviewPrice"></span> &mdash; edit total to include shipping</div>
</div>
<div class="text-center flex-shrink-0" style="min-width:45px;">1</div>
<div class="flex-shrink-0" style="min-width:90px;">
<input type="number" id="customPowderOrderPriceInput"
class="form-control form-control-sm text-end"
min="0" step="0.01" placeholder="0.00"
title="Enter total including any shipping"
oninput="onCustomPowderPriceEdit(this.value)">
</div>
<div style="min-width:66px;"></div>
</div>
</div>
</div>
<div id="hiddenFieldsContainer"></div>
<div id="aiPhotoTempIdsContainer"></div>
</div>
@@ -411,7 +433,7 @@
sequence = c.Sequence,
inventoryItemId = c.InventoryItemId,
colorName = c.ColorName,
vendorId = c.VendorId,
supplierId = c.VendorId,
colorCode = c.ColorCode,
finish = c.Finish,
coverageSqFtPerLb = c.CoverageSqFtPerLb,
@@ -326,6 +326,13 @@
<i class="bi bi-plus-circle me-1"></i>Add Item
</button>
</div>
@if (Model.Items.Any(i => i.Description != null && i.Description.StartsWith("Custom Powder Order")))
{
<div class="alert alert-warning alert-permanent mx-3 mt-3 mb-0" role="alert">
<i class="bi bi-truck me-2"></i><strong>Custom Powder Order</strong> &mdash;
The line item below shows material cost only. Remember to add shipping before invoicing this job.
</div>
}
@if (Model.Items.Any())
{
var allItems = Model.Items.ToList();
+23 -1
View File
@@ -185,6 +185,28 @@
<p class="small">Click <strong>Add Item</strong> to get started.</p>
</div>
<div id="itemCardsContainer"></div>
<div id="customPowderOrderPreview" class="d-none mt-2">
<div class="quote-item-card border-warning" style="border-style:dashed!important;background:var(--bs-warning-bg-subtle,#fff8e1);">
<div class="d-flex align-items-start gap-2">
<div class="flex-grow-1">
<div class="d-flex align-items-center flex-wrap gap-1 mb-1">
<span class="badge bg-warning text-dark item-badge"><i class="bi bi-truck me-1"></i>Powder Order</span>
<span class="fw-semibold" id="customPowderOrderPreviewDesc">Custom Powder Order</span>
</div>
<div class="text-muted small"><span id="customPowderOrderPreviewPrice"></span> &mdash; edit total to include shipping</div>
</div>
<div class="text-center flex-shrink-0" style="min-width:45px;">1</div>
<div class="flex-shrink-0" style="min-width:90px;">
<input type="number" id="customPowderOrderPriceInput"
class="form-control form-control-sm text-end"
min="0" step="0.01" placeholder="0.00"
title="Enter total including any shipping"
oninput="onCustomPowderPriceEdit(this.value)">
</div>
<div style="min-width:66px;"></div>
</div>
</div>
</div>
<div id="hiddenFieldsContainer"></div>
<div id="aiPhotoTempIdsContainer"></div>
</div>
@@ -396,7 +418,7 @@
sequence = c.Sequence,
inventoryItemId = c.InventoryItemId,
colorName = c.ColorName,
vendorId = c.VendorId,
supplierId = c.VendorId,
colorCode = c.ColorCode,
finish = c.Finish,
coverageSqFtPerLb = c.CoverageSqFtPerLb,
@@ -50,6 +50,28 @@
<p class="small">Click <strong>Add Item</strong> to get started.</p>
</div>
<div id="itemCardsContainer"></div>
<div id="customPowderOrderPreview" class="d-none mt-2">
<div class="quote-item-card border-warning" style="border-style:dashed!important;background:var(--bs-warning-bg-subtle,#fff8e1);">
<div class="d-flex align-items-start gap-2">
<div class="flex-grow-1">
<div class="d-flex align-items-center flex-wrap gap-1 mb-1">
<span class="badge bg-warning text-dark item-badge"><i class="bi bi-truck me-1"></i>Powder Order</span>
<span class="fw-semibold" id="customPowderOrderPreviewDesc">Custom Powder Order</span>
</div>
<div class="text-muted small"><span id="customPowderOrderPreviewPrice"></span> &mdash; edit total to include shipping</div>
</div>
<div class="text-center flex-shrink-0" style="min-width:45px;">1</div>
<div class="flex-shrink-0" style="min-width:90px;">
<input type="number" id="customPowderOrderPriceInput"
class="form-control form-control-sm text-end"
min="0" step="0.01" placeholder="0.00"
title="Enter total including any shipping"
oninput="onCustomPowderPriceEdit(this.value)">
</div>
<div style="min-width:66px;"></div>
</div>
</div>
</div>
<!-- Hidden fields written by wizard JS -->
<div id="hiddenFieldsContainer"></div>
@@ -149,7 +171,7 @@
sequence = c.Sequence,
inventoryItemId = c.InventoryItemId,
colorName = c.ColorName,
vendorId = c.VendorId,
supplierId = c.VendorId,
colorCode = c.ColorCode,
finish = c.Finish,
coverageSqFtPerLb = c.CoverageSqFtPerLb,