pl-plan-archive-brand-view.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
  2. import type { PlanStopProductFormRow } from '@/utils/pl-plan-stop-form'
  3. export interface ArchiveBrandViewRow {
  4. brandId: string
  5. brandName: string
  6. qty: number
  7. price?: number
  8. amount?: number
  9. }
  10. export interface ArchiveBrandViewItem {
  11. key: string
  12. productName: string
  13. unit?: string
  14. planQty?: number
  15. segmentQty: number
  16. brandMode?: string
  17. rows: ArchiveBrandViewRow[]
  18. }
  19. function getSegmentQty(
  20. row: PlanStopProductFormRow,
  21. mode: 'inbound' | 'outbound',
  22. planRow?: PlanFormProductRow
  23. ): number {
  24. const primary = mode === 'inbound' ? row.inboundQty : row.outboundQty
  25. if (primary != null && Number.isFinite(Number(primary))) return Number(primary)
  26. const fallback = mode === 'inbound' ? planRow?.inboundQty : planRow?.outboundQty
  27. if (fallback != null && Number.isFinite(Number(fallback))) return Number(fallback)
  28. return 0
  29. }
  30. function getSegmentPrice(
  31. row: PlanStopProductFormRow,
  32. mode: 'inbound' | 'outbound',
  33. planRow?: PlanFormProductRow
  34. ): number | undefined {
  35. const primary = mode === 'inbound' ? row.inboundPrice : row.outboundPrice
  36. if (primary != null && Number.isFinite(Number(primary))) return Number(primary)
  37. const fallback = mode === 'inbound' ? planRow?.inboundPrice : planRow?.outboundPrice
  38. if (fallback != null && Number.isFinite(Number(fallback))) return Number(fallback)
  39. if (planRow?.taxPrice != null && Number.isFinite(Number(planRow.taxPrice))) {
  40. return Number(planRow.taxPrice)
  41. }
  42. return undefined
  43. }
  44. function getPlanBrandRows(planRow?: PlanFormProductRow) {
  45. return (planRow?.brandQtyRows || []).filter((row) => Number(row.qty) > 0)
  46. }
  47. function getStopBrandRows(stopRow: PlanStopProductFormRow) {
  48. return (stopRow.brandQtys || []).filter((row) => Number(row.qty) > 0)
  49. }
  50. function getAllocationMap(
  51. allocations: Map<string, Map<string, number>>,
  52. planProductId: string
  53. ): Map<string, number> {
  54. let map = allocations.get(planProductId)
  55. if (!map) {
  56. map = new Map()
  57. allocations.set(planProductId, map)
  58. }
  59. return map
  60. }
  61. /** 按段顺序贪心分配计划级品牌量(旧数据无段级品牌明细时回退) */
  62. function allocateBrandRows(
  63. brandRows: Array<{ brandId: string; brandName: string; qty: number }>,
  64. segmentQty: number,
  65. price: number | undefined,
  66. allocated: Map<string, number>
  67. ): ArchiveBrandViewRow[] {
  68. if (segmentQty <= 0 || !brandRows.length) return []
  69. let remaining = segmentQty
  70. const result: ArchiveBrandViewRow[] = []
  71. for (const brand of brandRows) {
  72. const total = Number(brand.qty || 0)
  73. if (total <= 0) continue
  74. const used = allocated.get(brand.brandId) || 0
  75. const available = total - used
  76. if (available <= 0.0005) continue
  77. const take = Math.min(available, remaining)
  78. if (take <= 0.0005) continue
  79. allocated.set(brand.brandId, used + take)
  80. remaining -= take
  81. result.push({
  82. brandId: brand.brandId,
  83. brandName: brand.brandName,
  84. qty: take,
  85. price,
  86. amount: price != null ? take * price : undefined
  87. })
  88. if (remaining <= 0.0005) break
  89. }
  90. return result
  91. }
  92. function buildRowsFromStopBrandQtys(
  93. stopRow: PlanStopProductFormRow,
  94. fallbackPrice: number | undefined
  95. ): ArchiveBrandViewRow[] {
  96. return getStopBrandRows(stopRow).map((brand) => {
  97. const qty = Number(brand.qty || 0)
  98. const price =
  99. brand.unitPrice != null && Number.isFinite(Number(brand.unitPrice))
  100. ? Number(brand.unitPrice)
  101. : fallbackPrice
  102. return {
  103. brandId: String(brand.brandId || ''),
  104. brandName: String(brand.brandName || ''),
  105. qty,
  106. price,
  107. amount: price != null ? qty * price : undefined
  108. }
  109. })
  110. }
  111. export function buildArchiveBrandViewItems(
  112. stopProducts: PlanStopProductFormRow[],
  113. planProducts: PlanFormProductRow[],
  114. mode: 'inbound' | 'outbound',
  115. allocations: Map<string, Map<string, number>>
  116. ): ArchiveBrandViewItem[] {
  117. const planById = new Map(planProducts.filter((row) => row.id).map((row) => [String(row.id), row]))
  118. const planByProductId = new Map(planProducts.map((row) => [String(row.productId), row]))
  119. const items: ArchiveBrandViewItem[] = []
  120. for (const stopRow of stopProducts) {
  121. const planRow =
  122. (stopRow.planProductId ? planById.get(String(stopRow.planProductId)) : undefined) ||
  123. planByProductId.get(String(stopRow.productId))
  124. const brandMode = stopRow.brandMode || planRow?.brandMode
  125. if (brandMode !== 'any' && brandMode !== 'required') continue
  126. const planProductId = String(stopRow.planProductId || planRow?.id || '').trim()
  127. if (!planProductId) continue
  128. const segmentQty = getSegmentQty(stopRow, mode, planRow)
  129. if (segmentQty <= 0) continue
  130. const price = getSegmentPrice(stopRow, mode, planRow)
  131. // 优先使用段级归档时填写的品牌数量
  132. let rows = buildRowsFromStopBrandQtys(stopRow, price)
  133. if (!rows.length) {
  134. const brandRows = getPlanBrandRows(planRow)
  135. if (!brandRows.length) continue
  136. const allocated = getAllocationMap(allocations, planProductId)
  137. rows = allocateBrandRows(brandRows, segmentQty, price, allocated)
  138. }
  139. if (!rows.length) continue
  140. items.push({
  141. key: `${planProductId}-${stopRow.id || stopRow.productId}`,
  142. productName:
  143. stopRow.standardProductName ||
  144. stopRow.productName ||
  145. planRow?.standardProductName ||
  146. planRow?.productName ||
  147. '—',
  148. unit: stopRow.unit || planRow?.unit,
  149. planQty: stopRow.planQty ?? planRow?.quantity,
  150. segmentQty,
  151. brandMode,
  152. rows
  153. })
  154. }
  155. return items
  156. }
  157. export function buildArchiveBrandViewsBySegment(input: {
  158. loadProducts: PlanStopProductFormRow[]
  159. deliveries: Array<{ key: string; products: PlanStopProductFormRow[] }>
  160. planProducts: PlanFormProductRow[]
  161. }): Map<string, ArchiveBrandViewItem[]> {
  162. const result = new Map<string, ArchiveBrandViewItem[]>()
  163. // 装货(入库)与送货(出库)各自独立分配池,互不占位(仅旧数据回退贪心时使用)
  164. const inboundAllocations = new Map<string, Map<string, number>>()
  165. const outboundAllocations = new Map<string, Map<string, number>>()
  166. result.set(
  167. 'load',
  168. buildArchiveBrandViewItems(
  169. input.loadProducts,
  170. input.planProducts,
  171. 'inbound',
  172. inboundAllocations
  173. )
  174. )
  175. for (const delivery of input.deliveries) {
  176. result.set(
  177. delivery.key,
  178. buildArchiveBrandViewItems(
  179. delivery.products,
  180. input.planProducts,
  181. 'outbound',
  182. outboundAllocations
  183. )
  184. )
  185. }
  186. return result
  187. }
  188. export function formatArchiveBrandAmount(qty?: number, price?: number): string {
  189. if (qty == null || price == null) return '—'
  190. const amount = Number(qty) * Number(price)
  191. return Number.isFinite(amount) ? amount.toFixed(2) : '—'
  192. }