collectionDialog.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  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, getFinReceivableInfoAPI } 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 getFinReceivableInfoAPI(id);
  393. this.datasource = data.productList || [];
  394. this.form = {
  395. ...this.form,
  396. id: data.id || '',
  397. queryDimension: data.queryDimension != null ? data.queryDimension : 1,
  398. receivedDate: data.receivedDate || '',
  399. paymentType: data.paymentType ?? '',
  400. files: data.files || [],
  401. remark: data.remark || ''
  402. };
  403. },
  404. // 构建单行派生字段
  405. buildRow(item, orderNo, isReturn) {
  406. const outboundCount = +item.totalCount || 0;
  407. const count = isReturn ? -Math.abs(outboundCount) : outboundCount;
  408. const statemented = +item.statementedCount || 0;
  409. let statementCount = item.statementCount;
  410. if (statementCount === undefined || statementCount === null || statementCount === '') {
  411. statementCount = count;
  412. }
  413. const singlePrice = +item.singlePrice;
  414. const discountRatio =
  415. item.discountRatio === undefined || item.discountRatio === null || item.discountRatio === ''
  416. ? 100
  417. : +item.discountRatio;
  418. const taxRate =
  419. item.taxRate === undefined || item.taxRate === null || item.taxRate === ''
  420. ? ''
  421. : +item.taxRate;
  422. const amount =
  423. Math.round(statementCount * singlePrice * (discountRatio / 100) * 100) / 100;
  424. const noTaxSinglePrice =
  425. singlePrice && taxRate ? +((singlePrice / (1 + taxRate / 100)).toFixed(4)) : '';
  426. const totalPrice = Math.round(statementCount * singlePrice * 100) / 100;
  427. const discountSinglePrice = Math.round(singlePrice * (discountRatio / 100) * 100) / 100;
  428. const discountTotalPrice = Math.round(totalPrice * (discountRatio / 100) * 100) / 100;
  429. this.$set(item, 'saleOrderNo', orderNo);
  430. this.$set(item, 'statementSubOrderNo', item.statementSubOrderNo || item.subOrderNo || '');
  431. this.$set(item, 'productOperateTime', item.productOperateTime || item.deliveryDate || '');
  432. this.$set(item, 'outboundCount', count);
  433. this.$set(item, 'statementedCount', statemented);
  434. this.$set(item, 'statementCount', statementCount);
  435. this.$set(item, 'singlePrice', singlePrice);
  436. this.$set(item, 'taxRate', taxRate);
  437. this.$set(item, 'discountRatio', discountRatio);
  438. this.$set(item, 'statementAmount', item?.receivedTotalPrice || amount);
  439. this.$set(item, 'noTaxSinglePrice', noTaxSinglePrice);
  440. this.$set(item, 'totalPrice', totalPrice);
  441. this.$set(item, 'discountSinglePrice', discountSinglePrice);
  442. this.$set(item, 'discountTotalPrice', discountTotalPrice);
  443. this.$set(item, 'isReturn', isReturn);
  444. return item;
  445. },
  446. // 汇总
  447. calcTotals() {
  448. const amount =
  449. this.flatList.reduce((pre, cur) => pre + Math.round((+cur.statementAmount || 0) * 100), 0) / 100;
  450. const count = this.flatList.reduce((pre, cur) => pre + (+cur.statementCount || 0), 0);
  451. this.totalAmount = amount;
  452. this.totalCount = count;
  453. },
  454. // 本次对账金额变化(唯一可编辑字段)
  455. handleAmountChange(row, index) {
  456. this.calcTotals();
  457. },
  458. // 获取勾选行数据
  459. getSelection() {
  460. return this.selection;
  461. },
  462. // 校验勾选行的本次对账金额必填
  463. validateSelection() {
  464. return new Promise((resolve, reject) => {
  465. if (!this.selection.length) {
  466. this.$message.warning('请先勾选需要提交的应收明细');
  467. reject(false);
  468. return;
  469. }
  470. const tips = [];
  471. this.selection.forEach((row, i) => {
  472. if (row.statementAmount === '' || row.statementAmount == null) {
  473. tips.push(`第${i + 1}行「本次对账金额」未填写`);
  474. }
  475. });
  476. if (tips.length) {
  477. this.$message.warning('勾选的应收明细存在未填写项:' + tips.join('、'));
  478. reject(false);
  479. } else {
  480. resolve(true);
  481. }
  482. });
  483. },
  484. async save() {
  485. // 1. 校验收款信息表单
  486. const formValid = await new Promise((resolve) => {
  487. this.$refs.form.validate((valid) => resolve(valid));
  488. });
  489. if (!formValid) {
  490. return this.$message.warning('请填写完整收款信息');
  491. }
  492. // 2. 获取勾选的应收明细并校验必填
  493. const selection = this.getSelection();
  494. if (!selection.length) {
  495. return this.$message.warning('请先勾选需要提交的应收明细');
  496. }
  497. const selectionValid = await this.validateSelection().catch(() => false);
  498. if (!selectionValid) return;
  499. this.loading = true;
  500. // 3. 提交:收款信息 + 勾选的应收明细(已收金额 = 勾选行本次对账金额之和)
  501. const params = {
  502. ...this.form,
  503. receivedTotalPrice: this.receivedAmount,
  504. receivableId: this.row.id,
  505. receivableCode: this.row.code,
  506. productList: selection
  507. };
  508. finReceivableSaveAPI(params)
  509. .then(() => {
  510. this.loading = false;
  511. this.$message.success('收款成功');
  512. this.cancel();
  513. this.done();
  514. })
  515. .catch(() => {
  516. this.loading = false;
  517. });
  518. },
  519. // 刷新主列表数据
  520. done() {
  521. this.$emit('reload');
  522. },
  523. // 关闭弹窗
  524. cancel() {
  525. this.$emit('update:collectionDialogFlag', false);
  526. }
  527. }
  528. }
  529. </script>
  530. <style scoped lang="scss">
  531. .detail-toolbar {
  532. display: flex;
  533. justify-content: flex-start;
  534. align-items: center;
  535. margin-bottom: 10px;
  536. }
  537. </style>