'use strict'; const { AppParamError, AppInputError } = require('../helpers/app_error'), { UserTypes, Errors, InvoiceStatus, Units, ExportType, InvCreateOption } = require('../helpers/constants'), { JobInvoiceStatus } = require('../helpers/job_constants'), { User, Job, Invoice, InvoiceSetting, Client } = require('../model'), utils = require('../helpers/utils'), randomString = require('randomstring'), moment = require('moment'), BigNumber = require('bignumber.js'), { toFixedNumber, getPercentValue } = require('../helpers/utils'), { Parser } = require('@json2csv/plainjs'), { string: stringFormatterCtor } = require('@json2csv/formatters'), ObjectId = require('mongodb').ObjectId, mongoUtil = require('../helpers/mongo'), assert = require('assert'), path = require('path'), { flattenDeep } = require('lodash'); function getParamsUpdateJobs(invoice) { assert(!utils.isEmptyObj(invoice), AppInputError.create()); const newJobInvoiceStatus = invoice.status !== InvoiceStatus.Void ? JobInvoiceStatus.INVOICED : JobInvoiceStatus.NONE; if (newJobInvoiceStatus === JobInvoiceStatus.NONE) return { $set: { invoiceStatus: newJobInvoiceStatus }, $unset: { invoiceId: undefined } }; return { $set: { invoiceStatus: newJobInvoiceStatus, invoiceId: invoice._id } } } function generateRandomInvoiceCode(countInvoice) { const prefix = randomString.generate({ length: 6, capitalization: 'uppercase', charset: 'alphabetic' }); const year = new Date().getFullYear() % 100; const invoiceUnique = utils.padZero(countInvoice, 6) const preInvoiceCode = `${prefix}-${year}-${invoiceUnique}`; return preInvoiceCode; } async function getInvoices_get(req, res) { const puid = req.userInfo?.puid; if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID); const applicator = await User.findById(puid, 'name address phone email', { lean: true }); if (!applicator) AppParamError.throw(Errors.APPLICATOR_NOT_FOUND); const isClientRole = req.ut === UserTypes.CLIENT; const userId = req.uid; const filter = { byPuid: puid }; if (isClientRole) filter['clients.billTo'] = userId; const invoices = await Invoice.find(filter) .populate({ path: 'clients.billTo', select: 'name email -kind' }) .select('code status createdAt openDate dueDate paymentTerm clients.subTotal clients.discount clients.split clients.taxRate'); return res.json(invoices); } async function createInvoice_post(req, res) { const invoiceBody = req.body; const puid = req.userInfo?.puid; assert(invoiceBody, AppParamError.create()); if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID); // Check whether clients are existed in database const clientIds = invoiceBody?.clients?.map(client => client?.billTo); const numOfFoundClients = await Client.countDocuments({ _id: { $in: clientIds } }); assert(!utils.isEmptyArray(clientIds) && numOfFoundClients === clientIds.length, AppParamError.create(Errors.CLIENTS_NOT_FOUND)); // Check whether jobs are existed in database const jobIds = invoiceBody?.jobs?.map(job => job?.job); const numOfFoundJobs = await Job.countDocuments({ _id: { $in: jobIds } }); assert(!utils.isEmptyArray(jobIds) && numOfFoundJobs === jobIds.length, AppParamError.create(Errors.JOBS_NOT_FOUND)); const applicator = await User.findById(puid); if (!applicator) AppParamError.throw(Errors.APPLICATOR_NOT_FOUND); const currentTotalInvoice = await Invoice.countDocuments({ byPuid: puid }); const invoiceCode = generateRandomInvoiceCode(currentTotalInvoice); const clients = invoiceBody?.clients?.map((client, index) => { client.code = invoiceCode + '-' + Number(index + 1); client.logo = client.logo ? path.basename(client.logo) : client.logo; if (!utils.isNumber(client.paymentTerm)) client.paymentTerm = invoiceBody.paymentTerm; return client; }); invoiceBody.clients = clients; invoiceBody.logo = invoiceBody.logo ? path.basename(invoiceBody.logo) : invoiceBody.logo; const newInvoice = new Invoice({ ...invoiceBody, code: invoiceCode, byPuid: puid, createdBy: req.uid }); let createdInvoice; await mongoUtil.runInTransaction(async (session) => { createdInvoice = await newInvoice.save({ session }); if (jobIds.length) { await Job.updateMany({ _id: { $in: jobIds } }, getParamsUpdateJobs(createdInvoice), { session }); } }); res.json(createdInvoice); } async function getInvoiceIssuerInfo(invoice, defaultInvIssuerSetting, parentUser) { assert(!utils.isEmptyObj(invoice) && !utils.isEmptyObj(parentUser) && utils.isObjectId(parentUser?._id), AppInputError.create()); const invoiceObject = invoice.toObject(); const clientsInvInfo = []; for (const client of invoiceObject?.clients) { let clientInvIssuerSetting = await InvoiceSetting.findOne({ userId: { $eq: client?.billTo?._id }, byPuid: parentUser._id }); clientInvIssuerSetting = clientInvIssuerSetting?.toObject(); clientsInvInfo.push({ ...client, issuerCompanyName: clientInvIssuerSetting ? clientInvIssuerSetting.companyName : defaultInvIssuerSetting?.companyName || parentUser?.name, issuerAddress: clientInvIssuerSetting ? clientInvIssuerSetting.address : defaultInvIssuerSetting?.address || parentUser?.address, issuerPhone: parentUser?.phone, issuerEmail: parentUser?.email, clientName: client?.billTo?.name, clientAddress: client?.billTo?.address, clientPhone: client?.billTo?.phone, clientEmail: client?.billTo?.email, paymentTerm: defaultInvIssuerSetting.paymentTerm, logo: clientInvIssuerSetting ? clientInvIssuerSetting.logo : defaultInvIssuerSetting?.logo }); } invoiceObject.clients = clientsInvInfo; return invoiceObject; } async function getInvoiceById_get(req, res) { const invoiceId = req.params.id; const puid = req.userInfo?.puid; if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID); const applicator = await User.findById(puid, 'name address phone email', { lean: true }); if (!applicator) AppParamError.throw(Errors.APPLICATOR_NOT_FOUND); const isClientUser = (req.ut === UserTypes.CLIENT); const userId = req.uid; const option = { _id: invoiceId, byPuid: puid }; if (isClientUser) option['clients.billTo'] = userId; let invoice = await Invoice.findOne(option).populate({ path: 'clients.billTo', select: 'name email address phone' }); if (!invoice) AppParamError.throw(Errors.INVOICE_NOT_FOUND); if (isClientUser) { const clients = invoice?.clients?.filter((client) => client?.billTo?._id.equals(ObjectId(userId))); invoice.clients = clients; } const defaultInvIssuerSetting = await InvoiceSetting.findOne({ userId: { $eq: null }, byPuid: puid }); invoice = await getInvoiceIssuerInfo(invoice, defaultInvIssuerSetting, applicator); res.json(invoice); } async function getPrintInformation_get(req, res) { const query = req.query; const puid = req.userInfo?.puid; const { invoiceId, clientId } = query; if (!puid || !ObjectId.isValid(puid) || !ObjectId.isValid(invoiceId) || !ObjectId.isValid(clientId)) AppParamError.throw(); const invoice = await Invoice.findOne({ 'clients.billTo': ObjectId(clientId), _id: ObjectId(invoiceId), byPuid: ObjectId(puid) }) .populate({ path: 'clients.billTo', select: 'name address phone email' }); if (!invoice) AppParamError.throw(Errors.INVOICE_NOT_FOUND); const invoiceObject = invoice.toObject(); const client = invoiceObject.clients?.find((e) => e.billTo?._id?.equals(ObjectId(clientId))); delete invoiceObject.clients; const applicator = await User.findById(puid, '-password', { lean: true }); let defaultInvIssuerSetting = await InvoiceSetting.findOne({ userId: { $eq: null }, byPuid: puid }); defaultInvIssuerSetting = defaultInvIssuerSetting?.toObject(); let clientInvIssuerSetting = await InvoiceSetting.findOne({ userId: { $eq: client?.billTo?._id }, byPuid: puid }); clientInvIssuerSetting = clientInvIssuerSetting?.toObject(); invoiceObject.client = { ...client, issuerCompanyName: clientInvIssuerSetting ? clientInvIssuerSetting.companyName : defaultInvIssuerSetting?.companyName || applicator?.name, issuerAddress: clientInvIssuerSetting ? clientInvIssuerSetting.address : defaultInvIssuerSetting?.address || applicator?.address, issuerPhone: applicator?.phone, issuerEmail: applicator?.email, clientName: client?.billTo?.name, clientAddress: client?.billTo?.address, clientPhone: client?.billTo?.phone, clientEmail: client?.billTo?.email, paymentTerm: defaultInvIssuerSetting.paymentTerm, logo: clientInvIssuerSetting ? clientInvIssuerSetting.logo : defaultInvIssuerSetting?.logo }; res.json(invoiceObject); } const costingUnit = Object.entries(Units).reduce((a, b) => { a[b[1].toString()] = b[0]; return a; }, {}); function getUnitName(unit) { return costingUnit[unit?.toString()?.toLowerCase()]?.toLowerCase() ?? ''; } async function calInvoiceExport(req, invoiceIds) { const puid = req.userInfo?.puid; if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID); const queryPrams = { _id: { $in: invoiceIds }, byPuid: puid }; if (req.ut === UserTypes.CLIENT) queryPrams['clients.billTo'] = req.uid; const invoices = await Invoice.find(queryPrams) .populate({ path: 'clients.billTo', select: 'name address phone email' }) .lean(); if (!invoices.length) AppParamError.throw(Errors.INVOICE_NOT_FOUND); const dataExport = invoices.map(invoiceObject => { if (req.ut === UserTypes.CLIENT) invoiceObject.clients = invoiceObject?.clients?.filter((e) => e.billTo._id.toString() === req.uid); return invoiceObject?.clients?.map(client => invoiceObject?.jobs?.map((job, indexJob) => job?.costings?.items?.map((item, indexItem) => { const isFirstItem = indexJob === 0 && indexItem === 0; const commonValueField = (val) => (isFirstItem ? val : ''); const itemAmountOrigin = BigNumber(item?.quantity).multipliedBy(item?.price).multipliedBy(getPercentValue(client?.split)); const taxAmount = toFixedNumber(BigNumber(itemAmountOrigin).multipliedBy(getPercentValue(client?.taxRate))); return { // common field customer: commonValueField(client?.billTo?.name), invoiceDate: commonValueField(moment(invoiceObject.openDate).format('YYYY-MM-DD')), dueDate: commonValueField(moment(invoiceObject.dueDate).format('YYYY-MM-DD')), terms: commonValueField(invoiceObject.createOp === InvCreateOption.Option1 ? `Net ${invoiceObject.paymentTerm}` : 'Due on receipt'), memo: '', // field of costing item invoiceNo: client?.code, product: `${item?.name} (${getUnitName(item?.unit)})`, itemQuantity: BigNumber(item?.quantity ?? 0) .multipliedBy(getPercentValue(client?.split)) .toFixed(2), rate: item?.price, amount: toFixedNumber(itemAmountOrigin), taxCode: `${client?.taxRate ?? 0}%`, taxAmount: taxAmount, currency: job.costings.currency, // only for IIF quantity: item?.quantity, clientEmail: client?.billTo?.email, clientName: client?.billTo?.name, clientAddress: client?.billTo?.address, amountDue: client?.amountDue, costingName: item?.name, unit: item?.unit, discount: client?.discount, clientNo: client?.code, poNumber: invoiceObject.poNumber, split: client?.split, openDate_iif: moment(invoiceObject.openDate).format('MM/DD/YY'), dueDate_iif: moment(invoiceObject.dueDate).format('MM/DD/YY'), costingId: item?.item?.toString(), clientId: client?.billTo?._id.toString(), invoiceOpenDate: invoiceObject.openDate, invoiceDueDate: invoiceObject.dueDate, }; }) ) ); }); return dataExport; } async function geInvoiceCsv(req, invoiceIds) { const dataExport = flattenDeep(await calInvoiceExport(req, invoiceIds)); const parser = new Parser({ fields: [ { value: 'invoiceNo', label: '*InvoiceNo' }, { value: 'customer', label: '*Customer' }, { value: 'invoiceDate', label: '*InvoiceDate' }, { value: 'dueDate', label: '*DueDate' }, { value: 'terms', label: 'Terms' }, { value: 'empty', label: 'Location' }, { value: 'memo', label: 'Memo' }, { value: 'product', label: 'Item(Product/Service)' }, { value: 'empty', label: 'ItemDescription' }, { value: 'itemQuantity', label: 'ItemQuantity' }, { value: 'rate', label: 'ItemRate' }, { value: 'amount', label: '*ItemAmount' }, { value: 'taxCode', label: '*ItemTaxCode' }, { value: 'taxAmount', label: 'ItemTaxAmount' }, { value: 'currency', label: 'Currency' }, { value: 'empty', label: 'Service Date' }, ], delimiter: ',', formatters: { string: stringFormatterCtor({ quote: '' }) }, }); return parser.parse(dataExport); } async function geInvoiceIIF(req, invoiceIds) { const invoices = await calInvoiceExport(req, invoiceIds); const rows = []; const invitems = []; const invitemsLast = []; const listTrans = []; for (const invoice of invoices) { for (const jobs of invoice) { const trans = []; const custs = []; const allCostings = jobs?.flat() ?? []; // jobs const client = jobs?.[0]?.[0] ?? {}; const { openDate_iif = '', dueDate_iif = '', invoiceOpenDate, invoiceDueDate, taxCode, amountDue, discount } = client; const clientName = `"${client.clientName ?? ''}"`; const addresses = Array(5).fill(''); !(utils.isEmpty(client?.clientAddress)) && (addresses[0] = client.clientAddress); const totalAmountTrans = allCostings .reduce( (a, b) => a.minus( BigNumber(b.rate ?? 0).multipliedBy( BigNumber(b.quantity ?? 0) .multipliedBy(getPercentValue(client.split)) .toFixed(2) ) ), BigNumber(0) ) .toFixed(2); const totalDiscount = BigNumber(totalAmountTrans).multipliedBy(-1).multipliedBy(discount.replace('%', '')).dividedBy(100).toFixed(2); const totalTax = BigNumber(totalAmountTrans).plus(totalDiscount).multipliedBy(taxCode.replace('%', '')).dividedBy(100).toFixed(2); const totalLast = BigNumber(totalAmountTrans).plus(totalDiscount).plus(totalTax).multipliedBy(-1).toFixed(2); const isTax = Number(taxCode.replace('%', '')) > 0 ? 'Y' : 'N'; const isPaid = !Number(amountDue ?? 1) ? 'Y' : 'N'; // push to list custs.push({ name: clientName }); // field Invoice trans.push([['TRNS', '', 'INVOICE', openDate_iif, 'Accounts Receivable'], [clientName, '', totalLast, '', client.clientNo], ['N', 'Y', isTax, addresses[0], addresses[1]], [addresses[2], addresses[3], addresses[4], dueDate_iif, `Net ${moment(invoiceOpenDate).diff(invoiceDueDate, 'day')}`], [isPaid, '', '', '', ''], ['', '', client.poNumber ?? '', '', ''], ['', '', '', '', ''], ['', '', '', '', 'N'], ['N']].flat()); for (const costing of allCostings) { if (!costing) { continue; } const { openDate_iif = '', taxCode, rate = '', costingId } = costing; const costingName = `"${costing.costingName ?? ''}"`; const quantity = BigNumber(costing.quantity ?? 0).multipliedBy(getPercentValue(client.split)).multipliedBy(-1).toFixed(2); const amount = BigNumber(rate).multipliedBy(quantity).toFixed(2); // list if (!invitems.find((e) => e.id === costingId)) { invitems.push({ id: costingId, name: costingName, type: 'SERV', desc: getUnitName(costing.unit), accnt: '"Income Account"', price: rate.toString(), taxable: 'Y', saletax: 'Tax', taxVend: '', isNew: 'Y' }); } // field Costing trans.push( [ ['SPL', '', 'INVOICE', openDate_iif, '"Income Account"'], ['', '', amount, '', ''], ['N', quantity, rate, costingName, ''], [Number(taxCode.replace('%', '')) > 0 ? 'Y' : 'N', 'N', 'NOTHING', '', ''], ['', '', '', '', ''], ].flat() ); } if (Number(discount.replace('%', '')) > 0) { trans.push( [ ['SPL', '', 'INVOICE', openDate_iif, '"Sales Discount"'], ['', '', totalDiscount, '', `Discount ${discount.replace('%', '')}%`], ['N', '', `-${discount.replace('%', '')}%`, '"Sales Discount"', ''], ['Y', 'N', 'NOTHING', '', ''], ['', '', '', '', ''], ].flat() ); } trans.push( [ ['SPL', '', 'INVOICE', openDate_iif, '"Sales Tax Payable"'], ['TaxAgencyVendor', '', totalTax, '', `Tax ${taxCode.replace('%', '')}%`], ['N', '', `${taxCode.replace('%', '')}%`, `"Sales Tax ${taxCode.replace('%', '')}%"`, ''], ['N', 'N', 'NOTHING', '', ''], ['', '', '', '', 'AUTOSTAX'], ].flat() ); trans.push('ENDTRNS'); if (!invitemsLast.length) { invitemsLast.push({ name: '"Sales Discount"', type: 'DISC', desc: '', accnt: '"Sales Discount"', price: `-${discount.replace('%', '')}%`, taxable: 'Y', saletax: '', taxVend: '', isNew: 'N' }); } if (!invitemsLast.find((e) => e.price === client.taxCode)) { invitemsLast.push({ name: `"Sales Tax ${client.taxCode}"`, type: 'COMPTAX', desc: '', accnt: '"Sales Tax Payable"', price: client.taxCode, taxable: 'N', saletax: '', taxVend: 'TaxAgencyVendor', isNew: 'Y' }); } // rows.push(['!JOBTYPE', 'NAME', 'REFNUM', 'TIMESTAMP', 'HIDDEN']); listTrans.push( [ ['!CUST', 'NAME', 'REFNUM', 'TIMESTAMP', 'BADDR1'], ['BADDR2', 'BADDR3', 'BADDR4', 'BADDR5', 'SADDR1'], ['SADDR2', 'SADDR3', 'SADDR4', 'SADDR5', 'PHONE1'], ['PHONE2', 'FAXNUM', 'EMAIL', 'NOTE', 'CONT1'], ['CONT2', 'CTYPE', 'TERMS', 'TAXABLE', 'SALESTAXCODE'], ['LIMIT', 'RESALENUM', 'REP', 'TAXITEM', 'NOTEPAD'], ['SALUTATION', 'COMPANYNAME', 'FIRSTNAME', 'MIDINIT', 'LASTNAME'], ['CUSTFLD1', 'CUSTFLD2', 'CUSTFLD3', 'CUSTFLD4', 'CUSTFLD5'], ['CUSTFLD6', 'CUSTFLD7', 'CUSTFLD8', 'CUSTFLD9', 'CUSTFLD10'], ['CUSTFLD11', 'CUSTFLD12', 'CUSTFLD13', 'CUSTFLD14', 'CUSTFLD15'], ['JOBDESC', 'JOBTYPE', 'JOBSTATUS', 'JOBSTART', 'JOBPROJEND'], ['JOBEND', 'HIDDEN', 'DELCOUNT', 'PRICELEVEL'], ].flat() ); for (const cust of custs) { listTrans.push( [ ['CUST', cust.name, '', moment().unix(), ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', 'Y', 'Tax'], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '0', '', ''], ['', 'N', '0', ''], ].flat() ); } listTrans.push([['!TRNS', 'TRNSID', 'TRNSTYPE', 'DATE', 'ACCNT'], ['NAME', 'CLASS', 'AMOUNT', 'DOCNUM', 'MEMO'], ['CLEAR', 'TOPRINT', 'NAMEISTAXABLE', 'ADDR1', 'ADDR2'], ['ADDR3', 'ADDR4', 'ADDR5', 'DUEDATE', 'TERMS'], ['PAID', 'PAYMETH', 'SHIPVIA', 'SHIPDATE', 'OTHER1'], ['REP', 'FOB', 'PONUM', 'INVTITLE', 'INVMEMO'], ['SADDR1', 'SADDR2', 'SADDR3', 'SADDR4', 'SADDR5'], ['PAYITEM', 'YEARTODATE', 'WAGEBASE', 'EXTRA', 'TOSEND'], ['ISAJE']].flat()); listTrans.push( [ ['!SPL', 'SPLID', 'TRNSTYPE', 'DATE', 'ACCNT'], ['NAME', 'CLASS', 'AMOUNT', 'DOCNUM', 'MEMO'], ['CLEAR', 'QNTY', 'PRICE', 'INVITEM', 'PAYMETH'], ['TAXABLE', 'VALADJ', 'REIMBEXP', 'SERVICEDATE', 'OTHER2'], ['OTHER3', 'PAYITEM', 'YEARTODATE', 'WAGEBASE', 'EXTRA'], ].flat() ); listTrans.push('!ENDTRNS'); listTrans.push(...trans); } } rows.push( [ ['!HDR', 'PROD', 'VER', 'REL', 'IIFVER'], ['DATE', 'TIME', 'ACCNTNT', 'ACCNTNTSPLITTIME'], ].flat() ); const currentDate = moment(); rows.push( [ ['HDR', '"QuickBooks Enterprise Solutions"', '"Version 16.0D"', '"Release R7P"', '1'], [currentDate.format('MM/DD/YY'), currentDate.unix(), 'N', '0', ''], ].flat() ); rows.push( [ ['!ACCNT', 'NAME', 'REFNUM', 'TIMESTAMP', 'ACCNTTYPE'], ['OBAMOUNT', 'DESC', 'ACCNUM', 'SCD', 'BANKNUM'], ['EXTRA', 'HIDDEN', 'DELCOUNT', 'USEID', 'WKPAPERREF'], ].flat() ); const accts = [ { name: '"Accounts Receivable"', type: 'AR', accNum: '1200' }, { name: '"Income Account"', type: 'INC', accNum: '' }, { name: '"Sales Discount"', type: 'EXP', accNum: '' }, ]; for (const acc of accts) { rows.push([['ACCNT', acc.name, '', moment().unix()], [acc.type, '', '', acc.accNum, '0'], ['', '', 'N', '0', 'N'], ['']].flat()); } rows.push( [ ['!INVITEM', 'NAME', 'REFNUM', 'TIMESTAMP', 'INVITEMTYPE'], ['DESC', 'PURCHASEDESC', 'ACCNT', 'ASSETACCNT', 'COGSACCNT'], ['QNTY', 'PRICE', 'COST', 'TAXABLE', 'SALESTAXCODE'], ['PAYMETH', 'TAXVEND', 'PREFVEND', 'REORDERPOINT', 'EXTRA'], ['CUSTFLD1', 'CUSTFLD2', 'CUSTFLD3', 'CUSTFLD4', 'CUSTFLD5'], ['DEP_TYPE', 'ISPASSEDTHRU', 'HIDDEN', 'DELCOUNT', 'USEID'], ['ISNEW', 'PO_NUM', 'SERIALNUM', 'WARRANTY', 'LOCATION'], ['VENDOR', 'ASSETDESC', 'SALEDATE', 'SALEEXPENSE', 'NOTES'], ['ASSETNUM', 'COSTBASIS', 'ACCUMDEPR', 'UNRECBASIS', 'PURCHASEDATE'], ].flat() ); for (const invi of [invitems, invitemsLast].flat()) { rows.push( [ ['INVITEM', invi.name, '', moment().unix(), invi.type], [invi.desc, '', invi.accnt, '', ''], ['0', invi.price, '0', invi.taxable, invi.saletax], ['', invi.taxVend, '', '', ''], ['', '', '', '', ''], ['0', 'N', 'N', '0', 'N'], [invi.isNew, '', '', '', ''], ['', '', '', '0', ''], ['', '0', '0', '0', ''], ].flat() ); } rows.push( [ ['!VEND', 'NAME', 'REFNUM', 'TIMESTAMP', 'PRINTAS'], ['ADDR1', 'ADDR2', 'ADDR3', 'ADDR4', 'ADDR5'], ['VTYPE', 'CONT1', 'CONT2', 'PHONE1', 'PHONE2'], ['FAXNUM', 'EMAIL', 'NOTE', 'TAXID', 'LIMIT'], ['TERMS', 'NOTEPAD', 'SALUTATION', 'COMPANYNAME', 'FIRSTNAME'], ['MIDINIT', 'LASTNAME', 'CUSTFLD1', 'CUSTFLD2', 'CUSTFLD3'], ['CUSTFLD4', 'CUSTFLD5', 'CUSTFLD6', 'CUSTFLD7', 'CUSTFLD8'], ['CUSTFLD9', 'CUSTFLD10', 'CUSTFLD11', 'CUSTFLD12', 'CUSTFLD13'], ['CUSTFLD14', 'CUSTFLD15', '1099', 'HIDDEN', 'DELCOUNT'], ].flat() ); rows.push( [ ['VEND', 'TaxAgencyVendor', '', moment().unix(), ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', 'N', 'N', '0'], ].flat() ); rows.push(...listTrans); const dataPrint = rows.map((row) => (Array.isArray(row) ? row.join('\t') : row)).join('\n'); return dataPrint; } async function exportInvoices_get(req, res) { const { invoiceIds, exportType } = req?.query; assert(exportType && !utils.isEmptyArray(invoiceIds)); const baseFilename = `agm_invoice_${moment().format('YYMMDDHHmm')}`; if (exportType === ExportType.IIF) { const iif = await geInvoiceIIF(req, invoiceIds); res.header('Content-Type', 'text/iif'); res.attachment(`${baseFilename}.iif`); res.send(iif); } else { const csv = await geInvoiceCsv(req, invoiceIds); res.header('Content-Type', 'text/csv'); res.attachment(`agm_invoice_${baseFilename}.csv`); res.send(csv); } } async function updateInvoiceById_put(req, res) { const invoiceId = req?.params?.id; const newInvoice = req?.body; const puid = req?.userInfo?.puid; assert(invoiceId && !utils.isEmptyObj(newInvoice), AppParamError.create()); if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID); const applicator = await User.findById(puid, 'name address phone email', { lean: true }); if (!applicator) AppParamError.throw(Errors.APPLICATOR_NOT_FOUND); const theInvoice = await Invoice.findOne({ _id: invoiceId, byPuid: puid }); if (!theInvoice) AppParamError.throw(Errors.INVOICE_NOT_FOUND); newInvoice.clients = newInvoice?.clients?.map((client, index) => ({ ...client, code: client.code ? client.code : theInvoice.code + '-' + Number(index + 1), ...(newInvoice.status === InvoiceStatus.Void && { amountDue: 0 }), logo: client.logo ? path.basename(client.logo) : client.logo })); newInvoice.logo = newInvoice.logo ? path.basename(newInvoice.logo) : newInvoice.logo; newInvoice.updatedBy = req?.uid; let updatedInvoice; await mongoUtil.runInTransaction(async (session) => { const prevJobIds = theInvoice?.jobs?.map(job => job?.job); const curJobIds = newInvoice?.jobs?.map(job => job?.job); updatedInvoice = await Invoice.findByIdAndUpdate(ObjectId(invoiceId), { $set: newInvoice }, { session }); // Update related jobs with invoicing status and invoiceId if (updatedInvoice) { const removedJobIds = prevJobIds?.filter(id => curJobIds && !curJobIds?.includes(id)); const addedJobIds = curJobIds?.filter(id => !prevJobIds?.includes(id)); const sameJobIds = curJobIds?.filter(id => prevJobIds?.includes(id)); if (removedJobIds?.length && newInvoice.status !== InvoiceStatus.Void) { await Job.updateMany({ _id: { $in: removedJobIds } }, { $set: { invoiceStatus: JobInvoiceStatus.NONE }, $unset: { invoiceId: undefined } }, { session }); } if (addedJobIds?.length) { await Job.updateMany({ _id: { $in: addedJobIds } }, getParamsUpdateJobs(updatedInvoice), { session }); } if (sameJobIds?.length || updatedInvoice.status === InvoiceStatus.Void) { const isChangeStatus = (theInvoice.status !== updatedInvoice.status && updatedInvoice.status === InvoiceStatus.Void); if (isChangeStatus) await Job.updateMany({ _id: { $in: sameJobIds } }, getParamsUpdateJobs(updatedInvoice), { session }); } } }); if (updatedInvoice) { const defaultInvIssuerSetting = await InvoiceSetting.findOne({ userId: { $eq: null }, byPuid: puid }); await updatedInvoice.populate({ path: 'clients.billTo', select: 'name email address phone' }); updatedInvoice = await getInvoiceIssuerInfo(updatedInvoice, defaultInvIssuerSetting, applicator); } res.json(updatedInvoice); } async function deleteInvoicesByIds(invoiceIds, puid) { if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID); assert(!utils.isEmptyArray(invoiceIds), AppParamError.create()); const removedInvIds = []; await mongoUtil.runInTransaction(async (session) => { let invJobIds = []; for (const invoiceId of invoiceIds) { const invoice = await Invoice.findOneAndDelete({ _id: invoiceId, byPuid: puid }, { session }); if (invoice && !utils.isEmptyArray(invoice.jobs)) { invJobIds = invJobIds.concat(invoice.jobs.map(j => j.job)); removedInvIds.push({ _id: invoice._id }); } } if (!utils.isEmptyArray(invJobIds)) { await Job.updateMany({ _id: { $in: invJobIds } }, { $set: { invoiceStatus: JobInvoiceStatus.NONE }, $unset: { invoiceId: undefined } }, { session }); } }); return removedInvIds; } async function deleteInvoiceById(req, res) { const removedInvIds = await deleteInvoicesByIds([req.params?.id], req.userInfo?.puid); res.json(removedInvIds.length ? removedInvIds[0] : []); } async function deleteInvoices(req, res) { const removedInvIds = await deleteInvoicesByIds(req?.body?.invoiceIds, req.userInfo?.puid); res.json(removedInvIds); } module.exports = { getInvoices_get, createInvoice_post, getInvoiceById_get, updateInvoiceById_put, deleteInvoiceById, deleteInvoices, getPrintInformation_get, exportInvoices_get, };