UNPKG

synt_backend

Version:

Synt light-weight node backend service

1,547 lines (1,425 loc) 44.4 kB
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, }); }); */ } }