- feat: Sales estimates.

- feat: Sales invoices.
- feat: Sales payment receives.
- feat: Purchases bills.
- feat: Purchases bills payments that made to the vendors.
This commit is contained in:
Ahmed Bouhuolia
2020-08-03 22:46:50 +02:00
parent 56278a25f0
commit db28cd2aef
56 changed files with 3290 additions and 1208 deletions

View File

@@ -1,30 +1,244 @@
import { omit } from "lodash";
import { BillPayment } from '@/models';
export default class BillPaymentsService {
import express from 'express';
import { omit } from 'lodash';
import { check, query, validationResult, param } from 'express-validator';
import { BillPayment, BillPaymentEntry, Vendor } from '@/models';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import ServiceItemsEntries from '../Sales/ServiceItemsEntries';
import AccountsService from '../Accounts/AccountsService';
import JournalPoster from '../Accounting/JournalPoster';
import JournalEntry from '../Accounting/JournalEntry';
export default class BillPaymentsService {
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
*
* Precedures
* ------
* - Records the bill payment transaction.
* - Records the bill payment associated entries.
* - Increment the payment amount of the given vendor bills.
* - Decrement the vendor balance.
* - Records payment journal entries.
*
* @param {IBillPayment} billPayment
*/
static async createBillPayment(billPayment) {
const storedBillPayment = await BillPayment.tenant().query().insert({
...omit(billPayment, ['entries']),
const amount = sumBy(billPayment.entries, 'paymentAmount');
const storedBillPayment = await BillPayment.tenant()
.query()
.insert({
amount,
...omit(billPayment, ['entries']),
});
const storeOpers = [];
billPayment.entries.forEach((entry) => {
const oper = BillPaymentEntry.tenant()
.query()
.insert({
bill_payment_id: storedBillPayment.id,
...entry,
});
// Increment the bill payment amount.
const billOper = BillPayment.changePaymentAmount(
entry.billId,
entry.paymentAmount
);
storeOpers.push(billOper);
storeOpers.push(oper);
});
// Decrement the vendor balance after bills payments.
const vendorDecrementOper = Vendor.changeBalanace(
billPayment.vendor_id,
amount * -1
);
// Records the journal transactions after bills payment
// and change diff acoount balance.
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries({
id: storedBillPayment.id,
...billPayment,
});
await Promise.all([
...storeOpers,
recordJournalTransaction,
vendorDecrementOper,
]);
return storedBillPayment;
}
editBillPayment(billPaymentId, billPayment) {
/**
* Edits the details of the given bill payment.
*
* Preceducres.
* -------
* - Update the bill payment transaction.
* - Insert the new bill payment entries that have no ids.
* - Update the bill paymeny entries that have ids.
* - Delete the bill payment entries that not presented.
* - Re-insert the journal transactions and update the diff accounts balance.
* - Update the diff vendor balance.
* - Update the diff bill payment amount.
*
* @param {Integer} billPaymentId
* @param {IBillPayment} billPayment
* @param {IBillPayment} oldBillPayment
*/
static async editBillPayment(billPaymentId, billPayment, oldBillPayment) {
const amount = sumBy(bilPayment.entries, 'payment_amount');
const updateBillPayment = await BillPayment.tenant()
.query()
.where('id', billPaymentId)
.update({
amount,
...omit(billPayment, ['entries']),
});
const opers = [];
const entriesHasIds = billpayment.entries.filter((i) => i.id);
const entriesHasNoIds = billPayment.entries.filter((e) => !e.id);
const entriesIds = entriesHasIds.map((e) => e.id);
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted(
oldBillPayment.entries,
entriesHasIds
);
if (entriesIdsShouldDelete.length > 0) {
const deleteOper = BillPaymentEntry.tenant()
.query()
.bulkDelete(entriesIdsShouldDelete);
opers.push(deleteOper);
}
// Entries that should be update to the storage.
if (entriesHasIds.length > 0) {
const updateOper = BillPaymentEntry.tenant()
.query()
.bulkUpdate(entriesHasIds, { where: 'id' });
opers.push(updateOper);
}
// Entries that should be inserted to the storage.
if (entriesHasNoIds.length > 0) {
const insertOper = BillPaymentEntry.tenant()
.query()
.bulkInsert(
entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId }))
);
opers.push(insertOper);
}
// Records the journal transactions after bills payment and change
// different acoount balance.
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries({
id: storedBillPayment.id,
...billPayment,
});
// Change the different vendor balance between the new and old one.
const changeDiffBalance = Vendor.changeDiffBalance(
billPayment.vendor_id,
oldBillPayment.vendor_id,
billPayment.amount,
oldBillPayment.amount
);
await Promise.all([
...opers,
recordJournalTransaction,
changeDiffBalance,
]);
}
static async isBillPaymentExists(billPaymentId) {
const foundBillPayments = await BillPayment.tenant().query().where('id', billPaymentId);
return foundBillPayments.lengh > 0;
/**
* Deletes the bill payment and associated transactions.
* @param {Integer} billPaymentId -
* @return {Promise}
*/
static async deleteBillPayment(billPaymentId) {
const billPayment = await BillPayment.tenant().query().where('id', billPaymentId).first();
await BillPayment.tenant().query().where('id', billPaymentId).delete();
await BillPaymentEntry.tenant()
.query()
.where('bill_payment_id', billPaymentId)
.delete();
const deleteTransactionsOper = this.deleteJournalTransactions(
billPaymentId,
'BillPayment'
);
const revertVendorBalance = Vendor.changeBalanace(
billpayment.vendor_id,
billPayment.amount * -1,
);
return Promise.all([
deleteTransactionsOper,
revertVendorBalance,
]);
}
static async isBillPaymentNumberExists(billPaymentNumber) {
const foundPayments = await Bill.tenant().query().where('bill_payment_number', billPaymentNumber);
return foundPayments.length > 0;
/**
* Records bill payment receive journal transactions.
* @param {BillPayment} billPayment
* @param {Integer} billPaymentId
*/
static async recordPaymentReceiveJournalEntries(billPayment) {
const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD');
const payableAccount = await AccountsService.getAccountByType(
'accounts_payable'
);
const accountsDepGraph = await Account.tenant().depGraph().query();
const journal = new JournalPoster(accountsDepGraph);
const commonJournal = {
debit: 0,
credit: 0,
referenceId: billPayment.id,
referenceType: 'BillPayment',
date: formattedDate,
};
if (billPayment.id) {
const transactions = await AccountTransaction.tenant()
.query()
.whereIn('reference_type', ['BillPayment'])
.where('reference_id', billPayment.id)
.withGraphFetched('account.type');
journal.loadEntries(transactions);
journal.removeEntries();
}
const debitReceivable = new JournalEntry({
...commonJournal,
debit: paymentAmount,
contactType: 'Vendor',
contactId: billpayment.vendor_id,
account: payableAccount.id,
});
const creditPaymentAccount = new JournalEntry({
...commonJournal,
credit: paymentAmount,
account: billPayment.payment_account_id,
});
journal.debit(debitReceivable);
journal.credit(creditPaymentAccount);
await Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
]);
}
isBillPaymentsExist(billPaymentIds) {
static async getBillPayment(billPaymentId) {
}
}
/**
* Detarmines whether the bill payment exists on the storage.
* @param {Integer} billPaymentId
*/
static async isBillPaymentExists(billPaymentId) {
const billPayment = await BillPayment.tenant().query()
.where('id', billPaymentId)
.first();
return billPayment.length > 0;
}
}

View File

@@ -1,96 +1,316 @@
import { omit } from 'lodash';
import { Bill, BillPayment } from '@/models';
import { Item } from '@/models';
import { Account } from '../../models';
import JournalPoster from '../Accounting/JournalPoster';
import { omit, sumBy, difference } from 'lodash';
import moment from 'moment';
import {
Bill,
Vendor,
InventoryTransaction,
ItemEntry,
Item,
Account,
} 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 '../Inventory/Inventory';
import { AccountTransaction } from '../../models';
/**
* Vendor bills services.
*/
export default class BillsService {
/**
* 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(bill) {
const storedBill = await Bill.tenant().query().insert({
...omit(bill, ['entries']),
const amount = sumBy(bill.entries, 'amount');
const saveEntriesOpers = [];
const storedBill = await Bill.tenant()
.query()
.insert({
amount,
...omit(bill, ['entries']),
});
bill.entries.forEach((entry) => {
const oper = ItemEntry.tenant()
.query()
.insert({
reference_type: 'Bill',
reference_id: storedBill.id,
...omit(entry, ['amount']),
});
saveEntriesOpers.push(oper);
});
// Increment vendor balance.
const incrementOper = Vendor.changeBalance(bill.vendor_id, amount);
await Promise.all([
...saveEntriesOpers,
incrementOper,
this.recordInventoryTransactions(bill, storedBill.id),
this.recordJournalTransactions({ ...bill, id: storedBill.id }),
]);
return storedBill;
}
/**
* Patch items entries to the storage.
*
* @param {Array} newEntries
* @param {Array} oldEntries
* @param {String} referenceType
*
* @return {Promise}
*/
static async patchItemsEntries(newEntries, oldEntries, referenceType, billId) {
const entriesHasIds = newEntries.filter((entry) => entry.id);
const entriesHasNoIds = newEntries.filter((entry) => !entry.id);
const entriesIds = entriesHasIds.map(entry => entry.id);
const oldEntriesIds = oldEntries.map((e) => e.id);
const opers = [];
const entriesIdsShouldDelete = difference(
oldEntriesIds,
entriesIds,
);
if (entriesIdsShouldDelete.length > 0) {
const deleteOper = ItemEntry.tenant()
.query()
.whereIn('id', entriesIdsShouldDelete)
.delete();
opers.push(deleteOper);
}
entriesHasIds.forEach((entry) => {
const updateOper = ItemEntry.tenant()
.query()
.where('id', entry.id)
.update({
...omit(entry, ['id']),
});
opers.push(updateOper);
});
entriesHasNoIds.forEach((entry) => {
const insertOper = ItemEntry.tenant()
.query()
.insert({
reference_id: billId,
reference_type: referenceType,
...omit(entry, ['id', 'amount']),
});
opers.push(insertOper);
});
return Promise.all([...opers]);
};
/**
* Edits details of the given bill id with associated entries.
* @param {Integer} billId
* @param {IBill} bill
*
* 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
* @param {IBill} bill
*/
static async editBill(billId, bill) {
const updatedBill = await Bill.tenant().query().insert({
...omit(bill, ['entries']),
const amount = sumBy(bill.entries, 'amount');
// Update the bill transaction.
const updatedBill = await Bill.tenant()
.query()
.where('id', billId)
.update({
amount,
...omit(bill, ['entries'])
});
// Old stored entries.
const storedEntries = await ItemEntry.tenant()
.query()
.where('reference_id', billId)
.where('reference_type', 'Bill');
// Patch the bill entries.
const patchEntriesOper = this.patchItemsEntries(bill.entries, storedEntries, 'Bill', billId);
// Record bill journal transactions.
const recordTransactionsOper = this.recordJournalTransactions(bill, billId);
await Promise.all([
patchEntriesOper,
recordTransactionsOper,
]);
}
/**
* Records inventory transactions.
* @param {IBill} bill -
* @return {void}
*/
static async recordInventoryTransactions(bill, billId) {
const storeInventoryTransactions = [];
const entriesItemsIds = bill.entries.map((e) => e.item_id);
const inventoryItems = await Item.tenant()
.query()
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');
const inventoryItemsIds = inventoryItems.map((i) => i.id);
const inventoryEntries = bill.entries.filter(
(entry) => inventoryItemsIds.indexOf(entry.item_id) !== -1
);
inventoryEntries.forEach((entry) => {
const oper = InventoryTransaction.tenant().query().insert({
direction: 'IN',
date: bill.bill_date,
item_id: entry.item_id,
quantity: entry.quantity,
rate: entry.rate,
remaining: entry.quantity,
transaction_type: 'Bill',
transaction_id: billId,
});
storeInventoryTransactions.push(oper);
});
return Promise.all([...storeInventoryTransactions]);
}
/**
* Records the bill journal transactions.
* @param {IBill} bill
* @async
* @param {IBill} bill
* @param {Integer} billId
*/
async recordJournalTransactions(bill) {
const entriesItemsIds = bill.entries.map(entry => entry.item_id);
const payableTotal = sumBy(bill, 'entries.total');
const storedItems = await Item.tenant().query().whereIn('id', entriesItemsIds);
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 payableAccount = await Account.tenant().query();
const formattedDate = moment(saleInvoice.invoice_date).format('YYYY-MM-DD');
const storedItems = await Item.tenant()
.query()
.whereIn('id', entriesItemsIds);
const accountsDepGraph = await Account.depGraph().query().remember();
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
const payableAccount = await AccountsService.getAccountByType(
'accounts_payable'
);
if (!payableAccount) {
throw new Error('New payable account on the storage.');
}
const accountsDepGraph = await Account.tenant().depGraph().query();
const journal = new JournalPoster(accountsDepGraph);
const commonJournalMeta = {
debit: 0,
credit: 0,
referenceId: bill.id,
referenceId: billId,
referenceType: 'Bill',
date: formattedDate,
accural: true,
};
const payableEntry = await JournalEntry({
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,
contactId: bill.vendorId,
account: payableAccount.id,
contactId: bill.vendor_id,
contactType: 'Vendor',
});
journal.credit(payableEntry);
bill.entries.forEach((item) => {
if (['inventory'].indexOf(item.type) !== -1) {
const inventoryEntry = new JournalEntry({
...commonJournalMeta,
account: item.inventoryAccountId,
});
journal.debit(inventoryEntry);
} else {
const costEntry = new JournalEntry({
...commonJournalMeta,
account: item.costAccountId,
});
journal.debit(costEntry);
}
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);
});
await Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
])
]);
}
/**
* Deletes the bill with associated entries.
* @param {Integer} billId
* @param {Integer} billId
* @return {void}
*/
static async deleteBill(billId) {
await BillPayment.tenant().query().where('id', billId);
const bill = await Bill.tenant().query().where('id', billId).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.deleteTransactions(
billId,
'Bill'
);
// Revert vendor balance.
const revertVendorBalance = Vendor.changeBalance(billId, bill.amount * -1);
await Promise.all([
deleteBillOper,
deleteBillEntriesOper,
deleteTransactionsOper,
deleteInventoryTransOper,
revertVendorBalance,
]);
}
/**
* Detarmines whether the bill exists on the storage.
* @param {Integer} billId
* @param {Integer} billId
* @return {Boolean}
*/
static async isBillExists(billId) {
@@ -100,15 +320,31 @@ export default class BillsService {
/**
* Detarmines whether the given bills exist on the storage in bulk.
* @param {Array} billsIds
* @param {Array} billsIds
* @return {Boolean}
*/
isBillsExist(billsIds) {
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);
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();
}
}