agmission/Development/server/controllers/invoice.js

681 lines
28 KiB
JavaScript

'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 isClientRole = req.ut === UserTypes.CLIENT;
const userId = req.uid;
const option = { byPuid: puid };
if (isClientRole) option['clients.billTo'] = userId;
const invoices = await Invoice.find(option)
.populate({ path: 'clients.billTo', select: 'name email address phone' })
.populate({ path: 'createdBy', select: 'name email address phone' })
.populate({ path: 'updatedBy', select: 'name email address phone' });
let checkedInvoices = invoices || [];
if (isClientRole) {
const clientInvoices = checkedStatusInvoices?.map(invoice => {
const clients = invoice.clients?.filter(client => client?.billTo?.id === userId);
invoice.clients = clients;
return invoice;
});
checkedInvoices = clientInvoices;
}
const resultWithApplicatorInfo = [];
for (const invoice of checkedInvoices) {
resultWithApplicatorInfo.push(await getInvoiceWithFullInfo(invoice, puid))
}
res.json(resultWithApplicatorInfo);
}
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 getInvoiceWithFullInfo(invoice, puid) {
assert(!utils.isEmptyObj(invoice), AppInputError.create());
const applicator = await User.findById(puid, '-password', { lean: true });
if (!applicator) AppParamError.throw(Errors.APPLICATOR_NOT_FOUND);
const invoiceObject = invoice.toObject({ getters: true });
const defaultInvoiceSetting = await InvoiceSetting.findOne({ userId: { $eq: null }, byPuid: puid });
const clients = [];
for (const client of invoiceObject?.clients) {
const clientSetting = await InvoiceSetting.findOne({ userId: { $eq: client?.billTo?.id }, byPuid: puid });
clients.push({
...client,
applicatorCompanyName: clientSetting ? clientSetting.companyName : defaultInvoiceSetting?.companyName || applicator?.name,
applicatorAddress: clientSetting ? clientSetting.address : defaultInvoiceSetting?.address || applicator?.address,
applicatorPhone: applicator?.phone,
applicatorEmail: applicator?.email,
clientName: client?.billTo?.name,
clientAddress: client?.billTo?.address,
clientPhone: client?.billTo?.phone,
clientEmail: client?.billTo?.email,
...(!clientSetting || !utils.isNumber(client.paymentTerm)) && { paymentTerm: defaultInvoiceSetting.paymentTerm }
});
}
invoiceObject.clients = clients;
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 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' })
.populate({ path: 'createdBy', select: 'name email address phone' })
.populate({ path: 'updatedBy', select: 'name email address phone' });
if (!invoice) AppParamError.throw(Errors.INVOICE_NOT_FOUND);
if (isClientUser) {
const clients = invoice?.clients?.filter((client) => client?.billTo?.id === userId);
invoice.clients = clients;
}
// get applicator info
invoice = await getInvoiceWithFullInfo(invoice, puid);
res.json(invoice);
}
async function getPrintInformation_get(req, res) {
const query = req.query;
const puid = req.userInfo?.puid;
if (!puid || !ObjectId.isValid(puid)) AppParamError.throw(Errors.INVALID_PUID);
const { invoiceId, clientId } = query;
assert(!!(invoiceId && clientId), AppParamError.create());
const invoice = await Invoice.findOne({ 'clients.billTo': clientId, _id: invoiceId, byPuid: puid })
.populate({ path: 'clients.billTo', select: 'name address phone email' })
if (!invoice) AppParamError.throw(Errors.INVOICE_NOT_FOUND);
const client = invoice.clients?.find((client) => client?.billTo?.id === clientId);
const invoiceObject = invoice.toObject({ getters: true });
delete invoiceObject.clients;
const applicator = await User.findById(puid, '-password', { lean: true });
const defaultInvoiceSetting = await InvoiceSetting.findOne({ userId: { $eq: null }, byPuid: puid });
const clientSetting = await InvoiceSetting.findOne({ userId: { $eq: client?.billTo?.id }, byPuid: puid });
invoiceObject.client = {
...client.toObject({ getters: true }),
applicatorCompanyName: clientSetting ? clientSetting.companyName : defaultInvoiceSetting?.companyName || applicator?.name,
applicatorAddress: clientSetting ? clientSetting.address : defaultInvoiceSetting?.address || applicator?.address,
applicatorPhone: applicator?.phone,
applicatorEmail: applicator?.email,
clientName: client?.billTo?.name,
clientAddress: client?.billTo?.address,
clientPhone: client?.billTo?.phone,
clientEmail: client?.billTo?.email,
...(!clientSetting || !utils.isNumber(client.paymentTerm)) && { paymentTerm: defaultInvoiceSetting.paymentTerm }
};
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);
}
function splitAddress(inputAddress) {
let address = inputAddress?.trim();
const addrs = [];
const count = 5;
const length = 40;
for (let i = 0; i <= count; i++) {
const maxStr = address?.substring(0, length + 1);
const indexOfComma = maxStr?.lastIndexOf(',');
const indexOfSpace = maxStr?.lastIndexOf(' ');
let lastLindex = indexOfSpace > indexOfComma ? indexOfSpace : indexOfComma;
lastLindex = address?.length <= length ? address?.length : lastLindex >= 0 ? lastLindex : length;
addrs.push(maxStr?.substring(0, lastLindex).trim().replace(/,$/g, ''));
address = address?.substring(lastLindex >= 0 ? lastLindex : 0).trim();
}
return addrs;
}
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 = splitAddress(client.clientAddress ?? '')?.map((e) => `"${e}"`);
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 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) {
await updatedInvoice.populate({ path: 'clients.billTo', select: 'name email address phone' });
updatedInvoice = await getInvoiceWithFullInfo(updatedInvoice, puid);
}
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,
};