Files
bigcapital/server/src/services/Purchases/Bills.js
2020-08-23 23:38:42 +02:00

358 lines
10 KiB
JavaScript

import { omit, sumBy, pick } from 'lodash';
import moment from 'moment';
import {
Account,
Bill,
Vendor,
ItemEntry,
Item,
AccountTransaction,
} from '@/models';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import AccountsService from '@/services/Accounts/AccountsService';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import InventoryService from '@/services/Inventory/Inventory';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import { formatDateFields } from '@/utils';
/**
* Vendor bills services.
* @service
*/
export default class BillsService extends SalesInvoicesCost {
/**
* Creates a new bill and stored it to the storage.
*
* Precedures.
* ----
* - Insert bill transactions to the storage.
* - Insert bill entries to the storage.
* - Increment the given vendor id.
* - Record bill journal transactions on the given accounts.
* - Record bill items inventory transactions.
* ----
* @param {IBill} bill -
* @return {void}
*/
static async createBill(billDTO) {
const invLotNumber = await InventoryService.nextLotNumber();
const bill = {
...formatDateFields(billDTO, ['bill_date', 'due_date']),
amount: sumBy(billDTO.entries, 'amount'),
invLotNumber: billDTO.invLotNumber || invLotNumber
};
const saveEntriesOpers = [];
const storedBill = await Bill.tenant()
.query()
.insert({
...omit(bill, ['entries']),
});
bill.entries.forEach((entry) => {
const oper = ItemEntry.tenant()
.query()
.insertAndFetch({
reference_type: 'Bill',
reference_id: storedBill.id,
...omit(entry, ['amount']),
}).then((itemEntry) => {
entry.id = itemEntry.id;
});
saveEntriesOpers.push(oper);
});
// Await save all bill entries operations.
await Promise.all([...saveEntriesOpers]);
// Increments vendor balance.
const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount);
// Rewrite the inventory transactions for inventory items.
const writeInvTransactionsOper = this.recordInventoryTransactions(
bill, storedBill.id
);
// Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions({
id: storedBill.id, ...bill,
});
await Promise.all([
incrementOper,
writeInvTransactionsOper,
writeJEntriesOper,
]);
// Schedule bill re-compute based on the item cost
// method and starting date.
await this.scheduleComputeBillItemsCost(bill);
return storedBill;
}
/**
* Edits details of the given bill id with associated entries.
*
* Precedures:
* -------
* - Update the bill transaction on the storage.
* - Update the bill entries on the storage and insert the not have id and delete
* once that not presented.
* - Increment the diff amount on the given vendor id.
* - Re-write the inventory transactions.
* - Re-write the bill journal transactions.
*
* @param {Integer} billId - The given bill id.
* @param {IBill} bill - The given new bill details.
*/
static async editBill(billId, billDTO) {
const oldBill = await Bill.tenant().query().findById(billId);
const bill = {
...formatDateFields(billDTO, ['bill_date', 'due_date']),
amount: sumBy(billDTO.entries, 'amount'),
invLotNumber: oldBill.invLotNumber,
};
// Update the bill transaction.
const updatedBill = await Bill.tenant()
.query()
.where('id', billId)
.update({
...omit(bill, ['entries', 'invLotNumber'])
});
// Old stored entries.
const storedEntries = await ItemEntry.tenant()
.query()
.where('reference_id', billId)
.where('reference_type', 'Bill');
// Patch the bill entries.
const patchEntriesOper = HasItemsEntries.patchItemsEntries(
bill.entries, storedEntries, 'Bill', billId,
);
// Changes the diff vendor balance between old and new amount.
const changeVendorBalanceOper = Vendor.changeDiffBalance(
bill.vendor_id,
oldBill.vendorId,
bill.amount,
oldBill.amount,
);
// Re-write the inventory transactions for inventory items.
const writeInvTransactionsOper = this.recordInventoryTransactions(bill, billId, true);
// Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions({
id: billId,
...bill,
}, billId);
await Promise.all([
patchEntriesOper,
changeVendorBalanceOper,
writeInvTransactionsOper,
writeJEntriesOper,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeBillItemsCost(bill);
}
/**
* Deletes the bill with associated entries.
* @param {Integer} billId
* @return {void}
*/
static async deleteBill(billId) {
const bill = await Bill.tenant().query()
.where('id', billId)
.withGraphFetched('entries')
.first();
// Delete all associated bill entries.
const deleteBillEntriesOper = ItemEntry.tenant()
.query()
.where('reference_type', 'Bill')
.where('reference_id', billId)
.delete();
// Delete the bill transaction.
const deleteBillOper = Bill.tenant().query().where('id', billId).delete();
// Delete associated bill journal transactions.
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
billId,
'Bill'
);
// Delete bill associated inventory transactions.
const deleteInventoryTransOper = InventoryService.deleteInventoryTransactions(
billId, 'Bill'
);
// Revert vendor balance.
const revertVendorBalance = Vendor.changeBalance(bill.vendorId, bill.amount * -1);
await Promise.all([
deleteBillOper,
deleteBillEntriesOper,
deleteTransactionsOper,
deleteInventoryTransOper,
revertVendorBalance,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeBillItemsCost(bill);
}
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill
* @param {number} billId
*/
static recordInventoryTransactions(bill, billId, override) {
const inventoryTransactions = bill.entries
.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate']),
lotNumber: bill.invLotNumber,
transactionType: 'Bill',
transactionId: billId,
direction: 'IN',
date: bill.bill_date,
entryId: entry.id,
}));
return InventoryService.recordInventoryTransactions(
inventoryTransactions, override
);
}
/**
* Records the bill journal transactions.
* @async
* @param {IBill} bill
* @param {Integer} billId
*/
static async recordJournalTransactions(bill, billId) {
const entriesItemsIds = bill.entries.map((entry) => entry.item_id);
const payableTotal = sumBy(bill.entries, 'amount');
const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD');
const storedItems = await Item.tenant()
.query()
.whereIn('id', entriesItemsIds);
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
const payableAccount = await AccountsService.getAccountByType('accounts_payable');
const accountsDepGraph = await Account.tenant().depGraph().query();
const journal = new JournalPoster(accountsDepGraph);
const commonJournalMeta = {
debit: 0,
credit: 0,
referenceId: bill.id,
referenceType: 'Bill',
date: formattedDate,
accural: true,
};
if (billId) {
const transactions = await AccountTransaction.tenant()
.query()
.whereIn('reference_type', ['Bill'])
.whereIn('reference_id', [billId])
.withGraphFetched('account.type');
journal.loadEntries(transactions);
journal.removeEntries();
}
const payableEntry = new JournalEntry({
...commonJournalMeta,
credit: payableTotal,
account: payableAccount.id,
contactId: bill.vendor_id,
contactType: 'Vendor',
});
journal.credit(payableEntry);
bill.entries.forEach((entry) => {
const item = storedItemsMap.get(entry.item_id);
const debitEntry = new JournalEntry({
...commonJournalMeta,
debit: entry.amount,
account:
['inventory'].indexOf(item.type) !== -1
? item.inventoryAccountId
: item.costAccountId,
});
journal.debit(debitEntry);
});
return Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
]);
}
/**
* Detarmines whether the bill exists on the storage.
* @param {Integer} billId
* @return {Boolean}
*/
static async isBillExists(billId) {
const foundBills = await Bill.tenant().query().where('id', billId);
return foundBills.length > 0;
}
/**
* Detarmines whether the given bills exist on the storage in bulk.
* @param {Array} billsIds
* @return {Boolean}
*/
static async isBillsExist(billsIds) {
const bills = await Bill.tenant().query().whereIn('id', billsIds);
return bills.length > 0;
}
/**
* Detarmines whether the given bill id exists on the storage.
* @param {Integer} billNumber
*/
static async isBillNoExists(billNumber) {
const foundBills = await Bill.tenant()
.query()
.where('bill_number', billNumber);
return foundBills.length > 0;
}
/**
* Retrieve the given bill details with associated items entries.
* @param {Integer} billId -
* @returns {Promise}
*/
static getBill(billId) {
return Bill.tenant().query().where('id', billId).first();
}
/**
* Retrieve the given bill details with associated items entries.
* @param {Integer} billId -
* @returns {Promise}
*/
static getBillWithMetadata(billId) {
return Bill.tenant()
.query()
.where('id', billId)
.withGraphFetched('vendor')
.withGraphFetched('entries')
.first();
}
/**
* Schedules compute bill items cost based on each item cost method.
* @param {IBill} bill
* @return {Promise}
*/
static scheduleComputeBillItemsCost(bill) {
return this.scheduleComputeItemsCost(
bill.entries.map((e) => e.item_id),
bill.bill_date,
);
}
}