681 lines
28 KiB
JavaScript
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,
|
|
};
|