fix: design flow of writing invoice journal entries.

This commit is contained in:
a.bouhuolia
2021-01-04 17:19:27 +02:00
parent 999e74b405
commit d5151c365e
15 changed files with 377 additions and 473 deletions

View File

@@ -272,10 +272,12 @@ export default class SaleInvoicesController extends BaseController {
next: NextFunction
) {
const { tenantId } = req;
const filter: ISalesInvoicesFilter = {
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {

View File

@@ -164,4 +164,5 @@ export default {
protocol: '',
hostname: '',
scheduleComputeItemCost: 'in 5 seconds'
};

View File

@@ -9,7 +9,8 @@ export interface ISaleInvoice {
dueAmount: number,
customerId: number,
entries: IItemEntry[],
deliveredAt: string|Date,
deliveredAt: string | Date,
userId: number,
}
export interface ISaleInvoiceDTO {

View File

@@ -2,12 +2,11 @@ import { Container } from 'typedi';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
export default class WriteInvoicesJournalEntries {
constructor(agenda) {
agenda.define(
'rewrite-invoices-journal-entries',
{ priority: 'normal', concurrency: 1, },
this.handler.bind(this),
{ priority: 'normal', concurrency: 1 },
this.handler.bind(this)
);
}
@@ -17,15 +16,24 @@ export default class WriteInvoicesJournalEntries {
const salesInvoicesCost = Container.get(SalesInvoicesCost);
Logger.info(`Write sales invoices journal entries - started: ${job.attrs.data}`);
Logger.info(
`Write sales invoices journal entries - started: ${job.attrs.data}`
);
try {
await salesInvoicesCost.writeJournalEntries(tenantId, startingDate, true);
Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
await salesInvoicesCost.writeInventoryCostJournalEntries(
tenantId,
startingDate,
true
);
Logger.info(
`Write sales invoices journal entries - completed: ${job.attrs.data}`
);
done();
} catch(e) {
Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
done(e);
} catch (e) {
Logger.info(
`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`
);
done(e);
}
}
}
}

View File

@@ -7,7 +7,6 @@ import dbManagerFactory from 'loaders/dbManager';
import i18n from 'loaders/i18n';
import repositoriesLoader from 'loaders/systemRepositories';
import Cache from 'services/Cache';
import redisLoader from './redisLoader';
import rateLimiterLoaders from './rateLimiterLoader';
export default ({ mongoConnection, knex }) => {

View File

@@ -23,14 +23,13 @@ export default class InventoryCostLotTracker extends TenantModel {
static get modifiers() {
return {
groupedEntriesCost(query) {
query.select(['entry_id', 'transaction_id', 'transaction_type']);
query.select(['date', 'item_id', 'transaction_id', 'transaction_type']);
query.sum('cost as cost');
query.groupBy('item_id');
query.groupBy('entry_id');
query.groupBy('transaction_id');
query.groupBy('transaction_type');
query.sum('cost as cost');
query.groupBy('date');
query.groupBy('item_id');
},
filterDateRange(query, startDate, endDate, type = 'day') {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';

View File

@@ -13,6 +13,7 @@ interface IJournalTransactionsFilter {
contactType?: string,
referenceType?: string[],
referenceId?: number[],
index: number|number[]
};
export default class AccountTransactionsRepository extends TenantRepository {
@@ -50,6 +51,13 @@ export default class AccountTransactionsRepository extends TenantRepository {
if (filter.referenceId && filter.referenceId.length > 0) {
query.whereIn('reference_id', filter.referenceId);
}
if (filter.index) {
if (Array.isArray(filter.index)) {
query.whereIn('index', filter.index);
} else {
query.where('index', filter.index);
}
}
});
});
}

View File

@@ -1,5 +1,5 @@
import { sumBy, chain } from 'lodash';
import moment from 'moment';
import moment, { LongDateFormatKey } from 'moment';
import { IBill, IManualJournalEntry, ISystemUser } from 'interfaces';
import JournalPoster from './JournalPoster';
import JournalEntry from './JournalEntry';
@@ -183,7 +183,7 @@ export default class JournalCommands {
async vendorOpeningBalance(
vendorId: number,
openingBalance: number,
openingBalanceAt: Date|string,
openingBalanceAt: Date | string,
authorizedUserId: ISystemUser
) {
const { accountRepository } = this.repositories;
@@ -225,10 +225,7 @@ export default class JournalCommands {
* Writes journal entries of expense model object.
* @param {IExpense} expense
*/
expense(
expense: IExpense,
userId: number,
) {
expense(expense: IExpense, userId: number) {
const mixinEntry = {
referenceType: 'Expense',
referenceId: expense.id,
@@ -279,14 +276,50 @@ export default class JournalCommands {
this.journal.removeEntries();
}
/**
* Reverts the sale invoice cost journal entries.
* @param {Date|string} startingDate
* @return {Promise<void>}
*/
async revertInventoryCostJournalEntries(
startingDate: Date | string
): Promise<void> {
const { transactionsRepository } = this.repositories;
const transactions = await transactionsRepository.journal({
fromDate: startingDate,
referenceType: ['SaleInvoice'],
index: [3, 4],
});
console.log(transactions);
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
}
/**
* Reverts sale invoice the income journal entries.
* @param {number} saleInvoiceId
*/
async revertInvoiceIncomeEntries(
saleInvoiceId: number,
) {
const { transactionsRepository } = this.repositories;
const transactions = await transactionsRepository.journal({
referenceType: ['SaleInvoice'],
referenceId: [saleInvoiceId],
});
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
}
/**
* Writes journal entries from manual journal model object.
* @param {IManualJournal} manualJournalObj
* @param {number} manualJournalId
*/
async manualJournal(
manualJournalObj: IManualJournal,
) {
async manualJournal(manualJournalObj: IManualJournal) {
manualJournalObj.entries.forEach((entry: IManualJournalEntry) => {
const jouranlEntry = new JournalEntry({
debit: entry.debit,
@@ -310,261 +343,68 @@ export default class JournalCommands {
});
}
/**
* Removes and revert accounts balance journal entries that associated
* to the given inventory transactions.
* @param {IInventoryTransaction[]} inventoryTransactions
* @param {Journal} journal
*/
revertEntriesFromInventoryTransactions(
inventoryTransactions: IInventoryTransaction[]
) {
const groupedInvTransactions = chain(inventoryTransactions)
.groupBy(
(invTransaction: IInventoryTransaction) =>
invTransaction.transactionType
)
.map((groupedTrans: IInventoryTransaction[], transType: string) => [
groupedTrans,
transType,
])
.value();
return Promise.all(
groupedInvTransactions.map(
async (grouped: [IInventoryTransaction[], string]) => {
const [invTransGroup, referenceType] = grouped;
const referencesIds = invTransGroup.map(
(trans: IInventoryTransaction) => trans.transactionId
);
const _transactions = await AccountTransaction.tenant()
.query()
.where('reference_type', referenceType)
.whereIn('reference_id', referencesIds)
.withGraphFetched('account.type');
if (_transactions.length > 0) {
this.journal.loadEntries(_transactions);
this.journal.removeEntries(_transactions.map((t: any) => t.id));
}
}
)
);
}
public async nonInventoryEntries(transactions: NonInventoryJEntries[]) {
const receivableAccount = { id: 10 };
const payableAccount = { id: 11 };
transactions.forEach((trans: NonInventoryJEntries) => {
const commonEntry = {
date: trans.date,
referenceId: trans.referenceId,
referenceType: trans.referenceType,
};
switch (trans.referenceType) {
case 'Bill':
const payableEntry: JournalEntry = new JournalEntry({
...commonEntry,
credit: trans.payable,
account: payableAccount.id,
});
const costEntry: JournalEntry = new JournalEntry({
...commonEntry,
});
this.journal.credit(payableEntry);
this.journal.debit(costEntry);
break;
case 'SaleInvoice':
const receivableEntry: JournalEntry = new JournalEntry({
...commonEntry,
debit: trans.receivable,
account: receivableAccount.id,
});
const saleIncomeEntry: JournalEntry = new JournalEntry({
...commonEntry,
credit: trans.income,
account: trans.incomeAccountId,
});
this.journal.debit(receivableEntry);
this.journal.credit(saleIncomeEntry);
break;
}
});
}
/**
*
* @param {string} referenceType -
* @param {number} referenceId -
* @param {ISaleInvoice[]} sales -
*/
public async inventoryEntries(transactions: IInventoryCostEntity[]) {
const receivableAccount = { id: 10 };
const payableAccount = { id: 11 };
transactions.forEach((sale: IInventoryCostEntity) => {
const commonEntry = {
date: sale.date,
referenceId: sale.referenceId,
referenceType: sale.referenceType,
};
switch (sale.referenceType) {
case 'Bill':
const inventoryDebit: JournalEntry = new JournalEntry({
...commonEntry,
debit: sale.inventory,
account: sale.inventoryAccount,
});
const payableEntry: JournalEntry = new JournalEntry({
...commonEntry,
credit: sale.inventory,
account: payableAccount.id,
});
this.journal.debit(inventoryDebit);
this.journal.credit(payableEntry);
break;
case 'SaleInvoice':
const receivableEntry: JournalEntry = new JournalEntry({
...commonEntry,
debit: sale.income,
account: receivableAccount.id,
});
const incomeEntry: JournalEntry = new JournalEntry({
...commonEntry,
credit: sale.income,
account: sale.incomeAccount,
});
// Cost journal transaction.
const costEntry: JournalEntry = new JournalEntry({
...commonEntry,
debit: sale.cost,
account: sale.costAccount,
});
const inventoryCredit: JournalEntry = new JournalEntry({
...commonEntry,
credit: sale.cost,
account: sale.inventoryAccount,
});
this.journal.debit(receivableEntry);
this.journal.debit(costEntry);
this.journal.credit(incomeEntry);
this.journal.credit(inventoryCredit);
break;
}
});
}
/**
* Writes journal entries for given sale invoice.
* ----------
* - Receivable accounts -> Debit -> XXXX
* - Income -> Credit -> XXXX
*
* -------
* - Cost of goods sold -> Debit -> YYYY
* - Inventory assets -> YYYY
* - Inventory assets -> Credit -> YYYY
*
* @param {ISaleInvoice} saleInvoice
* @param {JournalPoster} journal
*/
saleInvoice(
saleInvoice: ISaleInvoice & {
costTransactions: IInventoryLotCost[];
entries: IItemEntry & { item: IItem };
},
receivableAccountsId: number
saleInvoiceInventoryCost(
inventoryCostLot: IInventoryLotCost & { item: IItem }
) {
let inventoryTotal: number = 0;
const commonEntry = {
referenceType: 'SaleInvoice',
referenceId: saleInvoice.id,
date: saleInvoice.invoiceDate,
referenceId: inventoryCostLot.transactionId,
date: inventoryCostLot.date,
};
const costTransactions: Map<number, number> = new Map(
saleInvoice.costTransactions.map((trans: IInventoryLotCost) => [
trans.entryId,
trans.cost,
])
);
// XXX Debit - Receivable account.
const receivableEntry = new JournalEntry({
// XXX Debit - Cost account.
const costEntry = new JournalEntry({
...commonEntry,
debit: saleInvoice.balance,
account: receivableAccountsId,
index: 1,
debit: inventoryCostLot.cost,
account: inventoryCostLot.item.costAccountId,
index: 3,
});
this.journal.debit(receivableEntry);
saleInvoice.entries.forEach(
(entry: IItemEntry & { item: IItem }, index) => {
const cost: number = costTransactions.get(entry.id);
const income: number = entry.quantity * entry.rate;
if (entry.item.type === 'inventory' && cost) {
// XXX Debit - Cost account.
const costEntry = new JournalEntry({
...commonEntry,
debit: cost,
account: entry.item.costAccountId,
note: entry.description,
index: index + 3,
});
this.journal.debit(costEntry);
inventoryTotal += cost;
}
// XXX Credit - Income account.
const incomeEntry = new JournalEntry({
...commonEntry,
credit: income,
account: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
});
this.journal.credit(incomeEntry);
if (inventoryTotal > 0) {
// XXX Credit - Inventory account.
const inventoryEntry = new JournalEntry({
...commonEntry,
credit: inventoryTotal,
account: entry.item.inventoryAccountId,
index: index + 4,
});
this.journal.credit(inventoryEntry);
}
}
);
// XXX Credit - Inventory account.
const inventoryEntry = new JournalEntry({
...commonEntry,
credit: inventoryCostLot.cost,
account: inventoryCostLot.item.inventoryAccountId,
index: 4,
});
this.journal.credit(inventoryEntry);
this.journal.debit(costEntry);
}
/**
* Writes the sale invoice income journal entries.
* -----
* - Receivable accounts -> Debit -> XXXX
* - Income -> Credit -> XXXX
*
* @param {ISaleInvoice} saleInvoice
* @param {number} receivableAccountsId
* @param {number} authorizedUserId
* @param {ISaleInvoice} saleInvoice
* @param {number} receivableAccountsId
* @param {number} authorizedUserId
*/
saleInvoiceNonInventory(
async saleInvoiceIncomeEntries(
saleInvoice: ISaleInvoice & {
entries: IItemEntry & { item: IItem };
},
receivableAccountsId: number,
authorizedUserId: number,
) {
receivableAccountId: number
): Promise<void> {
const commonEntry = {
referenceType: 'SaleInvoice',
referenceId: saleInvoice.id,
date: saleInvoice.invoiceDate,
userId: authorizedUserId,
userId: saleInvoice.userId,
};
// XXX Debit - Receivable account.
const receivableEntry = new JournalEntry({
...commonEntry,
debit: saleInvoice.balance,
account: receivableAccountsId,
account: receivableAccountId,
index: 1,
});
this.journal.debit(receivableEntry);

View File

@@ -17,7 +17,6 @@ import {
IContactEditDTO,
IContact,
ISaleInvoice,
ISystemService,
ISystemUser,
} from 'interfaces';
import { ServiceError } from 'exceptions';

View File

@@ -1,10 +1,16 @@
import { Container, Service, Inject } from 'typedi';
import { pick } from 'lodash';
import config from 'config';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { IInventoryLotCost, IInventoryTransaction, IItem, IItemEntry } from 'interfaces'
import {
IInventoryLotCost,
IInventoryTransaction,
IItem,
IItemEntry,
} from 'interfaces';
import InventoryAverageCost from 'services/Inventory/InventoryAverageCost';
import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker';
import TenancyService from 'services/Tenancy/TenancyService';
@@ -27,9 +33,9 @@ export default class InventoryService {
itemEntries: IItemEntry[],
transactionType: string,
transactionId: number,
direction: 'IN'|'OUT',
date: Date|string,
lotNumber: number,
direction: 'IN' | 'OUT',
date: Date | string,
lotNumber: number
) {
return itemEntries.map((entry: IItemEntry) => ({
...pick(entry, ['itemId', 'quantity', 'rate']),
@@ -62,13 +68,21 @@ export default class InventoryService {
let costMethodComputer: IInventoryCostMethod;
// Switch between methods based on the item cost method.
switch('AVG') {
switch ('AVG') {
case 'FIFO':
case 'LIFO':
costMethodComputer = new InventoryCostLotTracker(tenantId, fromDate, itemId);
costMethodComputer = new InventoryCostLotTracker(
tenantId,
fromDate,
itemId
);
break;
case 'AVG':
costMethodComputer = new InventoryAverageCost(tenantId, fromDate, itemId);
costMethodComputer = new InventoryAverageCost(
tenantId,
fromDate,
itemId
);
break;
}
return costMethodComputer.computeItemCost();
@@ -77,20 +91,24 @@ export default class InventoryService {
/**
* Schedule item cost compute job.
* @param {number} tenantId
* @param {number} itemId
* @param {Date} startingDate
* @param {number} itemId
* @param {Date} startingDate
*/
async scheduleComputeItemCost(tenantId: number, itemId: number, startingDate: Date|string) {
async scheduleComputeItemCost(
tenantId: number,
itemId: number,
startingDate: Date | string
) {
const agenda = Container.get('agenda');
// Cancel any `compute-item-cost` in the queue has upper starting date
// Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item.
await agenda.cancel({
name: 'compute-item-cost',
nextRunAt: { $ne: null },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { "$gt": startingDate }
'data.startingDate': { $gt: startingDate },
});
// Retrieve any `compute-item-cost` in the queue has lower starting date
@@ -100,23 +118,29 @@ export default class InventoryService {
nextRunAt: { $ne: null },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { "$lte": startingDate }
'data.startingDate': { $lte: startingDate },
});
if (dependsJobs.length === 0) {
await agenda.schedule('in 30 seconds', 'compute-item-cost', {
startingDate, itemId, tenantId,
});
await agenda.schedule(
config.scheduleComputeItemCost,
'compute-item-cost',
{
startingDate,
itemId,
tenantId,
}
);
// Triggers `onComputeItemCostJobScheduled` event.
await this.eventDispatcher.dispatch(
events.inventory.onComputeItemCostJobScheduled,
{ startingDate, itemId, tenantId },
{ startingDate, itemId, tenantId }
);
}
}
/**
* Records the inventory transactions.
* Records the inventory transactions.
* @param {number} tenantId - Tenant id.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
@@ -125,27 +149,23 @@ export default class InventoryService {
async recordInventoryTransactions(
tenantId: number,
inventoryEntries: IInventoryTransaction[],
deleteOld: boolean,
deleteOld: boolean
): Promise<void> {
inventoryEntries.forEach(async (entry: IInventoryTransaction) => {
await this.recordInventoryTransaction(
tenantId,
entry,
deleteOld,
);
await this.recordInventoryTransaction(tenantId, entry, deleteOld);
});
}
/**
*
* @param {number} tenantId
* @param {IInventoryTransaction} inventoryEntry
* @param {boolean} deleteOld
*
* @param {number} tenantId
* @param {IInventoryTransaction} inventoryEntry
* @param {boolean} deleteOld
*/
async recordInventoryTransaction(
tenantId: number,
inventoryEntry: IInventoryTransaction,
deleteOld: boolean = false,
deleteOld: boolean = false
): Promise<IInventoryTransaction> {
const { InventoryTransaction, Item } = this.tenancy.models(tenantId);
@@ -153,7 +173,7 @@ export default class InventoryService {
await this.deleteInventoryTransactions(
tenantId,
inventoryEntry.transactionId,
inventoryEntry.transactionType,
inventoryEntry.transactionType
);
}
return InventoryTransaction.query().insert({
@@ -165,14 +185,14 @@ export default class InventoryService {
/**
* Deletes the given inventory transactions.
* @param {number} tenantId - Tenant id.
* @param {string} transactionType
* @param {number} transactionId
* @param {string} transactionType
* @param {number} transactionId
* @return {Promise}
*/
async deleteInventoryTransactions(
tenantId: number,
transactionId: number,
transactionType: string,
transactionType: string
): Promise<void> {
const { InventoryTransaction } = this.tenancy.models(tenantId);
@@ -184,16 +204,16 @@ export default class InventoryService {
/**
* Records the inventory cost lot transaction.
* @param {number} tenantId
* @param {IInventoryLotCost} inventoryLotEntry
* @param {number} tenantId
* @param {IInventoryLotCost} inventoryLotEntry
* @return {Promise<IInventoryLotCost>}
*/
async recordInventoryCostLotTransaction(
tenantId: number,
inventoryLotEntry: IInventoryLotCost,
inventoryLotEntry: IInventoryLotCost
): Promise<void> {
const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
return InventoryCostLotTracker.query().insert({
...inventoryLotEntry,
});
@@ -209,13 +229,14 @@ export default class InventoryService {
const LOT_NUMBER_KEY = 'lot_number_increment';
const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY });
return (storedLotNumber && storedLotNumber.value) ?
parseInt(storedLotNumber.value, 10) : 1;
return storedLotNumber && storedLotNumber.value
? parseInt(storedLotNumber.value, 10)
: 1;
}
/**
* Increment the next inventory LOT number.
* @param {number} tenantId
* @param {number} tenantId
* @return {Promise<number>}
*/
async incrementNextLotNumber(tenantId: number) {
@@ -236,4 +257,4 @@ export default class InventoryService {
return lotNumber;
}
}
}

View File

@@ -19,7 +19,7 @@ export default class JournalPosterService {
tenantId: number,
referenceId: number|number[],
referenceType: string
) {
): Promise<void> {
const journal = new JournalPoster(tenantId);
const journalCommand = new JournalCommands(journal);

View File

@@ -13,10 +13,13 @@ import {
IPaginationMeta,
IFilterMeta,
ISystemUser,
IItem,
IItemEntry,
} from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import events from 'subscribers/events';
import InventoryService from 'services/Inventory/Inventory';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
@@ -25,6 +28,7 @@ import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import JournalPosterService from './JournalPosterService';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
@@ -42,7 +46,7 @@ const ERRORS = {
* @service
*/
@Service()
export default class SaleInvoicesService extends SalesInvoicesCost {
export default class SaleInvoicesService {
@Inject()
tenancy: TenancyService;
@@ -70,6 +74,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject()
saleEstimatesService: SaleEstimateService;
@Inject()
journalService: JournalPosterService;
/**
* Validate whether sale invoice number unqiue on the storage.
*/
@@ -101,6 +108,28 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
}
}
/**
* Validate the sale invoice has no payment entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
*/
async validateInvoiceHasNoPaymentEntries(
tenantId: number,
saleInvoiceId: number
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
// Retrieve the sale invoice associated payment receive entries.
const entries = await PaymentReceiveEntry.query().where(
'invoice_id',
saleInvoiceId
);
if (entries.length > 0) {
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate whether sale invoice exists on the storage.
* @param {Request} req
@@ -148,7 +177,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
balance,
paymentAmount: 0,
entries: saleInvoiceDTO.entries.map((entry) => ({
reference_type: 'SaleInvoice',
referenceType: 'SaleInvoice',
...omit(entry, ['amount', 'id']),
})),
};
@@ -208,7 +237,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
});
this.logger.info('[sale_invoice] successfully inserted.', {
tenantId,
saleInvoice,
saleInvoiceId: saleInvoice.id,
});
return saleInvoice;
@@ -238,19 +267,19 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const saleInvoiceObj = this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice,
oldSaleInvoice
);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo,
saleInvoiceId,
saleInvoiceId
);
}
// Validate items ids existance.
@@ -261,7 +290,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries,
saleInvoiceDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
@@ -270,7 +299,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
'SaleInvoice',
saleInvoiceDTO.entries
);
this.logger.info('[sale_invoice] trying to update sale invoice.');
const saleInvoice: ISaleInvoice = await SaleInvoice.query().upsertGraphAndFetch(
{
@@ -280,10 +308,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
);
// Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice,
oldSaleInvoice,
tenantId,
saleInvoiceId,
saleInvoice,
oldSaleInvoice,
authorizedUser,
});
return saleInvoice;
@@ -303,51 +331,33 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve details of the given sale invoice id.
const saleInvoice = await this.getInvoiceOrThrowError(
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Throws error in case the sale invoice already published.
if (saleInvoice.isDelivered) {
if (oldSaleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await saleInvoiceRepository.update(
{
deliveredAt: moment().toMySqlDateTime(),
},
{ deliveredAt: moment().toMySqlDateTime(), },
{ id: saleInvoiceId }
);
}
/**
* Validate the sale invoice has no payment entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
*/
async validateInvoiceHasNoPaymentEntries(
tenantId: number,
saleInvoiceId: number
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
// Retrieve the sale invoice associated payment receive entries.
const entries = await PaymentReceiveEntry.query().where(
'invoice_id',
saleInvoiceId
);
if (entries.length > 0) {
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
// Triggers `onSaleInvoiceDelivered` event.
this.eventDispatcher.dispatch(events.saleInvoice.onDelivered, {
tenantId,
saleInvoiceId,
oldSaleInvoice,
});
}
/**
* Deletes the given sale invoice with associated entries
* and journal transactions.
* @async
* @param {number} tenantId - Tenant id.
* @param {Number} saleInvoiceId - The given sale invoice id.
* @param {ISystemUser} authorizedUser -
*/
public async deleteSaleInvoice(
tenantId: number,
@@ -376,15 +386,15 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
tenantId,
saleInvoiceId
);
this.logger.info('[sale_invoice] delete sale invoice with entries.');
await saleInvoiceRepository.deleteById(saleInvoiceId);
await ItemEntry.query()
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
.delete();
await saleInvoiceRepository.deleteById(saleInvoiceId);
// Triggers `onSaleInvoiceDeleted` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted, {
tenantId,
@@ -408,7 +418,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
tenantId: number,
saleInvoiceId: number,
saleInvoiceDate: Date,
override?: boolean,
override?: boolean
): Promise<void> {
// Gets the next inventory lot number.
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
@@ -451,41 +461,38 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
}
/**
* Records the journal entries of the given sale invoice just
* in case the invoice has no inventory items entries.
*
* @param {number} tenantId -
* @param {number} saleInvoiceId
* @param {boolean} override
* @return {Promise<void>}
* Writes the sale invoice income journal entries.
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoice} saleInvoice - Sale invoice id.
*/
public async recordNonInventoryJournalEntries(
public async writesIncomeJournalEntries(
tenantId: number,
saleInvoiceId: number,
authorizedUserId: number,
saleInvoice: ISaleInvoice & {
entries: IItemEntry & { item: IItem };
},
override: boolean = false
): Promise<void> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries = await this.itemsEntriesService.getInventoryEntries(
tenantId,
'SaleInvoice',
saleInvoiceId
);
// Can't continue if the sale invoice has inventory items entries.
if (inventoryEntries.length > 0) return;
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
const saleInvoice = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries.item'
);
await this.writeNonInventoryInvoiceEntries(
tenantId,
const receivableAccount = await accountRepository.findOne({
slug: 'accounts-receivable',
});
if (override) {
await journalCommands.revertInvoiceIncomeEntries(saleInvoice.id);
}
// Records the sale invoice journal entries.
await journalCommands.saleInvoiceIncomeEntries(
saleInvoice,
authorizedUserId,
override
receivableAccount.id
);
await Promise.all([
journal.deleteEntries(),
journal.saveBalance(),
journal.saveEntries()
]);
}
/**
@@ -519,6 +526,23 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
);
}
/**
* Reverting the sale invoice journal entries.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @return {Promise<void>}
*/
public async revertInvoiceJournalEntries(
tenantId: number,
saleInvoiceId: number | number[]
): Promise<void> {
return this.journalService.revertJournalTransactions(
tenantId,
saleInvoiceId,
'SaleInvoice'
);
}
/**
* Retrieve sale invoice with associated entries.
* @async

View File

@@ -24,7 +24,7 @@ export default class SaleInvoicesCost {
async scheduleComputeCostByItemsIds(
tenantId: number,
inventoryItemsIds: number[],
startingDate: Date,
startingDate: Date
) {
const asyncOpers: Promise<[]>[] = [];
@@ -32,7 +32,7 @@ export default class SaleInvoicesCost {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
inventoryItemId,
startingDate,
startingDate
);
asyncOpers.push(oper);
});
@@ -49,7 +49,7 @@ export default class SaleInvoicesCost {
*/
async scheduleComputeCostByInvoiceId(
tenantId: number,
saleInvoiceId: number,
saleInvoiceId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
@@ -62,7 +62,7 @@ export default class SaleInvoicesCost {
return this.scheduleComputeCostByEntries(
tenantId,
saleInvoice.entries,
saleInvoice.invoiceDate,
saleInvoice.invoiceDate
);
}
@@ -82,24 +82,24 @@ export default class SaleInvoicesCost {
const bill = await Bill.query()
.findById(billId)
.withGraphFetched('entries');
return this.scheduleComputeCostByEntries(
tenantId,
bill.entries,
bill.billDate,
bill.billDate
);
}
/**
* Schedules the compute inventory items by the given invoice.
* @param {number} tenantId
* @param {ISaleInvoice & { entries: IItemEntry[] }} saleInvoice
* @param {boolean} override
* @param {number} tenantId
* @param {ISaleInvoice & { entries: IItemEntry[] }} saleInvoice
* @param {boolean} override
*/
async scheduleComputeCostByEntries(
tenantId: number,
entries: IItemEntry[],
startingDate: Date,
startingDate: Date
) {
const { Item } = this.tenancy.models(tenantId);
@@ -121,105 +121,57 @@ export default class SaleInvoicesCost {
/**
* Schedule writing journal entries.
* @param {Date} startingDate
* @param {Date} startingDate
* @return {Promise<agenda>}
*/
scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) {
const agenda = Container.get('agenda');
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
startingDate, tenantId,
startingDate,
tenantId,
});
}
/**
* Writes journal entries from sales invoices.
* @param {number} tenantId - The tenant id.
* @param {Date} startingDate
* @param {boolean} override
* @param {Date} startingDate - Starting date.
* @param {boolean} override
*/
async writeJournalEntries(tenantId: number, startingDate: Date, override: boolean) {
const { AccountTransaction, SaleInvoice, Account } = this.tenancy.models(tenantId);
async writeInventoryCostJournalEntries(
tenantId: number,
startingDate: Date,
override: boolean
) {
const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const receivableAccount = await accountRepository.findOne({
slug: 'accounts-receivable',
});
const salesInvoices = await SaleInvoice.query()
.onBuild((builder: any) => {
builder.modify('filterDateRange', startingDate);
builder.orderBy('invoice_date', 'ASC');
const inventoryCostLotTrans = await InventoryCostLotTracker.query()
.where('direction', 'OUT')
.modify('groupedEntriesCost')
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.where('cost', '>', 0)
.withGraphFetched('item');
builder.withGraphFetched('entries.item');
builder.withGraphFetched('costTransactions(groupedEntriesCost)');
});
const accountsDepGraph = await accountRepository.getDependencyGraph();
const journal = new JournalPoster(tenantId, accountsDepGraph);
const journal = new JournalPoster(tenantId, accountsDepGraph);
const journalCommands = new JournalCommands(journal);
if (override) {
const oldTransactions = await AccountTransaction.query()
.whereIn('reference_type', ['SaleInvoice'])
.onBuild((builder: any) => {
builder.modify('filterDateRange', startingDate);
})
.withGraphFetched('account.type');
journal.fromTransactions(oldTransactions);
journal.removeEntries();
await journalCommands.revertInventoryCostJournalEntries(startingDate);
}
salesInvoices.forEach((saleInvoice: ISaleInvoice & {
costTransactions: IInventoryLotCost[],
entries: IItemEntry & { item: IItem },
}) => {
journalCommands.saleInvoice(saleInvoice, receivableAccount.id);
});
inventoryCostLotTrans.forEach(
(inventoryCostLot: IInventoryLotCost & { item: IItem }) => {
journalCommands.saleInvoiceInventoryCost(inventoryCostLot);
}
);
return Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
journal.saveBalance()
]);
}
/**
* Writes the sale invoice journal entries.
*/
async writeNonInventoryInvoiceEntries(
tenantId: number,
saleInvoice: ISaleInvoice,
authorizedUserId: number,
override: boolean = false,
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const { AccountTransaction } = this.tenancy.models(tenantId);
// Receivable account.
const receivableAccount = await accountRepository.findOne({
slug: 'accounts-receivable',
});
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
if (override) {
const oldTransactions = await AccountTransaction.query()
.where('reference_type', 'SaleInvoice')
.where('reference_id', saleInvoice.id)
.withGraphFetched('account.type');
journal.fromTransactions(oldTransactions);
journal.removeEntries();
}
journalCommands.saleInvoiceNonInventory(
saleInvoice,
receivableAccount.id,
authorizedUserId,
);
await Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
]);
}
}
}

View File

@@ -80,6 +80,7 @@ export default {
onEdited: 'onSaleInvoiceEdited',
onDelete: 'onSaleInvoiceDelete',
onDeleted: 'onSaleInvoiceDeleted',
onDelivered: 'onSaleInvoiceDelivered',
onBulkDelete: 'onSaleInvoiceBulkDeleted',
onPublished: 'onSaleInvoicePublished',
onInventoryTransactionsCreated: 'onInvoiceInventoryTransactionsCreated',

View File

@@ -7,6 +7,7 @@ import SettingsService from 'services/Settings/SettingsService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import SaleInvoicesService from 'services/Sales/SalesInvoices';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
@EventSubscriber()
export default class SaleInvoiceSubscriber {
@@ -16,6 +17,7 @@ export default class SaleInvoiceSubscriber {
saleEstimatesService: SaleEstimateService;
saleInvoicesService: SaleInvoicesService;
itemsEntriesService: ItemsEntriesService;
salesInvoicesCost: SalesInvoicesCost;
constructor() {
this.logger = Container.get('logger');
@@ -24,6 +26,7 @@ export default class SaleInvoiceSubscriber {
this.saleEstimatesService = Container.get(SaleEstimateService);
this.saleInvoicesService = Container.get(SaleInvoicesService);
this.itemsEntriesService = Container.get(ItemsEntriesService);
this.salesInvoicesCost = Container.get(SalesInvoicesCost);
}
/**
@@ -95,15 +98,38 @@ export default class SaleInvoiceSubscriber {
}
/**
* Records journal entries of the non-inventory invoice.
* Handles handle write income journal entries of sale invoice.
*/
@On(events.saleInvoice.onCreated)
@On(events.saleInvoice.onEdited)
public async handleWritingNonInventoryEntries({ tenantId, saleInvoice, authorizedUser }) {
await this.saleInvoicesService.recordNonInventoryJournalEntries(
public async handleWriteInvoiceIncomeJournalEntries({
tenantId,
saleInvoiceId,
saleInvoice,
authorizedUser,
}) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const saleInvoiceWithItems = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries.item'
);
await this.saleInvoicesService.writesIncomeJournalEntries(
tenantId,
saleInvoice.id,
authorizedUser.id,
saleInvoiceWithItems
);
}
/**
* Increments the sale invoice items once the invoice created.
*/
@On(events.saleInvoice.onCreated)
public async handleDecrementSaleInvoiceItemsQuantity({
tenantId,
saleInvoice,
}) {
await this.itemsEntriesService.decrementItemsQuantity(
tenantId,
saleInvoice.entries
);
}
@@ -146,6 +172,29 @@ export default class SaleInvoiceSubscriber {
);
}
/**
* Records journal entries of the non-inventory invoice.
*/
@On(events.saleInvoice.onEdited)
public async handleRewriteJournalEntriesOnceInvoiceEdit({
tenantId,
saleInvoiceId,
saleInvoice,
authorizedUser,
}) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const saleInvoiceWithItems = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries.item'
);
await this.saleInvoicesService.writesIncomeJournalEntries(
tenantId,
saleInvoiceWithItems,
true
);
}
/**
* Handles customer balance decrement once sale invoice deleted.
*/
@@ -166,6 +215,20 @@ export default class SaleInvoiceSubscriber {
);
}
/**
* Handle reverting journal entries once sale invoice delete.
*/
@On(events.saleInvoice.onDelete)
public async handleRevertingInvoiceJournalEntriesOnDelete({
tenantId,
saleInvoiceId,
}) {
await this.saleInvoicesService.revertInvoiceJournalEntries(
tenantId,
saleInvoiceId,
);
}
/**
* Handles deleting the inventory transactions once the invoice deleted.
*/
@@ -203,7 +266,7 @@ export default class SaleInvoiceSubscriber {
saleInvoiceId,
}
);
await this.saleInvoicesService.scheduleComputeCostByInvoiceId(
await this.salesInvoicesCost.scheduleComputeCostByInvoiceId(
tenantId,
saleInvoiceId
);
@@ -222,27 +285,13 @@ export default class SaleInvoiceSubscriber {
const startingDates = map(oldInventoryTransactions, 'date');
const startingDate = head(startingDates);
await this.saleInvoicesService.scheduleComputeCostByItemsIds(
await this.salesInvoicesCost.scheduleComputeCostByItemsIds(
tenantId,
inventoryItemsIds,
startingDate
);
}
/**
* Increments the sale invoice items once the invoice created.
*/
@On(events.saleInvoice.onCreated)
public async handleDecrementSaleInvoiceItemsQuantity({
tenantId,
saleInvoice,
}) {
await this.itemsEntriesService.decrementItemsQuantity(
tenantId,
saleInvoice.entries
);
}
/**
* Decrements the sale invoice items once the invoice deleted.
*/