weapp_contract_controller.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. 'use strict';
  2. const contractConst = require('../const/contract');
  3. const { buildCatalog } = require('../lib/weapp_contract_catalog');
  4. const moment = require('moment');
  5. async function resolveContractScope(ctx) {
  6. const spid = typeof ctx.query.spid === 'string' ? ctx.query.spid.trim() : '';
  7. const tid = ctx.query.tid === undefined ? null : Number(ctx.query.tid);
  8. if (!spid) throw '缺少项目ID';
  9. if (tid !== null && (!Number.isSafeInteger(tid) || tid <= 0)) throw '标段ID错误';
  10. const subProject = await ctx.service.subProject.getDataById(spid);
  11. if (!subProject || subProject.is_delete || subProject.is_folder ||
  12. String(subProject.project_id) !== String(ctx.session.sessionProject.id)) {
  13. throw '项目不存在或无权查看';
  14. }
  15. subProject.page_show = ctx.service.subProject.getPageShow(subProject.page_show);
  16. if (tid ? !subProject.page_show.openTenderContract : !subProject.page_show.openContract) {
  17. throw tid ? '该项目未开启标段合同管理' : '该项目未开启合同管理';
  18. }
  19. const user = ctx.session.sessionUser;
  20. if (user.is_admin) {
  21. subProject.permission = ctx.service.subProjPermission.adminPermission;
  22. } else {
  23. const permission = await ctx.service.subProjPermission.getSubProjectUserPermission(spid, user.accountId);
  24. if (!permission || (!tid && ![3, 4, 5].some(value => permission.contract_permission.includes(value)))) {
  25. throw '当前账号权限不足,请联系管理员添加权限';
  26. }
  27. subProject.permission = permission;
  28. }
  29. ctx.subProject = subProject;
  30. if (!tid) return { scope: { spid }, info: subProject, user };
  31. const tender = await ctx.service.tender.getDataById(tid);
  32. if (!tender || String(tender.spid) !== spid ||
  33. String(tender.project_id) !== String(ctx.session.sessionProject.id)) {
  34. throw '标段不存在或无权查看';
  35. }
  36. if (!user.is_admin) {
  37. const audit = await ctx.service.contractAudit.getDataByCondition({ tid, uid: user.accountId });
  38. if (!audit) throw '当前账号没有该标段的合同权限';
  39. }
  40. return { scope: { tid }, info: tender, user };
  41. }
  42. async function resolveVisibleContract(ctx) {
  43. const type = ctx.query.type;
  44. const cid = typeof ctx.query.cid === 'string' ? ctx.query.cid.trim() : '';
  45. if (!['expenses', 'income'].includes(type) || !cid) throw '合同参数错误';
  46. const { scope, user } = await resolveContractScope(ctx);
  47. const contractType = contractConst.type[type];
  48. const contracts = await ctx.service.contract.getListByUsers({ ...scope, contract_type: contractType }, user);
  49. const contract = contracts.find(item => String(item.id) === cid);
  50. if (!contract) throw '合同不存在或无权查看';
  51. return { scope, contractType, contract };
  52. }
  53. const fileMimeTypes = {
  54. pdf: 'application/pdf',
  55. doc: 'application/msword',
  56. docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  57. xls: 'application/vnd.ms-excel',
  58. xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  59. ppt: 'application/vnd.ms-powerpoint',
  60. pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  61. jpg: 'image/jpeg',
  62. jpeg: 'image/jpeg',
  63. png: 'image/png',
  64. gif: 'image/gif',
  65. };
  66. module.exports = app => {
  67. class WeappContractController extends app.BaseController {
  68. async tenderList(ctx) {
  69. try {
  70. const spid = typeof ctx.query.spid === 'string' ? ctx.query.spid.trim() : '';
  71. if (!spid) throw '缺少项目ID';
  72. const subProject = await ctx.service.subProject.getDataById(spid);
  73. if (!subProject || subProject.is_delete || subProject.is_folder ||
  74. String(subProject.project_id) !== String(ctx.session.sessionProject.id)) {
  75. throw '项目不存在或无权查看';
  76. }
  77. subProject.page_show = ctx.service.subProject.getPageShow(subProject.page_show);
  78. if (!subProject.page_show.openTenderContract) throw '该项目未开启标段合同管理';
  79. const user = ctx.session.sessionUser;
  80. if (!user.is_admin) {
  81. const permission = await ctx.service.subProjPermission.getSubProjectUserPermission(spid, user.accountId);
  82. if (!permission) throw '您无权查看该项目';
  83. subProject.permission = permission;
  84. }
  85. ctx.subProject = subProject;
  86. const accountInfo = await ctx.service.projectAccount.getDataById(user.accountId);
  87. const tenderList = await ctx.service.tender.getContractList('', null, user.is_admin ? 1 : 0);
  88. const categoryData = await ctx.service.category.getAllCategory(subProject);
  89. ctx.body = {
  90. code: 0, msg: '', data: {
  91. tenderList,
  92. categoryData,
  93. selfCategoryLevel: accountInfo ? accountInfo.self_category_level : '',
  94. },
  95. };
  96. } catch (error) {
  97. this.log(error);
  98. ctx.body = { code: -1, msg: error.toString(), data: null };
  99. }
  100. }
  101. async catalog(ctx) {
  102. try {
  103. const type = ctx.query.type;
  104. if (!['expenses', 'income'].includes(type)) throw '合同目录参数错误';
  105. const contractType = contractConst.type[type];
  106. const { scope, info, user } = await resolveContractScope(ctx);
  107. const options = { ...scope, contract_type: contractType };
  108. // Web 端首次打开详情时也会按项目模板初始化目录。
  109. await ctx.service.contractTree.insertTree(scope, info);
  110. const folders = await ctx.service.contractTree.getAllDataByCondition({ where: options });
  111. const contracts = await ctx.service.contract.getListByUsers(options, user);
  112. const contractIds = [...new Set(contracts.map(contract => contract.id))];
  113. const fileCounts = await ctx.service.contractAtt.getCounts(contractIds);
  114. for (const contract of contracts) {
  115. contract.fileCount = fileCounts.get(String(contract.id)) || 0;
  116. }
  117. ctx.body = { code: 0, msg: '', data: { nodes: buildCatalog(folders, contracts) } };
  118. } catch (error) {
  119. this.log(error);
  120. ctx.body = { code: -1, msg: error.toString(), data: null };
  121. }
  122. }
  123. async summary(ctx) {
  124. try {
  125. const { scope, user } = await resolveContractScope(ctx);
  126. const now = moment().startOf('month');
  127. const months = Array.from({ length: 6 }, (_, index) => now.clone().subtract(5 - index, 'months').format('YYYY-MM'));
  128. const start = now.clone().subtract(5, 'months').format('YYYY-MM-DD HH:mm:ss');
  129. const end = now.clone().add(1, 'month').format('YYYY-MM-DD HH:mm:ss');
  130. const summarize = async contractType => {
  131. const visibleContracts = await ctx.service.contract.getListByUsers({ ...scope, contract_type: contractType }, user);
  132. const contracts = [...new Map(visibleContracts.map(item => [item.id, item])).values()];
  133. const total = contracts.reduce((sum, item) => ctx.helper.add(sum, item.total_price || 0), 0);
  134. const settled = contracts.reduce((sum, item) => ctx.helper.add(sum, item.yf_price || 0), 0);
  135. const paid = contracts.reduce((sum, item) => ctx.helper.add(sum, item.sf_price || 0), 0);
  136. const unpaid = ctx.helper.sub(total, paid);
  137. const percent = total ? ctx.helper.round(ctx.helper.div(settled, total) * 100, 2) : 0;
  138. const ids = [...new Set(contracts.map(item => item.id))];
  139. const monthly = await ctx.service.contractPay.getMonthlyYfPrice(ids, start, end);
  140. const byMonth = new Map(monthly.map(item => [item.month, item.amount]));
  141. return { total, paid, unpaid, percent, trend: months.map(month => byMonth.get(month) || 0) };
  142. };
  143. const [expenses, income] = await Promise.all([
  144. summarize(contractConst.type.expenses),
  145. summarize(contractConst.type.income),
  146. ]);
  147. ctx.body = { code: 0, msg: '', data: { months, expenses, income } };
  148. } catch (error) {
  149. this.log(error);
  150. ctx.body = { code: -1, msg: error.toString(), data: null };
  151. }
  152. }
  153. async detail(ctx) {
  154. try {
  155. const { scope, contractType, contract } = await resolveVisibleContract(ctx);
  156. const where = { ...scope, contract_type: contractType, cid: contract.id };
  157. const [pays, files] = await Promise.all([
  158. ctx.service.contractPay.getAllDataByCondition({ where, orders: [['create_time', 'desc']] }),
  159. ctx.service.contractAtt.getAllDataByCondition({ where, orders: [['upload_time', 'desc']] }),
  160. ]);
  161. ctx.body = {
  162. code: 0, msg: '', data: {
  163. contract: {
  164. id: contract.id,
  165. c_code: contract.c_code,
  166. name: contract.name,
  167. create_time: contract.create_time,
  168. total_price: contract.total_price,
  169. pay_price: contract.pay_price,
  170. debit_price: contract.debit_price,
  171. yf_price: contract.yf_price,
  172. sf_price: contract.sf_price,
  173. party_a: contract.party_a,
  174. party_a_user: contract.party_a_user,
  175. party_b: contract.party_b,
  176. party_b_user: contract.party_b_user,
  177. sign_date: contract.sign_date,
  178. address: contract.address,
  179. entity: contract.entity,
  180. bank: contract.bank,
  181. bank_account: contract.bank_account,
  182. settle_code: contract.settle_code,
  183. remark: contract.remark,
  184. },
  185. pays: pays.map(pay => ({
  186. id: pay.id,
  187. pay_type: pay.pay_type,
  188. pay_time: pay.pay_time,
  189. sf_price: pay.sf_price,
  190. })),
  191. files: files.map(file => ({
  192. id: file.id,
  193. filename: file.filename,
  194. filesize: file.filesize,
  195. fileext: file.fileext,
  196. })),
  197. },
  198. };
  199. } catch (error) {
  200. this.log(error);
  201. ctx.body = { code: -1, msg: error.toString(), data: null };
  202. }
  203. }
  204. async downloadFile(ctx) {
  205. try {
  206. const { scope, contractType, contract } = await resolveVisibleContract(ctx);
  207. const file = await ctx.service.contractAtt.getDataById(ctx.params.fid);
  208. if (!file || String(file.cid) !== String(contract.id) ||
  209. Number(file.contract_type) !== contractType ||
  210. (scope.tid && Number(file.tid) !== scope.tid) ||
  211. (scope.spid && String(file.spid) !== scope.spid)) {
  212. throw '文件不存在或无权查看';
  213. }
  214. const ext = String(file.fileext || '').replace(/^\./, '').toLowerCase();
  215. ctx.response.set({
  216. 'Content-Type': fileMimeTypes[ext] || 'application/octet-stream',
  217. 'Content-Disposition': `inline; filename*=UTF-8''${encodeURIComponent(file.filename || 'file')}`,
  218. });
  219. ctx.body = await ctx.helper.ossFileGet(file.filepath);
  220. } catch (error) {
  221. this.log(error);
  222. ctx.status = 404;
  223. ctx.body = { code: -1, msg: error.toString(), data: null };
  224. }
  225. }
  226. }
  227. return WeappContractController;
  228. };