mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: average rate cost method.
This commit is contained in:
@@ -17,6 +17,7 @@ exports.up = function(knex) {
|
|||||||
table.string('inv_lot_number').index();
|
table.string('inv_lot_number').index();
|
||||||
|
|
||||||
table.date('delivered_at').index();
|
table.date('delivered_at').index();
|
||||||
|
table.integer('user_id').unsigned();
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ exports.up = function(knex) {
|
|||||||
table.integer('quantity').unsigned();
|
table.integer('quantity').unsigned();
|
||||||
table.decimal('rate', 13, 3).unsigned();
|
table.decimal('rate', 13, 3).unsigned();
|
||||||
|
|
||||||
table.integer('lot_number').index();
|
table.string('lot_number').index();
|
||||||
|
|
||||||
table.string('transaction_type').index();
|
table.string('transaction_type').index();
|
||||||
table.integer('transaction_id').unsigned().index();
|
table.integer('transaction_id').unsigned().index();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ exports.up = function(knex) {
|
|||||||
table.integer('quantity').unsigned().index();
|
table.integer('quantity').unsigned().index();
|
||||||
table.decimal('rate', 13, 3);
|
table.decimal('rate', 13, 3);
|
||||||
table.integer('remaining');
|
table.integer('remaining');
|
||||||
table.integer('cost');
|
table.decimal('cost', 13, 3);
|
||||||
table.integer('lot_number').index();
|
table.integer('lot_number').index();
|
||||||
|
|
||||||
table.string('transaction_type').index();
|
table.string('transaction_type').index();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface IInventoryTransaction {
|
|||||||
transactionType: string,
|
transactionType: string,
|
||||||
transactionId: string,
|
transactionId: string,
|
||||||
lotNumber: string,
|
lotNumber: string,
|
||||||
|
entryId: number
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IInventoryLotCost {
|
export interface IInventoryLotCost {
|
||||||
@@ -19,7 +20,9 @@ export interface IInventoryLotCost {
|
|||||||
itemId: number,
|
itemId: number,
|
||||||
rate: number,
|
rate: number,
|
||||||
remaining: number,
|
remaining: number,
|
||||||
|
cost: number,
|
||||||
lotNumber: string|number,
|
lotNumber: string|number,
|
||||||
transactionType: string,
|
transactionType: string,
|
||||||
transactionId: string,
|
transactionId: string,
|
||||||
|
entryId: number
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,30 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import moment from 'moment';
|
import {EventDispatcher} from "event-dispatch";
|
||||||
|
// import {
|
||||||
|
// EventDispatcher,
|
||||||
|
// } from 'decorators/eventDispatcher';
|
||||||
|
import events from 'subscribers/events';
|
||||||
import InventoryService from 'services/Inventory/Inventory';
|
import InventoryService from 'services/Inventory/Inventory';
|
||||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
|
||||||
|
|
||||||
export default class ComputeItemCostJob {
|
export default class ComputeItemCostJob {
|
||||||
depends: number;
|
|
||||||
agenda: any;
|
agenda: any;
|
||||||
startingDate: Date;
|
eventDispatcher: EventDispatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param agenda
|
||||||
|
*/
|
||||||
constructor(agenda) {
|
constructor(agenda) {
|
||||||
this.agenda = agenda;
|
this.agenda = agenda;
|
||||||
this.depends = 0;
|
this.eventDispatcher = new EventDispatcher();
|
||||||
this.startingDate = null;
|
|
||||||
|
|
||||||
this.agenda.on('complete:compute-item-cost', this.onJobFinished.bind(this));
|
agenda.define(
|
||||||
|
'compute-item-cost',
|
||||||
|
{ priority: 'high', concurrency: 1 },
|
||||||
|
this.handler.bind(this),
|
||||||
|
);
|
||||||
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));
|
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));
|
||||||
|
this.agenda.on('complete:compute-item-cost', this.onJobCompleted.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,12 +33,14 @@ export default class ComputeItemCostJob {
|
|||||||
*/
|
*/
|
||||||
public async handler(job, done: Function): Promise<void> {
|
public async handler(job, done: Function): Promise<void> {
|
||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
|
const inventoryService = Container.get(InventoryService);
|
||||||
|
|
||||||
|
const { startingDate, itemId, tenantId } = job.attrs.data;
|
||||||
|
|
||||||
Logger.info(`Compute item cost - started: ${job.attrs.data}`);
|
Logger.info(`Compute item cost - started: ${job.attrs.data}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
await inventoryService.computeItemCost(tenantId, startingDate, itemId);
|
||||||
Logger.info(`Compute item cost - completed: ${job.attrs.data}`);
|
Logger.info(`Compute item cost - completed: ${job.attrs.data}`);
|
||||||
done();
|
done();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -42,30 +54,24 @@ export default class ComputeItemCostJob {
|
|||||||
* @param {Job} job - .
|
* @param {Job} job - .
|
||||||
*/
|
*/
|
||||||
async onJobStart(job) {
|
async onJobStart(job) {
|
||||||
const { startingDate } = job.attrs.data;
|
const { startingDate, itemId, tenantId } = job.attrs.data;
|
||||||
this.depends += 1;
|
|
||||||
|
|
||||||
if (!this.startingDate || moment(this.startingDate).isBefore(startingDate)) {
|
await this.eventDispatcher.dispatch(
|
||||||
this.startingDate = startingDate;
|
events.inventory.onComputeItemCostJobStarted,
|
||||||
}
|
{ startingDate, itemId, tenantId }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle job complete items cost finished.
|
* Handle job complete items cost finished.
|
||||||
* @param {Job} job -
|
* @param {Job} job -
|
||||||
*/
|
*/
|
||||||
async onJobFinished() {
|
async onJobCompleted(job) {
|
||||||
const agenda = Container.get('agenda');
|
const { startingDate, itemId, tenantId } = job.attrs.data;
|
||||||
const startingDate = this.startingDate;
|
|
||||||
|
|
||||||
this.depends = Math.max(this.depends - 1, 0);
|
await this.eventDispatcher.dispatch(
|
||||||
|
events.inventory.onComputeItemCostJobCompleted,
|
||||||
if (this.depends === 0) {
|
{ startingDate, itemId, tenantId },
|
||||||
this.startingDate = null;
|
);
|
||||||
|
|
||||||
await agenda.now('rewrite-invoices-journal-entries', {
|
|
||||||
startingDate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,24 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
|||||||
|
|
||||||
export default class WriteInvoicesJournalEntries {
|
export default class WriteInvoicesJournalEntries {
|
||||||
|
|
||||||
|
constructor(agenda) {
|
||||||
|
agenda.define(
|
||||||
|
'rewrite-invoices-journal-entries',
|
||||||
|
{ priority: 'normal', concurrency: 1, },
|
||||||
|
this.handler.bind(this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async handler(job, done: Function): Promise<void> {
|
public async handler(job, done: Function): Promise<void> {
|
||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const { startingDate } = job.attrs.data;
|
const { startingDate, tenantId } = job.attrs.data;
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
await SalesInvoicesCost.writeJournalEntries(startingDate, true);
|
await salesInvoicesCost.writeJournalEntries(tenantId, startingDate, true);
|
||||||
Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
|
Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
|
||||||
done();
|
done();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ import 'subscribers/paymentMades';
|
|||||||
import 'subscribers/paymentReceives';
|
import 'subscribers/paymentReceives';
|
||||||
import 'subscribers/saleEstimates';
|
import 'subscribers/saleEstimates';
|
||||||
import 'subscribers/saleReceipts';
|
import 'subscribers/saleReceipts';
|
||||||
|
import 'subscribers/inventory';
|
||||||
@@ -19,17 +19,8 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
new UserInviteMailJob(agenda);
|
new UserInviteMailJob(agenda);
|
||||||
new SendLicenseViaEmailJob(agenda);
|
new SendLicenseViaEmailJob(agenda);
|
||||||
new SendLicenseViaPhoneJob(agenda);
|
new SendLicenseViaPhoneJob(agenda);
|
||||||
|
new ComputeItemCost(agenda);
|
||||||
agenda.define(
|
new RewriteInvoicesJournalEntries(agenda);
|
||||||
'compute-item-cost',
|
|
||||||
{ priority: 'high', concurrency: 20 },
|
|
||||||
new ComputeItemCost(agenda).handler,
|
|
||||||
);
|
|
||||||
agenda.define(
|
|
||||||
'rewrite-invoices-journal-entries',
|
|
||||||
{ priority: 'normal', concurrency: 1, },
|
|
||||||
new RewriteInvoicesJournalEntries().handler,
|
|
||||||
);
|
|
||||||
|
|
||||||
agenda.define(
|
agenda.define(
|
||||||
'send-sms-notification-subscribe-end',
|
'send-sms-notification-subscribe-end',
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
IExpense,
|
IExpense,
|
||||||
IExpenseCategory,
|
IExpenseCategory,
|
||||||
IItem,
|
IItem,
|
||||||
|
ISaleInvoice,
|
||||||
|
IInventoryLotCost,
|
||||||
|
IItemEntry,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
|
|
||||||
interface IInventoryCostEntity {
|
interface IInventoryCostEntity {
|
||||||
@@ -409,4 +412,119 @@ export default class JournalCommands{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes journal entries for given sale invoice.
|
||||||
|
* ----------
|
||||||
|
* - Receivable accounts -> Debit -> XXXX
|
||||||
|
* - Income -> Credit -> XXXX
|
||||||
|
*
|
||||||
|
* - Cost of goods sold -> Debit -> YYYY
|
||||||
|
* - Inventory assets -> YYYY
|
||||||
|
*
|
||||||
|
* @param {ISaleInvoice} saleInvoice
|
||||||
|
* @param {JournalPoster} journal
|
||||||
|
*/
|
||||||
|
saleInvoice(
|
||||||
|
saleInvoice: ISaleInvoice & {
|
||||||
|
costTransactions: IInventoryLotCost[],
|
||||||
|
entries: IItemEntry & { item: IItem },
|
||||||
|
},
|
||||||
|
receivableAccountsId: number,
|
||||||
|
) {
|
||||||
|
let inventoryTotal: number = 0;
|
||||||
|
|
||||||
|
const commonEntry = {
|
||||||
|
referenceType: 'SaleInvoice',
|
||||||
|
referenceId: saleInvoice.id,
|
||||||
|
date: saleInvoice.invoiceDate,
|
||||||
|
};
|
||||||
|
const costTransactions: Map<number, number> = new Map(
|
||||||
|
saleInvoice.costTransactions.map((trans: IInventoryLotCost) => [
|
||||||
|
trans.entryId, trans.cost,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
// XXX Debit - Receivable account.
|
||||||
|
const receivableEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
debit: saleInvoice.balance,
|
||||||
|
account: receivableAccountsId,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saleInvoiceNonInventory(
|
||||||
|
saleInvoice: ISaleInvoice & {
|
||||||
|
entries: IItemEntry & { item: IItem },
|
||||||
|
},
|
||||||
|
receivableAccountsId: number,
|
||||||
|
) {
|
||||||
|
const commonEntry = {
|
||||||
|
referenceType: 'SaleInvoice',
|
||||||
|
referenceId: saleInvoice.id,
|
||||||
|
date: saleInvoice.invoiceDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX Debit - Receivable account.
|
||||||
|
const receivableEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
debit: saleInvoice.balance,
|
||||||
|
account: receivableAccountsId,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
this.journal.debit(receivableEntry);
|
||||||
|
|
||||||
|
saleInvoice.entries.forEach((entry: IItemEntry & { item: IItem }, index: number) => {
|
||||||
|
const income: number = entry.quantity * entry.rate;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Container, Service, Inject } from 'typedi';
|
import { Container, Service, Inject } from 'typedi';
|
||||||
import { IInventoryTransaction, IItem } from 'interfaces'
|
import { pick } from 'lodash';
|
||||||
|
import {
|
||||||
|
EventDispatcher,
|
||||||
|
EventDispatcherInterface,
|
||||||
|
} from 'decorators/eventDispatcher';
|
||||||
|
import { IInventoryTransaction, IItem, IItemEntry } from 'interfaces'
|
||||||
import InventoryAverageCost from 'services/Inventory/InventoryAverageCost';
|
import InventoryAverageCost from 'services/Inventory/InventoryAverageCost';
|
||||||
import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker';
|
import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import events from 'subscribers/events';
|
||||||
|
|
||||||
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
|
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
|
||||||
|
|
||||||
@@ -11,6 +17,31 @@ export default class InventoryService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
|
|
||||||
|
@EventDispatcher()
|
||||||
|
eventDispatcher: EventDispatcherInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the items entries to inventory transactions.
|
||||||
|
*/
|
||||||
|
transformItemEntriesToInventory(
|
||||||
|
itemEntries: IItemEntry[],
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number,
|
||||||
|
direction: 'IN'|'OUT',
|
||||||
|
date: Date|string,
|
||||||
|
lotNumber: number,
|
||||||
|
) {
|
||||||
|
return itemEntries.map((entry: IItemEntry) => ({
|
||||||
|
...pick(entry, ['itemId', 'quantity', 'rate']),
|
||||||
|
lotNumber,
|
||||||
|
transactionType,
|
||||||
|
transactionId,
|
||||||
|
direction,
|
||||||
|
date,
|
||||||
|
entryId: entry.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the given item cost and records the inventory lots transactions
|
* Computes the given item cost and records the inventory lots transactions
|
||||||
* and journal entries based on the cost method FIFO, LIFO or average cost rate.
|
* and journal entries based on the cost method FIFO, LIFO or average cost rate.
|
||||||
@@ -21,25 +52,23 @@ export default class InventoryService {
|
|||||||
async computeItemCost(tenantId: number, fromDate: Date, itemId: number) {
|
async computeItemCost(tenantId: number, fromDate: Date, itemId: number) {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
const { Item } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const item = await Item.query()
|
// Fetches the item with assocaited item category.
|
||||||
.findById(itemId)
|
const item = await Item.query().findById(itemId);
|
||||||
.withGraphFetched('category');
|
|
||||||
|
|
||||||
// Cannot continue if the given item was not inventory item.
|
// Cannot continue if the given item was not inventory item.
|
||||||
if (item.type !== 'inventory') {
|
if (item.type !== 'inventory') {
|
||||||
throw new Error('You could not compute item cost has no inventory type.');
|
throw new Error('You could not compute item cost has no inventory type.');
|
||||||
}
|
}
|
||||||
const costMethod: TCostMethod = item.category.costMethod;
|
|
||||||
let costMethodComputer: IInventoryCostMethod;
|
let costMethodComputer: IInventoryCostMethod;
|
||||||
|
|
||||||
// Switch between methods based on the item cost method.
|
// Switch between methods based on the item cost method.
|
||||||
switch(costMethod) {
|
switch('AVG') {
|
||||||
case 'FIFO':
|
case 'FIFO':
|
||||||
case 'LIFO':
|
case 'LIFO':
|
||||||
costMethodComputer = new InventoryCostLotTracker(fromDate, itemId);
|
costMethodComputer = new InventoryCostLotTracker(tenantId, fromDate, itemId);
|
||||||
break;
|
break;
|
||||||
case 'AVG':
|
case 'AVG':
|
||||||
costMethodComputer = new InventoryAverageCost(fromDate, itemId);
|
costMethodComputer = new InventoryAverageCost(tenantId, fromDate, itemId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return costMethodComputer.computeItemCost();
|
return costMethodComputer.computeItemCost();
|
||||||
@@ -54,9 +83,36 @@ export default class InventoryService {
|
|||||||
async scheduleComputeItemCost(tenantId: number, itemId: number, startingDate: Date|string) {
|
async scheduleComputeItemCost(tenantId: number, itemId: number, startingDate: Date|string) {
|
||||||
const agenda = Container.get('agenda');
|
const agenda = Container.get('agenda');
|
||||||
|
|
||||||
return agenda.schedule('in 3 seconds', 'compute-item-cost', {
|
// Cancel any `compute-item-cost` in the queue has upper starting date
|
||||||
startingDate, itemId, tenantId,
|
// 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 }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Retrieve any `compute-item-cost` in the queue has lower starting date
|
||||||
|
// with the same given item.
|
||||||
|
const dependsJobs = await agenda.jobs({
|
||||||
|
name: 'compute-item-cost',
|
||||||
|
nextRunAt: { $ne: null },
|
||||||
|
'data.tenantId': tenantId,
|
||||||
|
'data.itemId': itemId,
|
||||||
|
'data.startingDate': { "$lte": startingDate }
|
||||||
|
});
|
||||||
|
if (dependsJobs.length === 0) {
|
||||||
|
await agenda.schedule('in 30 seconds', 'compute-item-cost', {
|
||||||
|
startingDate, itemId, tenantId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Triggers `onComputeItemCostJobScheduled` event.
|
||||||
|
await this.eventDispatcher.dispatch(
|
||||||
|
events.inventory.onComputeItemCostJobScheduled,
|
||||||
|
{ startingDate, itemId, tenantId },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,24 +124,11 @@ export default class InventoryService {
|
|||||||
*/
|
*/
|
||||||
async recordInventoryTransactions(
|
async recordInventoryTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
entries: IInventoryTransaction[],
|
inventoryEntries: IInventoryTransaction[],
|
||||||
deleteOld: boolean,
|
deleteOld: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { InventoryTransaction, Item } = this.tenancy.models(tenantId);
|
const { InventoryTransaction, Item } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Mapping the inventory entries items ids.
|
|
||||||
const entriesItemsIds = entries.map((e: any) => e.itemId);
|
|
||||||
const inventoryItems = await Item.query()
|
|
||||||
.whereIn('id', entriesItemsIds)
|
|
||||||
.where('type', 'inventory');
|
|
||||||
|
|
||||||
// Mapping the inventory items ids.
|
|
||||||
const inventoryItemsIds = inventoryItems.map((i: IItem) => i.id);
|
|
||||||
|
|
||||||
// Filter the bill entries that have inventory items.
|
|
||||||
const inventoryEntries = entries.filter(
|
|
||||||
(entry: IInventoryTransaction) => inventoryItemsIds.indexOf(entry.itemId) !== -1
|
|
||||||
);
|
|
||||||
inventoryEntries.forEach(async (entry: any) => {
|
inventoryEntries.forEach(async (entry: any) => {
|
||||||
if (deleteOld) {
|
if (deleteOld) {
|
||||||
await this.deleteInventoryTransactions(
|
await this.deleteInventoryTransactions(
|
||||||
@@ -108,14 +151,14 @@ export default class InventoryService {
|
|||||||
* @param {number} transactionId
|
* @param {number} transactionId
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
deleteInventoryTransactions(
|
async deleteInventoryTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
transactionId: number,
|
transactionId: number,
|
||||||
transactionType: string,
|
transactionType: string,
|
||||||
) {
|
): Promise<void> {
|
||||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
return InventoryTransaction.query()
|
await InventoryTransaction.query()
|
||||||
.where('transaction_type', transactionType)
|
.where('transaction_type', transactionType)
|
||||||
.where('transaction_id', transactionId)
|
.where('transaction_id', transactionId)
|
||||||
.delete();
|
.delete();
|
||||||
@@ -125,22 +168,37 @@ export default class InventoryService {
|
|||||||
* Retrieve the lot number after the increment.
|
* Retrieve the lot number after the increment.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
*/
|
*/
|
||||||
async nextLotNumber(tenantId: number) {
|
getNextLotNumber(tenantId: number) {
|
||||||
const { Option } = this.tenancy.models(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
const LOT_NUMBER_KEY = 'lot_number_increment';
|
const LOT_NUMBER_KEY = 'lot_number_increment';
|
||||||
const effectRows = await Option.query()
|
const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY });
|
||||||
.where('key', LOT_NUMBER_KEY)
|
|
||||||
.increment('value', 1);
|
|
||||||
|
|
||||||
if (effectRows === 0) {
|
return (storedLotNumber && storedLotNumber.value) ?
|
||||||
await Option.query()
|
parseInt(storedLotNumber.value, 10) : 1;
|
||||||
.insert({
|
}
|
||||||
key: LOT_NUMBER_KEY,
|
|
||||||
value: 1,
|
/**
|
||||||
});
|
* Increment the next inventory LOT number.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @return {Promise<number>}
|
||||||
|
*/
|
||||||
|
async incrementNextLotNumber(tenantId: number) {
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
|
const LOT_NUMBER_KEY = 'lot_number_increment';
|
||||||
|
const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY });
|
||||||
|
|
||||||
|
let lotNumber = 1;
|
||||||
|
|
||||||
|
if (storedLotNumber && storedLotNumber.value) {
|
||||||
|
lotNumber = parseInt(storedLotNumber.value, 10);
|
||||||
|
lotNumber += 1;
|
||||||
}
|
}
|
||||||
const options = await Option.query();
|
settings.set({ key: LOT_NUMBER_KEY }, lotNumber);
|
||||||
return options.getMeta(LOT_NUMBER_KEY, 1);
|
|
||||||
|
await settings.save();
|
||||||
|
|
||||||
|
return lotNumber;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
|
import { raw } from 'objection';
|
||||||
import { IInventoryTransaction } from 'interfaces';
|
import { IInventoryTransaction } from 'interfaces';
|
||||||
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
|
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
|
||||||
|
|
||||||
export default class InventoryAverageCostMethod extends InventoryCostMethod implements IInventoryCostMethod {
|
export default class InventoryAverageCostMethod
|
||||||
|
extends InventoryCostMethod
|
||||||
|
implements IInventoryCostMethod {
|
||||||
startingDate: Date;
|
startingDate: Date;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
costTransactions: any[];
|
costTransactions: any[];
|
||||||
@@ -13,12 +16,8 @@ export default class InventoryAverageCostMethod extends InventoryCostMethod impl
|
|||||||
* @param {Date} startingDate -
|
* @param {Date} startingDate -
|
||||||
* @param {number} itemId - The given inventory item id.
|
* @param {number} itemId - The given inventory item id.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(tenantId: number, startingDate: Date, itemId: number) {
|
||||||
tenantId: number,
|
super(tenantId, startingDate, itemId);
|
||||||
startingDate: Date,
|
|
||||||
itemId: number,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.startingDate = startingDate;
|
this.startingDate = startingDate;
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
@@ -27,82 +26,86 @@ export default class InventoryAverageCostMethod extends InventoryCostMethod impl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes items costs from the given date using average cost method.
|
* Computes items costs from the given date using average cost method.
|
||||||
*
|
* ----------
|
||||||
* - Calculate the items average cost in the given date.
|
* - Calculate the items average cost in the given date.
|
||||||
* - Remove the journal entries that associated to the inventory transacions
|
* - Remove the journal entries that associated to the inventory transacions
|
||||||
* after the given date.
|
* after the given date.
|
||||||
* - Re-compute the inventory transactions and re-write the journal entries
|
* - Re-compute the inventory transactions and re-write the journal entries
|
||||||
* after the given date.
|
* after the given date.
|
||||||
* ----------
|
* ----------
|
||||||
* @asycn
|
* @async
|
||||||
* @param {Date} startingDate
|
* @param {Date} startingDate
|
||||||
* @param {number} referenceId
|
* @param {number} referenceId
|
||||||
* @param {string} referenceType
|
* @param {string} referenceType
|
||||||
*/
|
*/
|
||||||
public async computeItemCost() {
|
public async computeItemCost() {
|
||||||
const { InventoryTransaction } = this.tenantModels;
|
const { InventoryTransaction } = this.tenantModels;
|
||||||
const openingAvgCost = await this.getOpeningAvaregeCost(this.startingDate, this.itemId);
|
const {
|
||||||
|
averageCost,
|
||||||
|
openingQuantity,
|
||||||
|
openingCost,
|
||||||
|
} = await this.getOpeningAvaregeCost(this.startingDate, this.itemId);
|
||||||
|
|
||||||
const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction.query()
|
const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction.query()
|
||||||
.modify('filterDateRange', this.startingDate)
|
.modify('filterDateRange', this.startingDate)
|
||||||
.orderBy('date', 'ASC')
|
.orderBy('date', 'ASC')
|
||||||
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
||||||
|
.orderBy('lot_number', 'ASC')
|
||||||
.where('item_id', this.itemId)
|
.where('item_id', this.itemId)
|
||||||
.withGraphFetched('item');
|
.withGraphFetched('item');
|
||||||
|
|
||||||
// Tracking inventroy transactions and retrieve cost transactions
|
// Tracking inventroy transactions and retrieve cost transactions based on
|
||||||
// based on average rate cost method.
|
// average rate cost method.
|
||||||
const costTransactions = this.trackingCostTransactions(
|
const costTransactions = this.trackingCostTransactions(
|
||||||
afterInvTransactions,
|
afterInvTransactions,
|
||||||
openingAvgCost,
|
openingQuantity,
|
||||||
|
openingCost
|
||||||
);
|
);
|
||||||
|
// Revert the inveout out lots transactions
|
||||||
|
await this.revertTheInventoryOutLotTrans();
|
||||||
|
|
||||||
|
// Store inventory lots cost transactions.
|
||||||
await this.storeInventoryLotsCost(costTransactions);
|
await this.storeInventoryLotsCost(costTransactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get items Avarege cost from specific date from inventory transactions.
|
* Get items Avarege cost from specific date from inventory transactions.
|
||||||
* @static
|
* @async
|
||||||
* @param {Date} startingDate
|
* @param {Date} closingDate
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
public async getOpeningAvaregeCost(startingDate: Date, itemId: number) {
|
public async getOpeningAvaregeCost(closingDate: Date, itemId: number) {
|
||||||
const { InventoryTransaction } = this.tenantModels;
|
const { InventoryCostLotTracker } = this.tenantModels;
|
||||||
|
|
||||||
const commonBuilder = (builder: any) => {
|
const commonBuilder = (builder: any) => {
|
||||||
if (startingDate) {
|
if (closingDate) {
|
||||||
builder.where('date', '<', startingDate);
|
builder.where('date', '<', closingDate);
|
||||||
}
|
}
|
||||||
builder.where('item_id', itemId);
|
builder.where('item_id', itemId);
|
||||||
builder.groupBy('rate');
|
|
||||||
builder.groupBy('quantity');
|
|
||||||
builder.groupBy('item_id');
|
|
||||||
builder.groupBy('direction');
|
|
||||||
builder.sum('rate as rate');
|
builder.sum('rate as rate');
|
||||||
builder.sum('quantity as quantity');
|
builder.sum('quantity as quantity');
|
||||||
|
builder.sum('cost as cost');
|
||||||
|
builder.first();
|
||||||
};
|
};
|
||||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||||
|
const inInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
|
||||||
// @todo total `IN` transactions.
|
|
||||||
const inInvSumationOper: Promise<any> = InventoryTransaction.query()
|
|
||||||
.onBuild(commonBuilder)
|
.onBuild(commonBuilder)
|
||||||
.where('direction', 'IN')
|
.where('direction', 'IN');
|
||||||
.first();
|
|
||||||
|
|
||||||
// Calculates the total inventory total quantity and rate `OUT` transactions.
|
// Calculates the total inventory total quantity and rate `OUT` transactions.
|
||||||
// @todo total `OUT` transactions.
|
const outInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
|
||||||
const outInvSumationOper: Promise<any> = InventoryTransaction.query()
|
|
||||||
.onBuild(commonBuilder)
|
.onBuild(commonBuilder)
|
||||||
.where('direction', 'OUT')
|
.where('direction', 'OUT');
|
||||||
.first();
|
|
||||||
|
|
||||||
const [inInvSumation, outInvSumation] = await Promise.all([
|
const [inInvSumation, outInvSumation] = await Promise.all([
|
||||||
inInvSumationOper,
|
inInvSumationOper,
|
||||||
outInvSumationOper,
|
outInvSumationOper,
|
||||||
]);
|
]);
|
||||||
return this.computeItemAverageCost(
|
return this.computeItemAverageCost(
|
||||||
inInvSumation?.quantity || 0,
|
inInvSumation?.cost || 0,
|
||||||
inInvSumation?.rate || 0,
|
inInvSumation?.quantity || 0,
|
||||||
outInvSumation?.quantity || 0,
|
outInvSumation?.cost || 0,
|
||||||
outInvSumation?.rate || 0
|
outInvSumation?.quantity || 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,17 +118,18 @@ export default class InventoryAverageCostMethod extends InventoryCostMethod impl
|
|||||||
* @param {number} rateOut
|
* @param {number} rateOut
|
||||||
*/
|
*/
|
||||||
public computeItemAverageCost(
|
public computeItemAverageCost(
|
||||||
quantityIn: number,
|
totalCostIn: number,
|
||||||
rateIn: number,
|
totalQuantityIn: number,
|
||||||
|
|
||||||
quantityOut: number,
|
totalCostOut: number,
|
||||||
rateOut: number,
|
totalQuantityOut: number
|
||||||
) {
|
) {
|
||||||
const totalQuantity = (quantityIn - quantityOut);
|
const openingCost = totalCostIn - totalCostOut;
|
||||||
const totalRate = (rateIn - rateOut);
|
const openingQuantity = totalQuantityIn - totalQuantityOut;
|
||||||
const averageCost = (totalRate) ? (totalQuantity / totalRate) : totalQuantity;
|
|
||||||
|
|
||||||
return averageCost;
|
const averageCost = openingQuantity ? openingCost / openingQuantity : 0;
|
||||||
|
|
||||||
|
return { averageCost, openingCost, openingQuantity };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,43 +142,100 @@ export default class InventoryAverageCostMethod extends InventoryCostMethod impl
|
|||||||
*/
|
*/
|
||||||
public trackingCostTransactions(
|
public trackingCostTransactions(
|
||||||
invTransactions: IInventoryTransaction[],
|
invTransactions: IInventoryTransaction[],
|
||||||
openingAverageCost: number,
|
openingQuantity: number = 0,
|
||||||
|
openingCost: number = 0
|
||||||
) {
|
) {
|
||||||
const costTransactions: any[] = [];
|
const costTransactions: any[] = [];
|
||||||
let accQuantity: number = 0;
|
|
||||||
let accCost: number = 0;
|
// Cumulative item quantity and cost. This will decrement after
|
||||||
|
// each out transactions depends on its quantity and cost.
|
||||||
|
let accQuantity: number = openingQuantity;
|
||||||
|
let accCost: number = openingCost;
|
||||||
|
|
||||||
invTransactions.forEach((invTransaction: IInventoryTransaction) => {
|
invTransactions.forEach((invTransaction: IInventoryTransaction) => {
|
||||||
const commonEntry = {
|
const commonEntry = {
|
||||||
invTransId: invTransaction.id,
|
invTransId: invTransaction.id,
|
||||||
...pick(invTransaction, ['date', 'direction', 'itemId', 'quantity', 'rate', 'entryId',
|
...pick(invTransaction, [
|
||||||
'transactionId', 'transactionType']),
|
'date',
|
||||||
|
'direction',
|
||||||
|
'itemId',
|
||||||
|
'quantity',
|
||||||
|
'rate',
|
||||||
|
'entryId',
|
||||||
|
'transactionId',
|
||||||
|
'transactionType',
|
||||||
|
'lotNumber',
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
switch(invTransaction.direction) {
|
switch (invTransaction.direction) {
|
||||||
case 'IN':
|
case 'IN':
|
||||||
|
const inCost = invTransaction.rate * invTransaction.quantity;
|
||||||
|
|
||||||
|
// Increases the quantity and cost in `IN` inventory transactions.
|
||||||
accQuantity += invTransaction.quantity;
|
accQuantity += invTransaction.quantity;
|
||||||
accCost += invTransaction.rate * invTransaction.quantity;
|
accCost += inCost;
|
||||||
|
|
||||||
costTransactions.push({
|
costTransactions.push({
|
||||||
...commonEntry,
|
...commonEntry,
|
||||||
|
cost: inCost,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'OUT':
|
case 'OUT':
|
||||||
const transactionAvgCost = accCost ? (accCost / accQuantity) : 0;
|
// Average cost = Total cost / Total quantity
|
||||||
const averageCost = transactionAvgCost;
|
const averageCost = accQuantity ? accCost / accQuantity : 0;
|
||||||
const cost = (invTransaction.quantity * averageCost);
|
|
||||||
const income = (invTransaction.quantity * invTransaction.rate);
|
|
||||||
|
|
||||||
accQuantity -= invTransaction.quantity;
|
const quantity =
|
||||||
accCost -= income;
|
accQuantity > 0
|
||||||
|
? Math.min(invTransaction.quantity, accQuantity)
|
||||||
|
: invTransaction.quantity;
|
||||||
|
|
||||||
|
// Cost = the transaction quantity * Average cost.
|
||||||
|
const cost = quantity * averageCost;
|
||||||
|
|
||||||
|
// Revenue = transaction quanity * rate.
|
||||||
|
// const revenue = quantity * invTransaction.rate;
|
||||||
costTransactions.push({
|
costTransactions.push({
|
||||||
...commonEntry,
|
...commonEntry,
|
||||||
|
quantity,
|
||||||
cost,
|
cost,
|
||||||
});
|
});
|
||||||
|
accQuantity = Math.max(accQuantity - quantity, 0);
|
||||||
|
accCost = Math.max(accCost - cost, 0);
|
||||||
|
|
||||||
|
if (invTransaction.quantity > quantity) {
|
||||||
|
const remainingQuantity = Math.max(
|
||||||
|
invTransaction.quantity - quantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const remainingIncome = remainingQuantity * invTransaction.rate;
|
||||||
|
|
||||||
|
costTransactions.push({
|
||||||
|
...commonEntry,
|
||||||
|
quantity: remainingQuantity,
|
||||||
|
cost: 0,
|
||||||
|
});
|
||||||
|
accQuantity = Math.max(accQuantity - remainingQuantity, 0);
|
||||||
|
accCost = Math.max(accCost - remainingIncome, 0);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return costTransactions;
|
return costTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts the inventory lots `OUT` transactions.
|
||||||
|
* @param {Date} openingDate - Opening date.
|
||||||
|
* @param {number} itemId - Item id.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async revertTheInventoryOutLotTrans(): Promise<void> {
|
||||||
|
const { InventoryCostLotTracker } = this.tenantModels;
|
||||||
|
|
||||||
|
await InventoryCostLotTracker.query()
|
||||||
|
.modify('filterDateRange', this.startingDate)
|
||||||
|
.orderBy('date', 'DESC')
|
||||||
|
.where('item_id', this.itemId)
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme
|
|||||||
itemId: number,
|
itemId: number,
|
||||||
costMethod: TCostMethod = 'FIFO'
|
costMethod: TCostMethod = 'FIFO'
|
||||||
) {
|
) {
|
||||||
super();
|
super(tenantId, startingDate, itemId);
|
||||||
|
|
||||||
this.startingDate = startingDate;
|
this.startingDate = startingDate;
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { Inject } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
import { IInventoryLotCost } from 'interfaces';
|
import { IInventoryLotCost } from 'interfaces';
|
||||||
|
|
||||||
export default class InventoryCostMethod {
|
export default class InventoryCostMethod {
|
||||||
@Inject()
|
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
tenantModels: any;
|
tenantModels: any;
|
||||||
|
|
||||||
@@ -13,27 +12,29 @@ export default class InventoryCostMethod {
|
|||||||
* @param {number} tenantId - The given tenant id.
|
* @param {number} tenantId - The given tenant id.
|
||||||
*/
|
*/
|
||||||
constructor(tenantId: number, startingDate: Date, itemId: number) {
|
constructor(tenantId: number, startingDate: Date, itemId: number) {
|
||||||
this.tenantModels = this.tenantModels.models(tenantId);
|
const tenancyService = Container.get(TenancyService);
|
||||||
|
|
||||||
|
this.tenantModels = tenancyService.models(tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the inventory lots costs transactions in bulk.
|
* Stores the inventory lots costs transactions in bulk.
|
||||||
* @param {IInventoryLotCost[]} costLotsTransactions
|
* @param {IInventoryLotCost[]} costLotsTransactions
|
||||||
* @return {Promise[]}
|
* @return {Promise[]}
|
||||||
*/
|
*/
|
||||||
public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> {
|
public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> {
|
||||||
const { InventoryLotCostTracker } = this.tenantModels;
|
const { InventoryCostLotTracker } = this.tenantModels;
|
||||||
const opers: any = [];
|
const opers: any = [];
|
||||||
|
|
||||||
costLotsTransactions.forEach((transaction: IInventoryLotCost) => {
|
costLotsTransactions.forEach((transaction: any) => {
|
||||||
if (transaction.lotTransId && transaction.decrement) {
|
if (transaction.lotTransId && transaction.decrement) {
|
||||||
const decrementOper = InventoryLotCostTracker.query()
|
const decrementOper = InventoryCostLotTracker.query()
|
||||||
.where('id', transaction.lotTransId)
|
.where('id', transaction.lotTransId)
|
||||||
.decrement('remaining', transaction.decrement);
|
.decrement('remaining', transaction.decrement);
|
||||||
opers.push(decrementOper);
|
opers.push(decrementOper);
|
||||||
|
|
||||||
} else if(!transaction.lotTransId) {
|
} else if(!transaction.lotTransId) {
|
||||||
const operation = InventoryLotCostTracker.query()
|
const operation = InventoryCostLotTracker.query()
|
||||||
.insert({
|
.insert({
|
||||||
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { difference } from 'lodash';
|
import { difference, map } from 'lodash';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
IItemEntry,
|
IItemEntry,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import { ItemEntry } from 'models';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||||
@@ -20,6 +21,39 @@ export default class ItemsEntriesService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the inventory items entries of the reference id and type.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} referenceType
|
||||||
|
* @param {string} referenceId
|
||||||
|
* @return {Promise<IItemEntry[]>}
|
||||||
|
*/
|
||||||
|
public async getInventoryEntries(
|
||||||
|
tenantId: number,
|
||||||
|
referenceType: string,
|
||||||
|
referenceId: number,
|
||||||
|
): Promise<IItemEntry[]> {
|
||||||
|
const { Item, ItemEntry } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const itemsEntries = await ItemEntry.query()
|
||||||
|
.where('reference_type', referenceType)
|
||||||
|
.where('reference_id', referenceId);
|
||||||
|
|
||||||
|
// Inventory items.
|
||||||
|
const inventoryItems = await Item.query()
|
||||||
|
.whereIn('id', map(itemsEntries, 'itemId'))
|
||||||
|
.where('type', 'inventory');
|
||||||
|
|
||||||
|
// Inventory items ids.
|
||||||
|
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||||
|
|
||||||
|
// Filtering the inventory items entries.
|
||||||
|
const inventoryItemsEntries = itemsEntries.filter(
|
||||||
|
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1
|
||||||
|
);
|
||||||
|
return inventoryItemsEntries;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the entries items ids.
|
* Validates the entries items ids.
|
||||||
* @async
|
* @async
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { omit, sumBy, pick, difference, assignWith } from 'lodash';
|
import { omit, sumBy, pick, map } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from 'decorators/eventDispatcher';
|
} from 'decorators/eventDispatcher';
|
||||||
import events from 'subscribers/events';
|
import events from 'subscribers/events';
|
||||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
|
||||||
import AccountsService from 'services/Accounts/AccountsService';
|
import AccountsService from 'services/Accounts/AccountsService';
|
||||||
import InventoryService from 'services/Inventory/Inventory';
|
import InventoryService from 'services/Inventory/Inventory';
|
||||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
IFilterMeta,
|
IFilterMeta,
|
||||||
IBillsFilter,
|
IBillsFilter,
|
||||||
IItemEntry,
|
IItemEntry,
|
||||||
|
IInventoryTransaction,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import ItemsService from 'services/Items/ItemsService';
|
import ItemsService from 'services/Items/ItemsService';
|
||||||
@@ -159,11 +159,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
oldBill?: IBill
|
oldBill?: IBill
|
||||||
) {
|
) {
|
||||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||||
let invLotNumber = oldBill?.invLotNumber;
|
|
||||||
|
|
||||||
// if (!invLotNumber) {
|
|
||||||
// invLotNumber = await this.inventoryService.nextLotNumber(tenantId);
|
|
||||||
// }
|
|
||||||
const entries = billDTO.entries.map((entry) => ({
|
const entries = billDTO.entries.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
amount: ItemEntry.calcAmount(entry),
|
amount: ItemEntry.calcAmount(entry),
|
||||||
@@ -176,7 +172,6 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
'dueDate',
|
'dueDate',
|
||||||
]),
|
]),
|
||||||
amount,
|
amount,
|
||||||
invLotNumber,
|
|
||||||
entries: entries.map((entry) => ({
|
entries: entries.map((entry) => ({
|
||||||
reference_type: 'Bill',
|
reference_type: 'Bill',
|
||||||
...omit(entry, ['amount', 'id']),
|
...omit(entry, ['amount', 'id']),
|
||||||
@@ -369,29 +364,51 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Records the inventory transactions from the given bill input.
|
* Records the inventory transactions from the given bill input.
|
||||||
* @param {Bill} bill
|
* @param {Bill} bill - Bill model object.
|
||||||
* @param {number} billId
|
* @param {number} billId - Bill id.
|
||||||
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async recordInventoryTransactions(
|
public async recordInventoryTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
bill: IBill,
|
billId: number,
|
||||||
|
billDate: Date,
|
||||||
override?: boolean
|
override?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const invTransactions = bill.entries.map((entry: IItemEntry) => ({
|
// Retrieve the next inventory lot number.
|
||||||
...pick(entry, ['itemId', 'quantity', 'rate']),
|
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
|
||||||
lotNumber: bill.invLotNumber,
|
|
||||||
transactionType: 'Bill',
|
|
||||||
transactionId: bill.id,
|
|
||||||
direction: 'IN',
|
|
||||||
date: bill.billDate,
|
|
||||||
entryId: entry.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
const inventoryEntries = await this.itemsEntriesService.getInventoryEntries(
|
||||||
|
tenantId,
|
||||||
|
'Bill',
|
||||||
|
billId
|
||||||
|
);
|
||||||
|
// Can't continue if there is no entries has inventory items in the bill.
|
||||||
|
if (inventoryEntries.length <= 0) return;
|
||||||
|
|
||||||
|
// Inventory transactions.
|
||||||
|
const inventoryTranscations = this.inventoryService.transformItemEntriesToInventory(
|
||||||
|
inventoryEntries,
|
||||||
|
'Bill',
|
||||||
|
billId,
|
||||||
|
'IN',
|
||||||
|
billDate,
|
||||||
|
lotNumber,
|
||||||
|
);
|
||||||
|
// Records the inventory transactions.
|
||||||
await this.inventoryService.recordInventoryTransactions(
|
await this.inventoryService.recordInventoryTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
invTransactions,
|
inventoryTranscations,
|
||||||
override
|
override
|
||||||
);
|
);
|
||||||
|
// Save the next lot number settings.
|
||||||
|
await this.inventoryService.incrementNextLotNumber(tenantId);
|
||||||
|
|
||||||
|
// Triggers `onInventoryTransactionsCreated` event.
|
||||||
|
this.eventDispatcher.dispatch(events.bill.onInventoryTransactionsCreated, {
|
||||||
|
tenantId,
|
||||||
|
billId,
|
||||||
|
billDate,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -404,7 +421,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
await this.inventoryService.deleteInventoryTransactions(
|
await this.inventoryService.deleteInventoryTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
billId,
|
billId,
|
||||||
'Bill',
|
'Bill'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,22 +536,26 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
* @param {IBill} bill -
|
* @param {IBill} bill -
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
public async scheduleComputeBillItemsCost(tenantId: number, bill) {
|
public async scheduleComputeBillItemsCost(tenantId: number, billId: number) {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
const { Item, Bill } = this.tenancy.models(tenantId);
|
||||||
const billItemsIds = bill.entries.map((entry) => entry.item_id);
|
|
||||||
|
// Retrieve the bill with associated entries.
|
||||||
|
const bill = await Bill.query()
|
||||||
|
.findById(billId)
|
||||||
|
.withGraphFetched('entries');
|
||||||
|
|
||||||
// Retrieves inventory items only.
|
// Retrieves inventory items only.
|
||||||
const inventoryItems = await Item.query()
|
const inventoryItems = await Item.query()
|
||||||
.whereIn('id', billItemsIds)
|
.whereIn('id', map(bill.entries, 'itemId'))
|
||||||
.where('type', 'inventory');
|
.where('type', 'inventory');
|
||||||
|
|
||||||
const inventoryItemsIds = inventoryItems.map((i) => i.id);
|
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||||
|
|
||||||
if (inventoryItemsIds.length > 0) {
|
if (inventoryItemsIds.length > 0) {
|
||||||
await this.scheduleComputeItemsCost(
|
await this.scheduleComputeItemsCost(
|
||||||
tenantId,
|
tenantId,
|
||||||
inventoryItemsIds,
|
inventoryItemsIds,
|
||||||
bill.bill_date
|
bill.billDate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { omit, sumBy, pick, chain } from 'lodash';
|
import { omit, sumBy, pick, map } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import uniqid from 'uniqid';
|
||||||
import {
|
import {
|
||||||
EventDispatcher,
|
EventDispatcher,
|
||||||
EventDispatcherInterface,
|
EventDispatcherInterface,
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
IFilterMeta,
|
IFilterMeta,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import events from 'subscribers/events';
|
import events from 'subscribers/events';
|
||||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
|
||||||
import InventoryService from 'services/Inventory/Inventory';
|
import InventoryService from 'services/Inventory/Inventory';
|
||||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
@@ -71,7 +71,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
saleEstimatesService: SaleEstimateService;
|
saleEstimatesService: SaleEstimateService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Validate whether sale invoice number unqiue on the storage.
|
* Validate whether sale invoice number unqiue on the storage.
|
||||||
*/
|
*/
|
||||||
async validateInvoiceNumberUnique(
|
async validateInvoiceNumberUnique(
|
||||||
@@ -166,8 +165,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
): Promise<ISaleInvoice> {
|
): Promise<ISaleInvoice> {
|
||||||
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
|
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
const invLotNumber = 1;
|
|
||||||
|
|
||||||
// Transform DTO object to model object.
|
// Transform DTO object to model object.
|
||||||
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
|
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
|
||||||
|
|
||||||
@@ -176,7 +173,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceDTO.customerId
|
saleInvoiceDTO.customerId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validate sale invoice number uniquiness.
|
// Validate sale invoice number uniquiness.
|
||||||
if (saleInvoiceDTO.invoiceNo) {
|
if (saleInvoiceDTO.invoiceNo) {
|
||||||
await this.validateInvoiceNumberUnique(
|
await this.validateInvoiceNumberUnique(
|
||||||
@@ -258,13 +254,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceDTO.entries
|
saleInvoiceDTO.entries
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validate non-sellable entries items.
|
// Validate non-sellable entries items.
|
||||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceDTO.entries
|
saleInvoiceDTO.entries
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validate the items entries existance.
|
// Validate the items entries existance.
|
||||||
await this.itemsEntriesService.validateEntriesIdsExistance(
|
await this.itemsEntriesService.validateEntriesIdsExistance(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -337,7 +331,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceId
|
saleInvoiceId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unlink the converted sale estimates from the given sale invoice.
|
// Unlink the converted sale estimates from the given sale invoice.
|
||||||
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
|
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -360,91 +353,113 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records the inventory transactions from the givne sale invoice input.
|
* Records the inventory transactions of the given sale invoice in case
|
||||||
* @parma {number} tenantId - Tenant id.
|
* the invoice has inventory entries only.
|
||||||
|
*
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
|
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
|
||||||
* @param {number} saleInvoiceId - Sale invoice id.
|
* @param {number} saleInvoiceId - Sale invoice id.
|
||||||
* @param {boolean} override - Allow to override old transactions.
|
* @param {boolean} override - Allow to override old transactions.
|
||||||
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public recordInventoryTranscactions(
|
public async recordInventoryTranscactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
saleInvoice: ISaleInvoice,
|
saleInvoiceId: number,
|
||||||
|
saleInvoiceDate: Date,
|
||||||
override?: boolean
|
override?: boolean
|
||||||
) {
|
): Promise<void> {
|
||||||
this.logger.info('[sale_invoice] saving inventory transactions');
|
// Gets the next inventory lot number.
|
||||||
const invTransactions: IInventoryTransaction[] = saleInvoice.entries.map(
|
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
|
||||||
(entry: IItemEntry) => ({
|
|
||||||
...pick(entry, ['itemId', 'quantity', 'rate']),
|
|
||||||
lotNumber: 1,
|
|
||||||
transactionType: 'SaleInvoice',
|
|
||||||
transactionId: saleInvoice.id,
|
|
||||||
direction: 'OUT',
|
|
||||||
date: saleInvoice.invoiceDate,
|
|
||||||
entryId: entry.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.inventoryService.recordInventoryTransactions(
|
// Loads the inventory items entries of the given sale invoice.
|
||||||
|
const inventoryEntries = await this.itemsEntriesService.getInventoryEntries(
|
||||||
tenantId,
|
tenantId,
|
||||||
invTransactions,
|
'SaleInvoice',
|
||||||
|
saleInvoiceId
|
||||||
|
);
|
||||||
|
// Can't continue if there is no entries has inventory items in the invoice.
|
||||||
|
if (inventoryEntries.length <= 0) return;
|
||||||
|
|
||||||
|
// Inventory transactions.
|
||||||
|
const inventoryTranscations = this.inventoryService.transformItemEntriesToInventory(
|
||||||
|
inventoryEntries,
|
||||||
|
'SaleInvoice',
|
||||||
|
saleInvoiceId,
|
||||||
|
'OUT',
|
||||||
|
saleInvoiceDate,
|
||||||
|
lotNumber
|
||||||
|
);
|
||||||
|
// Records the inventory transactions of the given sale invoice.
|
||||||
|
await this.inventoryService.recordInventoryTransactions(
|
||||||
|
tenantId,
|
||||||
|
inventoryTranscations,
|
||||||
override
|
override
|
||||||
);
|
);
|
||||||
|
// Increment and save the next lot number settings.
|
||||||
|
await this.inventoryService.incrementNextLotNumber(tenantId);
|
||||||
|
|
||||||
|
// Triggers `onInventoryTransactionsCreated` event.
|
||||||
|
await this.eventDispatcher.dispatch(
|
||||||
|
events.saleInvoice.onInventoryTransactionsCreated,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>}
|
||||||
|
*/
|
||||||
|
public async recordNonInventoryJournalEntries(
|
||||||
|
tenantId: number,
|
||||||
|
saleInvoiceId: number,
|
||||||
|
override: boolean = false
|
||||||
|
): Promise<void> {
|
||||||
|
const { SaleInvoice } = this.tenancy.models(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 saleInvoice = await SaleInvoice.query()
|
||||||
|
.findById(saleInvoiceId)
|
||||||
|
.withGraphFetched('entries.item');
|
||||||
|
|
||||||
|
await this.writeNonInventoryInvoiceEntries(tenantId, saleInvoice, override);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverting the inventory transactions once the invoice deleted.
|
* Reverting the inventory transactions once the invoice deleted.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {number} billId - Bill id.
|
* @param {number} billId - Bill id.
|
||||||
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public revertInventoryTransactions(
|
public async revertInventoryTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
billId: number
|
saleInvoiceId: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.inventoryService.deleteInventoryTransactions(
|
await this.inventoryService.deleteInventoryTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
billId,
|
saleInvoiceId,
|
||||||
'SaleInvoice'
|
'SaleInvoice'
|
||||||
);
|
);
|
||||||
}
|
// Triggers 'onInventoryTransactionsDeleted' event.
|
||||||
|
this.eventDispatcher.dispatch(
|
||||||
/**
|
events.saleInvoice.onInventoryTransactionsDeleted,
|
||||||
* Deletes the inventory transactions.
|
{ tenantId, saleInvoiceId },
|
||||||
* @param {string} transactionType
|
);
|
||||||
* @param {number} transactionId
|
|
||||||
*/
|
|
||||||
private async revertInventoryTransactions_(
|
|
||||||
tenantId: number,
|
|
||||||
inventoryTransactions: array
|
|
||||||
) {
|
|
||||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
|
||||||
const opers: Promise<[]>[] = [];
|
|
||||||
|
|
||||||
this.logger.info('[sale_invoice] reverting inventory transactions');
|
|
||||||
|
|
||||||
inventoryTransactions.forEach((trans: any) => {
|
|
||||||
switch (trans.direction) {
|
|
||||||
case 'OUT':
|
|
||||||
if (trans.inventoryTransactionId) {
|
|
||||||
const revertRemaining = InventoryTransaction.query()
|
|
||||||
.where('id', trans.inventoryTransactionId)
|
|
||||||
.where('direction', 'OUT')
|
|
||||||
.increment('remaining', trans.quanitity);
|
|
||||||
|
|
||||||
opers.push(revertRemaining);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'IN':
|
|
||||||
const removeRelationOper = InventoryTransaction.query()
|
|
||||||
.where('inventory_transaction_id', trans.id)
|
|
||||||
.where('direction', 'IN')
|
|
||||||
.update({
|
|
||||||
inventory_transaction_id: null,
|
|
||||||
});
|
|
||||||
opers.push(removeRelationOper);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Promise.all(opers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -480,63 +495,29 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
|||||||
saleInvoiceId: number,
|
saleInvoiceId: number,
|
||||||
override?: boolean
|
override?: boolean
|
||||||
) {
|
) {
|
||||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
const { SaleInvoice, Item } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Retrieve the sale invoice with associated entries.
|
||||||
const saleInvoice: ISaleInvoice = await SaleInvoice.query()
|
const saleInvoice: ISaleInvoice = await SaleInvoice.query()
|
||||||
.findById(saleInvoiceId)
|
.findById(saleInvoiceId)
|
||||||
.withGraphFetched('entries.item');
|
.withGraphFetched('entries');
|
||||||
|
|
||||||
const inventoryItemsIds = chain(saleInvoice.entries)
|
// Retrieve the inventory items that associated to the sale invoice entries.
|
||||||
.filter((entry: IItemEntry) => entry.item.type === 'inventory')
|
const inventoryItems = await Item.query()
|
||||||
.map((entry: IItemEntry) => entry.itemId)
|
.whereIn('id', map(saleInvoice.entries, 'itemId'))
|
||||||
.uniq()
|
.where('type', 'inventory');
|
||||||
.value();
|
|
||||||
|
|
||||||
if (inventoryItemsIds.length === 0) {
|
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||||
await this.writeNonInventoryInvoiceJournals(
|
|
||||||
tenantId,
|
if (inventoryItemsIds.length > 0) {
|
||||||
saleInvoice,
|
|
||||||
override
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await this.scheduleComputeItemsCost(
|
await this.scheduleComputeItemsCost(
|
||||||
tenantId,
|
tenantId,
|
||||||
inventoryItemsIds,
|
inventoryItemsIds,
|
||||||
saleInvoice.invoice_date
|
saleInvoice.invoiceDate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the sale invoice journal entries.
|
|
||||||
* @param {SaleInvoice} saleInvoice -
|
|
||||||
*/
|
|
||||||
async writeNonInventoryInvoiceJournals(
|
|
||||||
tenantId: number,
|
|
||||||
saleInvoice: ISaleInvoice,
|
|
||||||
override: boolean
|
|
||||||
) {
|
|
||||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const journal = new JournalPoster(tenantId);
|
|
||||||
|
|
||||||
if (override) {
|
|
||||||
const oldTransactions = await AccountTransaction.query()
|
|
||||||
.where('reference_type', 'SaleInvoice')
|
|
||||||
.where('reference_id', saleInvoice.id)
|
|
||||||
.withGraphFetched('account.type');
|
|
||||||
|
|
||||||
journal.loadEntries(oldTransactions);
|
|
||||||
journal.removeEntries();
|
|
||||||
}
|
|
||||||
this.saleInvoiceJournal(saleInvoice, journal);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
journal.deleteEntries(),
|
|
||||||
journal.saveEntries(),
|
|
||||||
journal.saveBalance(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sales invoices filterable and paginated list.
|
* Retrieve sales invoices filterable and paginated list.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import JournalPoster from 'services/Accounting/JournalPoster';
|
|||||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||||
import InventoryService from 'services/Inventory/Inventory';
|
import InventoryService from 'services/Inventory/Inventory';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
import { ISaleInvoice, IItemEntry } from 'interfaces';
|
import { ISaleInvoice, IItemEntry, IInventoryLotCost, IItem } from 'interfaces';
|
||||||
|
import JournalCommands from 'services/Accounting/JournalCommands';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SaleInvoicesCost {
|
export default class SaleInvoicesCost {
|
||||||
@@ -45,6 +46,7 @@ export default class SaleInvoicesCost {
|
|||||||
*/
|
*/
|
||||||
scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) {
|
scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) {
|
||||||
const agenda = Container.get('agenda');
|
const agenda = Container.get('agenda');
|
||||||
|
|
||||||
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
|
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
|
||||||
startingDate, tenantId,
|
startingDate, tenantId,
|
||||||
});
|
});
|
||||||
@@ -58,16 +60,23 @@ export default class SaleInvoicesCost {
|
|||||||
*/
|
*/
|
||||||
async writeJournalEntries(tenantId: number, startingDate: Date, override: boolean) {
|
async writeJournalEntries(tenantId: number, startingDate: Date, override: boolean) {
|
||||||
const { AccountTransaction, SaleInvoice, Account } = this.tenancy.models(tenantId);
|
const { AccountTransaction, SaleInvoice, Account } = this.tenancy.models(tenantId);
|
||||||
|
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
|
const receivableAccount = await accountRepository.findOne({
|
||||||
|
slug: 'accounts-receivable',
|
||||||
|
});
|
||||||
const salesInvoices = await SaleInvoice.query()
|
const salesInvoices = await SaleInvoice.query()
|
||||||
.onBuild((builder: any) => {
|
.onBuild((builder: any) => {
|
||||||
builder.modify('filterDateRange', startingDate);
|
builder.modify('filterDateRange', startingDate);
|
||||||
builder.orderBy('invoice_date', 'ASC');
|
builder.orderBy('invoice_date', 'ASC');
|
||||||
|
|
||||||
builder.withGraphFetched('entries.item')
|
builder.withGraphFetched('entries.item');
|
||||||
builder.withGraphFetched('costTransactions(groupedEntriesCost)');
|
builder.withGraphFetched('costTransactions(groupedEntriesCost)');
|
||||||
});
|
});
|
||||||
const accountsDepGraph = await Account.depGraph().query();
|
const accountsDepGraph = await accountRepository.getDependencyGraph();
|
||||||
const journal = new JournalPoster(accountsDepGraph);
|
const journal = new JournalPoster(tenantId, accountsDepGraph);
|
||||||
|
|
||||||
|
const journalCommands = new JournalCommands(journal);
|
||||||
|
|
||||||
if (override) {
|
if (override) {
|
||||||
const oldTransactions = await AccountTransaction.query()
|
const oldTransactions = await AccountTransaction.query()
|
||||||
@@ -77,12 +86,14 @@ export default class SaleInvoicesCost {
|
|||||||
})
|
})
|
||||||
.withGraphFetched('account.type');
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
journal.loadEntries(oldTransactions);
|
journal.fromTransactions(oldTransactions);
|
||||||
journal.removeEntries();
|
journal.removeEntries();
|
||||||
}
|
}
|
||||||
|
salesInvoices.forEach((saleInvoice: ISaleInvoice & {
|
||||||
salesInvoices.forEach((saleInvoice: ISaleInvoice) => {
|
costTransactions: IInventoryLotCost[],
|
||||||
this.saleInvoiceJournal(saleInvoice, journal);
|
entries: IItemEntry & { item: IItem },
|
||||||
|
}) => {
|
||||||
|
journalCommands.saleInvoice(saleInvoice, receivableAccount.id);
|
||||||
});
|
});
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
journal.deleteEntries(),
|
journal.deleteEntries(),
|
||||||
@@ -92,64 +103,39 @@ export default class SaleInvoicesCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes journal entries for given sale invoice.
|
* Writes the sale invoice journal entries.
|
||||||
* @param {ISaleInvoice} saleInvoice
|
* @param {SaleInvoice} saleInvoice -
|
||||||
* @param {JournalPoster} journal
|
|
||||||
*/
|
*/
|
||||||
saleInvoiceJournal(saleInvoice: ISaleInvoice, journal: JournalPoster) {
|
async writeNonInventoryInvoiceEntries(
|
||||||
let inventoryTotal: number = 0;
|
tenantId: number,
|
||||||
const receivableAccount = { id: 10 };
|
saleInvoice: ISaleInvoice,
|
||||||
const commonEntry = {
|
override: boolean
|
||||||
referenceType: 'SaleInvoice',
|
) {
|
||||||
referenceId: saleInvoice.id,
|
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||||
date: saleInvoice.invoiceDate,
|
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||||
};
|
|
||||||
const costTransactions: Map<number, number> = new Map(
|
// Receivable account.
|
||||||
saleInvoice?.costTransactions?.map((trans: IItemEntry) => [
|
const receivableAccount = await accountRepository.findOne({
|
||||||
trans.entryId, trans.cost,
|
slug: 'accounts-receivable',
|
||||||
]),
|
|
||||||
);
|
|
||||||
// XXX Debit - Receivable account.
|
|
||||||
const receivableEntry = new JournalEntry({
|
|
||||||
...commonEntry,
|
|
||||||
debit: saleInvoice.balance,
|
|
||||||
account: receivableAccount.id,
|
|
||||||
});
|
});
|
||||||
journal.debit(receivableEntry);
|
const journal = new JournalPoster(tenantId);
|
||||||
|
const journalCommands = new JournalCommands(journal);
|
||||||
|
|
||||||
saleInvoice.entries.forEach((entry: IItemEntry) => {
|
if (override) {
|
||||||
const cost: number = costTransactions.get(entry.id);
|
const oldTransactions = await AccountTransaction.query()
|
||||||
const income: number = entry.quantity * entry.rate;
|
.where('reference_type', 'SaleInvoice')
|
||||||
|
.where('reference_id', saleInvoice.id)
|
||||||
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
if (entry.item.type === 'inventory' && cost) {
|
journal.fromTransactions(oldTransactions);
|
||||||
// XXX Debit - Cost account.
|
journal.removeEntries();
|
||||||
const costEntry = new JournalEntry({
|
}
|
||||||
...commonEntry,
|
journalCommands.saleInvoiceNonInventory(saleInvoice, receivableAccount.id);
|
||||||
debit: cost,
|
|
||||||
account: entry.item.costAccountId,
|
|
||||||
note: entry.description,
|
|
||||||
});
|
|
||||||
journal.debit(costEntry);
|
|
||||||
inventoryTotal += cost;
|
|
||||||
}
|
|
||||||
// XXX Credit - Income account.
|
|
||||||
const incomeEntry = new JournalEntry({
|
|
||||||
...commonEntry,
|
|
||||||
credit: income,
|
|
||||||
account: entry.item.sellAccountId,
|
|
||||||
note: entry.description,
|
|
||||||
});
|
|
||||||
journal.credit(incomeEntry);
|
|
||||||
|
|
||||||
if (inventoryTotal > 0) {
|
await Promise.all([
|
||||||
// XXX Credit - Inventory account.
|
journal.deleteEntries(),
|
||||||
const inventoryEntry = new JournalEntry({
|
journal.saveEntries(),
|
||||||
...commonEntry,
|
journal.saveBalance(),
|
||||||
credit: inventoryTotal,
|
]);
|
||||||
account: entry.item.inventoryAccountId,
|
|
||||||
});
|
|
||||||
journal.credit(inventoryEntry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,8 @@ export default class BillSubscriber {
|
|||||||
this.logger.info('[bill] writing the inventory transactions', { tenantId });
|
this.logger.info('[bill] writing the inventory transactions', { tenantId });
|
||||||
this.billsService.recordInventoryTransactions(
|
this.billsService.recordInventoryTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
bill,
|
bill.id,
|
||||||
|
bill.billDate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +130,8 @@ export default class BillSubscriber {
|
|||||||
this.logger.info('[bill] overwriting the inventory transactions.', { tenantId });
|
this.logger.info('[bill] overwriting the inventory transactions.', { tenantId });
|
||||||
this.billsService.recordInventoryTransactions(
|
this.billsService.recordInventoryTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
bill,
|
bill.id,
|
||||||
|
bill.billDate,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -145,4 +147,19 @@ export default class BillSubscriber {
|
|||||||
billId,
|
billId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules items cost compute jobs once the inventory transactions created
|
||||||
|
* of the bill transaction.
|
||||||
|
*/
|
||||||
|
@On(events.bill.onInventoryTransactionsCreated)
|
||||||
|
public async handleComputeItemsCosts({ tenantId, billId }) {
|
||||||
|
this.logger.info('[bill] trying to compute the bill items cost.', {
|
||||||
|
tenantId, billId,
|
||||||
|
});
|
||||||
|
await this.billsService.scheduleComputeBillItemsCost(
|
||||||
|
tenantId,
|
||||||
|
billId,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export default {
|
|||||||
onDeleted: 'onSaleInvoiceDeleted',
|
onDeleted: 'onSaleInvoiceDeleted',
|
||||||
onBulkDelete: 'onSaleInvoiceBulkDeleted',
|
onBulkDelete: 'onSaleInvoiceBulkDeleted',
|
||||||
onPublished: 'onSaleInvoicePublished',
|
onPublished: 'onSaleInvoicePublished',
|
||||||
|
onInventoryTransactionsCreated: 'onInvoiceInventoryTransactionsCreated',
|
||||||
|
onInventoryTransactionsDeleted: 'onInvoiceInventoryTransactionsDeleted',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +127,7 @@ export default {
|
|||||||
onDeleted: 'onBillDeleted',
|
onDeleted: 'onBillDeleted',
|
||||||
onBulkDeleted: 'onBillBulkDeleted',
|
onBulkDeleted: 'onBillBulkDeleted',
|
||||||
onPublished: 'onBillPublished',
|
onPublished: 'onBillPublished',
|
||||||
|
onInventoryTransactionsCreated: 'onBillInventoryTransactionsCreated'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,5 +168,14 @@ export default {
|
|||||||
onEdited: 'onItemEdited',
|
onEdited: 'onItemEdited',
|
||||||
onDeleted: 'onItemDeleted',
|
onDeleted: 'onItemDeleted',
|
||||||
onBulkDeleted: 'onItemBulkDeleted',
|
onBulkDeleted: 'onItemBulkDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inventory service.
|
||||||
|
*/
|
||||||
|
inventory: {
|
||||||
|
onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled',
|
||||||
|
onComputeItemCostJobStarted: 'onComputeItemCostJobStarted',
|
||||||
|
onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
server/src/subscribers/inventory.ts
Normal file
34
server/src/subscribers/inventory.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import { EventSubscriber, On } from 'event-dispatch';
|
||||||
|
import events from 'subscribers/events';
|
||||||
|
import SaleInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||||
|
|
||||||
|
@EventSubscriber()
|
||||||
|
export class InventorySubscriber {
|
||||||
|
depends: number = 0;
|
||||||
|
startingDate: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle run writing the journal entries once the compute items jobs completed.
|
||||||
|
*/
|
||||||
|
@On(events.inventory.onComputeItemCostJobCompleted)
|
||||||
|
async onComputeItemCostJobFinished({ itemId, tenantId, startingDate }) {
|
||||||
|
const saleInvoicesCost = Container.get(SaleInvoicesCost);
|
||||||
|
const agenda = Container.get('agenda');
|
||||||
|
|
||||||
|
const dependsComputeJobs = await agenda.jobs({
|
||||||
|
name: 'compute-item-cost',
|
||||||
|
nextRunAt: { $ne: null },
|
||||||
|
'data.tenantId': tenantId,
|
||||||
|
});
|
||||||
|
// There is no scheduled compute jobs waiting.
|
||||||
|
if (dependsComputeJobs.length === 0) {
|
||||||
|
this.startingDate = null;
|
||||||
|
|
||||||
|
await saleInvoicesCost.scheduleWriteJournalEntries(
|
||||||
|
tenantId,
|
||||||
|
startingDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,64 @@ export default class SaleInvoiceSubscriber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles sale invoice next number increment once invoice created.
|
||||||
|
*/
|
||||||
|
@On(events.saleInvoice.onCreated)
|
||||||
|
public async handleInvoiceNextNumberIncrement({
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceId,
|
||||||
|
saleInvoice,
|
||||||
|
}) {
|
||||||
|
await this.settingsService.incrementNextNumber(tenantId, {
|
||||||
|
key: 'next_number',
|
||||||
|
group: 'sales_invoices',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the writing inventory transactions once the invoice created.
|
||||||
|
*/
|
||||||
|
@On(events.saleInvoice.onCreated)
|
||||||
|
public async handleWritingInventoryTransactions({ tenantId, saleInvoice }) {
|
||||||
|
this.logger.info('[sale_invoice] trying to write inventory transactions.', {
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
await this.saleInvoicesService.recordInventoryTranscactions(
|
||||||
|
tenantId,
|
||||||
|
saleInvoice.id,
|
||||||
|
saleInvoice.invoiceDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records journal entries of the non-inventory invoice.
|
||||||
|
*/
|
||||||
|
@On(events.saleInvoice.onCreated)
|
||||||
|
@On(events.saleInvoice.onEdited)
|
||||||
|
public async handleWritingNonInventoryEntries({ tenantId, saleInvoice }) {
|
||||||
|
await this.saleInvoicesService.recordNonInventoryJournalEntries(
|
||||||
|
tenantId,
|
||||||
|
saleInvoice.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@On(events.saleInvoice.onEdited)
|
||||||
|
public async handleRewritingInventoryTransactions({ tenantId, saleInvoice }) {
|
||||||
|
this.logger.info('[sale_invoice] trying to write inventory transactions.', {
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
await this.saleInvoicesService.recordInventoryTranscactions(
|
||||||
|
tenantId,
|
||||||
|
saleInvoice.id,
|
||||||
|
saleInvoice.invoiceDate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles customer balance diff balnace change once sale invoice edited.
|
* Handles customer balance diff balnace change once sale invoice edited.
|
||||||
*/
|
*/
|
||||||
@@ -103,35 +161,6 @@ export default class SaleInvoiceSubscriber {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles sale invoice next number increment once invoice created.
|
|
||||||
*/
|
|
||||||
@On(events.saleInvoice.onCreated)
|
|
||||||
public async handleInvoiceNextNumberIncrement({
|
|
||||||
tenantId,
|
|
||||||
saleInvoiceId,
|
|
||||||
saleInvoice,
|
|
||||||
}) {
|
|
||||||
await this.settingsService.incrementNextNumber(tenantId, {
|
|
||||||
key: 'next_number',
|
|
||||||
group: 'sales_invoices',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the writing inventory transactions once the invoice created.
|
|
||||||
*/
|
|
||||||
@On(events.saleInvoice.onCreated)
|
|
||||||
public async handleWritingInventoryTransactions({ tenantId, saleInvoice }) {
|
|
||||||
this.logger.info('[sale_invoice] trying to write inventory transactions.', {
|
|
||||||
tenantId,
|
|
||||||
});
|
|
||||||
await this.saleInvoicesService.recordInventoryTranscactions(
|
|
||||||
tenantId,
|
|
||||||
saleInvoice,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deleting the inventory transactions once the invoice deleted.
|
* Handles deleting the inventory transactions once the invoice deleted.
|
||||||
*/
|
*/
|
||||||
@@ -145,4 +174,18 @@ export default class SaleInvoiceSubscriber {
|
|||||||
saleInvoiceId,
|
saleInvoiceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules compute invoice items cost job.
|
||||||
|
*/
|
||||||
|
@On(events.saleInvoice.onInventoryTransactionsCreated)
|
||||||
|
public async handleComputeItemsCosts({ tenantId, saleInvoiceId }) {
|
||||||
|
this.logger.info('[sale_invoice] trying to compute the invoice items cost.', {
|
||||||
|
tenantId, saleInvoiceId,
|
||||||
|
});
|
||||||
|
await this.saleInvoicesService.scheduleComputeInvoiceItemsCost(
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user