'use strict'; const assert = require('assert'); const _ = require('lodash'); const { ObjectId } = require('mongoose').Types; const { CrudService } = require('naf-framework-mongoose/lib/service'); const { BusinessError, ErrorCode } = require('naf-core').Error; class TransportService extends CrudService { constructor(ctx) { super(ctx, 'transport'); this.model = this.ctx.model.Transport; this.os = this.ctx.service.order.order; this.util = this.ctx.service.util.util; } async create(data) { const { goods, no, send_time } = data; // 需要将发走的货物找到其对应的订单,然后添加订单的发货数据记录 await this.os.sendGoods(goods, no, send_time); const res = await this.model.create(data); return res; } /** * 签收 * @param {Object} data 运输单数据 */ async sign(data) { const { id, sign_time } = data; const transport = await this.model.findOne({ _id: ObjectId(id) }); if (!transport) { throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到运输单信息'); } const { goods, no } = data; // 修改订单部分 await this.os.arriveGoods(goods, no); // 修改运输单部分 transport.status = '1'; if (sign_time) transport.sign_time = sign_time; return await transport.save(); } /** * 运输核算信息组织 * @param {Object} { ids, type } ids:运输单id集合; type:once=>单趟 ; car=> 单车 */ async calculate({ ids }) { // 获取要核算的运输列表 let tList = await this.model.find({ _id: ids.map(i => ObjectId(i)) }); if (tList.length > 0) tList = JSON.parse(JSON.stringify(tList)); const res = { inBill: await this.getInBill(tList), outBill: this.getOutBill(tList), }; // 计算合计 const count = {}; // 收入采用税后实收 count.im = _.round( res.inBill.reduce((p, n) => p + (n.sh_ss || 0), 0), 2 ); // 运输支出合计 count.om = _.round( res.outBill.reduce((p, n) => p + (n.money || 0), 0), 2 ); // 总计 count.total = _.round(count.im - count.om, 2); res.count = count; return res; } /** * 单车核算 * @param {Object} { car_no, start, end } 车id, 开始时间, 结束时间: 时间默认为发货时间 */ async carCalculate({ car_no, start, end }) { assert(start, '缺少时间范围-开始时间'); assert(end, '缺少时间范围-结束时间'); let query = { 'send_time@start': start, 'send_time@end': end, 'supplier.car_no': car_no, }; let tList = await this.query(query); if (tList.length > 0) tList = JSON.parse(JSON.stringify(tList)); const res = { inBill: await this.getInBill(tList), outBill: this.getOutBill(tList), }; // 日常维护部分 query = { 'date@start': start, 'date@end': end }; const daily = await this.ctx.service.car.daily.query({ car_no, ...query }); res.daily = daily; // 计算合计 const count = {}; // 收入采用税后实收 count.im = _.round( res.inBill.reduce((p, n) => p + (n.sh_ss || 0), 0), 2 ); // 运输支出合计 count.om = _.round( res.outBill.reduce((p, n) => p + (n.money || 0), 0), 2 ); // 日常支出 count.dm = _.round( res.daily.reduce((p, n) => p + (n.money || 0), 0), 2 ); // 总计 count.total = _.round(count.im - count.om - count.dm, 2); res.count = count; return res; } /** * 将所选的运输单的收入整合至一个列表 * @param {Array} list 运输列表 */ async getInBill(list) { // 原则,少查询,多操作 const split_ids = list.map(i => i.goods.map(g => g.split_id)).flat(); const orders = await this.ctx.model.Order.find({ 'split._id': split_ids.map(i => ObjectId(i)), }); list = list.map(i => { const { no, route } = i; i.goods = i.goods.map(g => { const order = orders.find(f => f.split.find(s => ObjectId(s._id).equals(g.split_id)) ); if (order) { g.order_no = _.get(order, 'order_no'); g.no = no; g.route = route; } return g; }); return i.goods; }); return _.flattenDeep(list); } /** * 将所选的运输单的运输支出整合至一个列表 * @param {Array} list 运输列表 */ getOutBill(list) { list = list.map(i => { const { no, route } = i; i.out_bill = i.out_bill.map(o => { o.no = no; o.route = route; return o; }); return i.out_bill; }); return _.flattenDeep(list); } /** * 核算准备导出 * @param {Object} { type, ...query } type:单车(car)/单趟(once); query:查询条件 */ async toExport({ type, ...query }) { let data = {}; if (type === 'car') data = await this.carCalculate(query); else data = await this.calculate(query); const res = await this.export({ data, ...query }); return res; } /** * 核算整理数据导出 * @param {Object} Object { data } data:数据 */ async export({ data, car_no }) { // console.log(data); // 获取车牌号 const car = await this.ctx.model.Car.findById(car_no); if (car) car_no = car.car_no; let arr = []; // 根据内容,计算标题单元格结束位置的字母 const keys = Object.keys(data); let eletter = ''; if (keys.includes('daily')) eletter = 'L'; else eletter = 'H'; const headObj = this.getHead(eletter); // 占 1-4行 arr.push(headObj); // 先拼count部分 const countArr = this.getCount(data, eletter); // 占 5-6行 arr.push(...countArr); // 各部分 // 收入部分 前4个(A-D), 运输支出部分中间4个(E-H),日常维护部分最后4个(I-L) let excelData = this.getExcelData(data); excelData = excelData.map(i => { const obj = { content: i }; return obj; }); arr.push(...excelData); // 文字位置样式,都加上 const text = { vertAlign: 'superscript', size: 18 }; const alignment = { vertical: 'middle', horizontal: 'center' }; arr = arr.map(i => { i.alignment = { ...alignment, ..._.get(i, alignment, {}) }; i.font = { ...text, ..._.get(i, 'font', {}) }; return i; }); const res = await this.ctx.service.util.excel.toExcel({ data: arr }); return res; } /** * 整理收入/支出数据 * @param {Object} data 核算结果集 */ getExcelData(data) { const inBill = _.get(data, 'inBill', []); const outBill = _.get(data, 'outBill', []); const daily = _.get(data, 'daily', []); const max = _.max([ inBill.length, outBill.length, daily.length ]); const lineSeven = { A7: '订单号', B7: '运输单号', C7: '货物', D7: '收入金额', E7: '运输单号', F7: '支出项目', G7: '支出金额', H7: '运输支出备注', I7: '维护项目', J7: '维护金额', K7: '维护日期', L7: '维护备注', }; // 默认第八行开始=>标题1-4行,合计5-6行,7行上面 const num = 8; const arr = []; arr.push(lineSeven); // 先取出收入(A-D),再去运输支出(E-H),再取日常支出(I-L) for (let i = 1; i <= max; i++) { const lineInBill = _.get(inBill, i - 1, {}); const lineOutBill = _.get(outBill, i - 1, {}); const lineDaily = _.get(daily, i - 1, {}); // console.log(lineInBill); // console.log(lineOutBill); // console.log(lineDaily); // 收入部分 const obj = {}; obj[`A${num - 1 + i}`] = _.get(lineInBill, 'order_no'); obj[`B${num - 1 + i}`] = _.get(lineInBill, 'no'); obj[`C${num - 1 + i}`] = _.get(lineInBill, 'name'); obj[`D${num - 1 + i}`] = _.get(lineInBill, 'sh_ss'); // 运输支出 obj[`E${num - 1 + i}`] = _.get(lineOutBill, 'no'); obj[`F${num - 1 + i}`] = _.get(lineOutBill, 'item'); obj[`G${num - 1 + i}`] = _.get(lineOutBill, 'money'); obj[`H${num - 1 + i}`] = _.get(lineOutBill, 'remark'); // 日常维护 obj[`I${num - 1 + i}`] = _.get(lineDaily, 'item'); obj[`J${num - 1 + i}`] = _.get(lineDaily, 'money'); obj[`K${num - 1 + i}`] = _.get(lineDaily, 'date'); obj[`L${num - 1 + i}`] = _.get(lineDaily, 'remark'); arr.push(obj); } return arr; } /** * 拼接count部分 * @param {Object} data 要导出的数据 * @param {String} letter 字母 */ getCount(data, letter) { const arr = []; const count = _.get(data, 'count'); if (!count) { throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到合计数据'); } // 总计行 if (count.total || count.total === 0) { const countTotal = { content: `总计:${count.total || 0}`, scell: 'A5', ecell: `${letter}5`, }; arr.push(countTotal); } // 各部分总计行, 每部分之占4列 // 收入 if (count.im || count.im === 0) { const countIn = { content: `收入${count.im || 0}`, scell: 'A6', ecell: 'D6', }; arr.push(countIn); } // 运输支出 if (count.om || count.om === 0) { const countOut = { content: `运输支出${count.om || 0}`, scell: 'E6', ecell: 'H6', }; arr.push(countOut); } // 日常支出 if (count.dm || count.dm === 0) { const countDaily = { content: `日常维护支出${count.om || 0}`, scell: 'I6', ecell: 'L6', }; arr.push(countDaily); } return arr; } /** * 获取核算标题 * @param {String} letter 字母 */ getHead(letter) { const headObj = { content: '核算单', scell: 'A1', ecell: `${letter}4`, font: { size: 22, bold: true }, alignment: { vertical: 'middle' }, }; return headObj; } /** * 供应商结算查询 * @param {Object} query 查询条件 */ async supplierCalculate(query) { query = this.util.turnDateRangeQuery(this.util.turnFilter(query)); const sclient = _.get(query, 'supplier.client'); const scar = _.get(query, 'supplier.car_no'); if (sclient && scar) { throw new BusinessError( ErrorCode.BUSINESS, '请只选择 供应商 或 第三方车号 方式之一进行查询' ); } if (!(sclient || scar)) { throw new BusinessError( ErrorCode.BUSINESS, '请至少选择 供应商 或 第三方车号 方式之一进行查询' ); } let list; list = await this.model.find({ ...query, is_js: false }); if (list.length > 0) list = JSON.parse(JSON.stringify(list)); // 供应商方式来的,列表中不显示过多的信息,主要是路线,订单号 const split_ids = _.flattenDeep( list.map(i => i.goods.map(ii => ObjectId(ii.split_id))) ); // 运输单列表的所有货物所在的订单列表 const orderList = await this.ctx.model.Order.find({ 'split._id': split_ids, }); // 进行匹配,拼接订单号 list = list.map(i => { const orders = orderList.filter(o => { const { split } = o; if (!(split && _.isArray(split))) return false; const r = split.find(og => i.goods.find(tg => ObjectId(tg.split_id).equals(og._id)) ); return r; }); i.order_nos = orders.map(i => i.order_no).join(';'); i.sh_ys = i.goods.reduce((p, n) => p + (n.sh_ys || 0), 0); i.sq_ys = i.goods.reduce((p, n) => p + (n.sq_ys || 0), 0); i.cost_item = '运费'; i.taxes = _.get(_.head(i.goods), 'taxes', '1'); return i; }); // 支出的钱有2部分: 货物(运费)+订单的该供应商的支出数据(该供应商的额外支出费用) // 还需要查出如果这个车只是某订单中的中间环节的支出 const orderQuery = _.omitBy(query, (value, key) => key.includes('supplier')); if (sclient) orderQuery['out_bill.client'] = _.get(query, 'supplier.client'); else orderQuery['out_bill.car_no'] = _.get(query, 'supplier.car_no'); let outOrderList = await this.ctx.model.Order.find(orderQuery); if (outOrderList.length > 0) outOrderList = JSON.parse(JSON.stringify(outOrderList)); let arr = []; for (const order of outOrderList) { const { order_no: order_nos, out_bill, _id: order_id } = order; let lr = out_bill.filter(f => { if (sclient) return f.client === orderQuery['out_bill.client']; return f.car_no === orderQuery['out_bill.car_no']; }); if (lr.length > 0) { // 结算的不要 lr = lr.filter(f => !f.is_js); lr = lr.map(i => ({ ...i, order_nos, order_id })); arr.push(lr); } } arr = _.flattenDeep(arr); list = [ ...list, ...arr ]; return list; } /** * 选择指定的支出,导出收入excel * excel的形式最后要拆到货物对应的供应商身上 * @param {Object} query 查询条件 * @property Array ids 订单id集合 */ async supplierExport(query) { console.log(query); const { ids, outOrderIds, ...params } = query; // 查出选择的运输单 let list = await this.model.find({ _id: ids.map(i => ObjectId(i)) }); if (list.length > 0) list = JSON.parse(JSON.stringify(list)); // 检查是否是供应商类型,如果是的话,需要将合同,客户名称 list = await this.checkSupplier(list); const split_ids = _.flattenDeep( list.map(i => i.goods.map(ii => ObjectId(ii.split_id))) ); // 运输单列表的所有货物所在的订单列表 const orderList = await this.ctx.model.Order.find({ 'split._id': split_ids, }); for (const i of list) { for (const g of i.goods) { for (const o of orderList) { const { split, order_no } = o; if (!(split && _.isArray(split))) return false; const r = split.find(og => i.goods.find(tg => ObjectId(tg.split_id).equals(og._id)) ); if (r) { g.order_no = order_no; break; } } } } // 以上整理完了运费相关的数据;下面该把选择的非运费收费项拿出来了 let outOrderList = await this.ctx.model.Order.find({ _id: outOrderIds.map(i => ObjectId(i)) }); if (outOrderList.length > 0) outOrderList = JSON.parse(JSON.stringify(outOrderList)); let orderOut = outOrderList.map(i => { const { out_bill, order_no } = i; const { client, car_no } = params; let l = out_bill.filter(f => { if (client) return f.client === client; return f.car_no === car_no; }); // 将客户这部分信息整合成一致的形式 if (l.length > 0) l = l.map(i => ({ ...i, order_no, supplier: { taxes: i.taxes } })); return l; }); orderOut = _.flattenDeep(orderOut); orderOut = await this.otherCheckSupplier(orderOut); // 数据整理完了,该拼了 const midData = this.setData(list, orderOut); const header = this.getHeader(); let pkeys = Object.keys(header); pkeys = pkeys.map(i => { const reg = /[^a-zA-Z]/i; const res = i.replace(reg, ''); return res; }); let lastData = midData; // this.toResetExcelData(pkeys, midData) lastData.push({ content: header }); const alignment = { vertical: 'middle', horizontal: 'center' }; lastData = lastData.map(i => ({ ...i, alignment })); // console.log(lastData); const res = await this.ctx.service.util.excel.toExcel({ data: lastData }); console.log(res); return res; } /** * 检查其他支出项是否是供应商类型,是的话就把信息转换了 * @param {Array} list 其他支出项 */ async otherCheckSupplier(list) { const is_supplier = list.every(e => e.supply_type === '1'); if (!is_supplier) return list; let rl = await this.dynamicData('client', list.map(i => i.client)); list = list.map(i => { const id = _.get(i, 'client'); if (!id) return i; const r = rl.find(f => ObjectId(f._id).equals(id)); if (r) i.supplier.client = r; return i; }); rl = await this.dynamicData('treaty', list.map(i => i.treaty)); list = list.map(i => { const id = _.get(i, 'treaty'); if (!id) return i; const r = rl.find(f => ObjectId(f._id).equals(id)); if (r) i.supplier.treaty = r; return i; }); rl = await this.dynamicData('item', list.map(i => i.item)); list = list.map(i => { const id = _.get(i, 'item'); if (!id) return i; const r = rl.find(f => ObjectId(f._id).equals(id)); if (r) i.supplier.item = r; return i; }); return list; } /** * 检查运输单是否是供应商类型,如果是供应商类型,就需要把供应商部分的信息转换过来 * @param {Array} list 运输单列表 */ async checkSupplier(list) { // 使用every是因为一定只选择一个供应商/车号的内容,所以这里其实多余判断 const is_supplier = list.every(e => e.supply_type === '1'); if (!is_supplier) return list; list = await this.dynamicGetSupplier('client', list); list = await this.dynamicGetSupplier('treaty', list); list = await this.dynamicGetSupplier('item', list); list = await this.dynamicGetSupplier('route', list); list = await this.dynamicGetSupplier('mode', list); return list; } /** * 动态查询供应商信息 * @param {String} type 表/字段名 * @param {Array} list 数据列表 */ async dynamicGetSupplier(type, list) { const toCheck = _.compact(list.map(i => ObjectId(_.get(i.supplier, type)))); if (toCheck.length <= 0) return list; const res = await this.dynamicData(type, toCheck); for (const i of list) { const { supplier } = i; if (!supplier) continue; const id = _.get(supplier, type); if (!id) continue; const r = await res.find(f => ObjectId(f._id).equals(id)); if (r) supplier[type] = r; } return list; } /** * 动态查询 * @param {String} type 表/字段名 * @param {Array} ids id数组 */ async dynamicData(type, ids) { let res = await this.ctx.model[`${_.capitalize(type)}`].find({ _id: ids, }); if (res.length > 0) res = JSON.parse(JSON.stringify(res)); return res; } /** * 整理导出字段 * @param {Array} transport 运输单数组 * @param {Array} orderOut 支出 */ setData(transport, orderOut) { let tarr = transport.map(i => { const { supplier, goods, route } = i; const list = goods.map(g => ({ ...g, supplier, route })); return list; }); tarr = _.flattenDeep(tarr); let list = [ ...tarr, ...orderOut ]; list = _.orderBy(list, [ 'order_no' ], [ 'asc' ]); // 给头预留的位置 list.unshift({}); const arr = []; for (let i = 1; i < list.length; i++) { const e = list[i]; const os = i + 1; const { supplier } = e; const content = {}; content[`A${os}`] = _.get(e, 'order_no'); // 订单号 // 客户部分 content[`B${os}`] = _.get(supplier.client, 'name'); content[`C${os}`] = _.get(supplier.client, 'address'); content[`D${os}`] = _.get(supplier.client, 'legal'); content[`E${os}`] = _.get(supplier.client, 'mobile'); content[`F${os}`] = _.get(supplier.client, 'taxes_no'); content[`G${os}`] = _.get(supplier.client, 'account_bank'); content[`H${os}`] = _.get(supplier.client, 'account'); // 合同部分 content[`I${os}`] = _.get(supplier.treaty, 'number'); content[`J${os}`] = _.get(supplier.treaty, 'jf'); content[`K${os}`] = _.get(supplier.treaty, 'yf'); content[`L${os}`] = _.get(supplier.treaty, 'period'); content[`M${os}`] = _.get(supplier.treaty, 'settle_up'); // 项目 content[`N${os}`] = _.get(supplier.item, 'name'); // 线路 content[`O${os}`] = _.get(e, 'route'); // 费用相关 content[`P${os}`] = _.get(e, 'name'); content[`Q${os}`] = _.get(e, 'cost_item', '运费'); content[`R${os}`] = _.get(e, 'taxes'); content[`S${os}`] = _.get(e, 'sq_ys'); content[`T${os}`] = _.get(e, 'sq_ss'); content[`U${os}`] = _.get(e, 'sh_ys'); content[`V${os}`] = _.get(e, 'sh_ss'); content[`W${os}`] = _.get(e, 'remark'); arr.push({ content }); } return arr; } /** * 合并处理数据 * @param {Array} keys 列字母数组 * @param {Array} data excel数据(不含头) */ toResetExcelData(keys, data) { const reg = /[^a-zA-Z]/i; const arr = []; const clear = []; console.log(keys); for (const key of keys) { // 找出每列的内容 const col = data.map(i => { const lks = Object.keys(i); const r = lks.find(f => { const rr = f.replace(reg, ''); return rr === key; }); if (r) return { key: r, value: _.get(i, r) }; }); // 同一列满足以下条件可以合并: // 1,值相同;2数字连贯;3指定范围内 // 先查范围 const head = _.head(col); if (!head) continue; const l = col.length; const ul = _.uniqBy(col, 'value').length; if (ul === l) continue; // 可以合并,需要重新拼个Object,{scell,ecell,content} // scell 是上面head的key, ecell是last获取key,content,随意拿出一个就行 const obj = {}; obj.scell = _.get(head, 'key'); obj.ecell = _.get(_.last(col), 'key'); obj.content = _.get(head, 'value'); clear.push(_.get(head, 'key'), _.get(_.last(col), 'key')); arr.push(obj); } // 将scell和ecell都干掉 data = data.map(i => { i = _.omitBy(i, (value, key) => { return clear.includes(key); }); return { content: i }; }); data = [ ...data, ...arr ]; return data; } /** * 获取导出的excel头 */ getHeader() { const obj = {}; obj.A1 = '订单编号'; // 客户部分 obj.B1 = '客户名称'; obj.C1 = '地址'; obj.D1 = '法人'; obj.E1 = '联系电话'; obj.F1 = '税号'; obj.G1 = '开户行'; obj.H1 = '银行账号'; // 合同部分 obj.I1 = '合同编号'; obj.J1 = '甲方'; obj.K1 = '乙方'; obj.L1 = '合同周期'; obj.M1 = '结算方式'; // 项目部分 obj.N1 = '项目名称'; // 线路部分 obj.O1 = '线路'; // 费用相关 obj.P1 = '货物'; obj.Q1 = '费用名称'; obj.R1 = '税率'; obj.S1 = '税前应收'; obj.T1 = '税前实收'; obj.U1 = '税后应收'; obj.V1 = '税后实收'; obj.W1 = '备注'; return obj; } /** * 供应商结算 * @param {Object} {ids, outOrderIds} 供应商要结算的运费,供应商给订单服务的支出结算 */ async js({ ids, outOrderIds, client, car_no, owner, ...info }) { assert(client, '缺少供应商信息'); assert(owner, '缺少创建人信息'); const params = { ids, outOrderIds, client, car_no, owner, info }; const bill = await this.ctx.model.Bill.create({ client: client || car_no, owner, ...info, params }); if (!bill) { throw new BusinessError(ErrorCode.DATABASE_FAULT, '结算单创建失败'); } await this.model.updateMany( { _id: ids.map(i => ObjectId(i)) }, { is_js: true } ); // 修改订单的支出状态,并记录 await this.ctx.service.order.order.outJs({ ids: outOrderIds, client, car_no }); } } module.exports = TransportService;