calc_tmpl.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. 'use strict';
  2. /**
  3. * 计算模板
  4. *
  5. * 如需修改请联系作者,常量&私有方法(以下划线开头)勿动
  6. *
  7. * @author Mai
  8. * @date
  9. * @version
  10. */
  11. const math = require('mathjs');
  12. math.config({
  13. number: 'BigNumber',
  14. });
  15. math.import({
  16. if: function(con, a, b) { return con ? a : b; },
  17. });
  18. const ValidTemplateType = ['posCalc', 'cost'];
  19. const PosCalc = (function(){
  20. const EmptySpreadCache = {
  21. cols: [],
  22. emptyRows: 6,
  23. headRows: 1,
  24. headRowHeight: [32],
  25. defaultRowHeight: 21,
  26. headerFont: '12px 微软雅黑',
  27. font: '12px 微软雅黑',
  28. frozenLineColor: '#93b5e4',
  29. };
  30. const BaseSpreadColSetting = {
  31. str: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
  32. num: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
  33. spec: { title: '规格', colSpan: '1', rowSpan: '1', field: 'spec', hAlign: 1, width: 80, formatter: '@', cellType: 'spec', relaCol: '' },
  34. qty: { title: '数量', colSpan: '1', rowSpan: '1', field: 'qty', hAlign: 2, width: 80, type: 'Number', readOnly: true },
  35. };
  36. const ValidColInfo = [
  37. { key: 'str', name: '文本', fields: ['str1', 'str2', 'str3', 'str4'], valid: ['title', 'width'], def: { title: '文字', width: 80, type: 'str'} },
  38. {
  39. key: 'num', name: '数值/计算',
  40. fields: ['num_a', 'num_b', 'num_c', 'num_d', 'num_e', 'num_f', 'num_g', 'num_h', 'num_i', 'num_j', 'num_k', 'num_l', 'num_m', 'num_n', 'num_o', 'num_p', 'num_q', 'num_r', 'num_s', 'num_t', 'num_u'], // 'num_v', 'num_w', 'num_x', 'num_y', 'num_z'],
  41. valid: ['title', 'width', 'calc_code', 'decimal', 'unit', 'expr'],
  42. def: { title: '数值', width: 80, decimal: 2, type: 'num', unit: ''}
  43. },
  44. { key: 'spec', name: '规格', fields: ['spec'], valid: ['title', 'width', 'rela_col', 'spec_set'], def: { title: '规格', width: 80, rela_col: '', type: 'spec'} },
  45. { key: 'qty', name: '数量', fields: ['qty'], valid: ['title', 'width', 'expr', 'decimal'], def: { title: '数量', width: 80, decimal: 2, expr: '', type: 'qty' } },
  46. ];
  47. ValidColInfo.forEach(vci => { return vci.count = vci.fields.length; });
  48. const TestData = {
  49. col_set: [
  50. { title: '名称', width: 80, type: 'str', field: 'str1' },
  51. { title: '长度', width: 80, type: 'num', field: 'num_a', decimal: 2, unit: 'm', calc_code: 'A' },
  52. { title: '宽度', width: 80, type: 'num', field: 'num_b', decimal: 2, unit: 'm', calc_code: 'B' },
  53. { title: '高度', width: 80, type: 'num', field: 'num_c', decimal: 2, unit: 'm', calc_code: 'C' },
  54. { title: '数量', width: 80, type: 'num', field: 'qty', decimal: 2, },
  55. ]
  56. };
  57. return { EmptySpreadCache, BaseSpreadColSetting, ValidColInfo, TestData, ValidCount: 50 };
  58. })();
  59. const Cost = (function(){
  60. const EmptySpreadCache = {
  61. cols: [],
  62. emptyRows: 0,
  63. headRows: 1,
  64. headRowHeight: [32],
  65. defaultRowHeight: 21,
  66. headerFont: '12px 微软雅黑',
  67. font: '12px 微软雅黑',
  68. frozenColCount: 2,
  69. frozenLineColor: '#93b5e4',
  70. };
  71. const BaseSpreadColSetting = {
  72. code: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
  73. name: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
  74. party_b: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
  75. yf_excl_tax_tp: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
  76. in_excl_tax_tp: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
  77. sf_excl_tax_tp: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
  78. str: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
  79. num: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
  80. postil: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@', cellType: 'ellipsisAutoTip'},
  81. sf_percent: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number', readOnly: true },
  82. };
  83. const ValidColInfo = [
  84. { key: 'code', name: '编号', fields: ['code'], valid: ['title', 'width'], def: { title: '编号', width: 80, type: 'code'} },
  85. { key: 'name', name: '名称', fields: ['name'], valid: ['title', 'width'], def: { title: '名称', width: 80, type: 'name'} },
  86. { key: 'party_b', name: '乙方', fields: ['party_b'], valid: ['title', 'width'], def: { title: '乙方', width: 80, type: 'party_b'} },
  87. { key: 'yf_excl_tax_tp', name: '应付(不含税)', fields: ['yf_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal', 'sum'], def: { title: '已结(不含税)', width: 80, type: 'yf_excl_tax_tp', decimal: 6} },
  88. { key: 'in_excl_tax_tp', name: '入账(不含税)', fields: ['in_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal', 'sum'], def: { title: '入账(不含税)', width: 80, type: 'in_excl_tax_tp', decimal: 6} },
  89. { key: 'sf_excl_tax_tp', name: '实付(不含税)', fields: ['sf_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal', 'sum'], def: { title: '实付(不含税)', width: 80, type: 'sf_excl_tax_tp', decimal: 6} },
  90. { key: 'str', name: '文本', fields: ['str1', 'str2', 'str3', 'str4'], valid: ['title', 'width'], def: { title: '文本', width: 80, type: 'str'} },
  91. {
  92. key: 'num', name: '数值/计算',
  93. fields: ['num_a', 'num_b', 'num_c', 'num_d', 'num_e', 'num_f', 'num_g', 'num_h', 'num_i', 'num_j', 'num_k', 'num_l', 'num_m', 'num_n', 'num_o', 'num_p'], // 'num_q', 'num_r', 'num_s', 'num_t', 'num_u', 'num_v', 'num_w', 'num_x', 'num_y', 'num_z'],
  94. valid: ['title', 'width', 'calc_code', 'decimal', 'unit', 'expr', 'sum'],
  95. def: { title: '数值/计算', width: 80, decimal: 6, type: 'num', unit: ''}
  96. },
  97. { key: 'postil', name: '本期批注', fields: ['postil'], valid: ['title', 'width'], def: { title: '本期批注', width: 120, type: 'postil'} },
  98. { key: 'sf_percent', name: '实付比例', fields: ['sf_percent'], valid: ['title', 'width', 'calc_code'], def: { title: '实付比例', width: 80, type: 'sf_percent', decimal: 2} },
  99. ];
  100. ValidColInfo.forEach(vci => { return vci.count = vci.fields.length; });
  101. return { EmptySpreadCache, BaseSpreadColSetting, ValidColInfo, ValidCount: 10, subSpreadReadOnly: ['code', 'name', 'party_b', 'yf_excl_tax_tp', 'sf_excl_tax_tp', 'in_excl_tax_tp'] };
  102. })();
  103. function randomWord(randomFlag, min, max){
  104. let str = "",
  105. range = min,
  106. arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
  107. pos;
  108. // 随机产生
  109. if(randomFlag){
  110. range = Math.round(Math.random() * (max-min)) + min;
  111. }
  112. for(var i=0; i<range; i++){
  113. pos = Math.round(Math.random() * (arr.length-1));
  114. str += arr[pos];
  115. }
  116. return str;
  117. }
  118. module.exports = app => {
  119. class CalcTmpl extends app.BaseService {
  120. /**
  121. * 构造函数
  122. *
  123. * @param {Object} ctx - egg全局变量
  124. * @return {void}
  125. */
  126. constructor(ctx) {
  127. super(ctx);
  128. this.tableName = 'calc_tmpl';
  129. this.TemplateRela = { posCalc: PosCalc, cost: Cost };
  130. }
  131. async getAllTemplate(pid, type, sort = 'asc') {
  132. // return this.getAllDataByCondition({
  133. // columns: ['id', 'pid', 'name', 'create_user_id', 'is_locked'],
  134. // where: { pid, type },
  135. // orders: [['create_time', sort]]
  136. // });
  137. const sql = 'SELECT ct.id, ct.pid, ct.folder_id, ct.name, ct.create_user_id, ct.is_locked, pa.name AS user_name' +
  138. ` FROM ${this.tableName} ct LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON ct.create_user_id = pa.id WHERE ct.pid = ? and ct.type = ? ORDER BY ct.create_time ${sort}`;
  139. return this.db.query(sql, [pid, type]);
  140. }
  141. async analysisTemplate(data) {
  142. const datas = data instanceof Array ? data : [data];
  143. datas.forEach(x => {
  144. x.col_set = x.col_set ? JSON.parse(x.col_set) : [];
  145. x.spread_cache = x.spread_cache ? JSON.parse(x.spread_cache) : {};
  146. x.field_cache = x.field_cache ? JSON.parse(x.field_cache) : {};
  147. x.decimal = x.decimal ? JSON.parse(x.decimal) : {};
  148. x.multi_header = x.multi_header ? JSON.parse(x.multi_header) : undefined;
  149. x.calc_expr = x.calc_expr ? JSON.parse(x.calc_expr) : [];
  150. x.calc_order = x.calc_order ? x.calc_order.split(',') : [];
  151. x.sub_spread_cache = x.sub_spread_cache ? JSON.parse(x.sub_spread_cache) : {};
  152. });
  153. for (const d of datas) {
  154. if (d.spec_set) {
  155. d.specValue = await this.ctx.service.stdExtra.getAllDataByCondition({ columns: ['spec', 'value'], where: { list_id: d.spec_set }, orders:[['order', 'asc']]});
  156. } else {
  157. d.specValue = [];
  158. }
  159. }
  160. }
  161. async getAllTemplateDetail(pid, type) {
  162. const result = await this.getAllDataByCondition({
  163. where: { pid, type },
  164. orders: [['create_time', 'desc']]
  165. });
  166. await this.analysisTemplate(result);
  167. return result;
  168. }
  169. async getTemplate(id) {
  170. const result = await this.getDataById(id);
  171. await this.analysisTemplate(result);
  172. return result;
  173. }
  174. async getProjectCalcTmplSet () {
  175. if (this.projectSet) return;
  176. const defaultSet = { posCalc: { validCount: 50 }, cost: { validCount: 10 } };
  177. const project = await this.ctx.service.project.getDataById(this.ctx.session.sessionProject.id);
  178. if (!project.calc_tmpl_set) {
  179. this.projectSet = defaultSet;
  180. } else {
  181. this.projectSet = JSON.parse(project.calc_tmpl_set);
  182. for (const prop in defaultSet) {
  183. if (this.projectSet[prop]) {
  184. this.projectSet[prop] = this.ctx.helper._.assign(defaultSet[prop], this.projectSet[prop]);
  185. } else {
  186. this.projectSet[prop] = defaultSet[prop];
  187. }
  188. }
  189. }
  190. }
  191. //-------------------------- 以下方法不可随意修改 --------------------------
  192. // 根据列设置计算spreadjs配置相关,方法命名须严格按照【方法_类型】命名
  193. _getFillRowCount(rowSpan) {
  194. let count = 0;
  195. rowSpan.forEach(x => {
  196. count = count + x ? x : 1;
  197. });
  198. return count;
  199. }
  200. _calcMultiColHeader(iCol, spreadCol, colTitle, multiHeader) {
  201. if (multiHeader) {
  202. const colSpan = [], rowSpan = [], title = [];
  203. for (let iRow = 0; iRow < multiHeader.headRows - 1; iRow ++) {
  204. const setSpan = multiHeader.headSpan.find(x => { return x.col === iCol && x.row === iRow; });
  205. if (setSpan) {
  206. colSpan.push(setSpan.colCount);
  207. rowSpan.push(setSpan.rowCount);
  208. title.push(setSpan.title);
  209. } else {
  210. const relaSpan = multiHeader.headSpan.find(x => { return iCol >= x.col && iCol < x.col + x.colCount && iRow >= x.row && iRow < x.row + x.rowCount; });
  211. if (relaSpan) {
  212. colSpan.push('');
  213. rowSpan.push('');
  214. title.push('');
  215. }
  216. }
  217. }
  218. if (colSpan.length === 0) {
  219. spreadCol.rowSpan = multiHeader.headRows + '';
  220. spreadCol.title = colTitle;
  221. } else {
  222. colSpan.push(1);
  223. rowSpan.push(multiHeader.headRows - this._getFillRowCount(rowSpan));
  224. title.push(colTitle);
  225. spreadCol.colSpan = colSpan.join('|');
  226. spreadCol.rowSpan = rowSpan.join('|');
  227. spreadCol.title = title.join('|');
  228. }
  229. } else {
  230. spreadCol.title = colTitle;
  231. }
  232. }
  233. _transExpr(expr, colSet) {
  234. if (!expr) return '';
  235. let newExpr = expr;
  236. colSet.forEach(x => {
  237. if (x.calc_code) newExpr = newExpr.replace(new RegExp(x.calc_code, 'gm'), x.field);
  238. });
  239. return newExpr;
  240. }
  241. _calcSpreadCache_posCalc(colSet) {
  242. const relaConst = this.TemplateRela.posCalc;
  243. const result = JSON.parse(JSON.stringify(relaConst.EmptySpreadCache));
  244. for (const col of colSet) {
  245. const info = relaConst.ValidColInfo.find(x => { return x.key === col.type });
  246. const spreadCol = JSON.parse(JSON.stringify(relaConst.BaseSpreadColSetting[col.type]));
  247. spreadCol.field = col.field;
  248. info.valid.forEach(v => {
  249. if (v === 'unit' && col.unit) {
  250. spreadCol.title = col.title + '(' + col.unit + ')';
  251. } else if (v === 'expr') {
  252. spreadCol.expr = this._transExpr(col.expr, colSet);
  253. spreadCol.readOnly = spreadCol.expr;
  254. } else {
  255. if (col[v] !== undefined) spreadCol[v] = col[v];
  256. }
  257. });
  258. if (col.calc_code) spreadCol.title = spreadCol.title + '\n' + col.calc_code;
  259. if (col.expr) spreadCol.title = spreadCol.title + '\n' + col.expr;
  260. result.cols.push(spreadCol);
  261. }
  262. const specCol = colSet.find(x => { return x.type === 'spec'; });
  263. if (specCol) {
  264. const relaSet = colSet.find(x => { return x.calc_code === specCol.rela_col; });
  265. const relaCol = result.cols.find(x => { return x.field === relaSet.field; });
  266. relaCol.readOnly = true;
  267. }
  268. return result;
  269. }
  270. _calcSpreadCache_cost(colSet, multiHeader) {
  271. const relaConst = this.TemplateRela.cost;
  272. const result = JSON.parse(JSON.stringify(relaConst.EmptySpreadCache));
  273. if (multiHeader) {
  274. result.headRows = multiHeader.headRows;
  275. result.headRowHeight = multiHeader.headRowHeight;
  276. }
  277. for (const [iCol, col] of colSet.entries()) {
  278. const info = relaConst.ValidColInfo.find(x => { return x.key === col.type });
  279. const spreadCol = JSON.parse(JSON.stringify(relaConst.BaseSpreadColSetting[col.type]));
  280. spreadCol.field = col.field;
  281. let colTitle = col.title;
  282. info.valid.forEach(v => {
  283. if (v === 'unit' && col.unit) {
  284. colTitle = colTitle + '(' + col.unit + ')';
  285. } else if (v === 'expr') {
  286. spreadCol.expr = this._transExpr(col.expr, colSet);
  287. spreadCol.readOnly = spreadCol.expr;
  288. } else {
  289. if (col[v] !== undefined) spreadCol[v] = col[v];
  290. }
  291. });
  292. if (col.calc_code) {
  293. if (col.expr) {
  294. colTitle = colTitle + '\n' + col.calc_code + '(' + col.expr + ')';
  295. } else {
  296. colTitle = colTitle + '\n' + col.calc_code;
  297. }
  298. }
  299. this._calcMultiColHeader(iCol, spreadCol, colTitle, multiHeader);
  300. result.cols.push(spreadCol);
  301. }
  302. return result;
  303. }
  304. calcSpreadCache(type, colSet, multiHeader) {
  305. const funName = '_calcSpreadCache_' + type;
  306. if (this[funName]) return this[funName](colSet, multiHeader);
  307. return this.TemplateRela[type] ? this.TemplateRela[type].EmptySpreadCache : {};
  308. }
  309. async getCalcTestData_posCalc(colSet, count) {
  310. const result = [];
  311. const specCol = colSet.find(x => { return x.type === 'spec'; });
  312. const specSet = specCol && specCol.spec_set ? await this.ctx.service.stdExtra.getAllDataByCondition({ columns: ['spec', 'value'], where: { list_id: specCol.spec_set }, orders:[['order', 'asc']]}) : [];
  313. const relaCol = specCol && specCol.rela_col ? colSet.find(x => { return x.calc_code === specCol.rela_col; }) : null;
  314. for (let i = 0; i < count; i++) {
  315. const testData = {};
  316. colSet.forEach(x => {
  317. if (x.type === 'num') testData[x.field] = Math.max(Math.floor(Math.random()*10), 1);
  318. if (x.type === 'str') testData[x.field] = randomWord(true, 3, 6);
  319. });
  320. if (specCol && specSet.length > 0) {
  321. testData.spec = specSet[0].spec;
  322. if (relaCol) testData[relaCol.field] = specSet[0].value;
  323. }
  324. result.push(testData);
  325. }
  326. return result;
  327. }
  328. async getCalcTestData_cost(colSet, count) {
  329. const result = [];
  330. for (let i = 0; i < count; i++) {
  331. const testData = {};
  332. colSet.forEach(x => {
  333. if (x.type === 'num') testData[x.field] = Math.max(Math.floor(Math.random()*10), 1);
  334. if (x.type === 'str') testData[x.field] = randomWord(true, 3, 6);
  335. });
  336. result.push(testData);
  337. }
  338. return result;
  339. }
  340. async getCalcTestData(colSet, type, count = 1) {
  341. const funName = 'getCalcTestData_' + type;
  342. if (this[funName]) return await this[funName](colSet, count);
  343. return [];
  344. }
  345. async _checkTemplateUsed_posCalc(template) {
  346. const templates = template instanceof Array ? template : [template];
  347. templates.forEach(x => { x.used = []; x.used_count = 0; });
  348. const tender = await this.ctx.service.tender.getAllDataByCondition({ columns: ['id'], where: { project_id: templates[0].pid }});
  349. for (const t of tender) {
  350. const used = await this.ctx.service.ledgerExtra.getUsedCalcTemplate(t.id);
  351. const ancGclUsed = await this.ctx.service.ancillaryGcl.getUsedCalcTemplate(t.id);
  352. templates.forEach(x => {
  353. const u = used.find(ut => { return ut.calc_template === x.id; }) || { count: 0 };
  354. const u2 = ancGclUsed.find(ut => { return ut.calc_template === x.id; }) || { count: 0 };
  355. if (u.count > 0 || u2.count > 0) x.used.push(t.id);
  356. x.used_count = this.ctx.helper.sum([ x.used_count, u.count, u2.count ]);
  357. });
  358. }
  359. }
  360. async _checkTemplateUsed_cost(template) {
  361. const templates = template instanceof Array ? template : [template];
  362. templates.forEach(x => { x.used = []; x.used_count = 0; });
  363. // todo 检查使用情况
  364. }
  365. async checkTemplateUsed(template, type) {
  366. if (!template || template.length === 0) return;
  367. const funName = '_checkTemplateUsed_' + type;
  368. if (this[funName]) return await this[funName](template);
  369. }
  370. // ----------------------------------------------------------------------
  371. async _addTemplate(name, type, folder_id) {
  372. if (ValidTemplateType.indexOf(type) < 0) throw '新增的模板类型非法';
  373. await this.getProjectCalcTmplSet();
  374. const count = await this.count({pid: this.ctx.session.sessionProject.id, type});
  375. if (count >= this.projectSet[type].validCount) throw '已达模板使用上限';
  376. const relaConst = this.TemplateRela[type];
  377. const insertData = {
  378. id: this.uuid.v4(),
  379. pid: this.ctx.session.sessionProject.id,
  380. // spid: this.ctx.subProject.id,
  381. // tid: this.ctx.tender.id,
  382. folder_id: folder_id || '',
  383. name: name || '新增计算模板',
  384. type,
  385. create_user_id: this.ctx.session.sessionUser.accountId,
  386. update_user_id: this.ctx.session.sessionUser.accountId,
  387. // 关键默认值
  388. col_set: '[]',
  389. spread_cache: JSON.stringify(relaConst.EmptySpreadCache),
  390. field_cache: '{}',
  391. decimal: JSON.stringify([{ def: 2 }]),
  392. sub_spread_cache: '[]',
  393. };
  394. await this.db.insert(this.tableName, insertData);
  395. const result = await this.getTemplate(insertData.id);
  396. result.user_name = this.ctx.session.sessionUser.name;
  397. return result;
  398. }
  399. async checkTemplateEdit(id) {
  400. const template = await this.getDataById(id);
  401. if (!template) throw '编辑的模板不存在';
  402. if (template.pid !== this.ctx.session.sessionProject.id) throw '模板不属于当前项目,请刷新后重试';
  403. if (template.create_user_id !== this.ctx.session.sessionUser.accountId) throw '非您创建的模板';
  404. if (template.is_locked) throw '模板已被锁定';
  405. await this.checkTemplateUsed(template, template.type);
  406. if (template.used.length > 0) throw '模板已被使用';
  407. return template;
  408. }
  409. _getCalcLeaf(cur, cols, parentField) {
  410. if (!cur || !cur.expr) return [];
  411. const codeReg = /num_[a-z]{1}/gm;
  412. const codeParam = cur.expr.match(codeReg);
  413. if (!codeParam || codeParam.length === 0) return [];
  414. const result = [...codeParam];
  415. for (const op of codeParam) {
  416. const relaCol = cols.find(x => { return x.field === op; });
  417. if (relaCol.field === cur.field || op === parentField) {
  418. result.push(op);
  419. } else {
  420. const sub = this._getCalcLeaf(relaCol, cols, cur.field);
  421. if (sub.length > 0) {
  422. result.push(...sub);
  423. } else {
  424. result.push(op);
  425. }
  426. }
  427. }
  428. return this.ctx.helper._.uniq(result);
  429. }
  430. _getCalcExpr(cols) {
  431. const calcOrder = [];
  432. for (const c of cols) {
  433. if (c.type !== 'Number') continue;
  434. calcOrder.push({ field: c.field, expr: c.expr || '', decimal: c.decimal, calc_code: c.calc_code });
  435. }
  436. for (const co of calcOrder) {
  437. co.leaf = this._getCalcLeaf(co, calcOrder);
  438. }
  439. calcOrder.sort((x, y) => {return x.leaf.length - y.leaf.length; });
  440. return calcOrder;
  441. }
  442. async _saveTemplate(id, data) {
  443. const org = await this.checkTemplateEdit(id);
  444. const updateData = { id };
  445. if (data.name !== undefined) updateData.name = data.name;
  446. if (data.col_set !== undefined || data.multi_header !== undefined) {
  447. await this.checkTemplateUsed(org, org.type);
  448. if (org.used.length > 0) throw '模板已使用,不可修改配置!';
  449. updateData.col_set = JSON.stringify(data.col_set);
  450. updateData.multi_header = JSON.stringify(data.multi_header);
  451. const spread_cache = this.calcSpreadCache(org.type, data.col_set, data.multi_header);
  452. if (org.type === 'cost') {
  453. spread_cache.cols[0].cellType = 'tree';
  454. const treeSpreadCache = this.ctx.helper.clone(spread_cache);
  455. treeSpreadCache.cols.splice(treeSpreadCache.cols.findIndex(x => { return x.field === 'party_b';}), 1);
  456. updateData.spread_cache = JSON.stringify(treeSpreadCache);
  457. const treeCol = treeSpreadCache.cols.find(x => { return x.cellType === 'tree'});
  458. delete treeCol.cellType;
  459. for (const col of treeSpreadCache.cols) {
  460. if (this.TemplateRela.cost.subSpreadReadOnly.indexOf(col.field) >= 0) col.readOnly = true;
  461. }
  462. const sub_spread_cache = { common: treeSpreadCache };
  463. const dealSpreadCache = this.ctx.helper.clone(spread_cache);
  464. const treeDCol = dealSpreadCache.cols.find(x => { return x.cellType === 'tree'});
  465. delete treeDCol.cellType;
  466. sub_spread_cache.deal = dealSpreadCache;
  467. for (const col of dealSpreadCache.cols) {
  468. if (this.TemplateRela.cost.subSpreadReadOnly.indexOf(col.field) >= 0) col.readOnly = true;
  469. }
  470. updateData.sub_spread_cache = JSON.stringify(sub_spread_cache);
  471. } else {
  472. updateData.spread_cache = JSON.stringify(spread_cache);
  473. }
  474. const field_cache = {};
  475. data.col_set.forEach(x => { field_cache[x.field] = x.title; });
  476. updateData.field_cache = JSON.stringify(field_cache);
  477. const decimal = { def: 2 };
  478. spread_cache.cols.forEach(x => { if (x.decimal !== null && x.decimal !== undefined) decimal[x.field] = x.decimal; });
  479. updateData.decimal = JSON.stringify(decimal);
  480. const calc_expr = this._getCalcExpr(spread_cache.cols);
  481. updateData.calc_expr = JSON.stringify(calc_expr);
  482. updateData.calc_order = calc_expr.map(x => { return x.field; }).join(',');
  483. const specCol = data.col_set.find(x => { return x.field === 'spec' });
  484. if (specCol) {
  485. updateData.spec_set = specCol.spec_set;
  486. updateData.spec_rela = data.col_set.find(x => { return x.calc_code === specCol.rela_col; }).field;
  487. } else {
  488. updateData.spec_set = 0;
  489. updateData.spec_rela = '';
  490. }
  491. }
  492. await this.db.update(this.tableName, updateData);
  493. return await this.getTemplate(id);
  494. }
  495. async _delTemplate(id) {
  496. const template = await this.checkTemplateEdit(id);
  497. await this.db.delete(this.tableName, { id });
  498. return template;
  499. }
  500. async _moveTemplate(id, folder_id) {
  501. const org = await this.getAllDataByCondition({ where: { id }});
  502. if (org.length !== id.length) throw '选择的模板不存在,请刷新后重试';
  503. const updateData = id.map(x => { return { id: x, folder_id }});
  504. await this.db.updateRows(this.tableName, updateData);
  505. return updateData;
  506. }
  507. async _copyTemplate(id, folder_id) {
  508. const org = await this.getDataById(id);
  509. if (!org || org.pid !== this.ctx.session.sessionProject.id) throw '复制的模板不存在,请刷新后重试';
  510. await this.getProjectCalcTmplSet();
  511. const count = await this.count({pid: this.ctx.session.sessionProject.id, type: org.type});
  512. if (count >= this.projectSet[org.type].validCount) throw '已达模板使用上限';
  513. const insertData = {
  514. id: this.uuid.v4(),
  515. pid: this.ctx.session.sessionProject.id,
  516. folder_id: folder_id,
  517. name: org.name,
  518. type: org.type,
  519. create_user_id: this.ctx.session.sessionUser.accountId,
  520. update_user_id: this.ctx.session.sessionUser.accountId,
  521. col_set: org.col_set,
  522. spread_cache: org.spread_cache,
  523. sub_spread_cache: org.sub_spread_cache,
  524. field_cache: org.field_cache,
  525. decimal: org.decimal,
  526. calc_expr: org.calc_expr,
  527. calc_order: org.calc_order,
  528. spec_set: org.spec_set,
  529. spec_rela: org.spec_rela,
  530. multi_header: org.multi_header,
  531. };
  532. await this.db.insert(this.tableName, insertData);
  533. const result = await this.getTemplate(insertData.id);
  534. result.user_name = this.ctx.session.sessionUser.name;
  535. return result;
  536. }
  537. async saveTemplate(data) {
  538. const result = {};
  539. if (data.add) result.add = await this._addTemplate(data.add.name, data.type, data.add.folder_id);
  540. if (data.del) result.del = await this._delTemplate(data.del);
  541. if (data.update) result.update = await this._saveTemplate(data.update.id, data.update);
  542. if (data.move) result.move = await this._moveTemplate(data.move, data.folder_id);
  543. if (data.copy) result.copy = await this._copyTemplate(data.copy.id, data.copy.folder_id);
  544. return result;
  545. }
  546. async reCalcTemplate(id, force) {
  547. const data = await this.checkTemplateEdit(id);
  548. const updateData = { id };
  549. if (!force) {
  550. await this.checkTemplateUsed(data);
  551. if (data.used.length > 0) throw '模板已使用,不可修改配置!';
  552. }
  553. await this.analysisTemplate(data);
  554. updateData.col_set = JSON.stringify(data.col_set);
  555. updateData.multi_header = JSON.stringify(data.multi_header);
  556. const spread_cache = this.calcSpreadCache(data.type, data.col_set, data.multi_header);
  557. updateData.spread_cache = JSON.stringify(spread_cache);
  558. const field_cache = {};
  559. data.col_set.forEach(x => { field_cache[x.field] = x.title; });
  560. updateData.field_cache = JSON.stringify(field_cache);
  561. const decimal = { def: 2 };
  562. spread_cache.cols.forEach(x => { if (x.decimal !== null && x.decimal !== undefined) decimal[x.field] = x.decimal; });
  563. updateData.decimal = JSON.stringify(decimal);
  564. const calc_expr = this._getCalcExpr(spread_cache.cols);
  565. updateData.calc_expr = JSON.stringify(calc_expr);
  566. updateData.calc_order = calc_expr.map(x => { return x.field; }).join(',');
  567. const specCol = data.col_set.find(x => { return x.field === 'spec' });
  568. if (specCol) {
  569. updateData.spec_set = specCol.spec_set;
  570. updateData.spec_rela = data.col_set.find(x => { return x.calc_code === specCol.rela_col; }).field;
  571. } else {
  572. updateData.spec_set = 0;
  573. updateData.spec_rela = '';
  574. }
  575. await this.db.update(this.tableName, updateData);
  576. }
  577. _calcExpr(formula) {
  578. const percentReg = /((\d+)|((\d+)(\.\d+)))%/g;
  579. const percent = formula.match(percentReg);
  580. if (percent) {
  581. for (const p of percent) {
  582. const v = math.eval(p.replace(new RegExp('%', 'gm'), '/100'));
  583. formula = formula.replace(p, v);
  584. }
  585. }
  586. try {
  587. // 使用mathjs计算 math.eval('17259401.95*0.9') = 15533461.754999999,正确应为 15533461.755
  588. // const value = math.eval(formula);
  589. // 使用逆波兰法四则运算,可防止出现误差,但是只支持四则运算,不支持科学运算
  590. // const value = this.ctx.helper.calcExprStrRpn(formula);
  591. // 使用mathjs的大数运算,可支持所有
  592. const value = parseFloat(math.eval(formula));
  593. return Number.isFinite(value) ? value : 0;
  594. } catch(err) {
  595. return 0;
  596. }
  597. }
  598. calcByTemplate(data, updateData, orgData, calcExpr, updateExprInfo) {
  599. const codeReg = /num_[a-z]{1}/gm;
  600. const specRegs = [];
  601. for (const ce of calcExpr) {
  602. if (!ce.field.match(codeReg)) specRegs.push(new RegExp(ce.field, 'gm'));
  603. }
  604. for (const ce of calcExpr) {
  605. if (ce.expr) {
  606. let calcExpr = ce.expr;
  607. for (const sr of specRegs) {
  608. const specParam = calcExpr.match(sr);
  609. if (specParam) {
  610. for (const sp of specParam) {
  611. calcExpr = calcExpr.replace(new RegExp(sp, 'gm'), (data[sp] !== undefined ? data[sp] : orgData[sp]) || 0);
  612. }
  613. }
  614. }
  615. const codeParam = calcExpr.match(codeReg);
  616. if (codeParam) {
  617. for (const cp of codeParam) {
  618. calcExpr = calcExpr.replace(new RegExp(cp, 'gm'), (data[cp] !== undefined ? data[cp] : orgData[cp]) || 0);
  619. }
  620. }
  621. if (updateExprInfo && updateExprInfo[ce.field]) updateExprInfo[ce.field] = calcExpr;
  622. try {
  623. data[ce.field] = this.ctx.helper.round(this._calcExpr(calcExpr.replace(new RegExp('%', 'gm'), '/100')), ce.decimal || 2);
  624. } catch(err) {
  625. data[ce.field] = 0;
  626. }
  627. } else {
  628. if (updateData[ce.field] !== undefined) data[ce.field] = this.ctx.helper.round(updateData[ce.field], ce.decimal || 2);
  629. }
  630. }
  631. }
  632. }
  633. return CalcTmpl;
  634. };