collectionDialog.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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>应收金额:{{ row.receivableTotalPrice || 0 }} 元</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. receivedAmount() {
  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. // 未收金额 = 应收金额 - 已收金额
  145. unreceivedAmount() {
  146. const total = parseFloat(this.row.receivableTotalPrice) || 0;
  147. const received = parseFloat(this.receivedAmount) || 0;
  148. return (total - received).toFixed(2);
  149. },
  150. queryDimension() {
  151. return this.form.queryDimension != null ? this.form.queryDimension : 1;
  152. },
  153. columns() {
  154. let columnsVersion = this.columnsVersion;
  155. return [
  156. {
  157. width: 45,
  158. type: 'selection',
  159. columnKey: 'selection',
  160. align: 'center',
  161. fixed: 'left'
  162. },
  163. {
  164. width: 60,
  165. label: '序号',
  166. type: 'index',
  167. columnKey: 'index',
  168. align: 'center',
  169. fixed: 'left'
  170. },
  171. {
  172. minWidth: 140,
  173. prop: 'orderNo',
  174. label: this.queryDimension == 3 ? '采购订单编码' : '销售订单编码',
  175. align: 'center',
  176. showOverflowTooltip: true
  177. },
  178. {
  179. minWidth: 140,
  180. prop: 'statementSubOrderNo',
  181. label: this.queryDimension == 3 ? '收货单编码' : this.queryDimension == 1 ? '发货单编码' : '调拨单编码',
  182. align: 'center',
  183. showOverflowTooltip: true
  184. },
  185. {
  186. minWidth: 120,
  187. prop: 'productOperateTime',
  188. label: this.queryDimension == 3 ? '收货日期' : this.queryDimension == 1 ? '发货日期' : '调拨单日期',
  189. align: 'center',
  190. showOverflowTooltip: true
  191. },
  192. {
  193. minWidth: 120,
  194. prop: 'productCode',
  195. label: '产品编码',
  196. align: 'center',
  197. showOverflowTooltip: true
  198. },
  199. {
  200. minWidth: 140,
  201. prop: 'productName',
  202. label: '产品名称',
  203. align: 'center',
  204. showOverflowTooltip: true
  205. },
  206. {
  207. minWidth: 120,
  208. prop: 'batchNo',
  209. label: '批次号',
  210. align: 'center',
  211. showOverflowTooltip: true
  212. },
  213. {
  214. minWidth: 110,
  215. prop: 'historicalStatementCount',
  216. label: '已对账数量',
  217. align: 'center'
  218. },
  219. {
  220. minWidth: 140,
  221. prop: 'statementCount',
  222. label: '数量',
  223. align: 'center'
  224. },
  225. {
  226. minWidth: 120,
  227. headerSlot: 'headerRequired',
  228. prop: 'statementAmount',
  229. label: '本次已收金额',
  230. align: 'center',
  231. slot: 'statementAmount'
  232. },
  233. {
  234. minWidth: 120,
  235. prop: 'totalCount',
  236. label: this.queryDimension == 3 ? '收货数量' : this.queryDimension == 1 ? '发货数量' : '调拨数量',
  237. align: 'center'
  238. },
  239. {
  240. minWidth: 140,
  241. prop: 'technologyRouteName',
  242. label: '工艺路线',
  243. align: 'center',
  244. showOverflowTooltip: true
  245. },
  246. {
  247. minWidth: 120,
  248. prop: 'singlePrice',
  249. label: '单价',
  250. align: 'center'
  251. },
  252. {
  253. minWidth: 100,
  254. prop: 'taxRate',
  255. label: '税率',
  256. align: 'center'
  257. },
  258. {
  259. minWidth: 100,
  260. prop: 'noTaxSinglePrice',
  261. label: '不含税单价',
  262. align: 'center'
  263. },
  264. {
  265. minWidth: 100,
  266. prop: 'totalPrice',
  267. label: '合计',
  268. align: 'center'
  269. },
  270. {
  271. minWidth: 120,
  272. prop: 'discountRatio',
  273. label: '折让比例',
  274. align: 'center'
  275. },
  276. {
  277. minWidth: 120,
  278. prop: 'discountSinglePrice',
  279. label: '折让单价',
  280. align: 'center'
  281. },
  282. {
  283. minWidth: 120,
  284. prop: 'discountTotalPrice',
  285. label: '折让合计',
  286. align: 'center'
  287. },
  288. {
  289. minWidth: 100,
  290. prop: 'specification',
  291. label: '规格',
  292. align: 'center',
  293. showOverflowTooltip: true
  294. },
  295. {
  296. minWidth: 100,
  297. prop: 'modelType',
  298. label: '型号',
  299. align: 'center',
  300. showOverflowTooltip: true
  301. },
  302. {
  303. minWidth: 80,
  304. prop: 'color',
  305. label: '颜色',
  306. align: 'center',
  307. showOverflowTooltip: true
  308. },
  309. {
  310. minWidth: 120,
  311. prop: 'outInCount',
  312. label: '出库数量',
  313. align: 'center'
  314. },
  315. {
  316. minWidth: 120,
  317. prop: 'measuringUnit',
  318. label: '计量单位',
  319. align: 'center'
  320. },
  321. {
  322. minWidth: 120,
  323. prop: 'sendTotalWeight',
  324. label: '出库重量',
  325. align: 'center'
  326. },
  327. {
  328. minWidth: 120,
  329. prop: 'weightUnit',
  330. label: '重量单位',
  331. align: 'center'
  332. },
  333. {
  334. minWidth: 120,
  335. prop: 'pricingWay',
  336. label: '计价方式',
  337. align: 'center',
  338. formatter: (row, column) => {
  339. return pricingWayList.find((item) => item.id == row.pricingWay)?.name;
  340. }
  341. }
  342. ];
  343. }
  344. },
  345. data() {
  346. return {
  347. row: {},
  348. dialogType: '',
  349. title: '',
  350. loading: false,
  351. info: {},
  352. form: {
  353. id: '',
  354. queryDimension: 1,
  355. receivedDate: '',
  356. paymentType: '',
  357. files: [],
  358. remark: '',
  359. },
  360. rules: {},
  361. datasource: [],
  362. flatList: [],
  363. selection: [],
  364. totalAmount: 0,
  365. totalCount: 0,
  366. columnsVersion: 1
  367. }
  368. },
  369. watch: {
  370. datasource: {
  371. handler(newVal) {
  372. this.$nextTick(() => {
  373. const list = (newVal || []).map((item) => this.buildRow(item, item.orderNo || '', false));
  374. this.flatList = list;
  375. this.calcTotals();
  376. });
  377. },
  378. immediate: true
  379. }
  380. },
  381. created() {},
  382. methods: {
  383. // 初始化
  384. async open(row, type) {
  385. this.row = row || {};
  386. this.dialogType = type || 'update';
  387. this.title = this.dialogType === 'view' ? '收款详情' : '收款';
  388. await this.getInfo(row.id);
  389. },
  390. // 获取详情(收款专用接口:仅返回产品列表,不带表单主信息)
  391. async getInfo(id) {
  392. const data = await getProductListForReceiptAPI(id);
  393. // 新接口只返回产品列表
  394. this.datasource = Array.isArray(data) ? data : (data.productList || []);
  395. this.form = {
  396. ...this.form,
  397. id: data?.id || '',
  398. queryDimension: data?.queryDimension != null ? data.queryDimension : 1,
  399. receivedDate: data?.receivedDate || '',
  400. paymentType: data?.paymentType ?? '',
  401. files: data?.files || [],
  402. remark: data?.remark || ''
  403. };
  404. },
  405. // 构建单行派生字段
  406. buildRow(item, orderNo, isReturn) {
  407. const outboundCount = +item.totalCount || 0;
  408. const count = isReturn ? -Math.abs(outboundCount) : outboundCount;
  409. const statemented = +item.statementedCount || 0;
  410. let statementCount = item.statementCount;
  411. if (statementCount === undefined || statementCount === null || statementCount === '') {
  412. statementCount = count;
  413. }
  414. const singlePrice = +item.singlePrice;
  415. const discountRatio =
  416. item.discountRatio === undefined || item.discountRatio === null || item.discountRatio === ''
  417. ? 100
  418. : +item.discountRatio;
  419. const taxRate =
  420. item.taxRate === undefined || item.taxRate === null || item.taxRate === ''
  421. ? ''
  422. : +item.taxRate;
  423. const amount =
  424. Math.round(statementCount * singlePrice * (discountRatio / 100) * 100) / 100;
  425. const noTaxSinglePrice =
  426. singlePrice && taxRate ? +((singlePrice / (1 + taxRate / 100)).toFixed(4)) : '';
  427. const totalPrice = Math.round(statementCount * singlePrice * 100) / 100;
  428. const discountSinglePrice = Math.round(singlePrice * (discountRatio / 100) * 100) / 100;
  429. const discountTotalPrice = Math.round(totalPrice * (discountRatio / 100) * 100) / 100;
  430. this.$set(item, 'saleOrderNo', orderNo);
  431. this.$set(item, 'statementSubOrderNo', item.statementSubOrderNo || item.subOrderNo || '');
  432. this.$set(item, 'productOperateTime', item.productOperateTime || item.deliveryDate || '');
  433. this.$set(item, 'outboundCount', count);
  434. this.$set(item, 'statementedCount', statemented);
  435. this.$set(item, 'statementCount', statementCount);
  436. this.$set(item, 'singlePrice', singlePrice);
  437. this.$set(item, 'taxRate', taxRate);
  438. this.$set(item, 'discountRatio', discountRatio);
  439. this.$set(item, 'statementAmount', item?.receivedTotalPrice || amount);
  440. this.$set(item, 'noTaxSinglePrice', noTaxSinglePrice);
  441. this.$set(item, 'totalPrice', totalPrice);
  442. this.$set(item, 'discountSinglePrice', discountSinglePrice);
  443. this.$set(item, 'discountTotalPrice', discountTotalPrice);
  444. this.$set(item, 'isReturn', isReturn);
  445. return item;
  446. },
  447. // 汇总
  448. calcTotals() {
  449. const amount =
  450. this.flatList.reduce((pre, cur) => pre + Math.round((+cur.statementAmount || 0) * 100), 0) / 100;
  451. const count = this.flatList.reduce((pre, cur) => pre + (+cur.statementCount || 0), 0);
  452. this.totalAmount = amount;
  453. this.totalCount = count;
  454. },
  455. // 本次对账金额变化(唯一可编辑字段)
  456. handleAmountChange(row, index) {
  457. this.calcTotals();
  458. },
  459. // 获取勾选行数据
  460. getSelection() {
  461. return this.selection;
  462. },
  463. // 校验勾选行的本次对账金额必填
  464. validateSelection() {
  465. return new Promise((resolve, reject) => {
  466. if (!this.selection.length) {
  467. this.$message.warning('请先勾选需要提交的应收明细');
  468. reject(false);
  469. return;
  470. }
  471. const tips = [];
  472. this.selection.forEach((row, i) => {
  473. if (row.statementAmount === '' || row.statementAmount == null) {
  474. tips.push(`第${i + 1}行「本次对账金额」未填写`);
  475. }
  476. });
  477. if (tips.length) {
  478. this.$message.warning('勾选的应收明细存在未填写项:' + tips.join('、'));
  479. reject(false);
  480. } else {
  481. resolve(true);
  482. }
  483. });
  484. },
  485. async save() {
  486. // 1. 校验收款信息表单
  487. const formValid = await new Promise((resolve) => {
  488. this.$refs.form.validate((valid) => resolve(valid));
  489. });
  490. if (!formValid) {
  491. return this.$message.warning('请填写完整收款信息');
  492. }
  493. // 2. 获取勾选的应收明细并校验必填
  494. const selection = this.getSelection();
  495. if (!selection.length) {
  496. return this.$message.warning('请先勾选需要提交的应收明细');
  497. }
  498. const selectionValid = await this.validateSelection().catch(() => false);
  499. if (!selectionValid) return;
  500. this.loading = true;
  501. // 3. 提交:收款信息 + 勾选的应收明细(已收金额 = 勾选行本次对账金额之和)
  502. const params = {
  503. ...this.form,
  504. receivedTotalPrice: this.receivedAmount,
  505. receivableId: this.row.id,
  506. receivableCode: this.row.code,
  507. productList: selection
  508. };
  509. finReceivableSaveAPI(params)
  510. .then(() => {
  511. this.loading = false;
  512. this.$message.success('收款成功');
  513. this.cancel();
  514. this.done();
  515. })
  516. .catch(() => {
  517. this.loading = false;
  518. });
  519. },
  520. // 刷新主列表数据
  521. done() {
  522. this.$emit('reload');
  523. },
  524. // 关闭弹窗
  525. cancel() {
  526. this.$emit('update:collectionDialogFlag', false);
  527. }
  528. }
  529. }
  530. </script>
  531. <style scoped lang="scss">
  532. .detail-toolbar {
  533. display: flex;
  534. justify-content: flex-start;
  535. align-items: center;
  536. margin-bottom: 10px;
  537. }
  538. </style>