synt_backend
Version:
Synt light-weight node backend service
1,547 lines (1,425 loc) • 44.4 kB
JavaScript
const express = require("express");
const router = express.Router();
import "dotenv/config";
var exactOnline = require("./../helpers/exact-online");
const userHelper = require("./../helpers/user");
import { validateVme } from "./../helpers/validations";
import db from "../mysql/models";
const { Op } = require("sequelize");
const Sequelize = require("sequelize");
import { attachUploadedFiles } from "./../helpers/db-storage";
import {
handleFormidableForm,
uploadFiles,
uploadFilesAsync,
} from "./../helpers/formidable";
import {
getSettlementDB,
formatCurrency,
groupBy,
} from "../database/accounting";
import { getBankTransactionsDB } from "../database/banking";
import { jobReminderManager } from "../helpers/JobReminderManager";
const notifier = require("./../helpers/notifier");
import { getPresignedUrl } from "./../helpers/upload";
// routes
router.get("/", getOverview);
router.get("/divisions", getDivisions);
router.get("/glaccounts", getGLAccounts);
router.post("/purchase", postPurchase);
router.get("/purchase/:PurchaseId", getPurchase);
router.post("/financialyear", postFinancialYear);
router.get("/settlement/financialyear/:FinancialYearId", getSettlement);
router.get("/settlement/currentbalance/:end_date", getCurrentBalance);
router.post("/settlement", postSettlement);
router.get("/financialyear/:FinancialYearId", getFinancialYear);
router.get("/financialyear", getFinancialYears);
router.delete("/financialyear/:FinancialYearId", deleteFinancialYear);
router.delete("/purchase/:PurchaseId", deletePurchase);
router.post("/transactions", postTransactions);
router.post("/provision", postProvision);
router.get("/funds", getFunds);
router.get("/provision/:ProvisionId", getProvision);
router.post("/generalledgeraccounts", postGeneralLedgerAccounts);
router.get("/generalledgeraccounts", getGeneralLedgerAccounts);
router.post("/payment", postPayment);
module.exports = router;
const SYNT_DIVISION = "869312";
const CLIENT_NAME = "Synt App";
async function deleteFinancialYear(req, res) {
const { t } = req;
const FinancialYearId = parseInt(req.params.FinancialYearId, 10);
try {
let User = await userHelper.getAuthUser(req);
if (!User.is_admin) {
return res.json({
success: false,
error: t(
"api.accounting.fiscalYear.errors.notAppropriateRole",
"You do not have the appropriate role to delete a fiscal year."
),
});
}
// TODO: Check provision and whether we can delete everything
const FinancialYear = await db.FinancialYear.findOne({
where: { id: FinancialYearId },
});
await FinancialYear.destroy();
jobReminderManager.relativeDatesAreChanged({
type: "financial_year",
vmeId: FinancialYear.VMEId,
});
return res.json({ success: true });
} catch (error) {
return res.json({ success: false, error });
}
}
async function deletePurchase(req, res) {
const { t } = req;
const PurchaseId = parseInt(req.params.PurchaseId, 10);
try {
let User = await userHelper.getAuthUser(req);
if (!User.is_admin) {
return res.json({
success: false,
error: t(
"api.accounting.purchase.errors.notAppropriateRole",
"You do not have the appropriate role to delete an invoice."
),
});
}
// TODO: SOFTDELETE
db.Purchase.findOne({ where: { id: PurchaseId } }).then((Purchase) => {
Purchase.destroy();
return res.json({ success: true });
});
} catch (error) {
return res.json({ success: false, error });
}
}
async function postGeneralLedgerAccounts(req, res) {
const { t } = req;
try {
console.log("start posting");
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const { Accounts } = req.body;
if (Accounts.length) {
// Accounts were sent
console.log("start posting, account: " + Accounts.length);
let GLAs = Accounts.map((A) => ({
VMEId: VME.id,
DistributionKeyId: A.DistributionKey.id,
order_number:
typeof A.order_number === "undefined" ? 0 : A.order_number,
...A,
start_value: A.start_value,
}));
db.GeneralLedgerAccount.bulkCreate(GLAs, {
updateOnDuplicate: [
"name",
"description",
"DistributionKeyId",
"start_value",
],
}).then(() => {
return res.json({
success: true,
message: t(
"api.accounting.generalLedgerAccount.message.accountsAdded",
"Accounts added..."
),
});
});
} else {
// Accounts empty
return res.json({
success: true,
message: t(
"api.accounting.generalLedgerAccount.message.accountsEmpty",
"Accounts empty..."
),
});
}
}
} catch (error) {
console.log("TEST " + error);
}
}
async function postPayment(req, res) {
const { t } = req;
try {
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId, User); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { role } = VmeValidation;
if (!(User.is_admin || role === "synt_authoriser")) {
return res.json({
success: false,
error: t(
"api.accounting.payment.errors.noPermissions",
"No permission"
),
});
}
const { paid_at, id, type } = req.body;
if (
type !== "Purchase" &&
type !== "Provision" &&
type !== "SettlementFile"
) {
console.log("Faulty type: Payment");
return res.json({ success: false });
}
await db[type].update(
{ paid_at: paid_at ? new Date() : null },
{ where: { id } }
);
return res.json({ success: true });
}
} catch (error) {
return res.json({ success: false, error });
}
}
function getDivisions(req, res) {
exactOnline.createClient(CLIENT_NAME).then(async (client) => {
client.setDivision(SYNT_DIVISION); // Synt BV
// Get current user
let Divisions = await client.getDivisions();
return res.json({ success: true, Divisions });
});
}
async function getGLAccounts(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
let GLAccounts = await VME.getGeneralLedgerAccounts({
include: db.DistributionKey,
});
return res.json({ success: true, GLAccounts });
}
/*
exactOnline.createClient(CLIENT_NAME).then(async (client) => {
client.setDivision(SYNT_DIVISION); // Synt BV
// Get current user
let GLAccounts = await client.getGLAccounts();
return res.json({ success: true, GLAccounts });
});
*/
}
async function getFinancialYears(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
VME.getFinancialYears({
include: [{ model: db.Meeting }, { model: db.Provision }],
}).then((FinancialYears) => {
return res.json({ success: true, FinancialYears });
});
}
}
async function getFinancialYear(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
const { FinancialYearId } = req.params;
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
db.FinancialYear.findOne({
where: { id: FinancialYearId },
}).then((FinancialYear) => {
if (!FinancialYear || FinancialYear.VMEId !== VME.id) {
return res.json({
success: false,
error: t(
"api.accounting.fiscalYear.errors.notInYourVme",
"This financial year does not belong to your VME."
),
});
} else {
return res.json({ success: true, FinancialYear });
}
});
}
}
async function postSettlement(req, res, next) {
const { t } = req;
try {
const { formFiles, formData } = await handleFormidableForm(req);
if (!formFiles) {
return res.json({
success: false,
error: t("api.accounting.settlement.errors.fileError", "File error."),
});
}
const fd = await uploadFilesAsync(formFiles, formData, "settlement-files");
// verify VME
// create settlement and save pdf details
try {
const S = await db.Settlement.create({
FinancialYearId: fd.FinancialYear.id,
PaymentConditionId: fd.PaymentConditionId,
});
await attachUploadedFiles(
"Settlement",
S,
fd.Files.map((F) => {
// api passes the id as filename (prevents randomisation, but best approach?)
F.UserId = parseInt(F.original_name); //formData.Commissioners[i].id;
let Commissioner = fd.Commissioners.find((C) => C.id === F.UserId);
F.original_name =
t("api.accounting.settlement.name", "Settlement") +
" " +
fd.FinancialYear.year +
" " +
Commissioner.full_name;
return F;
})
);
} catch (err) {
console.log(err);
}
// set financial as settled
await db.FinancialYear.update(
{ is_settled: true },
{ where: { id: fd.FinancialYear.id } }
);
jobReminderManager.relativeDatesAreChanged({
type: "financial_year",
// vmeId: where to get vmeId from? FIXME
});
return res.json({ success: true });
} catch (err) {
if (err) {
return next(err);
}
}
}
async function getSettlement(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
const { FinancialYearId } = req.params;
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const result = await getSettlementDB({ t, FinancialYearId, User, VME });
return res.json(result);
}
}
async function getCurrentBalance(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
const { end_date } = req.params;
if (!User) {
return;
}
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const FinancialYears = await db.FinancialYear.findAll({
where: { VMEId: VME.id },
});
if (FinancialYears.length === 0) {
return res.json({ success: false });
}
let start_date = FinancialYears[0].start_date;
for (let i = 1; i < FinancialYears.length; i++) {
if (new Date(start_date) > new Date(FinancialYears[i].start_date)) {
start_date = FinancialYears[i].state_date;
}
}
let Purchases = await db.Purchase.findAll({
where: {
VMEId: VME.id,
invoice_date: {
[Op.between]: [start_date, end_date],
},
},
include: [
{
model: db.PurchaseEntry,
include: [
{ model: db.PurchaseAllocation },
{
model: db.GeneralLedgerAccount,
include: [{ model: db.DistributionKey, include: db.Lot }],
},
],
},
],
});
let Provisions = await db.Provision.findAll({
where: {
invoice_date: {
[Op.between]: [start_date, end_date],
},
},
include: [
{
model: db.Lot,
include: [
{
model: db.LotPhase,
include: [
{
model: db.User,
attributes: {
exclude: [
"phone",
"email",
"email_verified_at",
"phone_verified_at",
],
},
include: db.Company,
},
],
},
],
},
{
model: db.GeneralLedgerAccount,
},
],
});
Provisions = Provisions.map((P) => {
// not last period, but based on invoice_date
// FIXME: important: who paid... that's it
let period = P.Lot.LotPhases.find(
(LP) =>
new Date(P.invoice_date) >= new Date(LP.starts_at) &&
(!LP.ends_at || new Date(P.invoice_date) <= new Date(LP.ends_at))
);
//let period = P.Lot.LotPhases.find((LP) => !LP.ends_at);
let Commissioner = period?.Users.find(
(U) => U.LotPhaseUser.is_commissioner
);
return {
...P.toJSON(),
Lot: { ...P.Lot.toJSON(), Commissioner },
};
});
// Provisions:
// working_capital: 410100
// guarantee_fund: 429200
// purchase_result: 440000
// Retrieve bank transactions and process for balance
let BankAccounts = [];
if (VME.has_banking) {
let bankTransactions = await getBankTransactionsDB({ t, User, VME });
if (bankTransactions.success) {
let start = new Date(start_date);
let end = new Date(end_date);
let accounts = bankTransactions.accounts;
accounts.forEach((account) => {
let incoming = 0;
let outgoing = 0;
account.transactions.forEach((transaction) => {
let date = new Date(transaction.executionDate);
if (date >= start && date < end) {
if (transaction.amount < 0) {
outgoing += -transaction.amount;
} else {
incoming += transaction.amount;
}
}
});
BankAccounts.push({
incoming,
outgoing,
reference: account.reference,
referenceType: account.referenceType,
product: account.product,
currency: account.currency,
});
});
}
}
let Balance = {
BankAccounts,
ProvisionAccounts: Object.entries(
groupBy("GeneralLedgerAccountId", Provisions)
).map(([GeneralLedgerAccountId, Ps]) => {
console.log(GeneralLedgerAccountId);
let total_paid = formatCurrency(
Ps.reduce((sum, P) => sum + (P.paid_at ? P.amount : 0), 0)
);
let total_unpaid = formatCurrency(
Ps.reduce((sum, P) => sum + (P.paid_at ? 0 : P.amount), 0)
);
return {
GeneralLedgerAccount: Ps[0].GeneralLedgerAccount,
total_paid,
total_unpaid,
};
}),
PurchaseAccounts: Object.entries(
groupBy(
"GeneralLedgerAccountId",
Purchases.reduce(
(entries, P) => [
...entries,
...P.PurchaseEntries.map((PE) => ({
...PE.toJSON(),
paid_at: P.paid_at,
})),
],
[]
)
)
).map(([GeneralLedgerAccountId, PEs]) => {
console.log(GeneralLedgerAccountId);
let total_paid = formatCurrency(
PEs.reduce(
(sum, PE) =>
sum + (PE.paid_at ? PE.amount * (1 + PE.vat_percentage) : 0),
0
)
);
let total_unpaid = formatCurrency(
PEs.reduce(
(sum, PE) =>
sum + (PE.paid_at ? 0 : PE.amount * (1 + PE.vat_percentage)),
0
)
);
return {
GeneralLedgerAccount: PEs[0].GeneralLedgerAccount,
total_paid,
total_unpaid,
};
}),
};
// TE BETALEN LEVERANCIERS
Balance.total_suppliers_paid = Balance.PurchaseAccounts.reduce(
(sum, PA) => sum + PA.total_paid,
0
);
Balance.total_suppliers_unpaid = Balance.PurchaseAccounts.reduce(
(sum, PA) => sum + PA.total_unpaid,
0
);
return res.json({
success: true,
Balance,
});
}
async function postFinancialYear(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const {
id,
year,
description,
start_date,
end_date,
months_per_invoice,
is_budget_changing,
is_budget_amount_confirmed,
is_budget_frequency_confirmed,
is_reserve_capital_confirmed,
is_guarantee_fund_confirmed,
budget,
reserve_capital,
guarantee_fund,
} = req.body;
if (
is_budget_changing &&
(typeof budget === "undefined" || budget === "")
) {
return res.json({
success: false,
errors: {
budget: t(
"api.accounting.fiscalYear.errors.budgetRequired",
"Budget is required"
),
},
});
}
const Data = {
VMEId: VME.id,
year,
start_date,
description,
end_date,
budget,
months_per_invoice,
months_invoiced: parseInt(budget) === 0 ? 12 : 0,
reserve_capital,
guarantee_fund,
};
if (id) {
// update exisiting purchase
db.FinancialYear.findOne({ where: { id } }).then((FinancialYear) => {
const update = {
description,
months_per_invoice,
budget,
reserve_capital,
guarantee_fund,
};
if (User.is_admin) {
// only admin can change this
update.is_budget_amount_confirmed = is_budget_amount_confirmed;
update.is_budget_frequency_confirmed = is_budget_frequency_confirmed;
update.is_reserve_capital_confirmed = is_reserve_capital_confirmed;
update.is_guarantee_fund_confirmed = is_guarantee_fund_confirmed;
}
FinancialYear.update(update);
return res.json({ success: true, FinancialYear });
});
} else {
// create new FinancialYear
if (User.is_admin) {
// only admin can change this
Data.is_budget_amount_confirmed = is_budget_amount_confirmed;
Data.is_budget_frequency_confirmed = is_budget_frequency_confirmed;
Data.is_reserve_capital_confirmed = is_reserve_capital_confirmed;
Data.is_guarantee_fund_confirmed = is_guarantee_fund_confirmed;
}
db.FinancialYear.create(Data).then((newItem) => {
return res.json({ success: true, FinancialYear: newItem });
});
}
}
}
async function getPurchase(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
const { PurchaseId } = req.params;
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
db.Purchase.findOne({
where: { id: PurchaseId },
include: [
{ model: db.PurchaseEntry },
{ model: db.PurchaseFile },
{ model: db.Supplier, include: db.Company },
],
}).then(async (Purchase) => {
if (Purchase.VMEId !== VME.id) {
return res.json({
success: false,
error: t(
"api.accounting.purchase.errors.notYourVme",
"This purchase does not belong to your VME."
),
});
} else {
await Promise.all(
Purchase.PurchaseFiles.map(async (PF) => {
DF.setDataValue("presignedUrl", await PF.getPresignedUrl());
})
);
return res.json({ success: true, Purchase });
}
});
}
}
function findFunds(VME) {
return db.Provision.findAll({
where: {
VMEId: VME.id,
[Op.or]: [{ type: "guarantee_fund" }, { type: "reserve_capital" }],
},
include: [db.Lot],
}).then((Provisions) => {
return Provisions;
});
}
async function getFunds(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
findFunds(VME).then((Provisions) => {
return res.json({ success: true, Funds: Provisions });
});
}
}
async function getProvision(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
const { ProvisionId } = req.params;
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
db.Provision.findOne({
where: { id: ProvisionId },
include: [db.Lot, db.FinancialYear, db.ProvisionFile],
}).then((Provision) => {
if (Provision.VMEId !== VME.id) {
return res.json({
success: false,
error: t(
"api.accounting.provision.errors.notYourVme",
"This contribution does not belong to your VME."
),
});
} else {
getPresignedUrl(Provision.ProvisionFile).then((url) => {
Provision.ProvisionFile.setDataValue("presignedUrl", url);
return res.json({ success: true, Provision });
});
}
});
}
}
async function allocatePurchase(PurchaseId) {
try {
const Purchase = await db.Purchase.findOne({
where: { id: PurchaseId },
include: [
{
model: db.PurchaseEntry,
include: [
{
model: db.GeneralLedgerAccount,
include: [{ model: db.DistributionKey, include: db.Lot }],
},
],
},
],
});
// FIXME: always delete all previous allocations?
await db.PurchaseAllocation.destroy({
where: { PurchaseEntryId: Purchase.PurchaseEntries.map((E) => E.id) },
});
// allocate the purchase
let Allocations = [];
Purchase.PurchaseEntries.forEach((Entry) => {
const Lots = Entry.GeneralLedgerAccount.DistributionKey.Lots;
const type = Entry.GeneralLedgerAccount.DistributionKey.type;
const total = Lots.reduce((sum, Lot) => {
sum = sum + parseInt(type === "S" ? Lot.share : 1);
return sum;
}, 0);
Lots.forEach((Lot) => {
// FIXME: Lot.UserId is id who made the last lot change
Allocations.push({
amount:
((type === "S" ? Lot.share : 1) / total) * parseFloat(Entry.amount),
LotId: Lot.id,
PurchaseEntryId: Entry.id,
});
});
});
await db.PurchaseAllocation.bulkCreate(Allocations);
} catch (err) {
console.log(err);
}
}
async function postPurchase(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const {
id,
SupplierId,
description,
PurchaseFiles,
PurchaseEntries,
invoice_date,
statement,
due_date,
paid_at,
} = req.body;
let errors = {};
if (!SupplierId) {
errors["SupplierId"] = t(
"api.accounting.purchase.errors.supplierRequired",
"Supplier is required"
);
}
if (!description) {
errors["description"] = t(
"api.accounting.purchase.errors.descriptionRequired",
"Description is required"
);
}
if (!invoice_date) {
errors["invoice_date"] = t(
"api.accounting.purchase.errors.invoiceDateRequired",
"Invoice date is required."
);
}
if (!due_date) {
errors["due_date"] = t(
"api.accounting.purchase.errors.dueDateRequired",
"Expiration date is required."
);
}
if (Object.keys(errors).length > 0) {
return res.json({
success: false,
errors,
});
}
let total_amount = 0;
let vat_amount = 0;
let glaRequired = false;
if (PurchaseEntries) {
PurchaseEntries.forEach((Entry) => {
console.log(Entry);
if (!Entry.GeneralLedgerAccountId) {
glaRequired = true;
}
total_amount =
total_amount +
formatCurrency(parseFloat(Entry.amount * (1 + Entry.vat_percentage)));
vat_amount = formatCurrency(
vat_amount + parseFloat(Entry.amount * Entry.vat_percentage)
);
});
}
// if ran within forEach, the rest of the code will run
if (glaRequired) {
return res.json({
success: false,
errors: {
PurchaseEntries: t(
"api.accounting.purchase.errors.ledgerAccountRequired",
"Ledger account is required."
),
},
});
}
const Data = {
VMEId: VME.id,
SupplierId,
total_amount,
vat_amount,
description,
statement,
invoice_date,
due_date,
JournalId: 1, // aankoop, code 600
};
if (id) {
// update exisiting purchase
const Purchase = await db.Purchase.findOne({ where: { id } });
await attachUploadedFiles("Purchase", Purchase, PurchaseFiles);
const update = {
description,
statement,
total_amount,
vat_amount,
invoice_date,
due_date,
paid_at,
SupplierId,
};
await Purchase.update(update);
// add or update entries
await db.PurchaseEntry.bulkCreate(
PurchaseEntries.map((E) => ({
["PurchaseId"]: Purchase.id,
...E,
})),
{
updateOnDuplicate: [
"amount",
"vat_percentage",
"description",
"statement",
],
}
);
await allocatePurchase(Purchase.id);
return res.json({ success: true, Purchase });
}
// create new purchase
const newItem = await db.Purchase.create(Data);
await attachUploadedFiles("Purchase", newItem, PurchaseFiles);
await db.PurchaseEntry.bulkCreate(
PurchaseEntries.map((E) => ({ ["PurchaseId"]: newItem.id, ...E }))
);
await allocatePurchase(newItem.id);
newItem.setUsers(User); // save who made this change
return res.json({ success: true, Purchase: newItem });
/*
exactOnline.createClient(CLIENT_NAME).then(async (client) => {
// set supplier info
let Supplier = {
Name: supplier_name,
VATNumber: supplier_vat_number,
};
client.setDivision(VME.division_id);
let response = await client.postSupplier(Supplier);
// prepare entry lines
let PurchaseEntryLines = [
{
AmountFC: amount,
GLAccount: GLAccount_ID,
},
];
// prepare purchase entry
let Entry = {
Journal: 600, // required
Supplier: response.ID, // required
PurchaseEntryLines, // required
Description: description,
//Purchase: "id",
};
client.postPurchaseEntry(Entry);
return res.json({ success: true });
});
*/
}
}
async function postProvision(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
handleFormidableForm(req).then(async ({ formFiles, formData }) => {
const {
id,
PaymentCondition,
PaymentConditionId,
type,
is_temp,
paid_at,
correction_only,
description,
email_message,
FinancialYear,
FinancialYearId,
DistributionKey,
is_notifiable = true,
is_force_payments = false,
} = formData;
let { invoice_date, amount } = formData;
let correction = null;
let PreviousFinancialYear;
console.log({ email_message });
if (
(type === "working_capital" ||
type === "reserve_capital" ||
type === "guarantee_fund") &&
!FinancialYearId
) {
return res.json({
success: false,
errors: {
FinancialYear: t(
"api.accounting.provision.errors.fiscalYearRequired",
"Fiscal year is required."
),
},
});
}
if (id) {
// update exisiting purchase
db.Provision.findOne({ where: { id } }).then((Provision) => {
const update = { description, email_message, paid_at };
Provision.update(update);
return res.json({ success: true, Provision });
});
} else {
// create new provisions
if (!formFiles) {
return res.json({
success: false,
errors: {
File: t(
"api.accounting.provision.errors.fileRequired",
"File error."
),
},
});
}
if (!DistributionKey) {
return res.json({
success: false,
errors: {
DistributionKey: t(
"api.accounting.provision.errors.distributionKeyRequired",
"Distribution key is required."
),
},
});
}
if (!invoice_date) {
return res.json({
success: false,
errors: {
invoice_date: t(
"api.accounting.provision.errors.invoiceDateRequired",
"Invoice date is required."
),
},
});
}
invoice_date = new Date(invoice_date);
if (type === "working_capital") {
const remaining_months =
12 -
parseInt(
FinancialYear?.months_invoiced +
FinancialYear?.months_invoiced ===
0
? PreviousFinancialYear?.temp_months_invoiced || 0
: 0
);
const invoice_months =
remaining_months >= 0 &&
remaining_months % FinancialYear?.months_per_invoice === 0
? Math.min(remaining_months, FinancialYear?.months_per_invoice)
: remaining_months % FinancialYear?.months_per_invoice;
amount = FinancialYear?.budget / (12 / invoice_months);
console.log("Working Capital: Amount calculation:");
console.log({ amount });
if (FinancialYear?.months_invoiced === 0) {
// First time financial year is requested
// Get previous financial year
let FinancialYears = await VME.getFinancialYears();
PreviousFinancialYear = FinancialYears.reduce((_, FY, index) => {
if (FY.id === FinancialYearId && index > 0) {
return FinancialYears[index - 1];
}
return null;
}, null);
if (PreviousFinancialYear?.temp_months_invoiced) {
amount = correction_only ? 0 : amount;
correction =
((PreviousFinancialYear?.temp_months_invoiced || 0) *
(FinancialYear?.budget - PreviousFinancialYear?.budget)) /
12;
}
console.log("Months invoices = 0, Amount: ");
console.log({ amount, correction });
}
}
const due_date = new Date(
invoice_date.getFullYear(),
invoice_date.getMonth() + PaymentCondition.payment_end_of_months,
invoice_date.getDate() + PaymentCondition.payment_days
);
let ProvisionRows = [];
const total_shares = DistributionKey.Lots.reduce((sum, L) => {
sum = sum + (DistributionKey.type === "S" ? L.share : 1);
return sum;
}, 0);
//let Funds = [];
if (type === "reserve_capital" || type === "guarantee_fund") {
//Funds = await findFunds(VME);
amount =
type === "reserve_capital"
? FinancialYear.reserve_capital
: type === "guarantee_fund"
? FinancialYear.guarantee_fund
: 0;
}
console.log("Ongoing");
console.log({ amount });
// add to a gla
// TODO:
const GLA = await db.GeneralLedgerAccount.findOne({
where: {
VMEId: VME.id,
code:
type === "working_capital"
? 410100
: type === "guarantee_fund"
? 429200
: type === "exceptional_capital"
? null
: type === "reserve_capital"
? null
: null,
},
});
// add provisions
DistributionKey.Lots.forEach((Lot) => {
let amount_share =
(amount || 0) *
((DistributionKey.type === "S" ? Lot.share : 1) / total_shares);
let correction_share =
(correction || 0) *
((DistributionKey.type === "S" ? Lot.share : 1) / total_shares);
console.log("Share calculation: ");
console.log({
amount_share,
Lot_share: Lot.share,
total_shares,
DK_type: DistributionKey.type,
correction_share,
});
if (type === "reserve_capital" || type === "guarantee_fund") {
/*
let totalExistingFunds = (Funds || []).reduce(
(sum, F) =>
F.LotId === Lot.id && F.type === type ? sum + F.amount : sum,
0
);
amount_share = amount_share - totalExistingFunds;
*/
if (amount_share === 0) {
return; // no remaining contribution
}
}
ProvisionRows.push({
type,
invoice_date,
PaymentConditionId,
LotId: Lot.id,
due_date,
description,
email_message,
amount: formatCurrency(amount_share),
correction: correction_share,
VMEId: VME.id,
JournalId: 2,
FinancialYearId,
Commissioner: Lot.Commissioner,
GeneralLedgerAccountId: GLA?.id ?? null,
paid_at: is_force_payments ? new Date() : null,
});
});
if (
(type === "reserve_capital" || type === "guarantee_fund") &&
ProvisionRows.length === 0
) {
return res.json({
success: false,
errors: {
type: t(
"api.accounting.provision.errors.noContributionsPrepared",
"All contributions for the fund have already been requested. No new contributions prepared."
),
},
});
}
console.log("Bulk creating:");
console.log(ProvisionRows);
db.Provision.bulkCreate(ProvisionRows).then((newItems) => {
newItems.forEach((P) => {
let Commissioner = ProvisionRows.find(
(PR) => PR.LotId === P.LotId
)?.Commissioner;
P.setUsers(Commissioner.id); // commissioner can see this document
});
// increment "counter"
if (type === "working_capital") {
// only for period working capital (automatic calculation)
if (correction !== null && correction >= 0) {
// Temp months were invoiced so correction needed
db.FinancialYear.update(
{ months_invoiced: PreviousFinancialYear.temp_months_invoiced },
{ where: { id: FinancialYear.id } }
);
/*
// Remember that financial year was corrected
db.FinancialYear.update(
{ temp_months_invoiced: 0 },
{ where: { id: PreviousFinancialYear.id } }
);
*/
}
if (correction === null || !correction_only) {
db.FinancialYear.increment(
is_temp
? {
temp_months_invoiced:
Sequelize.literal("months_per_invoice"),
}
: {
months_invoiced: Sequelize.literal("months_per_invoice"),
},
{
where: { id: FinancialYearId },
}
);
}
}
// save the files
// send notification to commissioner
uploadFiles(formFiles, formData, "provision-files", (formData) => {
newItems.forEach((P) => {
let Commissioner = ProvisionRows.find(
(PR) => PR.LotId === P.LotId
)?.Commissioner;
let File = formData.Files.find(
(F) => parseInt(F.original_name) === parseInt(Commissioner.id)
);
let Lot = DistributionKey.Lots.find((L) => L.id === P.LotId);
// upload
let ProvisionFile = {
...File,
original_name:
t(
"api.accounting.provision.contribution.name",
"Contribution"
) +
" " +
P.period +
" " +
Commissioner.full_name,
};
attachUploadedFiles("Provision", P, [ProvisionFile]);
// send
if (is_notifiable) {
console.log("Notifying " + JSON.stringify(Commissioner));
getPresignedUrl(ProvisionFile).then((url) => {
ProvisionFile.setDataValue("presignedUrl", url);
notifier.notify(Commissioner, "new_provision", {
Provision: { ...P.toJSON(), ProvisionFile, Lot },
VME,
attachments: [
{
// use URL as an attachment
filename: ProvisionFile?.original_name + ".pdf",
path: ProvisionFile?.presignedUrl,
contentType: "application/pdf",
},
],
});
});
}
});
});
});
return res.json({ success: true });
}
});
/*
exactOnline.createClient(CLIENT_NAME).then(async (client) => {
// set supplier info
let Customer = {
Name: customer_name,
};
client.setDivision(VME.division_id);
let response = await client.postCustomer(Customer);
// prepare entry lines
let SalesEntryLines = [
{
AmountFC: amount,
GLAccount: GLAccount_ID,
},
];
// prepare purchase entry
let Entry = {
Journal: 700, // required
Customer: response.ID, // required
SalesEntryLines, // required
Description: description,
YourRef: `BIJDRAGE ${customer_name} (${
VME.alias
}) ${new Date().toLocaleString()}`,
};
let test = await client.postSalesEntry(Entry);
return res.json({ success: true });
});
*/
}
}
async function postTransactions(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
exactOnline.createClient(CLIENT_NAME).then(async (client) => {
client.setDivision(VME.division_id); // set division
return res.json({ success: true });
});
}
}
async function getGeneralLedgerAccounts(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const GeneralLedgerDefinitions = await db.GeneralLedgerDefinition.findAll();
let GLAccounts = await VME.getGeneralLedgerAccounts({
include: db.DistributionKey,
});
GLAccounts = GLAccounts.map((A) => {
A.setDataValue(
"Subs",
GLAccounts.reduce(
(acc, item) =>
acc +
(item.GeneralLedgerDefinitionId === A.GeneralLedgerDefinitionId
? 1
: 0),
-1
)
);
return A;
});
const ids = GLAccounts.reduce(
(r, i) => [i.GeneralLedgerDefinitionId, ...r],
[]
);
const newAccounts = GeneralLedgerDefinitions.filter(
(A) => !ids.includes(A.id)
).map((A) => {
A.setDataValue("GeneralLedgerDefinitionId", A.id);
A.setDataValue("id", null);
return A;
});
return res.json({
success: true,
newAccounts,
GeneralLedgerAccounts: [...GLAccounts, ...newAccounts].sort((a, b) =>
a.code === b.code
? a.order_number > b.order_number
? 1
: -1
: a.code < b.code
? -1
: 1
),
});
}
}
async function getOverview(req, res) {
const { t } = req;
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await validateVme(t, User.VMEId);
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
let Purchases = await VME.getPurchases({
include: [{ model: db.Supplier, include: db.Company }],
});
let Provisions = (
await VME.getProvisions({
include: [{ model: db.Lot }, { model: db.PaymentCondition }],
})
).sort((a, b) => new Date(a.invoice_date) - new Date(b.invoice_date));
let GLAccounts = await VME.getGeneralLedgerAccounts();
let FinancialYears = (
await VME.getFinancialYears({
include: [{ model: db.Settlement, include: db.SettlementFile }],
})
).map((FY) => {
let Files = FY.Settlement?.SettlementFiles || [];
FY.setDataValue(
"count_payments",
Files.reduce((count, F) => count + (F.paid_at ? 1 : 0), 0)
);
FY.setDataValue("count_files", Files.length);
return FY;
});
return res.json({
Provisions,
transactions: [],
FinancialYears,
GLAccounts,
Purchases,
});
/*
exactOnline.createClient(CLIENT_NAME).then(async (client) => {
client.setDivision(VME.division_id); // set division
// Get current user
let me = await client.me();
// Get sales invoices user
let invoices = await client.salesInvoices();
// Get purchases user
let purchases = await client.purchaseEntries();
// Get payments user
let payments = await client.payments();
// Get receivables user
let receivables = await client.receivables();
return res.json({
me,
invoices,
transactions: [
...(payments || []).map((P) => {
P.is_payment = true;
P.is_receivable = false;
return P;
}),
...(receivables || []).map((R) => {
R.is_payment = false;
R.is_receivable = true;
return R;
}),
],
purchases,
});
});
*/
}
}