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
@@ -2429,24 +2429,32 @@ public class QuotesController : Controller
var (_, quotePhotoMax) = await _subscriptionService.GetQuotePhotoCountAsync(companyId, 0);
ViewBag.QuotePhotosEnabled = quotePhotoMax != 0; // 0 = feature disabled for this plan
var formulaTemplates = await _unitOfWork.CustomItemTemplates.FindAsync(
t => t.CompanyId == companyId && t.IsActive);
ViewBag.CustomFormulaTemplates = formulaTemplates
.OrderBy(t => t.DisplayOrder).ThenBy(t => t.Name)
.Select(t => new
{
id = t.Id,
name = t.Name,
description = t.Description,
outputMode = t.OutputMode,
fieldsJson = t.FieldsJson,
formula = t.Formula,
defaultRate = t.DefaultRate,
rateLabel = t.RateLabel,
diagramImagePath = string.IsNullOrEmpty(t.DiagramImagePath)
? (string?)null
: Url.Action("TemplateDiagram", "CompanySettings", new { templateId = t.Id })
}).ToList();
var allowFormulas = HttpContext.Items["AllowCustomFormulas"] as bool? ?? false;
if (allowFormulas)
{
var formulaTemplates = await _unitOfWork.CustomItemTemplates.FindAsync(
t => t.CompanyId == companyId && t.IsActive);
ViewBag.CustomFormulaTemplates = formulaTemplates
.OrderBy(t => t.DisplayOrder).ThenBy(t => t.Name)
.Select(t => new
{
id = t.Id,
name = t.Name,
description = t.Description,
outputMode = t.OutputMode,
fieldsJson = t.FieldsJson,
formula = t.Formula,
defaultRate = t.DefaultRate,
rateLabel = t.RateLabel,
diagramImagePath = string.IsNullOrEmpty(t.DiagramImagePath)
? (string?)null
: Url.Action("TemplateDiagram", "CompanySettings", new { templateId = t.Id })
}).ToList();
}
else
{
ViewBag.CustomFormulaTemplates = new List<object>();
}
// Customers
var customers = await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId);