diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py index 7fa320f..5ff8af7 100644 --- a/backend/apps/pisadmin/miscprocurement/views.py +++ b/backend/apps/pisadmin/miscprocurement/views.py @@ -1,4 +1,5 @@ import logging +from collections import defaultdict from decimal import Decimal from datetime import datetime from typing import Optional @@ -217,8 +218,9 @@ def _sync_misc_low_price_records(inquiry: Inquiry, part_id: str) -> None: 制程最低价落库(与比价展开明细一致): - 前端比价展开不依赖成本结构模板:材料按材质分组仅展示重量/单价/材料费用,加工按工站分组仅展示加工费;落库口径仍按下列规则。 - 询价单下**每个上阶物料料号**单独一套主/次表数据(多料号互不合并)。 - - **材料**:次表两行分别记录最低重量、最低单价;souce_no 为取得该最小值对应的报价单单号,单价若来自「杂采材料信息」 + - **材料**:次表按材质分组记录,每组两行(最低重量、最低单价);souce_no 为取得该最小值对应的报价单单号,单价若来自「杂采材料信息」 则存交易厂区(factory);主表材料行 souce_no 聚合为 W:…;U:…(重量来源与单价来源可能不同)。 + 主表材料行的 min_price 是所有材质分组的(最低重量×最低单价×数量)之和。 - **加工**:仅主表一行(无次表、不按工站);每报价单合计加工费后取最小,全空/全 0 仍写入 MinPrice=0。 - 注意:材料行必须用 quotation_no__in=报价单号列表 过滤,勿用 QuerySet(QuotationMaster) 作 __in,否则 ORM 按主键匹配会查不到材料行。 - **其它**:包装费、运输费分列取 min(优先 sup_quotation_other;无则用上阶物料 total_other_expense 回退)。采购端比价主表「其它成本」行制程最低价列与「加工成本」相同,可链向合计最低的报价单详情。 @@ -248,74 +250,118 @@ def _sync_misc_low_price_records(inquiry: Inquiry, part_id: str) -> None: MiscLowPriceHeader.objects.filter(inquiry_no=inquiry_no, part_id=pid).delete() MiscLowPriceDetail.objects.filter(inquiry_no=inquiry_no, part_id=pid).delete() - # —— 材料:次表两行(重量、单价);主表 = 最低重量×最低单价×数量。 + # —— 材料:按材质规格分组,每组次表两行(重量、单价);主表 = 所有分组的(最低重量×最低单价×数量)之和。 # FK 须用报价单单号列表:quotation_no__in=QuerySet(QuotationMaster) 会按主键匹配,导致材料行查不到。 materials = QuotationMaterial.objects.filter(quotation_no__in=qn_list, part_id=pid) - weight_candidates: list[tuple[Decimal, str]] = [] - unit_candidates: list[tuple[Decimal, str]] = [] + + # 按材质规格分组收集候选值 + from collections import defaultdict + spec_weight_candidates: dict[str, list[tuple[Decimal, str]]] = defaultdict(list) + spec_unit_candidates: dict[str, list[tuple[Decimal, str]]] = defaultdict(list) + for m in materials: qn_src = getattr(m, "quotation_no_id", None) or "" qn_src = str(qn_src).strip() + spec = (getattr(m, "material_spec", None) or "").strip() or "-" if m.weight is not None: try: w = Decimal(str(m.weight)) - weight_candidates.append((w, qn_src)) + spec_weight_candidates[spec].append((w, qn_src)) except Exception: pass if m.unit_price is not None: try: up = Decimal(str(m.unit_price)) - unit_candidates.append((up, qn_src)) + spec_unit_candidates[spec].append((up, qn_src)) except Exception: pass + + # 杂采材料信息:单价适用于所有材质分组 + misc_unit_candidates: list[tuple[Decimal, str]] = [] for mi in MiscProcurementMaterialInfo.objects.filter(status=1).only("price", "factory"): if mi.price is not None: try: up = Decimal(str(mi.price)) fac = (getattr(mi, "factory", None) or "").strip() or "MISC" - unit_candidates.append((up, fac)) + misc_unit_candidates.append((up, fac)) except Exception: pass - detail_spec = "-" - min_w_row = min(weight_candidates, key=lambda t: (t[0], t[1])) if weight_candidates else None - min_up_row = min(unit_candidates, key=lambda t: (t[0], t[1])) if unit_candidates else None - min_w = min_w_row[0] if min_w_row else None - min_up = min_up_row[0] if min_up_row else None - src_w = min_w_row[1] if min_w_row else None - src_up = min_up_row[1] if min_up_row else None qty_dec = _rfq_qty_decimal(inquiry, pid) + total_material_low = Decimal("0") + all_src_w_list: list[str] = [] + all_src_up_list: list[str] = [] - if min_w is not None: - MiscLowPriceDetail.objects.create( - inquiry_no=inquiry_no, - part_id=pid, - cost_type="1", - material_spec=detail_spec[:50], - item_no="1", - value=_clip_price_str(min_w), - souce_no=_norm_low_price_src((src_w or souce_ref)), - ) - if min_up is not None: - MiscLowPriceDetail.objects.create( - inquiry_no=inquiry_no, - part_id=pid, - cost_type="1", - material_spec=detail_spec[:50], - item_no="2", - value=_clip_price_str(min_up), - souce_no=_norm_low_price_src((src_up or souce_ref)), - ) + # 获取所有材质规格(有重量或单价数据的) + all_specs = set(spec_weight_candidates.keys()) | set(spec_unit_candidates.keys()) - if min_w is not None and min_up is not None and qty_dec is not None and qty_dec > 0: - material_low = min_w * min_up * qty_dec + for spec in all_specs: + weight_candidates = spec_weight_candidates.get(spec, []) + unit_candidates = spec_unit_candidates.get(spec, []) + + # 杂采单价也加入该分组的单价候选 + unit_candidates = unit_candidates + misc_unit_candidates + + min_w_row = min(weight_candidates, key=lambda t: (t[0], t[1])) if weight_candidates else None + min_up_row = min(unit_candidates, key=lambda t: (t[0], t[1])) if unit_candidates else None + min_w = min_w_row[0] if min_w_row else None + min_up = min_up_row[0] if min_up_row else None + src_w = min_w_row[1] if min_w_row else None + src_up = min_up_row[1] if min_up_row else None + + # 判断是否来自杂采材料信息 + is_misc_src = False + if min_up is not None and misc_unit_candidates: + for misc_up, misc_fac in misc_unit_candidates: + if abs(min_up - misc_up) < Decimal("0.0001"): + is_misc_src = True + break + + # 写入次表:最低重量 + if min_w is not None: + MiscLowPriceDetail.objects.create( + inquiry_no=inquiry_no, + part_id=pid, + cost_type="1", + material_spec=spec[:50], + item_no="1", + value=_clip_price_str(min_w), + souce_no=_norm_low_price_src((src_w or souce_ref)), + ) + if src_w: + all_src_w_list.append(src_w) + + # 写入次表:最低单价 + if min_up is not None: + MiscLowPriceDetail.objects.create( + inquiry_no=inquiry_no, + part_id=pid, + cost_type="1", + material_spec=spec[:50], + item_no="2", + value=_clip_price_str(min_up), + souce_no=_norm_low_price_src((src_up or souce_ref)) if not is_misc_src else _norm_low_price_src(src_up or souce_ref), + ) + if src_up: + all_src_up_list.append(src_up) + + # 累加到总材料费用 + if min_w is not None and min_up is not None and qty_dec is not None and qty_dec > 0: + spec_material_low = min_w * min_up * qty_dec + total_material_low += spec_material_low + + # 写入主表材料行:所有分组的材料费用总和 + if total_material_low > 0: + # 聚合来源:W:所有重量来源; U:所有单价来源 + aggregated_src_w = ",".join(sorted(set(all_src_w_list))) if all_src_w_list else None + aggregated_src_up = ",".join(sorted(set(all_src_up_list))) if all_src_up_list else None MiscLowPriceHeader.objects.create( inquiry_no=inquiry_no, part_id=pid, - souce_no=_material_cost_header_souce_no(src_w, src_up) or None, + souce_no=_material_cost_header_souce_no(aggregated_src_w, aggregated_src_up) or None, cost_type="1", item_no="材料", - min_price=_clip_price_str(material_low), + min_price=_clip_price_str(total_material_low), ) # —— 加工:仅主表一行,无次表、不按工站。每份报价单对该料号加工费合计(空/缺省按 0),再取最小; diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue index eff2daa..1d55274 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue @@ -742,7 +742,7 @@ const findProcessRowByStation = (q: any, station: string) => { return (q.process_costs || []).find((r: any) => String(r.process_station || '').trim() === t) } -/** 材料成本展开:按材料规格分组;「制程最低价」列对重量/单价/材料费用行使用全局口径(与后端落库一致) */ +/** 材料成本展开:按材料规格分组;「制程最低价」列对每个材质分组单独计算最低价 */ const buildMaterialDetailGroups = ( quotes: any[], supplierKeys: string[], @@ -770,7 +770,8 @@ const buildMaterialDetailGroups = ( const allDash = supplierKeys.every((sup) => values[sup] === '-') if (allDash) continue const line: ComparisonDetailRow = { label: m.label, values, isText: m.isText, ...calcCompareStats(values) } - if (lowPriceCtx) applyMaterialDetailLowPriceMin(line, m, lowPriceCtx, supplierKeys) + // 传入当前材质规格,使每个分组使用各自的最低价数据 + if (lowPriceCtx) applyMaterialDetailLowPriceMin(line, m, lowPriceCtx, supplierKeys, spec) lines.push(line) } if (lines.length) groups.push({ title: spec || '材料', lines }) @@ -939,8 +940,9 @@ const buildComparisonRowsFromPisQuotes = ( const matRow = rows.find((r) => r.key === 'material') if (matRow && materialGroups.length) matRow.detailGroups = materialGroups - if (matRow && lp?.materialProduct != null) { - matRow.min = lp.materialProduct + // 材料成本行的制程最低价是所有材质分组材料费用的总和 + if (matRow && lp?.totalMaterialProduct != null) { + matRow.min = lp.totalMaterialProduct matRow.minLink = buildMaterialCostMinLink(lp, quotes, { materialRowValues: matRow.values, supplierKeys diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx index 1630d95..4df494f 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx @@ -314,20 +314,35 @@ export type CompareMinLink = unitPriceSource: string } +/** 单个材质分组的最低价数据 */ +export type MaterialSpecLowPrice = { + /** 材质规格名称 */ + spec: string + /** 最低重量 */ + minW?: number + /** 最低单价 */ + minUp?: number + /** 材料费用 = 最低重量 × 最低单价 × 数量 */ + materialProduct?: number + /** 最低重量所在报价单主键 */ + minWeightQuotationId?: string | number + /** 最低单价所在报价单主键(若最低单价由杂采材料信息决定则为空) */ + minUnitPriceQuotationId?: string | number + /** 仅当杂采单价严格低于所有报价单价时,单价/材料成本链向杂采材料管理 */ + minUnitPriceFromMisc?: boolean + /** 杂采材料最低价对应交易厂区 */ + miscMinFactory?: string +} + /** 与后端制程最低价落库一致:全报价最低重量/最低单价(单价含杂采材料信息)、材料费=三者乘积;加工费=各报价单加工费合计之最小值 */ export type LowPriceMinContext = { - minW?: number - minUp?: number - materialProduct?: number + /** 按材质规格分组的最低价数据(每个材质单独计算最低价) */ + materialSpecs: MaterialSpecLowPrice[] + /** 所有材质分组的材料费用总和(用于材料成本行的制程最低价) */ + totalMaterialProduct?: number minProcessTotal?: number pid: string - /** 全报价中材料明细最低重量所在报价单主键 */ - minWeightQuotationId?: string | number - /** 全报价中材料明细最低单价所在报价单主键(若最低单价由杂采材料信息决定则为空) */ - minUnitPriceQuotationId?: string | number - /** 仅当杂采单价严格低于所有报价单价时,单价/材料成本链向杂采材料管理;与报价持平或更高则链向报价单 */ - minUnitPriceFromMisc?: boolean - /** 杂采材料最低价对应交易厂区(与杂采材料表 factory 等字段一致,供浮窗) */ + /** 杂采材料最低价对应交易厂区(全局,供浮窗) */ miscMinFactory?: string } @@ -359,64 +374,99 @@ export function computeLowPriceMinContext( ): LowPriceMinContext { const pid = String(quotes[0]?.rfq_items?.[0]?.part_id || '').trim() const qty = Number(quotes[0]?.rfq_items?.[0]?.qty) - let minW: number | undefined - /** 各报价单材料行单价之最小值(不含杂采) */ - let minQuoteUp: number | undefined - let minWeightQuotationId: string | number | undefined - let minUnitPriceQuotationId: string | number | undefined const qid = (q: any) => q?.autoid ?? q?.id - for (const q of quotes) { - for (const r of q.material_costs || []) { - if (pid && String(r.part_id || '').trim() !== pid) continue - const w = Number(r.weight) - if (Number.isFinite(w)) { - if (minW === undefined || w < minW) { - minW = w - minWeightQuotationId = qid(q) + // 收集所有材质规格 + const specs = new Set() + quotes.forEach((q) => { + ;(q.material_costs || []).forEach((r: any) => { + if (pid && String(r.part_id || '').trim() !== pid) return + const spec = String(r.material_spec || '').trim() || '材料' + specs.add(spec) + }) + }) + + // 为每个材质规格计算最低价 + const materialSpecs: MaterialSpecLowPrice[] = [] + let totalMaterialProduct = 0 + + for (const spec of specs) { + let specMinW: number | undefined + let specMinQuoteUp: number | undefined + let specMinWeightQuotationId: string | number | undefined + let specMinUnitPriceQuotationId: string | number | undefined + + // 遍历所有报价单,找出该材质规格的最低重量和最低单价 + for (const q of quotes) { + for (const r of q.material_costs || []) { + if (pid && String(r.part_id || '').trim() !== pid) continue + const rSpec = String(r.material_spec || '').trim() || '材料' + if (rSpec !== spec) continue + + const w = Number(r.weight) + if (Number.isFinite(w)) { + if (specMinW === undefined || w < specMinW) { + specMinW = w + specMinWeightQuotationId = qid(q) + } } - } - const up = Number(r.unit_price) - if (Number.isFinite(up)) { - if (minQuoteUp === undefined || up < minQuoteUp) { - minQuoteUp = up - minUnitPriceQuotationId = qid(q) + const up = Number(r.unit_price) + if (Number.isFinite(up)) { + if (specMinQuoteUp === undefined || up < specMinQuoteUp) { + specMinQuoteUp = up + specMinUnitPriceQuotationId = qid(q) + } } } } - } - let minUp: number | undefined = minQuoteUp - let minUnitPriceFromMisc = false - let miscFactoryOut: string | undefined - const pickMiscFactory = () => { - const f = miscMinFactory != null && String(miscMinFactory).trim() !== '' ? String(miscMinFactory).trim() : '' - if (f) miscFactoryOut = f - } + // 考虑杂采材料信息的单价 + let specMinUp = specMinQuoteUp + let specMinUnitPriceFromMisc = false + let specMiscMinFactory: string | undefined - if (miscMinUnitPrice != null && Number.isFinite(miscMinUnitPrice)) { - const misc = miscMinUnitPrice - if (minQuoteUp === undefined) { - minUp = misc - minUnitPriceFromMisc = true - minUnitPriceQuotationId = undefined - pickMiscFactory() - } else if (misc < minQuoteUp && !_nearlyEqualMin(misc, minQuoteUp)) { - /** 仅当杂采单价严格低于所有报价单价时,制程最低价单价才记为来自杂采 */ - minUp = misc - minUnitPriceFromMisc = true - minUnitPriceQuotationId = undefined - pickMiscFactory() - } else { - /** 杂采高于或与最低报价持平:取 min(报价最低, 杂采),来源优先报价单(含持平) */ - minUp = Math.min(minQuoteUp, misc) - minUnitPriceFromMisc = false + if (miscMinUnitPrice != null && Number.isFinite(miscMinUnitPrice)) { + const misc = miscMinUnitPrice + if (specMinQuoteUp === undefined) { + specMinUp = misc + specMinUnitPriceFromMisc = true + specMinUnitPriceQuotationId = undefined + specMiscMinFactory = miscMinFactory != null && String(miscMinFactory).trim() !== '' + ? String(miscMinFactory).trim() + : undefined + } else if (misc < specMinQuoteUp && !_nearlyEqualMin(misc, specMinQuoteUp)) { + specMinUp = misc + specMinUnitPriceFromMisc = true + specMinUnitPriceQuotationId = undefined + specMiscMinFactory = miscMinFactory != null && String(miscMinFactory).trim() !== '' + ? String(miscMinFactory).trim() + : undefined + } else { + specMinUp = Math.min(specMinQuoteUp, misc) + specMinUnitPriceFromMisc = false + } } + + // 计算该材质的材料费用(最低重量 × 最低单价 × 数量) + let specMaterialProduct: number | undefined + if (specMinW !== undefined && specMinUp !== undefined && Number.isFinite(qty) && qty > 0) { + specMaterialProduct = specMinW * specMinUp * qty + totalMaterialProduct += specMaterialProduct + } + + materialSpecs.push({ + spec, + minW: specMinW, + minUp: specMinUp, + materialProduct: specMaterialProduct, + minWeightQuotationId: specMinWeightQuotationId, + minUnitPriceQuotationId: specMinUnitPriceQuotationId, + minUnitPriceFromMisc: specMinUnitPriceFromMisc, + miscMinFactory: specMiscMinFactory + }) } - let materialProduct: number | undefined - if (minW !== undefined && minUp !== undefined && Number.isFinite(qty) && qty > 0) { - materialProduct = minW * minUp * qty - } + + // 加工成本:各报价单加工费合计之最小值(不按工站分组) let minProcessTotal: number | undefined for (const q of quotes) { let s = 0 @@ -431,20 +481,19 @@ export function computeLowPriceMinContext( } if (ok) minProcessTotal = minProcessTotal === undefined ? s : Math.min(minProcessTotal, s) } + return { - minW, - minUp, - materialProduct, + materialSpecs, + totalMaterialProduct: totalMaterialProduct || undefined, minProcessTotal, pid, - minWeightQuotationId, - minUnitPriceQuotationId, - minUnitPriceFromMisc, - miscMinFactory: miscFactoryOut + miscMinFactory: miscMinFactory != null && String(miscMinFactory).trim() !== '' + ? String(miscMinFactory).trim() + : undefined } } -/** 主表「材料成本」制程最低价:同源则链向报价单详情,异源则链样式 + 浮窗说明分项来源 */ +/** 主表「材料成本」制程最低价:材料成本行的最低价是所有材质分组材料费用的总和 */ export function buildMaterialCostMinLink( ctx: LowPriceMinContext, quotes: any[], @@ -455,65 +504,103 @@ export function buildMaterialCostMinLink( !!opts.supplierKeys?.length && allCompareSupplierValuesEqual(opts.materialRowValues, opts.supplierKeys) - /** 各报价一致且杂采单价严格低于所有报价:制程最低价指向杂采材料管理 */ - if (materialAllEqual && ctx.minUnitPriceFromMisc) { - return { - kind: 'misc_materials', - sourceFactory: ctx.miscMinFactory - } - } if (materialAllEqual) { return null } - if (ctx.materialProduct === undefined) return null - const w = ctx.minW - const up = ctx.minUp - if (w === undefined || up === undefined || !Number.isFinite(w) || !Number.isFinite(up)) return null - const wid = ctx.minWeightQuotationId - const uid = ctx.minUnitPriceQuotationId - const misc = ctx.minUnitPriceFromMisc + // 材料成本行使用 totalMaterialProduct(所有材质分组的材料费用总和) + if (ctx.totalMaterialProduct === undefined) return null - const quoteLabel = (qid: string | number | undefined) => { - if (qid == null || qid === '') return '' - const q = quotes.find((x) => String(x.autoid ?? x.id) === String(qid)) - const name = String(q?.supplier_name || q?.supplierName || '').trim() - const code = String(q?.supplier_code || q?.supplierCode || '').trim() - const label = name || code - return label || `报价单 #${qid}` + // 如果只有一个材质分组,可以链向该分组的来源 + if (ctx.materialSpecs.length === 1) { + const spec = ctx.materialSpecs[0] + if (spec.minUnitPriceFromMisc) { + return { + kind: 'misc_materials', + sourceFactory: spec.miscMinFactory + } + } + // 如果重量和单价来自同一个报价单,链向该报价单 + const wid = spec.minWeightQuotationId + const uid = spec.minUnitPriceQuotationId + if (wid != null && wid !== '' && uid != null && uid !== '' && String(wid) === String(uid)) { + return { kind: 'quotation', id: wid } + } + // 否则显示拆分来源 + const quoteLabel = (qid: string | number | undefined) => { + if (qid == null || qid === '') return '' + const q = quotes.find((x) => String(x.autoid ?? x.id) === String(qid)) + const name = String(q?.supplier_name || q?.supplierName || '').trim() + const code = String(q?.supplier_code || q?.supplierCode || '').trim() + return name || code || `报价单 #${qid}` + } + return { + kind: 'material_cost_split', + weight: spec.minW ?? 0, + unitPrice: spec.minUp ?? 0, + weightSource: quoteLabel(wid) || '—', + unitPriceSource: spec.minUnitPriceFromMisc ? '杂采材料信息' : quoteLabel(uid) || '—' + } } - const weightSrc = quoteLabel(wid) - const unitSrc = misc ? '杂采材料信息' : quoteLabel(uid) - - const sameQuotation = - !misc && wid != null && wid !== '' && uid != null && uid !== '' && String(wid) === String(uid) - - if (sameQuotation) { - return { kind: 'quotation', id: wid } + // 多个材质分组时,如果有任何一个分组使用了杂采材料信息,显示杂采来源 + const hasMiscSource = ctx.materialSpecs.some((s) => s.minUnitPriceFromMisc) + if (hasMiscSource) { + // 找到第一个使用杂采的分组的厂区信息 + const miscSpec = ctx.materialSpecs.find((s) => s.minUnitPriceFromMisc) + if (miscSpec) { + return { + kind: 'misc_materials', + sourceFactory: miscSpec.miscMinFactory + } + } } - return { - kind: 'material_cost_split', - weight: w, - unitPrice: up, - weightSource: weightSrc || '—', - unitPriceSource: unitSrc || '—' + // 多个材质分组,显示拆分来源(使用第一个分组的来源信息作为代表) + const firstSpec = ctx.materialSpecs[0] + if (firstSpec) { + const quoteLabel = (qid: string | number | undefined) => { + if (qid == null || qid === '') return '' + const q = quotes.find((x) => String(x.autoid ?? x.id) === String(qid)) + const name = String(q?.supplier_name || q?.supplierName || '').trim() + const code = String(q?.supplier_code || q?.supplierCode || '').trim() + return name || code || `报价单 #${qid}` + } + return { + kind: 'material_cost_split', + weight: firstSpec.minW ?? 0, + unitPrice: firstSpec.minUp ?? 0, + weightSource: quoteLabel(firstSpec.minWeightQuotationId) || '—', + unitPriceSource: quoteLabel(firstSpec.minUnitPriceQuotationId) || '—' + } } + + return null } -/** 展开仍按规格分行时,「制程最低价」列用全局口径覆盖该行 min */ +/** 展开仍按规格分行时,「制程最低价」列按材质分组口径覆盖该行 min */ export function applyMaterialDetailLowPriceMin( line: { min?: number; minLink?: CompareMinLink | null; values: Record }, m: ComparisonDetailMetric, ctx: LowPriceMinContext, - supplierKeys?: string[] + supplierKeys?: string[], + /** 当前行的材质规格,用于查找对应分组的最低价数据 */ + spec?: string ): void { const fk = m.fieldKey ? normCmpTplKey(m.fieldKey) : '' const label = String(m.label || '').trim() - if (ctx.minW !== undefined && (fk === 'weight' || /用量|重量/.test(label))) { - line.min = ctx.minW - const qid = ctx.minWeightQuotationId + + // 找到当前材质规格对应的最低价数据 + const specData = spec + ? ctx.materialSpecs.find((s) => s.spec === spec) + : ctx.materialSpecs[0] + + if (!specData) return + + // 重量行:使用该材质分组的最低重量 + if (specData.minW !== undefined && (fk === 'weight' || /用量|重量/.test(label))) { + line.min = specData.minW + const qid = specData.minWeightQuotationId line.minLink = qid != null && qid !== '' ? { kind: 'quotation', id: qid } @@ -523,17 +610,19 @@ export function applyMaterialDetailLowPriceMin( } return } + + // 单价行:使用该材质分组的最低单价 const isUnitPriceRow = fk === 'unitprice' || /材料单价/.test(label) || label === '单价' - if (ctx.minUp !== undefined && isUnitPriceRow) { - line.min = ctx.minUp - if (ctx.minUnitPriceFromMisc) { + if (specData.minUp !== undefined && isUnitPriceRow) { + line.min = specData.minUp + if (specData.minUnitPriceFromMisc) { line.minLink = { kind: 'misc_materials', - sourceFactory: ctx.miscMinFactory + sourceFactory: specData.miscMinFactory } } else { - const qid = ctx.minUnitPriceQuotationId + const qid = specData.minUnitPriceQuotationId line.minLink = qid != null && qid !== '' ? { kind: 'quotation', id: qid } @@ -542,14 +631,16 @@ export function applyMaterialDetailLowPriceMin( if ( supplierKeys?.length && allCompareSupplierValuesEqual(line.values, supplierKeys) && - !ctx.minUnitPriceFromMisc + !specData.minUnitPriceFromMisc ) { line.minLink = null } return } - if (ctx.materialProduct !== undefined && (fk === 'materialcost' || /材料费用|材料费/.test(label))) { - line.min = ctx.materialProduct + + // 材料费用行:使用该材质分组的材料费用(最低重量 × 最低单价 × 数量) + if (specData.materialProduct !== undefined && (fk === 'materialcost' || /材料费用|材料费/.test(label))) { + line.min = specData.materialProduct line.minLink = null } }