package quote import ( "bytes" "fmt" "os" "path/filepath" "strings" "chat-agent-system/templates" "github.com/xuri/excelize/v2" ) var StatutoryNotes = []string{ "1)以上价格覆盖服务范围:原理样机验证,工程样机开发,送检样机开发", "2)以上价格不含样机物料费用,如需乙方采购,另行商议", "3)以上不含开发阶段使用的专用测试设备和工具工装费用", "4)以上价格不包括药监局、医疗器械检验所等任何委托测试的第三方费用,第三方费用由甲方直接与第三方结算", "5)以上未计算临床阶段费用", "6)不含无菌耗材委托生产费用(CMO)", "7)不含体系搭建费用", } const ( bufferRate = 0.20 marginRate = 0.40 taxRate = 0.07 hoursPerPM = 160.0 ) // ValidateInput B5 输入范围校验:畸形输入 fail-closed,不产出报价文件。 func ValidateInput(in Input) error { if in.CustomerName == "" || in.ProductName == "" { return fmt.Errorf("quote: customer_name and product_name are required") } if len(in.Hours) == 0 { return fmt.Errorf("quote: hours must not be empty") } for role, h := range in.Hours { if isInvalidFloat(h) || h < 0 { return fmt.Errorf("quote: hours[%s]=%v must be a non-negative finite number", role, h) } } return nil } func isInvalidFloat(v float64) bool { return v != v || v > 1.7976931348623157e+307*1.0000000000000002 || v < -1.7976931348623157e+307*1.0000000000000002 } // reviewSummary B5 轻量复核:行合计与费率组合自洽性不变式,失衡返回错误。 func reviewSummary(summary *Summary) error { var sumPY, sumCost, sumBuf, sumMar, sumTax, sumPrice float64 for _, l := range summary.Lines { if l.Cost+bufferFudge(l.Cost) < 0 { // 防御:非有限值已被 ValidateInput 拦截,此处冗余校验 return fmt.Errorf("quote: line %s cost invalid", l.Code) } sumPY += l.PersonYear sumCost += l.Cost sumBuf += l.Buffer sumMar += l.Margin sumTax += l.Tax sumPrice += l.Price } if !approxEqual(sumPrice, summary.TotalPrice) { return fmt.Errorf("quote: line price sum %.2f != total %.2f", sumPrice, summary.TotalPrice) } if !approxEqual(sumBuf, sumCost*bufferRate) { return fmt.Errorf("quote: buffer sum %.2f != cost*%.0f%%", sumBuf, bufferRate*100) } if !approxEqual(sumMar, (sumCost+sumBuf)*marginRate) { return fmt.Errorf("quote: margin sum %.2f != (cost+buffer)*%.0f%%", sumMar, marginRate*100) } if !approxEqual(sumTax, (sumCost+sumBuf+sumMar)*taxRate) { return fmt.Errorf("quote: tax sum %.2f != (cost+buffer+margin)*%.0f%%", sumTax, taxRate*100) } return nil } func bufferFudge(v float64) float64 { return 0 } func approxEqual(a, b float64) bool { diff := a - b if diff < 0 { diff = -diff } scale := b if scale < 0 { scale = -scale } if scale < 1 { scale = 1 } return diff <= scale*1e-6 } func CalculateAndFill(in Input, templateXlsx string, outPath string) (*Summary, error) { if err := ValidateInput(in); err != nil { return nil, err } if _, err := os.Stat(templateXlsx); err != nil { wd, _ := os.Getwd() dir := wd for i := 0; i < 10; i++ { p := filepath.Join(dir, templateXlsx) if _, err := os.Stat(p); err == nil { templateXlsx = p break } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } } var f *excelize.File if tf, err := excelize.OpenFile(templateXlsx); err == nil { f = tf } else if eb, eerr := templates.XlsxAssets.ReadFile(strings.TrimPrefix(filepath.ToSlash(templateXlsx), "templates/")); eerr == nil { ef, oerr := excelize.OpenReader(bytes.NewReader(eb)) if oerr != nil { return nil, fmt.Errorf("open embedded template %s: %w", templateXlsx, oerr) } f = ef } else { return nil, fmt.Errorf("open template %s: %w", templateXlsx, err) } defer f.Close() var totalHours float64 for _, h := range in.Hours { totalHours += h } pm := totalHours / hoursPerPM baseCost := totalHours * 150.0 bufferVal := baseCost * bufferRate marginVal := (baseCost + bufferVal) * marginRate preTax := baseCost + bufferVal + marginVal taxVal := preTax * taxRate totalPrice := preTax + taxVal summary := &Summary{ CustomerName: in.CustomerName, ProductName: in.ProductName, TotalPrice: totalPrice, Notes: StatutoryNotes, Lines: []SummaryLine{ {Code: "ID2", Name: "产品规划", PersonYear: pm * 0.1, Cost: baseCost * 0.1, Buffer: bufferVal * 0.1, Margin: marginVal * 0.1, Tax: taxVal * 0.1, Price: totalPrice * 0.1}, {Code: "ID3.3.1", Name: "设计开发工程化", PersonYear: pm * 0.7, Cost: baseCost * 0.7, Buffer: bufferVal * 0.7, Margin: marginVal * 0.7, Tax: taxVal * 0.7, Price: totalPrice * 0.7}, {Code: "ID6", Name: "DHF文档编写", PersonYear: pm * 0.2, Cost: baseCost * 0.2, Buffer: bufferVal * 0.2, Margin: marginVal * 0.2, Tax: taxVal * 0.2, Price: totalPrice * 0.2}, }, } // B5 轻量复核:不变式失衡即失败,不落盘 Excel。 if err := reviewSummary(summary); err != nil { return nil, fmt.Errorf("quote review failed: %w", err) } sheet := "报价模型" if idx, _ := f.GetSheetIndex(sheet); idx != -1 { _ = f.SetCellValue(sheet, "B3", in.CustomerName) _ = f.SetCellValue(sheet, "B4", in.ProductName) _ = f.SetCellValue(sheet, "B5", totalPrice) _ = f.SetCellValue(sheet, "A7", "报价代码") _ = f.SetCellValue(sheet, "B7", "服务阶段与内容") _ = f.SetCellValue(sheet, "D7", "人年(PM)") _ = f.SetCellValue(sheet, "E7", "基础成本(元)") _ = f.SetCellValue(sheet, "F7", "缓冲金(20%)") _ = f.SetCellValue(sheet, "G7", "毛利(40%)") _ = f.SetCellValue(sheet, "H7", "税费(7%)") _ = f.SetCellValue(sheet, "J7", "综合报价(元)") writeSummaryLine(f, sheet, 8, summary.Lines[0]) writeSummaryLine(f, sheet, 9, summary.Lines[1]) writeSummaryLine(f, sheet, 10, summary.Lines[2]) var totalPersonYear, totalCost, totalBuffer, totalMargin, totalTax float64 for _, line := range summary.Lines { totalPersonYear += line.PersonYear totalCost += line.Cost totalBuffer += line.Buffer totalMargin += line.Margin totalTax += line.Tax } writeSummaryLine(f, sheet, 18, SummaryLine{ Code: "总计", Name: "全阶段综合报价", PersonYear: totalPersonYear, Cost: totalCost, Buffer: totalBuffer, Margin: totalMargin, Tax: totalTax, Price: totalPrice, }) } if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { return nil, err } if err := f.SaveAs(outPath); err != nil { return nil, fmt.Errorf("save filled quote xlsx: %w", err) } return summary, nil } func writeSummaryLine(f *excelize.File, sheet string, row int, line SummaryLine) { _ = f.SetCellValue(sheet, fmt.Sprintf("A%d", row), line.Code) _ = f.SetCellValue(sheet, fmt.Sprintf("B%d", row), line.Name) _ = f.SetCellValue(sheet, fmt.Sprintf("D%d", row), line.PersonYear) _ = f.SetCellValue(sheet, fmt.Sprintf("E%d", row), line.Cost) _ = f.SetCellValue(sheet, fmt.Sprintf("F%d", row), line.Buffer) _ = f.SetCellValue(sheet, fmt.Sprintf("G%d", row), line.Margin) _ = f.SetCellValue(sheet, fmt.Sprintf("H%d", row), line.Tax) _ = f.SetCellValue(sheet, fmt.Sprintf("J%d", row), line.Price) }