importBills.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. "use strict";
  2. /**
  3. *
  4. *
  5. * @author Zhong
  6. * @date 2018/8/2
  7. * @version
  8. */
  9. /*
  10. * 清单导入模块,前端导入excel,进行数据提取,用lz-string进行压缩上传处理
  11. * */
  12. const importBills = (function () {
  13. //单元格数据是否存在
  14. function _isDef(data) {
  15. return typeof data !== "undefined" && data !== null && data !== "";
  16. }
  17. //去除转义字符
  18. function _deESC(data) {
  19. return _isDef(data) ? data.toString().replace(/[\r\n\s\t]/g, "") : data;
  20. }
  21. function _deNR(data) {
  22. return _isDef(data) ? data.toString().replace(/[\r\n]/g, "") : data;
  23. }
  24. //find 返回最后匹配
  25. function findLast(datas, func) {
  26. let filter = datas.filter(func);
  27. if (filter.length > 0) {
  28. return filter[filter.length - 1];
  29. }
  30. return null;
  31. }
  32. const fileType = {
  33. gcl: 0, //工程量清单
  34. qdsl: 1, //清单示例
  35. gclex: 2, // 单机版导出的工程量清单预算表,需要转换成清单示例表来处理
  36. };
  37. //获取列字段对应
  38. function getColMapping(type) {
  39. if (type === 0) {
  40. //工程量清单
  41. return { code: 0, name: 1, unit: 2, quantity: 4, unitPrice: 5 };
  42. } else {
  43. //清单示例表
  44. return { code: 0, name: 1, unit: 2, quantity: 3, unitPrice: 4 };
  45. }
  46. }
  47. function isGCLHead(dataRow, nextDataRow) {
  48. const cell = dataRow[0];
  49. const nextCell = nextDataRow && nextDataRow[0];
  50. return cell && cell.value === "工程量清单" && (!nextCell || !/建设项目名称/.test(nextCell.value)); // 兼容招清单01-1表
  51. }
  52. function isGCLExtendHead(dataRow, nextDataRow) {
  53. const cell = dataRow[0];
  54. const nextCell = nextDataRow && nextDataRow[0];
  55. if ((cell && cell.value === "工程量清单预算表") || (nextCell && /建设项目名称/.test(nextCell.value))) {
  56. // 兼容招清单01-1表
  57. return true;
  58. }
  59. }
  60. //分析文件,1、工程量清单 2、清单示例表
  61. function getFileType(sheetData) {
  62. let dataTable = sheetData.data.dataTable,
  63. // rowCount = sheetData.rowCount;
  64. rowCount = sheetData.rows.length;
  65. for (let row = 0; row < rowCount; row++) {
  66. if (isGCLHead(dataTable[row], dataTable[row + 1])) {
  67. return fileType.gcl;
  68. }
  69. if (isGCLExtendHead(dataTable[row], dataTable[row + 1])) {
  70. return fileType.gclex;
  71. }
  72. }
  73. return fileType.qdsl;
  74. }
  75. //提取工程量清单数据
  76. //层级由depth确定,表格里最顶层depth为0(表头里一清单),表格内容里数据的depth为空格数+1
  77. function extractGCLDatas(sheetData, colMapping) {
  78. let dataTable = sheetData.data.dataTable,
  79. // rowCount = sheetData.rowCount;
  80. rowCount = sheetData.rows.length;
  81. let rst = [];
  82. for (let row = 0; row < rowCount; row++) {
  83. //表格中顶层节点
  84. if (isGCLHead(dataTable[row], dataTable[row + 1])) {
  85. let rootRow = dataTable[row + 2];
  86. let name = rootRow[0].value ? _deNR(rootRow[0].value) : "";
  87. let existsRoot = findLast(rst, (x) => x.name === name && x.depth === 0);
  88. if (!existsRoot) {
  89. let root = {
  90. ID: uuid.v1(),
  91. NextSiblingID: -1,
  92. ParentID: -1,
  93. name: name,
  94. depth: 0,
  95. parent: null,
  96. unitPriceAnalysis: 1,
  97. };
  98. let preData = findLast(rst, (x) => x.depth === root.depth);
  99. if (preData) {
  100. preData.NextSiblingID = root.ID;
  101. }
  102. rst.push(root);
  103. }
  104. row += 3;
  105. continue;
  106. }
  107. let code = dataTable[row][colMapping.code] ? dataTable[row][colMapping.code].value : null,
  108. name = dataTable[row][colMapping.name] ? _deNR(dataTable[row][colMapping.name].value) : null,
  109. unit = dataTable[row][colMapping.unit] ? dataTable[row][colMapping.unit].value : null,
  110. quantity = dataTable[row][colMapping.quantity] ? dataTable[row][colMapping.quantity].value : null,
  111. unitPrice = dataTable[row][colMapping.unitPrice] ? dataTable[row][colMapping.unitPrice].value : null;
  112. if ((!code && !name) || /合计/.test(code)) {
  113. //过滤掉同时没有编号和名称的、过滤合计行
  114. continue;
  115. }
  116. // “子目号”、“单位”、“数量”都为空,“子目名称”不为空时,应将此行清单名称合并到上一行
  117. let lastData = rst[rst.length - 1];
  118. if (!code && !unit && !quantity && name) {
  119. lastData.name += name;
  120. continue;
  121. }
  122. //表格内的数据
  123. code = String(code);
  124. let depth = getDepth(code);
  125. let data = {
  126. ID: uuid.v1(),
  127. NextSiblingID: -1,
  128. ParentID: -1,
  129. code: code,
  130. name: name,
  131. unit: unit,
  132. quantity: quantity,
  133. depth: depth,
  134. unitPriceAnalysis: 1,
  135. };
  136. if (+unitPrice && +quantity) {
  137. unitPrice = scMathUtil.roundForObj(unitPrice, decimalObj.bills.unitPrice);
  138. const totalPrice = scMathUtil.roundForObj(unitPrice * +quantity, decimalObj.bills.totalPrice);
  139. data.fees = [
  140. {
  141. fieldName: "common",
  142. tenderTotalFee: totalPrice,
  143. tenderUnitFee: unitPrice,
  144. totalFee: totalPrice,
  145. unitFee: unitPrice,
  146. },
  147. ];
  148. data.calcFlag = treeNodeCalcFlag.customUnitPrice;
  149. }
  150. //获取data的父节点链,成为兄弟节点,只能在父链里找前兄弟(不能跨父链)
  151. let parents = getParents(lastData);
  152. let preData = findLast(parents, (x) => x.depth === depth);
  153. if (preData) {
  154. preData.NextSiblingID = data.ID;
  155. data.ParentID = preData.ParentID;
  156. data.parent = preData.parent;
  157. } else {
  158. data.ParentID = lastData.ID;
  159. data.parent = lastData;
  160. }
  161. rst.push(data);
  162. }
  163. console.log(rst);
  164. return rst;
  165. function getDepth(code) {
  166. if (!code) {
  167. return 1;
  168. }
  169. let match = code.match(/\s/g);
  170. return match ? match.length + 1 : 1;
  171. }
  172. }
  173. function getParents(data) {
  174. let rst = [];
  175. let parent = data.parent;
  176. while (parent) {
  177. rst.push(parent);
  178. parent = parent.parent;
  179. }
  180. rst.push(data);
  181. return rst;
  182. }
  183. //获取编号前缀: 101-1 => 101 101-1-1 => 101-1
  184. function getPrefix(v) {
  185. if (!v) {
  186. return null;
  187. }
  188. let reg = /(.*)-/;
  189. let match = reg.exec(v);
  190. return match ? match[1] : null;
  191. }
  192. // 示例列映射
  193. const slColMap = { code: 0, name: 1, unit: 2, quantity: 3, unitPrice: 4 };
  194. function isValidGCLExRow(rowData) {
  195. if (rowData[0] && /编制[::]/.test(rowData[0].value)) {
  196. return false;
  197. }
  198. if (rowData[1] && /合计/.test(rowData[1].value)) {
  199. return false;
  200. }
  201. if (
  202. (!rowData[slColMap.code] || !rowData[slColMap.code].value) &&
  203. (!rowData[slColMap.name] || !rowData[slColMap.name].value) &&
  204. (!rowData[slColMap.unit] || !rowData[slColMap.unit].value) &&
  205. (!rowData[slColMap.quantity] || !rowData[slColMap.quantity].value)
  206. ) {
  207. return false;
  208. }
  209. return true;
  210. }
  211. // 将“工程量清单预算表”去掉表头表尾,并转换成清单示例表。
  212. // 工程量清单预算表的格式可参考需求:BUG #3037
  213. function transformGCLExToSL(sheetData) {
  214. const rst = {
  215. data: { dataTable: [] },
  216. rowCount: 0,
  217. };
  218. const dataTable = sheetData.data.dataTable;
  219. const rowCount = sheetData.rows.length;
  220. let preRootName;
  221. for (let row = 0; row < rowCount; row++) {
  222. const rowData = dataTable[row];
  223. if (isGCLExtendHead(rowData, dataTable[row + 1])) {
  224. const rootRowdata = dataTable[row + 3];
  225. const name = rootRowdata[0].value;
  226. if (name) {
  227. const rootName = name.replace("工程量清单", "清单");
  228. if (rootName !== preRootName) {
  229. preRootName = rootName;
  230. rst.data.dataTable.push({
  231. [slColMap.name]: { value: rootName },
  232. });
  233. }
  234. }
  235. row += 4;
  236. continue;
  237. }
  238. if (isValidGCLExRow(rowData)) {
  239. const cellData = {
  240. [slColMap.code]: { value: (rowData[slColMap.code] && rowData[slColMap.code].value) || null },
  241. [slColMap.name]: { value: (rowData[slColMap.name] && rowData[slColMap.name].value) || null },
  242. [slColMap.unit]: { value: (rowData[slColMap.unit] && rowData[slColMap.unit].value) || null },
  243. [slColMap.quantity]: { value: (rowData[slColMap.quantity] && rowData[slColMap.quantity].value) || null },
  244. [slColMap.unitPrice]: { value: (rowData[slColMap.unitPrice] && rowData[slColMap.unitPrice].value) || null },
  245. };
  246. rst.data.dataTable.push(cellData);
  247. }
  248. }
  249. rst.rowCount = rst.data.dataTable.length;
  250. return rst;
  251. }
  252. //提取清单示例数据
  253. function extractSLDatas(sheetData) {
  254. let dataTable = sheetData.data.dataTable,
  255. rowCount = sheetData.rows.length;
  256. let rst = [];
  257. let curRoot = null;
  258. for (let row = 0; row < rowCount; row++) {
  259. let code = dataTable[row][slColMap.code] && dataTable[row][slColMap.code].value ? String(dataTable[row][slColMap.code].value).trim() : null,
  260. name = dataTable[row][slColMap.name] ? _deNR(dataTable[row][slColMap.name].value) : null,
  261. unit = dataTable[row][slColMap.unit] ? dataTable[row][slColMap.unit].value : null,
  262. quantity = dataTable[row][slColMap.quantity] ? dataTable[row][slColMap.quantity].value : null,
  263. unitPrice = dataTable[row][slColMap.unitPrice] ? dataTable[row][slColMap.unitPrice].value : null;
  264. if (!code) {
  265. //没有编号的数据,名称必须为:清单 第xx章,认为新的表根节点
  266. const reg = /清单\s+第[^章]+章/;
  267. //if (name && /清单 第\d+章/.test(name)) {
  268. if (name && reg.test(name)) {
  269. curRoot = {
  270. code: null,
  271. name: name,
  272. ID: uuid.v1(),
  273. ParentID: -1,
  274. NextSiblingID: -1,
  275. parent: null,
  276. unitPriceAnalysis: 1,
  277. };
  278. rst.push(curRoot);
  279. } else {
  280. curRoot = null;
  281. }
  282. } else if (!curRoot) {
  283. //根节点为无效根节点,其下子数据全部过滤掉
  284. continue;
  285. } else {
  286. //有code且有有效表根节点
  287. let prefix = getPrefix(code);
  288. let data = {
  289. code: code,
  290. name: name,
  291. unit: unit,
  292. quantity: quantity,
  293. quantityEXP: "" + quantity,
  294. ID: uuid.v1(),
  295. NextSiblingID: -1,
  296. unitPriceAnalysis: 1,
  297. };
  298. if (+unitPrice && +quantity) {
  299. unitPrice = scMathUtil.roundForObj(unitPrice, decimalObj.bills.unitPrice);
  300. const totalPrice = scMathUtil.roundForObj(unitPrice * +quantity, decimalObj.bills.totalPrice);
  301. data.fees = [
  302. {
  303. fieldName: "common",
  304. tenderTotalFee: totalPrice,
  305. tenderUnitFee: unitPrice,
  306. totalFee: totalPrice,
  307. unitFee: unitPrice,
  308. },
  309. ];
  310. data.calcFlag = treeNodeCalcFlag.customUnitPrice;
  311. }
  312. let lastData = rst[rst.length - 1];
  313. let parents = getParents(lastData);
  314. //某数据编号为此数据的前缀,则某数据为此数据的父节点
  315. let parentData = findLast(parents, (x) => prefix === x.code);
  316. if (!parentData && prefix === "") {
  317. // -x的数据,在父链上找不到编号与prefix相同的数据时,父链上-x的数据,则这两数据为兄弟节点,没有则上一行数据为其父节点
  318. let samePrefixData = findLast(parents, (x) => getPrefix(x.code) === prefix);
  319. parentData = samePrefixData ? samePrefixData.parent : lastData;
  320. } else if (!parentData && prefix !== "") {
  321. //不是-x的数据,在父链上找不到编号与prefix相同的数据时,表根节点为其父节点
  322. parentData = curRoot;
  323. }
  324. data.ParentID = parentData.ID;
  325. data.parent = parentData;
  326. let preData = findLast(parents, (x) => x.ParentID === data.ParentID);
  327. if (preData) {
  328. preData.NextSiblingID = data.ID;
  329. }
  330. rst.push(data);
  331. }
  332. }
  333. console.log(rst);
  334. return rst;
  335. }
  336. function extactDatas(sheets) {
  337. let rst = [];
  338. let curSheetType = null;
  339. for (let sheetName in sheets) {
  340. let sheetData = sheets[sheetName];
  341. if (!sheetData.data.dataTable || sheetData.index !== 0) {
  342. continue;
  343. }
  344. let sheetType = getFileType(sheetData);
  345. if (curSheetType !== null && sheetType !== curSheetType) {
  346. throw "excel文件中存在不同格式的表格。";
  347. }
  348. curSheetType = sheetType;
  349. let colMapping = getColMapping(sheetType);
  350. let datas = [];
  351. if (sheetType === fileType.gcl) {
  352. datas = extractGCLDatas(sheetData, colMapping);
  353. } else if (sheetType === fileType.qdsl) {
  354. datas = extractSLDatas(sheetData, colMapping);
  355. } else {
  356. const slSheetData = transformGCLExToSL(sheetData);
  357. datas = extractSLDatas(slSheetData, colMapping);
  358. }
  359. rst = rst.concat(datas);
  360. }
  361. //编号去除空格 清除多余数据 设置数据
  362. for (let data of rst) {
  363. if (data.code && typeof data.code === "string") {
  364. data.code = data.code.replace(/\s/g, "");
  365. }
  366. if (data.unit === "㎡") {
  367. data.unit = "m2";
  368. } else if (data.unit === "m³") {
  369. data.unit = "m3";
  370. }
  371. data.projectID = projectObj.project.ID();
  372. data.type = billType.BILL;
  373. delete data.parent;
  374. delete data.depth;
  375. }
  376. //将表根节点的ParentID设置成第100章至700章清单的ID
  377. let fixedBill = projectObj.project.Bills.tree.roots.find(
  378. (node) => node.data && node.data.flagsIndex && node.data.flagsIndex.fixed && node.data.flagsIndex.fixed.flag === fixedFlag.ONE_SEVEN_BILLS
  379. );
  380. let rootDatas = rst.filter((data) => data.ParentID === -1);
  381. for (let root of rootDatas) {
  382. root.ParentID = fixedBill.data.ID;
  383. }
  384. //清单 第100章 总则清单需要加上固定ID
  385. let oneHundredBills = rootDatas.find((data) => data.name && /第100章/.test(data.name));
  386. if (oneHundredBills) {
  387. oneHundredBills.flags = [{ flag: fixedFlag.ONE_HUNDRED_BILLS, fieldName: "fixed" }];
  388. }
  389. return rst;
  390. }
  391. // 获取需要删除的D
  392. function getRemoveIDs(insertBills) {
  393. const rBillIDs = [];
  394. const rRationIDs = [];
  395. const rst = { rBillIDs, rRationIDs };
  396. if (!insertBills || !insertBills.length) {
  397. return rst;
  398. }
  399. const flag = fixedFlag.ONE_SEVEN_BILLS;
  400. const fixedNode = calcTools.getNodeByFlag(flag);
  401. if (!fixedNode) {
  402. return rst;
  403. }
  404. fixedNode.getPosterity().forEach(node => {
  405. if (node.sourceType === 'bills') {
  406. rBillIDs.push(node.data.ID);
  407. } else {
  408. rRationIDs.push(node.data.ID);
  409. }
  410. });
  411. return rst;
  412. }
  413. return { extactDatas, getRemoveIDs };
  414. })();