mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: Concurrency control items cost compute.
This commit is contained in:
@@ -6,6 +6,12 @@ exports.up = function (knex) {
|
|||||||
table.integer('parent_category_id').unsigned();
|
table.integer('parent_category_id').unsigned();
|
||||||
table.text('description');
|
table.text('description');
|
||||||
table.integer('user_id').unsigned();
|
table.integer('user_id').unsigned();
|
||||||
|
|
||||||
|
table.integer('cost_account_id').unsigned();
|
||||||
|
table.integer('sell_account_id').unsigned();
|
||||||
|
table.integer('inventory_account_id').unsigned();
|
||||||
|
|
||||||
|
table.string('cost_method');
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ exports.up = function(knex) {
|
|||||||
table.integer('lot_number');
|
table.integer('lot_number');
|
||||||
|
|
||||||
table.string('transaction_type');
|
table.string('transaction_type');
|
||||||
table.integer('transaction_id');
|
table.integer('transaction_id').unsigned();
|
||||||
|
|
||||||
|
table.integer('entry_id').unsigned();
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ exports.up = function(knex) {
|
|||||||
return knex.schema.createTable('inventory_cost_lot_tracker', table => {
|
return knex.schema.createTable('inventory_cost_lot_tracker', table => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.date('date');
|
table.date('date');
|
||||||
|
|
||||||
table.string('direction');
|
table.string('direction');
|
||||||
|
|
||||||
table.integer('item_id').unsigned();
|
table.integer('item_id').unsigned();
|
||||||
table.integer('quantity').unsigned();
|
table.integer('quantity').unsigned();
|
||||||
table.decimal('rate', 13, 3);
|
table.decimal('rate', 13, 3);
|
||||||
table.integer('remaining');
|
table.integer('remaining');
|
||||||
|
table.integer('cost');
|
||||||
table.integer('lot_number');
|
table.integer('lot_number');
|
||||||
|
|
||||||
table.string('transaction_type');
|
table.string('transaction_type');
|
||||||
table.integer('transaction_id');
|
table.integer('transaction_id').unsigned();
|
||||||
|
table.integer('entry_id').unsigned();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ export default {
|
|||||||
}
|
}
|
||||||
const lastLoginAt = moment().format('YYYY/MM/DD HH:mm:ss');
|
const lastLoginAt = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||||
|
|
||||||
|
const tenantDb = TenantsManager.knexInstance(user.tenant.organizationId);
|
||||||
|
TenantModel.knexBinded = tenantDb;
|
||||||
|
|
||||||
const updateTenantUser = TenantUser.tenant().query()
|
const updateTenantUser = TenantUser.tenant().query()
|
||||||
.where('id', user.id)
|
.where('id', user.id)
|
||||||
.update({ last_login_at: lastLoginAt });
|
.update({ last_login_at: lastLoginAt });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
export default class Ping {
|
export default class Ping {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import Ping from '@/http/controllers/Ping';
|
|||||||
import Agendash from '@/http/controllers/Agendash';
|
import Agendash from '@/http/controllers/Agendash';
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
// app.use('/api/oauth2', OAuth2.router());
|
|
||||||
app.use('/api/auth', Authentication.router());
|
app.use('/api/auth', Authentication.router());
|
||||||
app.use('/api/invite', InviteUsers.router());
|
app.use('/api/invite', InviteUsers.router());
|
||||||
|
|
||||||
|
|||||||
7
server/src/interfaces/IItem.ts
Normal file
7
server/src/interfaces/IItem.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface IItem{
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
interface IInventoryCostMethod {
|
interface IInventoryCostMethod {
|
||||||
computeItemsCost(fromDate: Date): void,
|
computeItemsCost(fromDate: Date): void,
|
||||||
initialize(): void,
|
storeInventoryLotsCost(transactions: any[]): void,
|
||||||
}
|
}
|
||||||
14
server/src/interfaces/ItemEntry.ts
Normal file
14
server/src/interfaces/ItemEntry.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface IItemEntry {
|
||||||
|
referenceType: string,
|
||||||
|
referenceId: number,
|
||||||
|
|
||||||
|
index: number,
|
||||||
|
|
||||||
|
itemId: number,
|
||||||
|
description: string,
|
||||||
|
discount: number,
|
||||||
|
quantity: number,
|
||||||
|
rate: number,
|
||||||
|
}
|
||||||
8
server/src/interfaces/SaleInvoice.ts
Normal file
8
server/src/interfaces/SaleInvoice.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface ISaleInvoice {
|
||||||
|
id: number,
|
||||||
|
balance: number,
|
||||||
|
invoiceDate: Date,
|
||||||
|
entries: [],
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { IInventoryTransaction, IInventoryLotCost } from './InventoryTransaction';
|
import { IInventoryTransaction, IInventoryLotCost } from './InventoryTransaction';
|
||||||
import { IBillPaymentEntry, IBillPayment } from './BillPayment';
|
import { IBillPaymentEntry, IBillPayment } from './BillPayment';
|
||||||
import { IInventoryCostMethod } from './IInventoryCostMethod';
|
import { IInventoryCostMethod } from './IInventoryCostMethod';
|
||||||
|
import { IItemEntry } from './ItemEntry';
|
||||||
|
import { IItem } from './Item';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IBillPaymentEntry,
|
IBillPaymentEntry,
|
||||||
@@ -8,4 +10,6 @@ export {
|
|||||||
IInventoryTransaction,
|
IInventoryTransaction,
|
||||||
IInventoryLotCost,
|
IInventoryLotCost,
|
||||||
IInventoryCostMethod,
|
IInventoryCostMethod,
|
||||||
|
IItemEntry
|
||||||
|
IItem,
|
||||||
};
|
};
|
||||||
@@ -6,9 +6,11 @@ export default class ComputeItemCostJob {
|
|||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
|
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
|
||||||
|
|
||||||
|
Logger.debug(`Compute item cost - started: ${job.attrs.data}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
||||||
Logger.debug(`Compute item cost: ${job.attrs.data}`);
|
Logger.debug(`Compute item cost - completed: ${job.attrs.data}`);
|
||||||
done();
|
done();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|||||||
22
server/src/jobs/writeInvoicesJEntries.ts
Normal file
22
server/src/jobs/writeInvoicesJEntries.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||||
|
|
||||||
|
export default class WriteInvoicesJournalEntries {
|
||||||
|
|
||||||
|
public async handler(job, done: Function): Promise<void> {
|
||||||
|
const Logger = Container.get('logger');
|
||||||
|
const { startingDate } = job.attrs.data;
|
||||||
|
|
||||||
|
Logger.debug(`Write sales invoices journal entries - started: ${job.attrs.data}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SalesInvoicesCost.writeJournalEntries(startingDate, true);
|
||||||
|
Logger.debug(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
|
||||||
|
done();
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
Logger.error(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Agenda from 'agenda';
|
import Agenda from 'agenda';
|
||||||
import WelcomeEmailJob from '@/Jobs/welcomeEmail';
|
import WelcomeEmailJob from '@/Jobs/welcomeEmail';
|
||||||
import ComputeItemCost from '@/Jobs/ComputeItemCost';
|
import ComputeItemCost from '@/Jobs/ComputeItemCost';
|
||||||
|
import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries';
|
||||||
|
|
||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
agenda.define(
|
agenda.define(
|
||||||
@@ -10,8 +11,13 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
);
|
);
|
||||||
agenda.define(
|
agenda.define(
|
||||||
'compute-item-cost',
|
'compute-item-cost',
|
||||||
{ priority: 'high' },
|
{ priority: 'high', concurrency: 20 },
|
||||||
new ComputeItemCost().handler,
|
new ComputeItemCost().handler,
|
||||||
);
|
);
|
||||||
|
agenda.define(
|
||||||
|
'rewrite-invoices-journal-entries',
|
||||||
|
{ priority: 'normal', concurrency: 1, },
|
||||||
|
new RewriteInvoicesJournalEntries().handler,
|
||||||
|
);
|
||||||
agenda.start();
|
agenda.start();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ export default class InventoryCostLotTracker extends TenantModel {
|
|||||||
*/
|
*/
|
||||||
static get modifiers() {
|
static get modifiers() {
|
||||||
return {
|
return {
|
||||||
|
groupedEntriesCost(query) {
|
||||||
|
query.select(['entry_id', 'transaction_id', 'transaction_type']);
|
||||||
|
|
||||||
|
query.groupBy('item_id');
|
||||||
|
query.groupBy('entry_id');
|
||||||
|
query.groupBy('transaction_id');
|
||||||
|
query.groupBy('transaction_type');
|
||||||
|
|
||||||
|
query.sum('cost as cost');
|
||||||
|
},
|
||||||
filterDateRange(query, startDate, endDate, type = 'day') {
|
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default class InventoryTransaction extends TenantModel {
|
|||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -38,7 +37,6 @@ export default class InventoryTransaction extends TenantModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,4 +32,19 @@ export default class ItemEntry extends TenantModel {
|
|||||||
|
|
||||||
return discount ? total - (total * discount * 0.01) : total;
|
return discount ? total - (total * discount * 0.01) : total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get relationMappings() {
|
||||||
|
const Item = require('@/models/Item');
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: this.relationBindKnex(Item.default),
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.itemId',
|
||||||
|
to: 'items.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
|||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||||
|
import InventoryCostLotTracker from './InventoryCostLotTracker';
|
||||||
|
|
||||||
export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +27,26 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
|||||||
return ['created_at', 'updated_at'];
|
return ['created_at', 'updated_at'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||||
|
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
query.where('invoice_date', '>=', fromDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
query.where('invoice_date', '<=', toDate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Due amount of the given.
|
* Due amount of the given.
|
||||||
*/
|
*/
|
||||||
@@ -40,6 +61,7 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
|||||||
const AccountTransaction = require('@/models/AccountTransaction');
|
const AccountTransaction = require('@/models/AccountTransaction');
|
||||||
const ItemEntry = require('@/models/ItemEntry');
|
const ItemEntry = require('@/models/ItemEntry');
|
||||||
const Customer = require('@/models/Customer');
|
const Customer = require('@/models/Customer');
|
||||||
|
const InventoryCostLotTracker = require('@/models/InventoryCostLotTracker');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entries: {
|
entries: {
|
||||||
@@ -73,6 +95,18 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
|||||||
filter(builder) {
|
filter(builder) {
|
||||||
builder.where('reference_type', 'SaleInvoice');
|
builder.where('reference_type', 'SaleInvoice');
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
costTransactions: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: this.relationBindKnex(InventoryCostLotTracker.default),
|
||||||
|
join: {
|
||||||
|
from: 'sales_invoices.id',
|
||||||
|
to: 'inventory_cost_lot_tracker.transactionId'
|
||||||
|
},
|
||||||
|
filter(builder) {
|
||||||
|
builder.where('transaction_type', 'SaleInvoice');
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,22 @@ export default class InventoryService {
|
|||||||
/**
|
/**
|
||||||
* 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.
|
||||||
* @param {Date} fromDate
|
* @param {Date} fromDate -
|
||||||
* @param {number} itemId
|
* @param {number} itemId -
|
||||||
*/
|
*/
|
||||||
static async computeItemCost(fromDate: Date, itemId: number) {
|
static async computeItemCost(fromDate: Date, itemId: number) {
|
||||||
const costMethod: TCostMethod = 'FIFO';
|
const item = await Item.tenant().query()
|
||||||
|
.findById(itemId)
|
||||||
|
.withGraphFetched('category');
|
||||||
|
|
||||||
|
// Cannot continue if the given item was not inventory item.
|
||||||
|
if (item.type !== 'inventory') {
|
||||||
|
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(costMethod) {
|
switch(costMethod) {
|
||||||
case 'FIFO':
|
case 'FIFO':
|
||||||
case 'LIFO':
|
case 'LIFO':
|
||||||
@@ -27,10 +36,9 @@ export default class InventoryService {
|
|||||||
break;
|
break;
|
||||||
case 'AVG':
|
case 'AVG':
|
||||||
costMethodComputer = new InventoryAverageCost(fromDate, itemId);
|
costMethodComputer = new InventoryAverageCost(fromDate, itemId);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
await costMethodComputer.initialize();
|
return costMethodComputer.computeItemCost();
|
||||||
await costMethodComputer.computeItemCost()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,10 +49,6 @@ export default class InventoryService {
|
|||||||
static async scheduleComputeItemCost(itemId: number, startingDate: Date|string) {
|
static async scheduleComputeItemCost(itemId: number, startingDate: Date|string) {
|
||||||
const agenda = Container.get('agenda');
|
const agenda = Container.get('agenda');
|
||||||
|
|
||||||
// Delete the scheduled job in case has the same given data.
|
|
||||||
await agenda.cancel({
|
|
||||||
name: 'compute-item-cost',
|
|
||||||
});
|
|
||||||
return agenda.schedule('in 3 seconds', 'compute-item-cost', {
|
return agenda.schedule('in 3 seconds', 'compute-item-cost', {
|
||||||
startingDate, itemId,
|
startingDate, itemId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,27 @@
|
|||||||
import { Account, InventoryTransaction } from '@/models';
|
import { pick } from 'lodash';
|
||||||
|
import { InventoryTransaction } from '@/models';
|
||||||
import { IInventoryTransaction } from '@/interfaces';
|
import { IInventoryTransaction } from '@/interfaces';
|
||||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
|
||||||
import JournalCommands from '@/services/Accounting/JournalCommands';
|
|
||||||
|
|
||||||
export default class InventoryAverageCostMethod implements IInventoryCostMethod {
|
export default class InventoryAverageCostMethod extends InventoryCostMethod implements IInventoryCostMethod {
|
||||||
journal: JournalPoster;
|
startingDate: Date;
|
||||||
journalCommands: JournalCommands;
|
|
||||||
fromDate: Date;
|
|
||||||
itemId: number;
|
itemId: number;
|
||||||
|
costTransactions: any[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {Date} fromDate -
|
* @param {Date} startingDate -
|
||||||
* @param {number} itemId -
|
* @param {number} itemId -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
fromDate: Date,
|
startingDate: Date,
|
||||||
itemId: number,
|
itemId: number,
|
||||||
) {
|
) {
|
||||||
this.fromDate = fromDate;
|
super();
|
||||||
|
|
||||||
|
this.startingDate = startingDate;
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
}
|
this.costTransactions = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the inventory average cost method.
|
|
||||||
* @async
|
|
||||||
*/
|
|
||||||
async initialize() {
|
|
||||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
|
||||||
|
|
||||||
this.journal = new JournalPoster(accountsDepGraph);
|
|
||||||
this.journalCommands = new JournalCommands(this.journal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,50 +34,41 @@ export default class InventoryAverageCostMethod implements IInventoryCostMethod
|
|||||||
* after the given date.
|
* after the given date.
|
||||||
* ----------
|
* ----------
|
||||||
* @asycn
|
* @asycn
|
||||||
* @param {Date} fromDate
|
* @param {Date} startingDate
|
||||||
* @param {number} referenceId
|
* @param {number} referenceId
|
||||||
* @param {string} referenceType
|
* @param {string} referenceType
|
||||||
*/
|
*/
|
||||||
public async computeItemCost() {
|
public async computeItemCost() {
|
||||||
const openingAvgCost = await this.getOpeningAvaregeCost(this.fromDate, this.itemId);
|
const openingAvgCost = await this.getOpeningAvaregeCost(this.startingDate, this.itemId);
|
||||||
|
|
||||||
// @todo from `invTransactions`.
|
|
||||||
const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction
|
const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction
|
||||||
.tenant()
|
.tenant()
|
||||||
.query()
|
.query()
|
||||||
.where('date', '>=', this.fromDate)
|
.modify('filterDateRange', this.startingDate)
|
||||||
// .where('direction', 'OUT')
|
.orderBy('date', 'ASC')
|
||||||
.orderBy('date', 'asc')
|
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
||||||
|
.where('item_id', this.itemId)
|
||||||
.withGraphFetched('item');
|
.withGraphFetched('item');
|
||||||
|
|
||||||
// Remove and revert accounts balance journal entries from
|
// Tracking inventroy transactions and retrieve cost transactions
|
||||||
// inventory transactions.
|
// based on average rate cost method.
|
||||||
await this.journalCommands
|
const costTransactions = this.trackingCostTransactions(
|
||||||
.revertEntriesFromInventoryTransactions(afterInvTransactions);
|
|
||||||
|
|
||||||
// Re-write the journal entries from the new recorded inventory transactions.
|
|
||||||
await this.jEntriesFromItemInvTransactions(
|
|
||||||
afterInvTransactions,
|
afterInvTransactions,
|
||||||
openingAvgCost,
|
openingAvgCost,
|
||||||
);
|
);
|
||||||
// Saves the new recorded journal entries to the storage.
|
await this.storeInventoryLotsCost(costTransactions);
|
||||||
await Promise.all([
|
|
||||||
this.journal.deleteEntries(),
|
|
||||||
this.journal.saveEntries(),
|
|
||||||
this.journal.saveBalance(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get items Avarege cost from specific date from inventory transactions.
|
* Get items Avarege cost from specific date from inventory transactions.
|
||||||
* @static
|
* @static
|
||||||
* @param {Date} fromDate
|
* @param {Date} startingDate
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
public async getOpeningAvaregeCost(fromDate: Date, itemId: number) {
|
public async getOpeningAvaregeCost(startingDate: Date, itemId: number) {
|
||||||
const commonBuilder = (builder: any) => {
|
const commonBuilder = (builder: any) => {
|
||||||
if (fromDate) {
|
if (startingDate) {
|
||||||
builder.where('date', '<', fromDate);
|
builder.where('date', '<', startingDate);
|
||||||
}
|
}
|
||||||
builder.where('item_id', itemId);
|
builder.where('item_id', itemId);
|
||||||
builder.groupBy('rate');
|
builder.groupBy('rate');
|
||||||
@@ -155,53 +137,45 @@ export default class InventoryAverageCostMethod implements IInventoryCostMethod
|
|||||||
* @param {number} referenceId
|
* @param {number} referenceId
|
||||||
* @param {JournalCommand} journalCommands
|
* @param {JournalCommand} journalCommands
|
||||||
*/
|
*/
|
||||||
async jEntriesFromItemInvTransactions(
|
public trackingCostTransactions(
|
||||||
invTransactions: IInventoryTransaction[],
|
invTransactions: IInventoryTransaction[],
|
||||||
openingAverageCost: number,
|
openingAverageCost: number,
|
||||||
) {
|
) {
|
||||||
const transactions: any[] = [];
|
const costTransactions: any[] = [];
|
||||||
let accQuantity: number = 0;
|
let accQuantity: number = 0;
|
||||||
let accCost: number = 0;
|
let accCost: number = 0;
|
||||||
|
|
||||||
invTransactions.forEach((invTransaction: IInventoryTransaction) => {
|
invTransactions.forEach((invTransaction: IInventoryTransaction) => {
|
||||||
const commonEntry = {
|
const commonEntry = {
|
||||||
date: invTransaction.date,
|
invTransId: invTransaction.id,
|
||||||
referenceType: invTransaction.transactionType,
|
...pick(invTransaction, ['date', 'direction', 'itemId', 'quantity', 'rate', 'entryId',
|
||||||
referenceId: invTransaction.transactionId,
|
'transactionId', 'transactionType']),
|
||||||
};
|
};
|
||||||
switch(invTransaction.direction) {
|
switch(invTransaction.direction) {
|
||||||
case 'IN':
|
case 'IN':
|
||||||
accQuantity += invTransaction.quantity;
|
accQuantity += invTransaction.quantity;
|
||||||
accCost += invTransaction.rate * invTransaction.quantity;
|
accCost += invTransaction.rate * invTransaction.quantity;
|
||||||
|
|
||||||
const inventory = invTransaction.quantity * invTransaction.rate;
|
costTransactions.push({
|
||||||
|
|
||||||
transactions.push({
|
|
||||||
...commonEntry,
|
...commonEntry,
|
||||||
inventory,
|
|
||||||
inventoryAccount: invTransaction.item.inventoryAccountId,
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'OUT':
|
case 'OUT':
|
||||||
const income = invTransaction.quantity * invTransaction.rate;
|
|
||||||
const transactionAvgCost = accCost ? (accCost / accQuantity) : 0;
|
const transactionAvgCost = accCost ? (accCost / accQuantity) : 0;
|
||||||
const averageCost = transactionAvgCost;
|
const averageCost = transactionAvgCost;
|
||||||
const cost = (invTransaction.quantity * averageCost);
|
const cost = (invTransaction.quantity * averageCost);
|
||||||
|
const income = (invTransaction.quantity * invTransaction.rate);
|
||||||
|
|
||||||
accQuantity -= invTransaction.quantity;
|
accQuantity -= invTransaction.quantity;
|
||||||
accCost -= accCost;
|
accCost -= income;
|
||||||
|
|
||||||
transactions.push({
|
costTransactions.push({
|
||||||
...commonEntry,
|
...commonEntry,
|
||||||
income,
|
|
||||||
cost,
|
cost,
|
||||||
incomeAccount: invTransaction.item.sellAccountId,
|
|
||||||
costAccount: invTransaction.item.costAccountId,
|
|
||||||
inventoryAccount: invTransaction.item.inventoryAccountId,
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.journalCommands.inventoryEntries(transactions);
|
return costTransactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,20 +3,15 @@ import moment from 'moment';
|
|||||||
import {
|
import {
|
||||||
InventoryTransaction,
|
InventoryTransaction,
|
||||||
InventoryLotCostTracker,
|
InventoryLotCostTracker,
|
||||||
Account,
|
|
||||||
Item,
|
Item,
|
||||||
} from "@/models";
|
} from "@/models";
|
||||||
import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces";
|
import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces";
|
||||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
|
||||||
import JournalCommands from '@/services/Accounting/JournalCommands';
|
|
||||||
|
|
||||||
type TCostMethod = 'FIFO' | 'LIFO';
|
type TCostMethod = 'FIFO' | 'LIFO';
|
||||||
|
|
||||||
export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
export default class InventoryCostLotTracker extends InventoryCostMethod implements IInventoryCostMethod {
|
||||||
journal: JournalPoster;
|
|
||||||
journalCommands: JournalCommands;
|
|
||||||
startingDate: Date;
|
startingDate: Date;
|
||||||
headDate: Date;
|
|
||||||
itemId: number;
|
itemId: number;
|
||||||
costMethod: TCostMethod;
|
costMethod: TCostMethod;
|
||||||
itemsById: Map<number, any>;
|
itemsById: Map<number, any>;
|
||||||
@@ -25,7 +20,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
costLotsTransactions: IInventoryLotCost[];
|
costLotsTransactions: IInventoryLotCost[];
|
||||||
inTransactions: any[];
|
inTransactions: any[];
|
||||||
outTransactions: IInventoryTransaction[];
|
outTransactions: IInventoryTransaction[];
|
||||||
revertInvoiceTrans: any[];
|
|
||||||
revertJEntriesTransactions: IInventoryTransaction[];
|
revertJEntriesTransactions: IInventoryTransaction[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +29,8 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
* @param {string} costMethod -
|
* @param {string} costMethod -
|
||||||
*/
|
*/
|
||||||
constructor(startingDate: Date, itemId: number, costMethod: TCostMethod = 'FIFO') {
|
constructor(startingDate: Date, itemId: number, costMethod: TCostMethod = 'FIFO') {
|
||||||
|
super();
|
||||||
|
|
||||||
this.startingDate = startingDate;
|
this.startingDate = startingDate;
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
this.costMethod = costMethod;
|
this.costMethod = costMethod;
|
||||||
@@ -49,18 +45,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
this.inTransactions = [];
|
this.inTransactions = [];
|
||||||
// Collects `OUT` transactions.
|
// Collects `OUT` transactions.
|
||||||
this.outTransactions = [];
|
this.outTransactions = [];
|
||||||
// Collects journal entries reference id and type that should be reverted.
|
|
||||||
this.revertInvoiceTrans = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the inventory average cost method.
|
|
||||||
* @async
|
|
||||||
*/
|
|
||||||
public async initialize() {
|
|
||||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
|
||||||
this.journal = new JournalPoster(accountsDepGraph);
|
|
||||||
this.journalCommands = new JournalCommands(this.journal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,30 +64,16 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
await this.fetchInvOUTTransactions();
|
await this.fetchInvOUTTransactions();
|
||||||
await this.fetchRevertInvJReferenceIds();
|
await this.fetchRevertInvJReferenceIds();
|
||||||
await this.fetchItemsMapped();
|
await this.fetchItemsMapped();
|
||||||
|
|
||||||
this.trackingInventoryINLots(this.inTransactions);
|
this.trackingInventoryINLots(this.inTransactions);
|
||||||
this.trackingInventoryOUTLots(this.outTransactions);
|
this.trackingInventoryOUTLots(this.outTransactions);
|
||||||
|
|
||||||
// Re-tracking the inventory `IN` and `OUT` lots costs.
|
// Re-tracking the inventory `IN` and `OUT` lots costs.
|
||||||
const storedTrackedInvLotsOper = this.storeInventoryLotsCost(
|
const storedTrackedInvLotsOper = this.storeInventoryLotsCost(
|
||||||
this.costLotsTransactions,
|
this.costLotsTransactions,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove and revert accounts balance journal entries from inventory transactions.
|
|
||||||
const revertJEntriesOper = this.revertJournalEntries(this.revertJEntriesTransactions);
|
|
||||||
|
|
||||||
// Records the journal entries operation.
|
|
||||||
this.recordJournalEntries(this.costLotsTransactions);
|
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
storedTrackedInvLotsOper,
|
storedTrackedInvLotsOper,
|
||||||
revertJEntriesOper.then(() =>
|
|
||||||
Promise.all([
|
|
||||||
// Saves the new recorded journal entries to the storage.
|
|
||||||
this.journal.deleteEntries(),
|
|
||||||
this.journal.saveEntries(),
|
|
||||||
this.journal.saveBalance(),
|
|
||||||
])),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +91,8 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
const afterInvTransactions: IInventoryTransaction[] =
|
const afterInvTransactions: IInventoryTransaction[] =
|
||||||
await InventoryTransaction.tenant()
|
await InventoryTransaction.tenant()
|
||||||
.query()
|
.query()
|
||||||
.modify('filterDateRange', this.startingDate)
|
.modify('filterDateRange', this.startingDate)
|
||||||
|
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
||||||
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
|
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
|
||||||
.onBuild(commonBuilder)
|
.onBuild(commonBuilder)
|
||||||
.withGraphFetched('item');
|
.withGraphFetched('item');
|
||||||
@@ -221,93 +192,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
return Promise.all([deleteInvLotsTrans, ...asyncOpers]);
|
return Promise.all([deleteInvLotsTrans, ...asyncOpers]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverts the journal entries from inventory lots costs transaction.
|
|
||||||
* @param {} inventoryLots
|
|
||||||
*/
|
|
||||||
async revertJournalEntries(
|
|
||||||
transactions: IInventoryLotCost[],
|
|
||||||
) {
|
|
||||||
return this.journalCommands
|
|
||||||
.revertEntriesFromInventoryTransactions(transactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records the journal entries transactions.
|
|
||||||
* @async
|
|
||||||
* @param {IInventoryLotCost[]} inventoryTransactions -
|
|
||||||
* @param {string} referenceType -
|
|
||||||
* @param {number} referenceId -
|
|
||||||
* @param {Date} date -
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
public recordJournalEntries(
|
|
||||||
inventoryLots: IInventoryLotCost[],
|
|
||||||
): void {
|
|
||||||
const outTransactions: any[] = [];
|
|
||||||
const inTransByLotNumber: any = {};
|
|
||||||
const transactions: any = [];
|
|
||||||
|
|
||||||
inventoryLots.forEach((invTransaction: IInventoryLotCost) => {
|
|
||||||
switch(invTransaction.direction) {
|
|
||||||
case 'IN':
|
|
||||||
inTransByLotNumber[invTransaction.lotNumber] = invTransaction;
|
|
||||||
break;
|
|
||||||
case 'OUT':
|
|
||||||
outTransactions.push(invTransaction);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
outTransactions.forEach((outTransaction: IInventoryLotCost) => {
|
|
||||||
const { lotNumber, quantity, rate, itemId } = outTransaction;
|
|
||||||
const income = quantity * rate;
|
|
||||||
const item = this.itemsById.get(itemId);
|
|
||||||
|
|
||||||
const transaction = {
|
|
||||||
date: outTransaction.date,
|
|
||||||
referenceType: outTransaction.transactionType,
|
|
||||||
referenceId: outTransaction.transactionId,
|
|
||||||
cost: 0,
|
|
||||||
income,
|
|
||||||
incomeAccount: item.sellAccountId,
|
|
||||||
costAccount: item.costAccountId,
|
|
||||||
inventoryAccount: item.inventoryAccountId,
|
|
||||||
};
|
|
||||||
if (lotNumber && inTransByLotNumber[lotNumber]) {
|
|
||||||
const inInvTrans = inTransByLotNumber[lotNumber];
|
|
||||||
transaction.cost = (outTransaction.quantity * inInvTrans.rate);
|
|
||||||
}
|
|
||||||
transactions.push(transaction);
|
|
||||||
});
|
|
||||||
this.journalCommands.inventoryEntries(transactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the inventory lots costs transactions in bulk.
|
|
||||||
* @param {IInventoryLotCost[]} costLotsTransactions
|
|
||||||
* @return {Promise[]}
|
|
||||||
*/
|
|
||||||
storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> {
|
|
||||||
const opers: any = [];
|
|
||||||
|
|
||||||
costLotsTransactions.forEach((transaction: IInventoryLotCost) => {
|
|
||||||
if (transaction.lotTransId && transaction.decrement) {
|
|
||||||
const decrementOper = InventoryLotCostTracker.tenant()
|
|
||||||
.query()
|
|
||||||
.where('id', transaction.lotTransId)
|
|
||||||
.decrement('remaining', transaction.decrement);
|
|
||||||
opers.push(decrementOper);
|
|
||||||
} else if(!transaction.lotTransId) {
|
|
||||||
const operation = InventoryLotCostTracker.tenant().query()
|
|
||||||
.insert({
|
|
||||||
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
|
||||||
});
|
|
||||||
opers.push(operation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Promise.all(opers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracking inventory `IN` lots transactions.
|
* Tracking inventory `IN` lots transactions.
|
||||||
* @public
|
* @public
|
||||||
@@ -352,7 +236,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
|
|
||||||
const commonLotTransaction: IInventoryLotCost = {
|
const commonLotTransaction: IInventoryLotCost = {
|
||||||
...pick(transaction, [
|
...pick(transaction, [
|
||||||
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId',
|
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'entryId',
|
||||||
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
@@ -373,6 +257,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0;
|
const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0;
|
||||||
const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining;
|
const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining;
|
||||||
const maxDecrement = Math.min(decrement, invRemaining);
|
const maxDecrement = Math.min(decrement, invRemaining);
|
||||||
|
const cost = maxDecrement * _invINTransaction.rate;
|
||||||
|
|
||||||
_invINTransaction.decrement += maxDecrement;
|
_invINTransaction.decrement += maxDecrement;
|
||||||
_invINTransaction.remaining = Math.max(
|
_invINTransaction.remaining = Math.max(
|
||||||
@@ -383,6 +268,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
|
|
||||||
this.costLotsTransactions.push({
|
this.costLotsTransactions.push({
|
||||||
...commonLotTransaction,
|
...commonLotTransaction,
|
||||||
|
cost,
|
||||||
quantity: maxDecrement,
|
quantity: maxDecrement,
|
||||||
lotNumber: _invINTransaction.lotNumber,
|
lotNumber: _invINTransaction.lotNumber,
|
||||||
});
|
});
|
||||||
@@ -392,7 +278,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (invRemaining > 0) {
|
if (invRemaining > 0) {
|
||||||
this.costLotsTransactions.push({
|
this.costLotsTransactions.push({
|
||||||
...commonLotTransaction,
|
...commonLotTransaction,
|
||||||
quantity: invRemaining,
|
quantity: invRemaining,
|
||||||
|
|||||||
31
server/src/services/Inventory/InventoryCostMethod.ts
Normal file
31
server/src/services/Inventory/InventoryCostMethod.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { omit } from 'lodash';
|
||||||
|
import { IInventoryLotCost } from '@/interfaces';
|
||||||
|
import { InventoryLotCostTracker } from '@/models';
|
||||||
|
|
||||||
|
export default class InventoryCostMethod {
|
||||||
|
/**
|
||||||
|
* Stores the inventory lots costs transactions in bulk.
|
||||||
|
* @param {IInventoryLotCost[]} costLotsTransactions
|
||||||
|
* @return {Promise[]}
|
||||||
|
*/
|
||||||
|
public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> {
|
||||||
|
const opers: any = [];
|
||||||
|
|
||||||
|
costLotsTransactions.forEach((transaction: IInventoryLotCost) => {
|
||||||
|
if (transaction.lotTransId && transaction.decrement) {
|
||||||
|
const decrementOper = InventoryLotCostTracker.tenant()
|
||||||
|
.query()
|
||||||
|
.where('id', transaction.lotTransId)
|
||||||
|
.decrement('remaining', transaction.decrement);
|
||||||
|
opers.push(decrementOper);
|
||||||
|
} else if(!transaction.lotTransId) {
|
||||||
|
const operation = InventoryLotCostTracker.tenant().query()
|
||||||
|
.insert({
|
||||||
|
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
||||||
|
});
|
||||||
|
opers.push(operation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.all(opers);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/src/services/Items/ItemsCostService.ts
Normal file
5
server/src/services/Items/ItemsCostService.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default class ItemsCostService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { difference } from "lodash";
|
import { difference } from "lodash";
|
||||||
import { Item } from '@/models';
|
import { Item, ItemTransaction } from '@/models';
|
||||||
|
|
||||||
export default class ItemsService {
|
export default class ItemsService {
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import AccountsService from '@/services/Accounts/AccountsService';
|
|||||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||||
import InventoryService from '@/services/Inventory/Inventory';
|
import InventoryService from '@/services/Inventory/Inventory';
|
||||||
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
||||||
|
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||||
import { formatDateFields } from '@/utils';
|
import { formatDateFields } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vendor bills services.
|
* Vendor bills services.
|
||||||
* @service
|
* @service
|
||||||
*/
|
*/
|
||||||
export default class BillsService {
|
export default class BillsService extends SalesInvoicesCost {
|
||||||
/**
|
/**
|
||||||
* Creates a new bill and stored it to the storage.
|
* Creates a new bill and stored it to the storage.
|
||||||
*
|
*
|
||||||
@@ -52,13 +53,18 @@ export default class BillsService {
|
|||||||
bill.entries.forEach((entry) => {
|
bill.entries.forEach((entry) => {
|
||||||
const oper = ItemEntry.tenant()
|
const oper = ItemEntry.tenant()
|
||||||
.query()
|
.query()
|
||||||
.insert({
|
.insertAndFetch({
|
||||||
reference_type: 'Bill',
|
reference_type: 'Bill',
|
||||||
reference_id: storedBill.id,
|
reference_id: storedBill.id,
|
||||||
...omit(entry, ['amount']),
|
...omit(entry, ['amount']),
|
||||||
|
}).then((itemEntry) => {
|
||||||
|
entry.id = itemEntry.id;
|
||||||
});
|
});
|
||||||
saveEntriesOpers.push(oper);
|
saveEntriesOpers.push(oper);
|
||||||
});
|
});
|
||||||
|
// Await save all bill entries operations.
|
||||||
|
await Promise.all([...saveEntriesOpers]);
|
||||||
|
|
||||||
// Increments vendor balance.
|
// Increments vendor balance.
|
||||||
const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount);
|
const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount);
|
||||||
|
|
||||||
@@ -68,18 +74,16 @@ export default class BillsService {
|
|||||||
);
|
);
|
||||||
// Writes the journal entries for the given bill transaction.
|
// Writes the journal entries for the given bill transaction.
|
||||||
const writeJEntriesOper = this.recordJournalTransactions({
|
const writeJEntriesOper = this.recordJournalTransactions({
|
||||||
id: storedBill.id,
|
id: storedBill.id, ...bill,
|
||||||
...bill
|
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...saveEntriesOpers,
|
incrementOper,
|
||||||
incrementOper,
|
|
||||||
writeInvTransactionsOper,
|
writeInvTransactionsOper,
|
||||||
writeJEntriesOper,
|
writeJEntriesOper,
|
||||||
]);
|
]);
|
||||||
// Schedule bill re-compute based on the item cost
|
// Schedule bill re-compute based on the item cost
|
||||||
// method and starting date.
|
// method and starting date.
|
||||||
await this.scheduleComputeItemsCost(bill);
|
await this.scheduleComputeBillItemsCost(bill);
|
||||||
|
|
||||||
return storedBill;
|
return storedBill;
|
||||||
}
|
}
|
||||||
@@ -147,7 +151,7 @@ export default class BillsService {
|
|||||||
]);
|
]);
|
||||||
// Schedule sale invoice re-compute based on the item cost
|
// Schedule sale invoice re-compute based on the item cost
|
||||||
// method and starting date.
|
// method and starting date.
|
||||||
await this.scheduleComputeItemsCost(bill);
|
await this.scheduleComputeBillItemsCost(bill);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,10 +196,10 @@ export default class BillsService {
|
|||||||
]);
|
]);
|
||||||
// Schedule sale invoice re-compute based on the item cost
|
// Schedule sale invoice re-compute based on the item cost
|
||||||
// method and starting date.
|
// method and starting date.
|
||||||
await this.scheduleComputeItemsCost(bill);
|
await this.scheduleComputeBillItemsCost(bill);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records the inventory transactions from the given bill input.
|
* Records the inventory transactions from the given bill input.
|
||||||
* @param {Bill} bill
|
* @param {Bill} bill
|
||||||
* @param {number} billId
|
* @param {number} billId
|
||||||
@@ -209,6 +213,7 @@ export default class BillsService {
|
|||||||
transactionId: billId,
|
transactionId: billId,
|
||||||
direction: 'IN',
|
direction: 'IN',
|
||||||
date: bill.bill_date,
|
date: bill.bill_date,
|
||||||
|
entryId: entry.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return InventoryService.recordInventoryTransactions(
|
return InventoryService.recordInventoryTransactions(
|
||||||
@@ -284,24 +289,6 @@ export default class BillsService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule a job to re-compute the bill's items based on cost method
|
|
||||||
* of the each one.
|
|
||||||
* @param {Bill} bill
|
|
||||||
*/
|
|
||||||
static scheduleComputeItemsCost(bill) {
|
|
||||||
const asyncOpers = [];
|
|
||||||
|
|
||||||
bill.entries.forEach((entry) => {
|
|
||||||
const oper = InventoryService.scheduleComputeItemCost(
|
|
||||||
entry.item_id || entry.itemId,
|
|
||||||
bill.bill_date || bill.billDate,
|
|
||||||
);
|
|
||||||
asyncOpers.push(oper);
|
|
||||||
});
|
|
||||||
return Promise.all(asyncOpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detarmines whether the bill exists on the storage.
|
* Detarmines whether the bill exists on the storage.
|
||||||
* @param {Integer} billId
|
* @param {Integer} billId
|
||||||
@@ -355,4 +342,16 @@ export default class BillsService {
|
|||||||
.withGraphFetched('entries')
|
.withGraphFetched('entries')
|
||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules compute bill items cost based on each item cost method.
|
||||||
|
* @param {IBill} bill
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static scheduleComputeBillItemsCost(bill) {
|
||||||
|
return this.scheduleComputeItemsCost(
|
||||||
|
bill.entries.map((e) => e.item_id),
|
||||||
|
bill.bill_date,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,24 @@ export default class HasItemEntries {
|
|||||||
});
|
});
|
||||||
return Promise.all([...opers]);
|
return Promise.all([...opers]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static filterNonInventoryEntries(entries: [], items: []) {
|
||||||
|
const nonInventoryItems = items.filter((item: any) => item.type !== 'inventory');
|
||||||
|
const nonInventoryItemsIds = nonInventoryItems.map((i: any) => i.id);
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry: any) => (
|
||||||
|
(nonInventoryItemsIds.indexOf(entry.item_id)) !== -1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
static filterInventoryEntries(entries: [], items: []) {
|
||||||
|
const inventoryItems = items.filter((item: any) => item.type === 'inventory');
|
||||||
|
const inventoryItemsIds = inventoryItems.map((i: any) => i.id);
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry: any) => (
|
||||||
|
(inventoryItemsIds.indexOf(entry.item_id)) !== -1
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,35 +11,14 @@ import JournalPoster from '@/services/Accounting/JournalPoster';
|
|||||||
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
||||||
import CustomerRepository from '@/repositories/CustomerRepository';
|
import CustomerRepository from '@/repositories/CustomerRepository';
|
||||||
import InventoryService from '@/services/Inventory/Inventory';
|
import InventoryService from '@/services/Inventory/Inventory';
|
||||||
|
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||||
import { formatDateFields } from '@/utils';
|
import { formatDateFields } from '@/utils';
|
||||||
import { Item } from '../../models';
|
|
||||||
import JournalCommands from '../Accounting/JournalCommands';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sales invoices service
|
* Sales invoices service
|
||||||
* @service
|
* @service
|
||||||
*/
|
*/
|
||||||
export default class SaleInvoicesService {
|
export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||||
|
|
||||||
static filterNonInventoryEntries(entries: [], items: []) {
|
|
||||||
const nonInventoryItems = items.filter((item: any) => item.type !== 'inventory');
|
|
||||||
const nonInventoryItemsIds = nonInventoryItems.map((i: any) => i.id);
|
|
||||||
|
|
||||||
return entries
|
|
||||||
.filter((entry: any) => (
|
|
||||||
(nonInventoryItemsIds.indexOf(entry.item_id)) !== -1
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
static filterInventoryEntries(entries: [], items: []) {
|
|
||||||
const inventoryItems = items.filter((item: any) => item.type === 'inventory');
|
|
||||||
const inventoryItemsIds = inventoryItems.map((i: any) => i.id);
|
|
||||||
|
|
||||||
return entries
|
|
||||||
.filter((entry: any) => (
|
|
||||||
(inventoryItemsIds.indexOf(entry.item_id)) !== -1
|
|
||||||
));
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale invoices and store it to the storage
|
* Creates a new sale invoices and store it to the storage
|
||||||
* with associated to entries and journal transactions.
|
* with associated to entries and journal transactions.
|
||||||
@@ -66,81 +45,36 @@ export default class SaleInvoicesService {
|
|||||||
saleInvoice.entries.forEach((entry: any) => {
|
saleInvoice.entries.forEach((entry: any) => {
|
||||||
const oper = ItemEntry.tenant()
|
const oper = ItemEntry.tenant()
|
||||||
.query()
|
.query()
|
||||||
.insert({
|
.insertAndFetch({
|
||||||
reference_type: 'SaleInvoice',
|
reference_type: 'SaleInvoice',
|
||||||
reference_id: storedInvoice.id,
|
reference_id: storedInvoice.id,
|
||||||
...omit(entry, ['amount', 'id']),
|
...omit(entry, ['amount', 'id']),
|
||||||
|
}).then((itemEntry) => {
|
||||||
|
entry.id = itemEntry.id;
|
||||||
});
|
});
|
||||||
opers.push(oper);
|
opers.push(oper);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Increment the customer balance after deliver the sale invoice.
|
// Increment the customer balance after deliver the sale invoice.
|
||||||
const incrementOper = Customer.incrementBalance(
|
const incrementOper = Customer.incrementBalance(
|
||||||
saleInvoice.customer_id,
|
saleInvoice.customer_id,
|
||||||
balance,
|
balance,
|
||||||
);
|
);
|
||||||
// Records the inventory transactions for inventory items.
|
|
||||||
const recordInventoryTransOpers = this.recordInventoryTranscactions(
|
|
||||||
saleInvoice, storedInvoice.id
|
|
||||||
);
|
|
||||||
// Records the non-inventory transactions of the entries items.
|
|
||||||
const recordNonInventoryJEntries = this.recordNonInventoryEntries(
|
|
||||||
saleInvoice, storedInvoice.id,
|
|
||||||
);
|
|
||||||
// Await all async operations.
|
// Await all async operations.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...opers,
|
...opers, incrementOper,
|
||||||
incrementOper,
|
|
||||||
recordNonInventoryJEntries,
|
|
||||||
recordInventoryTransOpers,
|
|
||||||
]);
|
]);
|
||||||
|
// Records the inventory transactions for inventory items.
|
||||||
|
await this.recordInventoryTranscactions(
|
||||||
|
saleInvoice, storedInvoice.id
|
||||||
|
);
|
||||||
// Schedule sale invoice re-compute based on the item cost
|
// Schedule sale invoice re-compute based on the item cost
|
||||||
// method and starting date.
|
// method and starting date.
|
||||||
// await this.scheduleComputeItemsCost(saleInvoice);
|
await this.scheduleComputeInvoiceItemsCost(saleInvoice);
|
||||||
|
|
||||||
return storedInvoice;
|
return storedInvoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Records the journal entries for non-inventory entries.
|
|
||||||
* @param {SaleInvoice} saleInvoice
|
|
||||||
*/
|
|
||||||
static async recordNonInventoryEntries(saleInvoice: any, saleInvoiceId: number) {
|
|
||||||
const saleInvoiceItems = saleInvoice.entries.map((entry: any) => entry.item_id);
|
|
||||||
|
|
||||||
// Retrieves items data to detarmines whether the item type.
|
|
||||||
const itemsMeta = await Item.tenant().query().whereIn('id', saleInvoiceItems);
|
|
||||||
const storedItemsMap = new Map(itemsMeta.map((item) => [item.id, item]));
|
|
||||||
|
|
||||||
// Filters the non-inventory and inventory entries based on the item type.
|
|
||||||
const nonInventoryEntries: any[] = this.filterNonInventoryEntries(saleInvoice.entries, itemsMeta);
|
|
||||||
|
|
||||||
const transactions: any = [];
|
|
||||||
const common = {
|
|
||||||
referenceType: 'SaleInvoice',
|
|
||||||
referenceId: saleInvoiceId,
|
|
||||||
date: saleInvoice.invoice_date,
|
|
||||||
};
|
|
||||||
nonInventoryEntries.forEach((entry) => {
|
|
||||||
const item = storedItemsMap.get(entry.item_id);
|
|
||||||
|
|
||||||
transactions.push({
|
|
||||||
...common,
|
|
||||||
income: entry.amount,
|
|
||||||
incomeAccountId: item.incomeAccountId,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
|
||||||
const journal = new JournalPoster(accountsDepGraph);
|
|
||||||
const journalCommands = new JournalCommands(journal);
|
|
||||||
|
|
||||||
journalCommands.nonInventoryEntries(transactions);
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
journal.deleteEntries(),
|
|
||||||
journal.saveEntries(),
|
|
||||||
journal.saveBalance(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit the given sale invoice.
|
* Edit the given sale invoice.
|
||||||
* @async
|
* @async
|
||||||
@@ -193,7 +127,7 @@ export default class SaleInvoicesService {
|
|||||||
|
|
||||||
// Schedule sale invoice re-compute based on the item cost
|
// Schedule sale invoice re-compute based on the item cost
|
||||||
// method and starting date.
|
// method and starting date.
|
||||||
await this.scheduleComputeItemsCost(saleInvoice);
|
await this.scheduleComputeInvoiceItemsCost(saleInvoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,12 +194,13 @@ export default class SaleInvoicesService {
|
|||||||
static recordInventoryTranscactions(saleInvoice, saleInvoiceId: number, override?: boolean){
|
static recordInventoryTranscactions(saleInvoice, saleInvoiceId: number, override?: boolean){
|
||||||
const inventortyTransactions = saleInvoice.entries
|
const inventortyTransactions = saleInvoice.entries
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
...pick(entry, ['item_id', 'quantity', 'rate']),
|
...pick(entry, ['item_id', 'quantity', 'rate',]),
|
||||||
lotNumber: saleInvoice.invLotNumber,
|
lotNumber: saleInvoice.invLotNumber,
|
||||||
transactionType: 'SaleInvoice',
|
transactionType: 'SaleInvoice',
|
||||||
transactionId: saleInvoiceId,
|
transactionId: saleInvoiceId,
|
||||||
direction: 'OUT',
|
direction: 'OUT',
|
||||||
date: saleInvoice.invoice_date,
|
date: saleInvoice.invoice_date,
|
||||||
|
entryId: entry.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return InventoryService.recordInventoryTransactions(
|
return InventoryService.recordInventoryTransactions(
|
||||||
@@ -273,27 +208,6 @@ export default class SaleInvoicesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule sale invoice re-compute based on the item
|
|
||||||
* cost method and starting date
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {SaleInvoice} saleInvoice -
|
|
||||||
* @return {Promise<Agenda>}
|
|
||||||
*/
|
|
||||||
private static scheduleComputeItemsCost(saleInvoice: any) {
|
|
||||||
const asyncOpers: Promise<[]>[] = [];
|
|
||||||
|
|
||||||
saleInvoice.entries.forEach((entry: any) => {
|
|
||||||
const oper: Promise<[]> = InventoryService.scheduleComputeItemCost(
|
|
||||||
entry.item_id || entry.itemId,
|
|
||||||
saleInvoice.bill_date || saleInvoice.billDate,
|
|
||||||
);
|
|
||||||
asyncOpers.push(oper);
|
|
||||||
});
|
|
||||||
return Promise.all(asyncOpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the inventory transactions.
|
* Deletes the inventory transactions.
|
||||||
* @param {string} transactionType
|
* @param {string} transactionType
|
||||||
@@ -392,4 +306,17 @@ export default class SaleInvoicesService {
|
|||||||
const notStoredInvoices = difference(invoicesIds, storedInvoicesIds);
|
const notStoredInvoices = difference(invoicesIds, storedInvoicesIds);
|
||||||
return notStoredInvoices;
|
return notStoredInvoices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules compute sale invoice items cost based on each item
|
||||||
|
* cost method.
|
||||||
|
* @param {ISaleInvoice} saleInvoice
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static scheduleComputeInvoiceItemsCost(saleInvoice) {
|
||||||
|
return this.scheduleComputeItemsCost(
|
||||||
|
saleInvoice.entries.map((e) => e.item_id),
|
||||||
|
saleInvoice.invoice_date,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
server/src/services/Sales/SalesInvoicesCost.ts
Normal file
145
server/src/services/Sales/SalesInvoicesCost.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import {
|
||||||
|
SaleInvoice,
|
||||||
|
Account,
|
||||||
|
AccountTransaction,
|
||||||
|
Item,
|
||||||
|
} from '@/models';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||||
|
import InventoryService from '@/services/Inventory/Inventory';
|
||||||
|
import { ISaleInvoice, IItemEntry, IItem } from '@/interfaces';
|
||||||
|
|
||||||
|
export default class SaleInvoicesCost {
|
||||||
|
/**
|
||||||
|
* Schedule sale invoice re-compute based on the item
|
||||||
|
* cost method and starting date.
|
||||||
|
* @param {number[]} itemIds -
|
||||||
|
* @param {Date} startingDate -
|
||||||
|
* @return {Promise<Agenda>}
|
||||||
|
*/
|
||||||
|
static async scheduleComputeItemsCost(itemIds: number[], startingDate: Date) {
|
||||||
|
const items: IItem[] = await Item.tenant().query().whereIn('id', itemIds);
|
||||||
|
|
||||||
|
const inventoryItems: IItem[] = items.filter((item: IItem) => item.type === 'inventory');
|
||||||
|
const asyncOpers: Promise<[]>[] = [];
|
||||||
|
|
||||||
|
inventoryItems.forEach((item: IItem) => {
|
||||||
|
const oper: Promise<[]> = InventoryService.scheduleComputeItemCost(
|
||||||
|
item.id,
|
||||||
|
startingDate,
|
||||||
|
);
|
||||||
|
asyncOpers.push(oper);
|
||||||
|
});
|
||||||
|
const writeJEntriesOper: Promise<any> = this.scheduleWriteJournalEntries(startingDate);
|
||||||
|
|
||||||
|
return Promise.all([...asyncOpers, writeJEntriesOper]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule writing journal entries.
|
||||||
|
* @param {Date} startingDate
|
||||||
|
* @return {Promise<agenda>}
|
||||||
|
*/
|
||||||
|
static scheduleWriteJournalEntries(startingDate?: Date) {
|
||||||
|
const agenda = Container.get('agenda');
|
||||||
|
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
|
||||||
|
startingDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes journal entries from sales invoices.
|
||||||
|
* @param {Date} startingDate
|
||||||
|
* @param {boolean} override
|
||||||
|
*/
|
||||||
|
static async writeJournalEntries(startingDate: Date, override: boolean) {
|
||||||
|
const salesInvoices = await SaleInvoice.tenant()
|
||||||
|
.query()
|
||||||
|
.onBuild((builder: any) => {
|
||||||
|
builder.modify('filterDateRange', startingDate);
|
||||||
|
builder.orderBy('invoice_date', 'ASC');
|
||||||
|
|
||||||
|
builder.withGraphFetched('entries.item')
|
||||||
|
builder.withGraphFetched('costTransactions(groupedEntriesCost)');
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||||
|
const journal = new JournalPoster(accountsDepGraph);
|
||||||
|
|
||||||
|
if (override) {
|
||||||
|
const oldTransactions = await AccountTransaction.tenant()
|
||||||
|
.query()
|
||||||
|
.whereIn('reference_type', ['SaleInvoice'])
|
||||||
|
.onBuild((builder: any) => {
|
||||||
|
builder.modify('filterDateRange', startingDate);
|
||||||
|
})
|
||||||
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
|
journal.loadEntries(oldTransactions);
|
||||||
|
journal.removeEntries();
|
||||||
|
}
|
||||||
|
const receivableAccount = { id: 10 };
|
||||||
|
|
||||||
|
salesInvoices.forEach((saleInvoice: ISaleInvoice) => {
|
||||||
|
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: IItemEntry) => [
|
||||||
|
trans.entryId, trans.cost,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
// XXX Debit - Receivable account.
|
||||||
|
const receivableEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
debit: saleInvoice.balance,
|
||||||
|
account: receivableAccount.id,
|
||||||
|
});
|
||||||
|
journal.debit(receivableEntry);
|
||||||
|
|
||||||
|
saleInvoice.entries.forEach((entry: IItemEntry) => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
// XXX Credit - Inventory account.
|
||||||
|
const inventoryEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
credit: inventoryTotal,
|
||||||
|
account: entry.item.inventoryAccountId,
|
||||||
|
});
|
||||||
|
journal.credit(inventoryEntry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.all([
|
||||||
|
journal.deleteEntries(),
|
||||||
|
journal.saveEntries(),
|
||||||
|
journal.saveBalance(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user