accountDetailTable.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. <template>
  2. <div class="account-detail-table">
  3. <el-form ref="detailForm" :model="{ flatList }">
  4. <ele-pro-table
  5. ref="detailTable"
  6. row-key="id"
  7. :needPage="false"
  8. :columns="columns"
  9. max-height="500px"
  10. :datasource="flatList"
  11. :cache-key="cacheKeyUrl"
  12. :selection.sync="selection"
  13. class="time-form"
  14. >
  15. <template v-slot:toolbar>
  16. <div class="detail-toolbar">
  17. <setAllValue :disabled="!selection.length" inputType="number" title="税率" valueKey="taxRate" @success="confirmBatchSet" />
  18. <setAllValue :disabled="!selection.length" inputType="number" title="单价" valueKey="singlePrice" @success="confirmBatchSet" />
  19. <setAllValue :disabled="!selection.length" inputType="number" title="折让比例" valueKey="discountRatio" @success="confirmBatchSet" />
  20. <!-- <el-button
  21. v-if="!isView"
  22. type="primary"
  23. size="small"
  24. @click="batchSetTaxRate"
  25. >批量设置税率</el-button
  26. >
  27. <el-button
  28. v-if="!isView"
  29. type="primary"
  30. size="small"
  31. @click="batchSetPrice"
  32. >批量设置单价</el-button
  33. >
  34. <el-button
  35. v-if="!isView"
  36. type="primary"
  37. size="small"
  38. @click="batchSetDiscount"
  39. >批量设置折让比例</el-button
  40. > -->
  41. <!-- <div class="summary-info">
  42. <span
  43. >本次总对账金额:<b>{{ totalStatementAmount }}</b> 元</span
  44. >
  45. <span
  46. >本次总对账数量:<b>{{ totalStatementCount }}</b></span
  47. >
  48. </div> -->
  49. </div>
  50. </template>
  51. <template v-slot:statementCount="scope">
  52. <el-form-item
  53. :prop="'flatList.' + scope.$index + '.statementCount'"
  54. :rules="{
  55. validator: (rule, value, cb) =>
  56. validateStatementCount(rule, value, cb, scope.row),
  57. trigger: 'change'
  58. }"
  59. >
  60. <el-input
  61. v-model="scope.row.statementCount"
  62. type="number"
  63. :disabled="isView || queryDimension == 1"
  64. @input="handleCountChange(scope.row, scope.$index)"
  65. />
  66. </el-form-item>
  67. </template>
  68. <template v-slot:technologyRouteName="scope">
  69. <el-input
  70. v-model="scope.row.technologyRouteName"
  71. placeholder="请选择"
  72. :disabled="isView"
  73. @click.native="openVersion(scope.$index)"
  74. />
  75. </template>
  76. <template v-slot:singlePrice="scope">
  77. <el-form-item
  78. :prop="'flatList.' + scope.$index + '.singlePrice'"
  79. :rules="{
  80. required: true,
  81. message: '请输入单价',
  82. trigger: 'change'
  83. }"
  84. >
  85. <el-input
  86. v-model="scope.row.singlePrice"
  87. type="number"
  88. :disabled="isView"
  89. @input="handlePriceChange(scope.row, scope.$index)"
  90. />
  91. <!-- <div v-if="isPriceInvalid(scope.row)" class="price-warning">
  92. 单价无效,请核对商品价格
  93. </div> -->
  94. </el-form-item>
  95. </template>
  96. <template v-slot:taxRate="scope">
  97. <el-form-item
  98. :prop="'flatList.' + scope.$index + '.taxRate'"
  99. :rules="{
  100. required: isTaxRate == 1,
  101. message: '请输入税率',
  102. trigger: 'change'
  103. }"
  104. >
  105. <el-input
  106. v-model="scope.row.taxRate"
  107. type="number"
  108. :disabled="isView"
  109. @input="handleTaxChange(scope.row, scope.$index)"
  110. />
  111. </el-form-item>
  112. </template>
  113. <template v-slot:discountRatio="scope">
  114. <el-form-item
  115. :prop="'flatList.' + scope.$index + '.discountRatio'"
  116. :rules="{
  117. required: true,
  118. message: '请输入折让比例',
  119. trigger: 'change'
  120. }"
  121. >
  122. <el-input
  123. v-model="scope.row.discountRatio"
  124. type="number"
  125. :min="0"
  126. :max="100"
  127. :disabled="isView"
  128. @input="handleDiscountChange(scope.row, scope.$index)"
  129. />
  130. </el-form-item>
  131. </template>
  132. <template v-slot:statementAmount="scope">
  133. <span>{{ scope.row.statementAmount }}</span>
  134. </template>
  135. <template v-slot:headerRequired="{ column }">
  136. <span class="is-required">{{ column.label }}</span>
  137. </template>
  138. <template v-slot:action="scope">
  139. <el-link
  140. v-if="!isView && queryDimension == 2"
  141. type="danger"
  142. :underline="false"
  143. icon="el-icon-delete"
  144. @click="removeRow(scope.$index)"
  145. >删除</el-link
  146. >
  147. <span v-else>--</span>
  148. </template>
  149. </ele-pro-table>
  150. </el-form>
  151. <!-- 批量设置弹窗 -->
  152. <el-dialog
  153. :visible.sync="batchDialogVisible"
  154. title="批量设置"
  155. width="400px"
  156. append-to-body
  157. >
  158. <el-form label-width="100px">
  159. <el-form-item :label="batchLabel">
  160. <el-input v-model="batchValue" type="number" />
  161. </el-form-item>
  162. </el-form>
  163. <div slot="footer">
  164. <el-button @click="batchDialogVisible = false">取消</el-button>
  165. <el-button
  166. type="primary"
  167. @click="confirmBatchSet({ key: batchType, value: batchValue })"
  168. >确定</el-button
  169. >
  170. </div>
  171. </el-dialog>
  172. <!-- 工艺路线选择弹窗 -->
  173. <ProductionVersion
  174. ref="versionRefs"
  175. @changeProduct="changeProduct"
  176. ></ProductionVersion>
  177. </div>
  178. </template>
  179. <script>
  180. import { parameterGetByCode } from '@/api/main/index.js';
  181. import { pricingWayList } from '@/enum/dict.js';
  182. import ProductionVersion from '@/components/ProductionVersion2/index.vue';
  183. import setAllValue from '@/BIZComponents/setAllValue.vue'; //批量修改
  184. import { getGoodsById } from '@/api/goodsManage/index';
  185. export default {
  186. name: 'accountDetailTable',
  187. components: {
  188. ProductionVersion,
  189. setAllValue
  190. },
  191. props: {
  192. datasource: {
  193. type: Array,
  194. default: () => []
  195. },
  196. dialogType: {
  197. type: String,
  198. default: 'add'
  199. },
  200. queryDimension: {
  201. type: Number,
  202. default: 1
  203. }
  204. },
  205. data() {
  206. return {
  207. cacheKeyUrl: 'eom-202608171423-account-detail-table',
  208. isTaxRate: 0,
  209. flatList: [],
  210. selection: [],
  211. totalStatementAmount: 0,
  212. totalStatementCount: 0,
  213. batchDialogVisible: false,
  214. batchType: '',
  215. batchValue: '',
  216. batchLabel: '',
  217. columnsVersion: 1,
  218. };
  219. },
  220. computed: {
  221. isView() {
  222. return this.dialogType === 'view';
  223. },
  224. columns() {
  225. let columnsVersion = this.columnsVersion;
  226. return [
  227. {
  228. width: 45,
  229. type: 'selection',
  230. columnKey: 'selection',
  231. align: 'center',
  232. fixed: 'left'
  233. },
  234. {
  235. width: 60,
  236. label: '序号',
  237. type: 'index',
  238. columnKey: 'index',
  239. align: 'center',
  240. fixed: 'left'
  241. },
  242. {
  243. minWidth: 140,
  244. prop: 'orderNo',
  245. label: this.queryDimension == 3 ? '采购订单编码' : '销售订单编码',
  246. align: 'center',
  247. showOverflowTooltip: true
  248. },
  249. {
  250. minWidth: 140,
  251. prop: 'statementSubOrderNo',
  252. label: this.queryDimension == 3 ? '收货单编码' : this.queryDimension == 1 ? '发货单编码' : '调拨单编码',
  253. align: 'center',
  254. showOverflowTooltip: true
  255. },
  256. {
  257. minWidth: 120,
  258. prop: 'productOperateTime',
  259. label: this.queryDimension == 3 ? '收货日期' : this.queryDimension == 1 ? '发货日期' : '调拨单日期',
  260. align: 'center',
  261. showOverflowTooltip: true
  262. },
  263. {
  264. minWidth: 120,
  265. prop: 'productCode',
  266. label: '产品编码',
  267. align: 'center',
  268. showOverflowTooltip: true
  269. },
  270. {
  271. minWidth: 140,
  272. prop: 'productName',
  273. label: '产品名称',
  274. align: 'center',
  275. showOverflowTooltip: true
  276. },
  277. {
  278. minWidth: 120,
  279. prop: 'batchNo',
  280. label: '批次号',
  281. align: 'center',
  282. showOverflowTooltip: true
  283. },
  284. {
  285. minWidth: 110,
  286. prop: 'historicalStatementCount',
  287. label: '已对账数量',
  288. align: 'center'
  289. },
  290. {
  291. minWidth: 140,
  292. headerSlot: 'headerRequired',
  293. prop: 'statementCount',
  294. label: '本次对账数量',
  295. align: 'center',
  296. slot: 'statementCount'
  297. },
  298. {
  299. minWidth: 120,
  300. headerSlot: 'headerRequired',
  301. prop: 'statementAmount',
  302. label: '本次对账金额',
  303. align: 'center',
  304. slot: 'statementAmount'
  305. },
  306. {
  307. minWidth: 120,
  308. prop: 'totalCount',
  309. label: this.queryDimension == 3 ? '收货数量' : this.queryDimension == 1 ? '发货数量' : '调拨数量',
  310. align: 'center',
  311. },
  312. {
  313. minWidth: 140,
  314. prop: 'technologyRouteName',
  315. label: '工艺路线',
  316. align: 'center',
  317. slot: 'technologyRouteName',
  318. showOverflowTooltip: true
  319. },
  320. {
  321. minWidth: 120,
  322. prop: 'singlePrice',
  323. label: '单价',
  324. align: 'center',
  325. slot: 'singlePrice'
  326. },
  327. {
  328. minWidth: 100,
  329. prop: 'taxRate',
  330. label: '税率',
  331. align: 'center',
  332. slot: 'taxRate'
  333. },
  334. {
  335. minWidth: 100,
  336. prop: 'noTaxSinglePrice',
  337. label: '不含税单价',
  338. align: 'center',
  339. },
  340. {
  341. minWidth: 100,
  342. prop: 'totalPrice',
  343. label: '合计',
  344. align: 'center',
  345. },
  346. {
  347. minWidth: 120,
  348. headerSlot: 'headerRequired',
  349. prop: 'discountRatio',
  350. label: '折让比例',
  351. align: 'center',
  352. slot: 'discountRatio'
  353. },
  354. {
  355. minWidth: 120,
  356. prop: 'discountSinglePrice',
  357. label: '折让单价',
  358. align: 'center',
  359. },
  360. {
  361. minWidth: 120,
  362. prop: 'discountTotalPrice',
  363. label: '折让合计',
  364. align: 'center',
  365. },
  366. {
  367. minWidth: 100,
  368. prop: 'specification',
  369. label: '规格',
  370. align: 'center',
  371. showOverflowTooltip: true
  372. },
  373. {
  374. minWidth: 100,
  375. prop: 'modelType',
  376. label: '型号',
  377. align: 'center',
  378. showOverflowTooltip: true
  379. },
  380. {
  381. minWidth: 80,
  382. prop: 'color',
  383. label: '颜色',
  384. align: 'center',
  385. showOverflowTooltip: true
  386. },
  387. {
  388. minWidth: 120,
  389. prop: 'outInCount',
  390. label: '出库数量',
  391. align: 'center',
  392. },
  393. {
  394. minWidth: 120,
  395. prop: 'measuringUnit',
  396. label: '计量单位',
  397. align: 'center',
  398. },
  399. {
  400. minWidth: 120,
  401. prop: 'sendTotalWeight',
  402. label: '出库重量',
  403. align: 'center',
  404. },
  405. {
  406. minWidth: 120,
  407. prop: 'weightUnit',
  408. label: '重量单位',
  409. align: 'center',
  410. },
  411. {
  412. minWidth: 120,
  413. prop: 'pricingWay',
  414. label: '计价方式',
  415. align: 'center',
  416. formatter: (row, column) => {
  417. return pricingWayList.find((item) => item.id == row.pricingWay)
  418. ?.name;
  419. }
  420. },
  421. ]
  422. }
  423. },
  424. watch: {
  425. datasource: {
  426. handler(newVal, oldVal) {
  427. this.$nextTick(() => {
  428. // 直接基于原始 productList 扁平行初始化派生字段(单价/税率/折让比例默认值及本次对账金额)
  429. const list = (newVal || []).map((item) => this.buildRow(item, item.orderNo || '', false));
  430. this.flatList = list;
  431. console.log(this.flatList)
  432. this.calcTotals();
  433. });
  434. },
  435. immediate: true
  436. },
  437. queryDimension() {
  438. // this.buildFlatList();
  439. this.calcTotals();
  440. }
  441. },
  442. created() {
  443. parameterGetByCode({ code: 'eom_saleOrder_order-taxRate' }).then(
  444. (res) => {
  445. this.isTaxRate = res.value;
  446. }
  447. );
  448. },
  449. methods: {
  450. buildRow(item, orderNo, isReturn) {
  451. const outboundCount = +item.totalCount || 0;
  452. // 退货单数量为负
  453. const count = isReturn ? -Math.abs(outboundCount) : outboundCount;
  454. const statemented = +item.statementedCount || 0;
  455. // 发货单:本次对账数量=出库数量(不可改);调拨单:默认=出库数量(可改)
  456. let statementCount = item.statementCount;
  457. if (
  458. statementCount === undefined ||
  459. statementCount === null ||
  460. statementCount === ''
  461. ) {
  462. statementCount = count;
  463. }
  464. const singlePrice = +item.singlePrice;
  465. const discountRatio =
  466. item.discountRatio === undefined ||
  467. item.discountRatio === null ||
  468. item.discountRatio === ''
  469. ? 100
  470. : +item.discountRatio;
  471. // 税率非必填,可空值:undefined/null/空字符串 均保持空,不默认 0
  472. const taxRate =
  473. item.taxRate === undefined ||
  474. item.taxRate === null ||
  475. item.taxRate === ''
  476. ? ''
  477. : +item.taxRate;
  478. const amount =
  479. Math.round(
  480. statementCount * singlePrice * (discountRatio / 100) * 100
  481. ) / 100;
  482. const noTaxSinglePrice =
  483. singlePrice && taxRate
  484. ? +((singlePrice / (1 + taxRate / 100)).toFixed(4))
  485. : '';
  486. const totalPrice = Math.round(statementCount * singlePrice * 100) / 100;
  487. const discountSinglePrice =
  488. Math.round(singlePrice * (discountRatio / 100) * 100) / 100;
  489. const discountTotalPrice =
  490. Math.round(totalPrice * (discountRatio / 100) * 100) / 100;
  491. // 直接修改原 item,保证编辑同步回 datasource
  492. // this.$set(item, 'id', item.id || Math.random().toString(36).slice(2));
  493. this.$set(item, 'saleOrderNo', orderNo);
  494. this.$set(
  495. item,
  496. 'statementSubOrderNo',
  497. item.statementSubOrderNo || item.subOrderNo || ''
  498. );
  499. this.$set(
  500. item,
  501. 'productOperateTime',
  502. item.productOperateTime || item.deliveryDate || ''
  503. );
  504. this.$set(item, 'outboundCount', count);
  505. this.$set(item, 'statementedCount', statemented);
  506. this.$set(item, 'statementCount', statementCount);
  507. this.$set(item, 'singlePrice', singlePrice);
  508. this.$set(item, 'taxRate', taxRate);
  509. this.$set(item, 'discountRatio', discountRatio);
  510. this.$set(item, 'statementAmount', amount);
  511. this.$set(item, 'noTaxSinglePrice', noTaxSinglePrice);
  512. this.$set(item, 'totalPrice', totalPrice);
  513. this.$set(item, 'discountSinglePrice', discountSinglePrice);
  514. this.$set(item, 'discountTotalPrice', discountTotalPrice);
  515. this.$set(item, 'isReturn', isReturn);
  516. this.$set(item, 'processRouteName', item.processRouteName || '');
  517. return item;
  518. },
  519. initDefaultValues() {
  520. this.datasource.forEach((item) => {
  521. if (item.singlePrice === undefined)
  522. this.$set(item, 'singlePrice', '');
  523. if (item.taxRate === undefined) this.$set(item, 'taxRate', '');
  524. if (item.discountRatio === undefined)
  525. this.$set(item, 'discountRatio', 100);
  526. });
  527. },
  528. buildFlatList() {
  529. const list = [];
  530. // this.datasource.forEach((order) => {
  531. // const orderNo = order.orderNo || '';
  532. // (order.deliveryProducts || []).forEach((item) => {
  533. // list.push(this.buildRow(item, orderNo, false));
  534. // });
  535. // (order.returnProducts || []).forEach((item) => {
  536. // list.push(this.buildRow(item, orderNo, true));
  537. // });
  538. // });
  539. // this.flatList = list;
  540. },
  541. calcTotals() {
  542. const amount =
  543. this.flatList.reduce(
  544. (pre, cur) => pre + Math.round((+cur.statementAmount || 0) * 100),
  545. 0
  546. ) / 100;
  547. const count = this.flatList.reduce(
  548. (pre, cur) => pre + (+cur.statementCount || 0),
  549. 0
  550. );
  551. this.totalStatementAmount = amount;
  552. this.totalStatementCount = count;
  553. this.$emit('totalChange', amount);
  554. this.$emit('countChange', count);
  555. // 按销售订单编码(saleOrderNo)汇总本次对账金额,供应收信息同步
  556. this.emitStatementAmountChange();
  557. },
  558. // 按销售订单编码汇总本次对账金额并抛出
  559. emitStatementAmountChange() {
  560. const summaryMap = {};
  561. this.flatList.forEach((row) => {
  562. const key = row.saleOrderNo;
  563. if (!key) return;
  564. summaryMap[key] =
  565. Math.round(
  566. ((summaryMap[key] || 0) + (+row.statementAmount || 0)) * 100
  567. ) / 100;
  568. });
  569. this.$emit('statementAmountChange', summaryMap);
  570. },
  571. // 重算某一行金额(参考 inventoryTable.vue 价格计算链)
  572. calcRow(row) {
  573. const count = +row.statementCount;
  574. const price = +row.singlePrice;
  575. // 折让比例未填写时按 100(不打折)计,避免金额被算成 0
  576. const ratio = row.discountRatio === '' ||
  577. row.discountRatio === null ||
  578. row.discountRatio === undefined ||
  579. isNaN(+row.discountRatio)
  580. ? 100
  581. : +row.discountRatio;
  582. const taxRate = +row.taxRate;
  583. // 本次对账金额
  584. const amount = Math.round(count * price * (ratio / 100) * 100) / 100;
  585. this.$set(row, 'statementAmount', amount);
  586. // 不含税单价 = 单价 / (1 + 税率/100) (与 inventoryTable.getNotaxSinglePrice 一致)
  587. if (price && taxRate) {
  588. this.$set(
  589. row,
  590. 'noTaxSinglePrice',
  591. +((price / (1 + taxRate / 100)).toFixed(4))
  592. );
  593. } else {
  594. this.$set(row, 'noTaxSinglePrice', '');
  595. }
  596. // 合计 = 对账数量 * 单价
  597. this.$set(row, 'totalPrice', Math.round(count * price * 100) / 100);
  598. // 折让单价 = 单价 * 折让比例/100
  599. this.$set(
  600. row,
  601. 'discountSinglePrice',
  602. Math.round(price * (ratio / 100) * 100) / 100
  603. );
  604. // 折让合计 = 合计 * 折让比例/100
  605. this.$set(
  606. row,
  607. 'discountTotalPrice',
  608. Math.round(row.totalPrice * (ratio / 100) * 100) / 100
  609. );
  610. },
  611. handleCountChange(row, index) {
  612. this.calcRow(row);
  613. this.calcTotals();
  614. this.emitChange();
  615. },
  616. handlePriceChange(row, index) {
  617. this.calcRow(row);
  618. this.calcTotals();
  619. this.emitChange();
  620. },
  621. handleTaxChange(row, index) {
  622. // 税率变化需重算不含税单价、合计及折让相关字段
  623. this.calcRow(row);
  624. this.calcTotals();
  625. this.emitChange();
  626. },
  627. handleDiscountChange(row, index) {
  628. this.calcRow(row);
  629. this.calcTotals();
  630. this.emitChange();
  631. },
  632. emitChange() {
  633. // 表格实际编辑的是 flatList,直接以其为新数据源回写父组件
  634. this.$emit('update:datasource', [...this.flatList]);
  635. },
  636. // 单价校验:单价缺失或为 0 视为无效(需结合商品价目表有效期进一步校验)
  637. isPriceInvalid(row) {
  638. const price = +row.singlePrice || 0;
  639. return price <= 0;
  640. },
  641. validateStatementCount(rule, value, callback, row) {
  642. const val = +value || 0;
  643. const max = Math.abs(row.outboundCount || 0);
  644. if (Math.abs(val) > max) {
  645. callback(new Error('不能超出库数量'));
  646. }
  647. // else if (this.queryDimension == 1 && val != row.outboundCount) {
  648. // callback(new Error('按发货单查询时不可修改'));
  649. // }
  650. else {
  651. callback();
  652. }
  653. },
  654. // 批量设置
  655. batchSetTaxRate() {
  656. this.batchType = 'taxRate';
  657. this.batchLabel = '税率';
  658. this.batchValue = '';
  659. this.batchDialogVisible = true;
  660. },
  661. batchSetPrice() {
  662. this.batchType = 'singlePrice';
  663. this.batchLabel = '单价';
  664. this.batchValue = '';
  665. this.batchDialogVisible = true;
  666. },
  667. batchSetDiscount() {
  668. this.batchType = 'discountRatio';
  669. this.batchLabel = '折让比例';
  670. this.batchValue = '';
  671. this.batchDialogVisible = true;
  672. },
  673. // 批量设置:只作用于选中的行(selection)
  674. confirmBatchSet({ key, value }) {
  675. if (!this.selection.length) {
  676. this.$message.warning('请先勾选需要批量设置的行');
  677. this.batchDialogVisible = false;
  678. return;
  679. }
  680. this.selection.forEach((item) => {
  681. this.$set(item, key, +value);
  682. this.calcRow(item);
  683. });
  684. this.calcTotals();
  685. this.batchDialogVisible = false;
  686. this.emitChange();
  687. },
  688. removeRow(index) {
  689. // 调拨单模式下允许删除行,基于扁平行数组操作,保持数据格式一致
  690. if (index < 0 || index >= this.flatList.length) return;
  691. const list = [...this.flatList];
  692. list.splice(index, 1);
  693. this.flatList = list;
  694. this.calcTotals();
  695. this.emitChange();
  696. },
  697. getValidForm() {
  698. return new Promise((resolve, reject) => {
  699. this.$refs.detailForm.validate((valid, invalidFields) => {
  700. if (valid) resolve(true);
  701. else {
  702. this.$message.warning(this.buildValidateTip(invalidFields));
  703. reject(false);
  704. }
  705. });
  706. });
  707. },
  708. // 获取勾选的行数据(供父组件提交时使用)
  709. getSelection() {
  710. return this.selection;
  711. },
  712. // 校验勾选行的必填字段
  713. validateSelection() {
  714. return new Promise((resolve, reject) => {
  715. if (!this.selection.length) {
  716. this.$message.warning('请先勾选需要提交的对账明细');
  717. reject(false);
  718. return;
  719. }
  720. const fieldLabelMap = {
  721. statementCount: '本次对账数量',
  722. singlePrice: '单价',
  723. taxRate: '税率',
  724. discountRatio: '折让比例'
  725. };
  726. const tips = [];
  727. this.selection.forEach((row) => {
  728. // 本次对账数量必填
  729. if (row.statementCount === '' || row.statementCount == null) {
  730. tips.push(`「${fieldLabelMap.statementCount}」未填写`);
  731. }
  732. // 单价必填
  733. if (row.singlePrice === '' || row.singlePrice == null) {
  734. tips.push(`「${fieldLabelMap.singlePrice}」未填写`);
  735. }
  736. // 折让比例必填
  737. if (row.discountRatio === '' || row.discountRatio == null) {
  738. tips.push(`「${fieldLabelMap.discountRatio}」未填写`);
  739. }
  740. // 税率(isTaxRate == 1 时必填)
  741. if (this.isTaxRate == 1 && (row.taxRate === '' || row.taxRate == null)) {
  742. tips.push(`「${fieldLabelMap.taxRate}」未填写`);
  743. }
  744. });
  745. if (tips.length) {
  746. this.$message.warning('勾选的对账明细存在未填写项:' + tips.join('、'));
  747. reject(false);
  748. } else {
  749. resolve(true);
  750. }
  751. });
  752. },
  753. // 根据校验失败字段生成具体提示
  754. buildValidateTip(invalidFields) {
  755. const fieldLabelMap = {
  756. statementCount: '本次对账数量',
  757. singlePrice: '单价',
  758. taxRate: '税率',
  759. discountRatio: '折让比例'
  760. };
  761. const tips = [];
  762. Object.keys(invalidFields || {}).forEach((path) => {
  763. // path 形如 flatList.0.singlePrice
  764. const match = path.match(/flatList\.(\d+)\.(.+)/);
  765. if (match) {
  766. const rowIndex = +match[1] + 1;
  767. const field = match[2];
  768. const label = fieldLabelMap[field] || field;
  769. const msg =
  770. (invalidFields[path] && invalidFields[path][0] && invalidFields[path][0].message) ||
  771. '';
  772. tips.push(`第${rowIndex}行「${label}」${msg || '未填写'}`);
  773. } else {
  774. tips.push(path);
  775. }
  776. });
  777. return tips.length ? tips.join(';') : '对账明细未填写完整';
  778. },
  779. // 打开工艺路线选择弹窗(参考 inventoryTable.openVersion)
  780. openVersion(index) {
  781. this.$refs.versionRefs.open(index);
  782. },
  783. // 工艺路线选择回调(参考 inventoryTable.changeProduct)
  784. async changeProduct(data, index) {
  785. const row = this.flatList[index];
  786. if (!row) return;
  787. // 先回填工艺路线信息
  788. this.$set(row, 'technologyRouteName', data.name);
  789. this.$set(row, 'technologyRouteId', data.id);
  790. // 根据工艺路线带出默认单价
  791. if (row.goodsId) {
  792. await this.setDefaultPrice(row, data.id);
  793. }
  794. // 单价变化后重算金额与汇总
  795. this.calcRow(row);
  796. this.calcTotals();
  797. this.emitChange();
  798. },
  799. // 根据商品 + 工艺路线匹配默认单价(有效期内)
  800. async setDefaultPrice(row, technologyRouteId) {
  801. try {
  802. const goodsInfo = await getGoodsById(row.goodsId);
  803. const priceList = goodsInfo?.goodsPriceList || [];
  804. // 命中当前工艺路线,且为默认价、在有效期内的价目
  805. const defaultItem = priceList.find((item) => {
  806. if (item.technologyRouteId != technologyRouteId) return false;
  807. if (item.isDefault != 1) return false;
  808. const expiry = new Date(item.priceExpiryDate);
  809. return !isNaN(expiry.getTime()) && expiry >= new Date();
  810. });
  811. if (defaultItem) {
  812. this.$set(row, 'singlePrice', defaultItem.singlePrice);
  813. }
  814. } catch (e) {
  815. console.error('获取商品价目失败:', e);
  816. }
  817. }
  818. }
  819. };
  820. </script>
  821. <style scoped lang="scss">
  822. .account-detail-table {
  823. .detail-toolbar {
  824. display: flex;
  825. justify-content: flex-start;
  826. align-items: center;
  827. margin-bottom: 10px;
  828. .summary-info {
  829. span {
  830. margin-left: 20px;
  831. b {
  832. color: #f56c6c;
  833. }
  834. }
  835. }
  836. }
  837. .time-form .el-form-item {
  838. margin-bottom: 0 !important;
  839. }
  840. .price-warning {
  841. color: #f56c6c;
  842. font-size: 12px;
  843. line-height: 1.4;
  844. margin-top: 2px;
  845. }
  846. }
  847. </style>