collectionDialog.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <ele-modal
  3. custom-class="ele-dialog-form long-dialog-form"
  4. :centered="true"
  5. :visible.sync="collectionDialogFlag"
  6. :title="title"
  7. :append-to-body="true"
  8. :close-on-click-modal="false"
  9. width="80%"
  10. :before-close="cancel"
  11. :maxable="true"
  12. :resizable="true"
  13. >
  14. <div style="margin: 10px 0; font-size: 14px; color: #606266;">
  15. <span>应收金额:{{ selectedReceivableAmount }} 元</span>
  16. <span style="margin-left: 20px;">已收金额:{{ receivedAmount }} 元</span>
  17. <span style="margin-left: 20px;">未收金额:{{ unreceivedAmount }} 元</span>
  18. </div>
  19. <el-form ref="form" :model="form" class="table-form" label-width="100px">
  20. <el-row :gutter="12">
  21. <el-col :span="8">
  22. <el-form-item
  23. label="收款时间"
  24. prop="receivedDate"
  25. :rules="{ required: true, message: '请选择收款时间', trigger: 'change' }"
  26. >
  27. <el-date-picker
  28. v-model="form.receivedDate"
  29. type="date"
  30. placeholder="年/月/日"
  31. value-format="yyyy-MM-dd"
  32. style="width: 100%"
  33. size="small"
  34. :disabled="isView"
  35. />
  36. </el-form-item>
  37. </el-col>
  38. <el-col :span="8">
  39. <el-form-item label="已收金额">
  40. <el-input
  41. :value="receivedAmount"
  42. placeholder="0"
  43. size="small"
  44. disabled
  45. />
  46. </el-form-item>
  47. </el-col>
  48. <el-col :span="8">
  49. <el-form-item label="支付类型" prop="paymentType" :rules="{ required: true, message: '请选择支付类型', trigger: 'change' }">
  50. <DictSelection
  51. v-model="form.paymentType"
  52. dictName="支付类型"
  53. placeholder="请选择"
  54. class="payment-select"
  55. :disabled="isView"
  56. />
  57. </el-form-item>
  58. </el-col>
  59. <el-col :span="16">
  60. <el-form-item label="备注" prop="remark">
  61. <el-input
  62. v-model="form.remark"
  63. placeholder="请输入"
  64. size="small"
  65. :disabled="isView"
  66. />
  67. </el-form-item>
  68. </el-col>
  69. <el-col :span="8">
  70. <el-form-item label="附件" prop="files">
  71. <fileMain v-model="form.files" :limit="1" :type="isView ? 'view' : 'add'"></fileMain>
  72. </el-form-item>
  73. </el-col>
  74. </el-row>
  75. </el-form>
  76. <headerTitle title="应收明细" style="margin-top: 30px"></headerTitle>
  77. <el-form ref="detailForm" :model="{ flatList }">
  78. <ele-pro-table
  79. ref="detailTable"
  80. row-key="id"
  81. :needPage="false"
  82. :columns="columns"
  83. max-height="500px"
  84. :datasource="flatList"
  85. :selection.sync="selection"
  86. class="time-form"
  87. >
  88. <!-- 本次对账金额:唯一可填写字段 -->
  89. <template v-slot:statementAmount="scope">
  90. <el-input
  91. v-model="scope.row.statementAmount"
  92. type="number"
  93. placeholder="请输入"
  94. size="small"
  95. :disabled="isView"
  96. @input="handleAmountChange(scope.row, scope.$index)"
  97. />
  98. </template>
  99. <template v-slot:headerRequired="{ column }">
  100. <span class="is-required">{{ column.label }}</span>
  101. </template>
  102. </ele-pro-table>
  103. </el-form>
  104. <div slot="footer">
  105. <el-button type="primary" @click="save()">保存</el-button>
  106. <el-button @click="cancel">返回</el-button>
  107. </div>
  108. </ele-modal>
  109. </template>
  110. <script>
  111. import { pricingWayList } from '@/enum/dict.js';
  112. import { finReceivableSaveAPI, getProductListForReceiptAPI } from "@/api/financialManage/receivableManage";
  113. export default {
  114. name: "collectionDialog",
  115. components: {},
  116. props: {
  117. collectionDialogFlag: {
  118. type: Boolean,
  119. default: false
  120. },
  121. // 汇总区总金额(如预收金额/应收金额)
  122. totalPrice: {
  123. type: [Number, String],
  124. default: 0
  125. },
  126. // 汇总区总金额标签(如"预收金额"/"应收金额")
  127. totalLabel: {
  128. type: String,
  129. default: '预收金额'
  130. }
  131. },
  132. computed: {
  133. isView() {
  134. return this.dialogType === 'view';
  135. },
  136. // 应收金额 = 勾选的应收明细「折让合计」之和
  137. selectedReceivableAmount() {
  138. const sum = this.selection.reduce((pre, item) => {
  139. const val = parseFloat(item.statementAmount);
  140. return pre + (isNaN(val) ? 0 : val);
  141. }, 0);
  142. return sum.toFixed(2);
  143. },
  144. // 已收金额 = 接口下发的 row.receivedTotalPrice
  145. receivedAmount() {
  146. return this.row.receivedTotalPrice || 0;
  147. },
  148. // 未收金额 = 接口下发的 row.unreceiveTotalPrice
  149. unreceivedAmount() {
  150. return this.row.unreceiveTotalPrice || 0;
  151. },
  152. queryDimension() {
  153. return this.form.queryDimension != null ? this.form.queryDimension : 1;
  154. },
  155. columns() {
  156. let columnsVersion = this.columnsVersion;
  157. return [
  158. {
  159. width: 45,
  160. type: 'selection',
  161. columnKey: 'selection',
  162. align: 'center',
  163. fixed: 'left'
  164. },
  165. {
  166. width: 60,
  167. label: '序号',
  168. type: 'index',
  169. columnKey: 'index',
  170. align: 'center',
  171. fixed: 'left'
  172. },
  173. {
  174. minWidth: 140,
  175. prop: 'orderNo',
  176. label: this.queryDimension == 3 ? '采购订单编码' : '销售订单编码',
  177. align: 'center',
  178. showOverflowTooltip: true
  179. },
  180. {
  181. minWidth: 140,
  182. prop: 'statementSubOrderNo',
  183. label: this.queryDimension == 3 ? '收货单编码' : this.queryDimension == 1 ? '发货单编码' : '调拨单编码',
  184. align: 'center',
  185. showOverflowTooltip: true
  186. },
  187. {
  188. minWidth: 120,
  189. prop: 'productOperateTime',
  190. label: this.queryDimension == 3 ? '收货日期' : this.queryDimension == 1 ? '发货日期' : '调拨单日期',
  191. align: 'center',
  192. showOverflowTooltip: true
  193. },
  194. {
  195. minWidth: 120,
  196. prop: 'productCode',
  197. label: '产品编码',
  198. align: 'center',
  199. showOverflowTooltip: true
  200. },
  201. {
  202. minWidth: 140,
  203. prop: 'productName',
  204. label: '产品名称',
  205. align: 'center',
  206. showOverflowTooltip: true
  207. },
  208. {
  209. minWidth: 120,
  210. prop: 'batchNo',
  211. label: '批次号',
  212. align: 'center',
  213. showOverflowTooltip: true
  214. },
  215. {
  216. minWidth: 110,
  217. prop: 'historicalStatementCount',
  218. label: '已对账数量',
  219. align: 'center'
  220. },
  221. {
  222. minWidth: 140,
  223. prop: 'statementCount',
  224. label: '数量',
  225. align: 'center'
  226. },
  227. {
  228. minWidth: 120,
  229. headerSlot: 'headerRequired',
  230. prop: 'statementAmount',
  231. label: '本次已收金额',
  232. align: 'center',
  233. slot: 'statementAmount'
  234. },
  235. {
  236. minWidth: 120,
  237. prop: 'totalCount',
  238. label: this.queryDimension == 3 ? '收货数量' : this.queryDimension == 1 ? '发货数量' : '调拨数量',
  239. align: 'center'
  240. },
  241. {
  242. minWidth: 140,
  243. prop: 'technologyRouteName',
  244. label: '工艺路线',
  245. align: 'center',
  246. showOverflowTooltip: true
  247. },
  248. {
  249. minWidth: 120,
  250. prop: 'singlePrice',
  251. label: '单价',
  252. align: 'center'
  253. },
  254. {
  255. minWidth: 100,
  256. prop: 'taxRate',
  257. label: '税率',
  258. align: 'center'
  259. },
  260. {
  261. minWidth: 100,
  262. prop: 'noTaxSinglePrice',
  263. label: '不含税单价',
  264. align: 'center'
  265. },
  266. {
  267. minWidth: 100,
  268. prop: 'totalPrice',
  269. label: '合计',
  270. align: 'center'
  271. },
  272. {
  273. minWidth: 120,
  274. prop: 'discountRatio',
  275. label: '折让比例',
  276. align: 'center'
  277. },
  278. {
  279. minWidth: 120,
  280. prop: 'discountSinglePrice',
  281. label: '折让单价',
  282. align: 'center'
  283. },
  284. {
  285. minWidth: 120,
  286. prop: 'discountTotalPrice',
  287. label: '折让合计',
  288. align: 'center'
  289. },
  290. {
  291. minWidth: 100,
  292. prop: 'specification',
  293. label: '规格',
  294. align: 'center',
  295. showOverflowTooltip: true
  296. },
  297. {
  298. minWidth: 100,
  299. prop: 'modelType',
  300. label: '型号',
  301. align: 'center',
  302. showOverflowTooltip: true
  303. },
  304. {
  305. minWidth: 80,
  306. prop: 'color',
  307. label: '颜色',
  308. align: 'center',
  309. showOverflowTooltip: true
  310. },
  311. {
  312. minWidth: 120,
  313. prop: 'outInCount',
  314. label: '出库数量',
  315. align: 'center'
  316. },
  317. {
  318. minWidth: 120,
  319. prop: 'measuringUnit',
  320. label: '计量单位',
  321. align: 'center'
  322. },
  323. {
  324. minWidth: 120,
  325. prop: 'sendTotalWeight',
  326. label: '出库重量',
  327. align: 'center'
  328. },
  329. {
  330. minWidth: 120,
  331. prop: 'weightUnit',
  332. label: '重量单位',
  333. align: 'center'
  334. },
  335. {
  336. minWidth: 120,
  337. prop: 'pricingWay',
  338. label: '计价方式',
  339. align: 'center',
  340. formatter: (row, column) => {
  341. return pricingWayList.find((item) => item.id == row.pricingWay)?.name;
  342. }
  343. }
  344. ];
  345. }
  346. },
  347. data() {
  348. return {
  349. row: {},
  350. dialogType: '',
  351. title: '',
  352. loading: false,
  353. info: {},
  354. form: {
  355. id: '',
  356. queryDimension: 1,
  357. receivedDate: '',
  358. paymentType: '',
  359. files: [],
  360. remark: '',
  361. },
  362. rules: {},
  363. datasource: [],
  364. flatList: [],
  365. selection: [],
  366. totalAmount: 0,
  367. totalCount: 0,
  368. columnsVersion: 1
  369. }
  370. },
  371. watch: {
  372. datasource: {
  373. handler(newVal) {
  374. this.$nextTick(() => {
  375. const list = (newVal || []).map((item) => this.buildRow(item, item.orderNo || '', false));
  376. this.flatList = list;
  377. this.calcTotals();
  378. });
  379. },
  380. immediate: true
  381. }
  382. },
  383. created() {},
  384. methods: {
  385. // 初始化
  386. async open(row, type) {
  387. this.row = row || {};
  388. this.dialogType = type || 'update';
  389. this.title = this.dialogType === 'view' ? '收款详情' : '收款';
  390. await this.getInfo(row.id);
  391. },
  392. // 获取详情(收款专用接口:仅返回产品列表,不带表单主信息)
  393. async getInfo(id) {
  394. const data = await getProductListForReceiptAPI(id);
  395. // 新接口只返回产品列表
  396. this.datasource = Array.isArray(data) ? data : (data.productList || []);
  397. // 把接口返回的已收/未收金额回填到 row,供顶部汇总和表单展示
  398. this.row = {
  399. ...this.row,
  400. receivedTotalPrice: data?.receivedTotalPrice ?? this.row.receivedTotalPrice ?? 0,
  401. unreceiveTotalPrice: data?.unreceiveTotalPrice ?? this.row.unreceiveTotalPrice ?? 0
  402. };
  403. this.form = {
  404. ...this.form,
  405. id: data?.id || '',
  406. queryDimension: data?.queryDimension != null ? data.queryDimension : 1,
  407. receivedDate: data?.receivedDate || '',
  408. paymentType: data?.paymentType ?? '',
  409. files: data?.files || [],
  410. remark: data?.remark || ''
  411. };
  412. },
  413. // 构建单行派生字段
  414. buildRow(item, orderNo, isReturn) {
  415. const outboundCount = +item.totalCount || 0;
  416. const count = isReturn ? -Math.abs(outboundCount) : outboundCount;
  417. const statemented = +item.statementedCount || 0;
  418. let statementCount = item.statementCount;
  419. if (statementCount === undefined || statementCount === null || statementCount === '') {
  420. statementCount = count;
  421. }
  422. const singlePrice = +item.singlePrice;
  423. const discountRatio =
  424. item.discountRatio === undefined || item.discountRatio === null || item.discountRatio === ''
  425. ? 100
  426. : +item.discountRatio;
  427. const taxRate =
  428. item.taxRate === undefined || item.taxRate === null || item.taxRate === ''
  429. ? ''
  430. : +item.taxRate;
  431. const amount =
  432. Math.round(statementCount * singlePrice * (discountRatio / 100) * 100) / 100;
  433. const noTaxSinglePrice =
  434. singlePrice && taxRate ? +((singlePrice / (1 + taxRate / 100)).toFixed(4)) : '';
  435. const totalPrice = Math.round(statementCount * singlePrice * 100) / 100;
  436. const discountSinglePrice = Math.round(singlePrice * (discountRatio / 100) * 100) / 100;
  437. const discountTotalPrice = Math.round(totalPrice * (discountRatio / 100) * 100) / 100;
  438. this.$set(item, 'saleOrderNo', orderNo);
  439. this.$set(item, 'statementSubOrderNo', item.statementSubOrderNo || item.subOrderNo || '');
  440. this.$set(item, 'productOperateTime', item.productOperateTime || item.deliveryDate || '');
  441. this.$set(item, 'outboundCount', count);
  442. this.$set(item, 'statementedCount', statemented);
  443. this.$set(item, 'statementCount', statementCount);
  444. this.$set(item, 'singlePrice', singlePrice);
  445. this.$set(item, 'taxRate', taxRate);
  446. this.$set(item, 'discountRatio', discountRatio);
  447. this.$set(item, 'statementAmount', item?.receivedTotalPrice || amount);
  448. this.$set(item, 'noTaxSinglePrice', noTaxSinglePrice);
  449. this.$set(item, 'totalPrice', totalPrice);
  450. this.$set(item, 'discountSinglePrice', discountSinglePrice);
  451. this.$set(item, 'discountTotalPrice', discountTotalPrice);
  452. this.$set(item, 'isReturn', isReturn);
  453. return item;
  454. },
  455. // 汇总
  456. calcTotals() {
  457. const amount =
  458. this.flatList.reduce((pre, cur) => pre + Math.round((+cur.statementAmount || 0) * 100), 0) / 100;
  459. const count = this.flatList.reduce((pre, cur) => pre + (+cur.statementCount || 0), 0);
  460. this.totalAmount = amount;
  461. this.totalCount = count;
  462. },
  463. // 本次对账金额变化(唯一可编辑字段)
  464. handleAmountChange(row, index) {
  465. this.calcTotals();
  466. },
  467. // 获取勾选行数据
  468. getSelection() {
  469. return this.selection;
  470. },
  471. // 校验勾选行的本次对账金额必填
  472. validateSelection() {
  473. return new Promise((resolve, reject) => {
  474. if (!this.selection.length) {
  475. this.$message.warning('请先勾选需要提交的应收明细');
  476. reject(false);
  477. return;
  478. }
  479. const tips = [];
  480. this.selection.forEach((row, i) => {
  481. if (row.statementAmount === '' || row.statementAmount == null) {
  482. tips.push(`第${i + 1}行「本次对账金额」未填写`);
  483. }
  484. });
  485. if (tips.length) {
  486. this.$message.warning('勾选的应收明细存在未填写项:' + tips.join('、'));
  487. reject(false);
  488. } else {
  489. resolve(true);
  490. }
  491. });
  492. },
  493. async save() {
  494. // 1. 校验收款信息表单
  495. const formValid = await new Promise((resolve) => {
  496. this.$refs.form.validate((valid) => resolve(valid));
  497. });
  498. if (!formValid) {
  499. return this.$message.warning('请填写完整收款信息');
  500. }
  501. // 2. 获取勾选的应收明细并校验必填
  502. const selection = this.getSelection();
  503. if (!selection.length) {
  504. return this.$message.warning('请先勾选需要提交的应收明细');
  505. }
  506. const selectionValid = await this.validateSelection().catch(() => false);
  507. if (!selectionValid) return;
  508. this.loading = true;
  509. // 3. 提交:收款信息 + 勾选的应收明细(已收金额 = 勾选行本次对账金额之和)
  510. const statementAmount = this.selection.reduce(
  511. (pre, item) => pre + (+item.statementAmount || 0),
  512. 0
  513. );
  514. // productList 里每一行需要补一个 receivedTotalPrice 字段(值取本次录入的 statementAmount)
  515. const productList = this.selection.map((item) => ({
  516. ...item,
  517. receivedTotalPrice: +(item.statementAmount || 0)
  518. }));
  519. const params = {
  520. ...this.form,
  521. statementAmount,
  522. receivableTotalPrice: Number(this.selectedReceivableAmount), // 应收金额(勾选行折让合计之和)
  523. receivedTotalPrice: this.row.receivedTotalPrice, // 历史已收总额(接口下发)
  524. unreceiveTotalPrice: this.row.unreceiveTotalPrice, // 历史未收金额(接口下发)
  525. receivableId: this.row.id,
  526. receivableCode: this.row.code,
  527. productList
  528. };
  529. finReceivableSaveAPI(params)
  530. .then(() => {
  531. this.loading = false;
  532. this.$message.success('收款成功');
  533. this.cancel();
  534. this.done();
  535. })
  536. .catch(() => {
  537. this.loading = false;
  538. });
  539. },
  540. // 刷新主列表数据
  541. done() {
  542. this.$emit('reload');
  543. },
  544. // 关闭弹窗
  545. cancel() {
  546. this.$emit('update:collectionDialogFlag', false);
  547. }
  548. }
  549. }
  550. </script>
  551. <style scoped lang="scss">
  552. .detail-toolbar {
  553. display: flex;
  554. justify-content: flex-start;
  555. align-items: center;
  556. margin-bottom: 10px;
  557. }
  558. </style>