'use strict'; const _ = require('lodash'); const assert = require('assert'); const { ObjectId } = require('mongoose').Types; const { CrudService } = require('naf-framework-mongoose/lib/service'); const { BusinessError, ErrorCode } = require('naf-core').Error; class OrderService extends CrudService { constructor(ctx) { super(ctx, 'order'); this.model = this.ctx.model.Order; this.util = this.ctx.service.util.util; } /** * 新添订单 * @param {Object} data 订单数据 */ async create(data) { const res = await this.model.create(data); // 复制进split中 if (!res) throw new BusinessError(ErrorCode.SERVICE_FAULT, '订单创建失败'); await this.copyToSplit(res._id); try { this.record(res._id, { method: 'create' }); } catch (error) { this.logger.error(`订单id:${res.id}记录创建失败:${error.toString()}`); } return res; } /** * 修改订单 * @param {Object} { id, ...data } 要修改的订单数据 */ async update({ id, ...data }) { const res = await this.model.findById(id); const { goods: newGoods, ...info } = data; let { goods: oldGoods, split } = res; if (oldGoods) oldGoods = JSON.parse(JSON.stringify(oldGoods)); if (split) split = JSON.parse(JSON.stringify(split)); // 找到删除项 // oldGoods中有,但是newGoods中却没有的数据,就是要删除的;且需要检验被删除的数据有没有被拆分 oldGoods = oldGoods.map(og => { const need_delete = newGoods.find(f => ObjectId(f._id).equals(og._id)); if (!need_delete) { const sobj = split.find(f => ObjectId(f.pid).equals(og._id)); if (sobj) { // 查找其有没有被拆分 const r = split.find(f => ObjectId(f.pid).equals(sobj._id)); if (r) { throw new BusinessError( ErrorCode.DATA_INVALID, '无法删除已被拆分过的货物' ); } const s_index = split.findIndex(f => ObjectId(f.pid).equals(og._id) ); split.splice(s_index, 1); } return undefined; } return og; }); oldGoods = _.compact(oldGoods); // 判断是否有修改项 const updateRes = await this.goodsUpdate(oldGoods, newGoods, split); if (updateRes) { const { oldGoods: ogs, split: splist } = updateRes; if (ogs) oldGoods = ogs; if (splist) split = splist; } // 判断有没有新添的数据 const addGoods = newGoods.filter(f => !f._id); if (addGoods.length > 0) { // 有新增货物项 // TODO,复制到split中和oldGoods中 for (const ng of addGoods) { oldGoods.push(ng); } } res.goods = oldGoods; res.split = split; await this.model.update({ _id: ObjectId(id) }, info); await res.save(); this.copyToSplit(id); try { this.record(res._id, { method: 'update' }); } catch (error) { this.logger.error(`订单id:${res.id}记录创建失败:${error.toString()}`); } return; } /** * 检查并处理有修改的货物:拆分/发车就不允许修改了 * @param {Array} oldGoods 查出修改前的货物列表 * @param {Array} newGoods 修改后的货物列表 * @param {Array} split 原拆分货物列表,用来检查是否可以修改 */ async goodsUpdate(oldGoods, newGoods, split) { oldGoods = oldGoods.map(og => { const is_split = split.find( f => ObjectId(f.pid).equals(og._id) && f.type === '1' && f.status === '0' ); if (is_split) return og; // 没有拆分,可以直接更换 const ng = newGoods.find(f => ObjectId(f._id).equals(og._id)); if (ng) return { ...ng, update: true }; return og; }); // 修改拆分货物列表 const toUpdateSplit = oldGoods.filter(f => f.update); split = split.map(i => { const res = toUpdateSplit.find(f => ObjectId(f._id).equals(i.pid)); if (res) { const obj = this.goodsCopy(res); return obj; } return i; }); // 将oldGoods的update摘出去 oldGoods = oldGoods.map(i => _.omit(i, [ 'update' ])); return { oldGoods, split }; } /** * 将货物复制成拆分货物的数据并返回 * @param {Object} data 货物列表的每项 * @return split */ goodsCopy(data) { const split = _.pick(data, [ 'name', 'number', 'weight', 'volume', 'transport_type', 'remark', ]); split.pid = data._id; return split; } /** * 根据订单id,复制货物 到 拆分货物,只是复制,其他操作在各自的方法中,这里只是复制 * @param {String} id 订单id */ async copyToSplit(id) { const order = await this.model.findById(id); if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单'); const { goods, split } = order; for (const g of goods) { const has = split.find(f => ObjectId(f.pid).equals(g._id)); if (!has) { order.split.push(this.goodsCopy(g)); } } await order.save(); } /** * 发货,添加记录及修改状态 * @param {Array} goods 发货的货物列表 * @param {String} no 运输单号 * @param {String} time 发货日期 */ async sendGoods(goods, no, time) { for (const g of goods) { const { split_id, name, number, weight, volume } = g; const order = await this.model.findOne({ 'split._id': ObjectId(split_id), }); if (!order) { throw new BusinessError( ErrorCode.DATA_NOT_EXIST, `未找到该${name}所在的订单信息` ); } const obj = { split_id, name, number, weight, volume, no, time }; // 添加该货物的发货记录 order.send_time.push(obj); // 修改该货物的状态 const good = order.split.id(split_id); good.status = '1'; order.goods_status = await this.checkGoodsStatus(order.split); await this.model.update({ _id: order._id }, order); // 更新记录 let message = `${name}已发货(数量:${number};重量:${weight}吨;体积:${volume}m³)`; const all_send = order.split.every(e => e.status === '0'); if (all_send) message = `${message}; 所有货物已发出`; try { this.record(order._id, { method: 'send', message }); } catch (error) { this.logger.error(`订单id:${order.id}记录创建失败:${error.toString()}`); } } } /** * 签收,添加记录及修改状态 * @param {Array} goods 签收的货物列表 * @param {String} no 运输单号 * @param {String} time 签收时间 */ async arriveGoods(goods, no, time) { for (const g of goods) { const { split_id, name, number, weight, volume } = g; const order = await this.model.findOne({ 'split._id': ObjectId(split_id), }); if (!order) { throw new BusinessError( ErrorCode.DATA_NOT_EXIST, `未找到该${name}所在的订单信息` ); } const obj = { split_id, name, number, weight, volume, no }; if (time) obj.time = time; // 添加收货记录 order.arrive_time.push(obj); // 修改该货物的状态 const good = order.split.id(split_id); good.status = '-1'; order.goods_status = await this.checkGoodsStatus(order.split); await this.model.update({ _id: order._id }, order); // 更新记录 let message = `${name}已签收(数量:${number};重量:${weight}吨;体积:${volume}m³)`; const all_arrive = order.split.every(e => e.status === '-1'); if (all_arrive) message = `${message}; 所有货物已签收`; try { this.record(order._id, { method: 'arrive', message }); } catch (error) { this.logger.error(`订单id:${order.id}记录创建失败:${error.toString()}`); } } } /** * 检查拆分货物列表,更新goods_status * @param {Array} splitList 拆分货物列表 */ async checkGoodsStatus(splitList) { // 未发车 const res = splitList.every(e => e.status === '0'); if (res) return '未发货'; // 检查是否全发车的货物 const all_send = splitList.every(e => e.status === '1'); if (all_send) return '所有货物已发出'; // 检查是否全到达了 const all_arrive = splitList.every(e => e.status === '-1'); if (all_arrive) return '所有货物全部到达'; // 检查是否有发货的 const is_send = splitList.some(e => e.status === '1'); // 检查是否有到达的 const is_arrive = splitList.some(e => e.status === '-1'); const word = []; if (is_send) word.push('部分货物已发出'); if (is_arrive) word.push('部分货物已到达'); if (word.length > 0) return word.join(';'); return '状态错误'; } /** * 修改订单负责人 * @param {Object} data 订单数据 */ async principalChange(data) { const { principal, _id } = data; const res = await this.model.update({ _id: ObjectId(_id) }, { principal }); try { // 跨库查询该用户姓名 const user = await this.ctx.service.util.httpUtil.cget( `/user/${principal}`, 'userAuth', { _tenant: 'zhwl' } ); const message = `变更订单负责人 ${user.name}`; this.record(_id, { method: 'principal', message }); } catch (error) { this.logger.error(`订单id:${res.id}记录创建失败:${error.toString()}`); } return res; } /** * 订单操作记录 * @param {String} id 订单id * @param {Object} {method:方法, message:自定义文字} 参数 */ async record(id, { method, message }) { const order = await this.model.findById(id); if (!order) { throw new BusinessError( ErrorCode.DATA_NOT_EXIST, `未找到订单,传入id:${id}` ); } const { authorization } = this.ctx.request.header; let user = decodeURI(authorization); if (!user) { throw new BusinessError(ErrorCode.USER_NOT_EXIST, '未找到操作人'); } user = JSON.parse(user); const { id: userid, name: username } = user; const record = { opera: username, operaid: userid }; // 创建记录 if (method === 'create') record.message = `${username}创建订单`; else if (method === 'update') record.message = `${username}修改订单`; else if (method === 'in') record.message = `${username}修改收入`; else if (method === 'out') record.message = `${username}修改支出`; else if (method === 'split') record.message = `${username}拆分货物`; else if (method === 'js') record.message = `${username}结算订单`; else if ( method === 'send' || method === 'arrive' || method === 'principal' || method === 'outJs' ) { record.message = `${message}`; } order.record.push(record); await order.save(); } /** * 客户结算查询 * @param {Object} query 查询条件 */ async clientCalculate(query) { query = this.util.turnDateRangeQuery(this.util.turnFilter(query)); let list = await this.model .$where('this.split.every(i=>i.status=== "-1")') .find({ ...query, is_js: false }) .populate([ { path: 'client', model: 'Client', }, { path: 'item', model: 'Item', }, { path: 'route', model: 'Route', }, { path: 'treaty', model: 'Treaty', }, { path: 'goods', populate: [ { path: 'mode', model: 'Mode', }, ], }, ]); if (list.length > 0) list = JSON.parse(JSON.stringify(list)); // 组织数据 list = this.toResetOrder(list); return list; } /** * 整合订单信息 * @param {Array} list 订单列表 */ toResetOrder(list) { list = list.map(i => { i = this.orderReset(i); i = this.orderIn(i); return i; }); return list; } /** * 将客户相关信息转换 * @param {Object} order 订单信息 */ orderReset(order) { order = JSON.parse(JSON.stringify(order)); const { client, item, route, treaty } = order; if (client && _.isObject(client)) { order.client_id = _.get(client, '_id'); order.client = _.get(client, 'name'); } if (item && _.isObject(item)) { order.item_id = _.get(item, '_id'); order.item = _.get(item, 'name'); } if (route && _.isObject(route)) { order.route_id = _.get(route, '_id'); order.route = _.get(route, 'name'); } if (treaty && _.isObject(treaty)) { order.treaty_id = _.get(treaty, '_id'); order.treaty = _.get(treaty, 'number'); } return order; } /** * 计算收入总价 * @param {Object} order 订单信息 */ orderIn(order) { const { goods, in_bill } = order; const gi = goods.reduce((p, n) => p + (n.sh_ss || 0), 0); const bi = in_bill.reduce((p, n) => p + (n.sh_ss || 0), 0); const sh_ss = gi + bi; order.sh_ss = sh_ss; return order; } /** * 选择指定的订单,导出收入excel * @param {Object} query 查询条件 * @property Array ids 订单id集合 */ async clientExport(query) { const { ids } = query; let list = await this.model .find({ _id: ids.map(i => ObjectId(i)) }) .populate([ { path: 'client', model: 'Client', }, { path: 'item', model: 'Item', }, { path: 'route', model: 'Route', }, { path: 'treaty', model: 'Treaty', }, { path: 'goods', populate: [ { path: 'mode', model: 'Mode', }, ], }, ]); if (list.length > 0) list = JSON.parse(JSON.stringify(list)); // 处理信息 const arr = []; // 获取头 const { header, otherList } = this.getHeader(list); // 已经被占用的行数 let ouse = 1; for (const order of list) { // 计算这个订单的开始行和结束行位置 const os = ouse + 1; // 订单开始行数 const { goods } = order; // 整理共同部分数据 const edata = this.getPublicExcelData(order, os); // 再处理额外项的头数据 const odata = this.getOtherInExcelData(order, os, otherList); arr.push({ ...edata, ...odata }); // 处理完后,更新被占用行数 ouse = ouse + goods.length; } // 处理头 let pkeys = Object.keys(header); pkeys = pkeys.map(i => { const reg = /[^a-zA-Z]/i; const res = i.replace(reg, ''); return res; }); let lastData = arr.map(i => ({ content: i })); // this.toResetExcelData(pkeys, arr); lastData.push({ content: header }); const alignment = { vertical: 'middle', horizontal: 'center' }; lastData = lastData.map(i => ({ ...i, alignment })); const res = await this.ctx.service.util.excel.toExcel({ data: lastData }); return res; } /** * 合并处理数据 * @param {Array} keys 列字母数组 * @param {Array} data excel数据(不含头) */ toResetExcelData(keys, data) { const reg = /[^a-zA-Z]/i; const arr = []; const clear = []; 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 letter = head.key.replace(reg, ''); const r3 = this.mergeRange(letter); if (!r3) 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; } /** * 查询该列是否可以合并 * @param {String} letter 字母 */ mergeRange(letter) { const arr = [ 'U', 'Z', 'AA', 'AB', 'AC' ]; return !arr.includes(letter); } /** * 整理成excel数据(先整理固定部分,即不包含额外收入部分) * @param {Object} order 订单信息 * @param {Number} os 该订单开始行数 */ getPublicExcelData(order, os) { // 如果不需要合并,那一个订单也就是1个object,如果需要合并,那就是多个object, // 即使是多个object也没问题,因为控制的是每个单元格,所以出问题一定是单元格没弄明白 // 除了货物,方式外,其他的固定项都需要判断是否需要合并单元格,所以先处理货物,方式 const arr = []; const content = {}; const { goods } = order; // 货物,方式部分 for (let i = 0; i < goods.length; i++) { const good = goods[i]; const { mode, name, costname = '运费', sq_ys, sq_ss, sh_ys, sh_ss, } = good; const { name: modeName, price, is_lf, send_type, computed_type } = mode; content[`S${i + os}`] = modeName; content[`T${i + os}`] = price; content[`U${i + os}`] = name; content[`V${i + os}`] = costname; content[`W${i + os}`] = is_lf ? '是' : '否'; content[`X${i + os}`] = send_type; content[`Y${i + os}`] = computed_type; content[`Z${i + os}`] = sq_ys; content[`AA${i + os}`] = sq_ss; content[`AB${i + os}`] = sh_ys; content[`AC${i + os}`] = sh_ss; // 订单 content[`A${i + os}`] = _.get(order, 'order_no'); content[`AD${i + os}`] = _.get(order, 'remark'); // 客户部分一定合并,不在这里处理 content[`B${i + os}`] = _.get(order.client, 'name'); content[`C${i + os}`] = _.get(order.client, 'address'); content[`D${i + os}`] = _.get(order.client, 'legal'); content[`E${i + os}`] = _.get(order.client, 'mobile'); content[`F${i + os}`] = _.get(order.client, 'taxes_no'); content[`G${i + os}`] = _.get(order.client, 'account_bank'); content[`H${i + os}`] = _.get(order.client, 'account'); // 合同部分 content[`I${i + os}`] = _.get(order.treaty, 'number'); content[`J${i + os}`] = _.get(order.treaty, 'jf'); content[`K${i + os}`] = _.get(order.treaty, 'yf'); content[`L${i + os}`] = _.get(order.treaty, 'period'); content[`M${i + os}`] = _.get(order.treaty, 'settle_up'); // 项目 content[`N${i + os}`] = _.get(order.item, 'name'); content[`O${i + os}`] = _.get(order.item, 'taxes'); // 线路 content[`P${i + os}`] = _.get(order.route, 'name'); content[`Q${i + os}`] = _.get(order.route, 's_p'); content[`R${i + os}`] = _.get(order.route, 'e_p'); } arr.push(content); return content; } /** * 填充额外收入项 * @param {Object} order 订单信息 * @param {Number} os 该订单开始行数 * @param {Array} inList 额外收入项 */ getOtherInExcelData(order, os, inList) { const obj = {}; const { in_bill } = order; for (const oin of inList) { const { key, item, value } = oin; const r = in_bill.find(f => f.item === item); if (r) { obj[`${key}${os}`] = _.get(r, value); } } return obj; } /** * 获取导出的订单头 * @param {Array} list 选择的订单 */ getHeader(list) { const obj = {}; // 同一客户下:B-H一定是合并的 // 同一订单下: A,AD1一定是合并的 // 订单部分 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 = '量份收费'; // 需要换成 是/否 obj.X1 = '发货方式'; obj.Y1 = '计费方式'; // 货物部分 obj.Z1 = '税前应收'; obj.AA1 = '税前实收'; obj.AB1 = '税后应收'; obj.AC1 = '税后实收'; // 订单部分 obj.AD1 = '订单备注'; // 处理额外收入头部 const in_item = _.uniqBy(list.map(i => i.in_bill).flat(), 'item'); const arr = []; let i = 31; for (const inInfo of in_item) { const { item } = inInfo; const fl = this.ctx.service.util.excel.numberToLetter(_.floor(i / 26)); // const sl = this.ctx.service.util.excel.numberToLetter(i % 26); const sqyl = this.ctx.service.util.excel.numberToLetter(i % 26); obj[`${fl}${sqyl}1`] = `${item}税前应收`; arr.push({ key: `${fl}${sqyl}`, item, value: 'sq_ys' }); i++; const sqsl = this.ctx.service.util.excel.numberToLetter(i % 26); obj[`${fl}${sqsl}1`] = `${item}税前实收`; arr.push({ key: `${fl}${sqsl}`, item, value: 'sq_ss' }); i++; const shyl = this.ctx.service.util.excel.numberToLetter(i % 26); obj[`${fl}${shyl}1`] = `${item}税后应收`; arr.push({ key: `${fl}${shyl}`, item, value: 'sh_ys' }); i++; const shsl = this.ctx.service.util.excel.numberToLetter(i % 26); obj[`${fl}${shsl}1`] = `${item}税后实收`; arr.push({ key: `${fl}${shsl}`, item, value: 'sh_ss' }); i++; } return { header: obj, otherList: arr }; } /** * 订单结算 * @param {Object} {ids} 要结算的订单 */ async js({ ids, client, owner, ...info }) { assert(ids, '缺少订单信息'); assert(client, '缺少客户信息'); assert(owner, '缺少创建人信息'); const params = { ids, client, owner }; const bill = await this.ctx.model.Bill.create({ params, client, owner, ...info, }); if (!bill) { throw new BusinessError(ErrorCode.DATABASE_FAULT, '结算单创建失败'); } const res = await this.model.updateMany( { _id: ids.map(i => ObjectId(i)) }, { is_js: true } ); try { for (const id of ids) { this.record(id, { method: 'js' }); } } catch (error) { this.logger.error(`订单id:${res.id}记录创建失败:${error.toString()}`); } } /** * 支出结算 * @param {Object} {ids,client,car_no} ids:订单id列表,client:客户id,car_no:车牌号(第三方)/车辆id(自运) */ async outJs({ ids, client, car_no }) { const orderList = await this.model.find({ _id: ids.map(i => ObjectId(i)), }); for (const order of orderList) { const { out_bill, _id } = order; let target; let res; if (client) { res = out_bill.filter(f => f.client === client); const cInfo = await this.ctx.model.Client.findById(client); if (!cInfo) { throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到指定供应商'); } target = _.get(cInfo, 'name'); } else { res = out_bill.filter(f => f.car_no === car_no); const reg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); if (reg.test(car_no)) { target = car_no; } else { const cInfo = await this.ctx.model.Car.findById(car_no); if (!cInfo) { throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到指定车辆'); } target = cInfo.car_no; } } for (const bill of res) { bill.is_js = true; } order.save(); try { this.record(_id, { method: 'outJs', message: `结算 ${target} 支出` }); } catch (error) { this.logger.error(`订单id:${res.id}记录创建失败:${error.toString()}`); } } } } module.exports = OrderService;