mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
- fix: Schedule write journal entries after item compute cost.
- fix: active vouchers query. - fix: remove babel loader in server-side.
This commit is contained in:
@@ -79,7 +79,6 @@ export default class ItemsController {
|
||||
check('type').exists().trim().escape()
|
||||
.isIn(['service', 'non-inventory', 'inventory']),
|
||||
check('sku').optional({ nullable: true }).trim().escape(),
|
||||
|
||||
// Purchase attributes.
|
||||
check('purchasable').optional().isBoolean().toBoolean(),
|
||||
check('cost_price')
|
||||
@@ -92,7 +91,6 @@ export default class ItemsController {
|
||||
.exists()
|
||||
.isInt()
|
||||
.toInt(),
|
||||
|
||||
// Sell attributes.
|
||||
check('sellable').optional().isBoolean().toBoolean(),
|
||||
check('sell_price')
|
||||
@@ -105,7 +103,6 @@ export default class ItemsController {
|
||||
.exists()
|
||||
.isInt()
|
||||
.toInt(),
|
||||
|
||||
check('inventory_account_id')
|
||||
.if(check('type').equals('inventory'))
|
||||
.exists()
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
|
||||
|
||||
export interface ISaleInvoice {
|
||||
id: number,
|
||||
balance: number,
|
||||
paymentAmount: number,
|
||||
invoiceDate: Date,
|
||||
entries: [],
|
||||
dueDate: Date,
|
||||
entries: any[],
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceOTD {
|
||||
invoiceDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
invoiceMessage: string,
|
||||
termsConditions: string,
|
||||
entries: any[],
|
||||
}
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
IVoucherPaymentMethod,
|
||||
IPaymentContext,
|
||||
} from './Payment';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceOTD,
|
||||
} from './SaleInvoice';
|
||||
|
||||
export {
|
||||
IBillPaymentEntry,
|
||||
@@ -31,4 +35,7 @@ export {
|
||||
IPaymentContext,
|
||||
IVoucherPaymentModel,
|
||||
IVoucherPaymentMethod,
|
||||
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceOTD,
|
||||
};
|
||||
@@ -1,7 +1,26 @@
|
||||
import { Container } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||
|
||||
export default class ComputeItemCostJob {
|
||||
depends: number;
|
||||
agenda: any;
|
||||
startingDate: Date;
|
||||
|
||||
constructor(agenda) {
|
||||
this.agenda = agenda;
|
||||
this.depends = 0;
|
||||
this.startingDate = null;
|
||||
|
||||
this.agenda.on('complete:compute-item-cost', this.onJobFinished.bind(this));
|
||||
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* The job handler.
|
||||
* @param {} -
|
||||
*/
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const Logger = Container.get('logger');
|
||||
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
|
||||
@@ -17,6 +36,38 @@ export default class ComputeItemCostJob {
|
||||
Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the job started.
|
||||
* @param {Job} job - .
|
||||
*/
|
||||
async onJobStart(job) {
|
||||
const { startingDate } = job.attrs.data;
|
||||
this.depends += 1;
|
||||
|
||||
if (!this.startingDate || moment(this.startingDate).isBefore(startingDate)) {
|
||||
this.startingDate = startingDate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle job complete items cost finished.
|
||||
* @param {Job} job -
|
||||
*/
|
||||
async onJobFinished() {
|
||||
const agenda = Container.get('agenda');
|
||||
const startingDate = this.startingDate;
|
||||
this.depends = Math.max(this.depends - 1, 0);
|
||||
|
||||
console.log(startingDate);
|
||||
|
||||
if (this.depends === 0) {
|
||||
this.startingDate = null;
|
||||
|
||||
await agenda.now('rewrite-invoices-journal-entries', {
|
||||
startingDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
agenda.define(
|
||||
'compute-item-cost',
|
||||
{ priority: 'high', concurrency: 20 },
|
||||
new ComputeItemCost().handler,
|
||||
new ComputeItemCost(agenda).handler,
|
||||
);
|
||||
agenda.define(
|
||||
'rewrite-invoices-journal-entries',
|
||||
{ priority: 'normal', concurrency: 1, },
|
||||
new RewriteInvoicesJournalEntries().handler,
|
||||
new RewriteInvoicesJournalEntries(agenda).handler,
|
||||
);
|
||||
agenda.define(
|
||||
'send-voucher-via-phone',
|
||||
|
||||
@@ -84,8 +84,7 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme
|
||||
*/
|
||||
private async fetchInvINTransactions() {
|
||||
const commonBuilder = (builder: any) => {
|
||||
builder.where('direction', 'IN');
|
||||
builder.orderBy('date', 'ASC');
|
||||
builder.orderBy('date', (this.costMethod === 'LIFO') ? 'DESC': 'ASC');
|
||||
builder.where('item_id', this.itemId);
|
||||
};
|
||||
const afterInvTransactions: IInventoryTransaction[] =
|
||||
@@ -93,8 +92,8 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme
|
||||
.query()
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
||||
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
|
||||
.onBuild(commonBuilder)
|
||||
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
|
||||
.withGraphFetched('item');
|
||||
|
||||
const availiableINLots: IInventoryLotCost[] =
|
||||
@@ -102,7 +101,8 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme
|
||||
.query()
|
||||
.modify('filterDateRange', null, this.startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.orderBy('lot_number', 'ASC')
|
||||
.where('direction', 'IN')
|
||||
.orderBy('lot_number', 'ASC')
|
||||
.onBuild(commonBuilder)
|
||||
.whereNot('remaining', 0);
|
||||
|
||||
|
||||
@@ -348,10 +348,18 @@ export default class BillsService extends SalesInvoicesCost {
|
||||
* @param {IBill} bill
|
||||
* @return {Promise}
|
||||
*/
|
||||
static scheduleComputeBillItemsCost(bill) {
|
||||
return this.scheduleComputeItemsCost(
|
||||
bill.entries.map((e) => e.item_id),
|
||||
bill.bill_date,
|
||||
);
|
||||
static async scheduleComputeBillItemsCost(bill) {
|
||||
const billItemsIds = bill.entries.map((entry) => entry.item_id);
|
||||
|
||||
// Retrieves inventory items only.
|
||||
const inventoryItems = await Item.tenant().query()
|
||||
.whereIn('id', billItemsIds)
|
||||
.where('type', 'inventory');
|
||||
|
||||
const inventoryItemsIds = inventoryItems.map(i => i.id);
|
||||
|
||||
if (inventoryItemsIds.length > 0) {
|
||||
await this.scheduleComputeItemsCost(inventoryItemsIds, bill.bill_date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { omit, sumBy, difference, pick } from 'lodash';
|
||||
import { omit, sumBy, difference, pick, chain } from 'lodash';
|
||||
import {
|
||||
SaleInvoice,
|
||||
AccountTransaction,
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
Account,
|
||||
ItemEntry,
|
||||
Customer,
|
||||
Item,
|
||||
} from '@/models';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
||||
import CustomerRepository from '@/repositories/CustomerRepository';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
|
||||
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from '@/interfaces';
|
||||
import { formatDateFields } from '@/utils';
|
||||
|
||||
/**
|
||||
@@ -26,11 +28,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {ISaleInvoice}
|
||||
* @return {ISaleInvoice}
|
||||
*/
|
||||
static async createSaleInvoice(saleInvoiceDTO: any) {
|
||||
static async createSaleInvoice(saleInvoiceDTO: ISaleInvoiceOTD) {
|
||||
const balance = sumBy(saleInvoiceDTO.entries, 'amount');
|
||||
const invLotNumber = await InventoryService.nextLotNumber();
|
||||
const saleInvoice = {
|
||||
...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']),
|
||||
const saleInvoice: ISaleInvoice = {
|
||||
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']),
|
||||
balance,
|
||||
paymentAmount: 0,
|
||||
invLotNumber,
|
||||
@@ -70,7 +72,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
);
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeInvoiceItemsCost(saleInvoice);
|
||||
await this.scheduleComputeInvoiceItemsCost(storedInvoice.id);
|
||||
|
||||
return storedInvoice;
|
||||
}
|
||||
@@ -92,7 +94,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
balance,
|
||||
invLotNumber: oldSaleInvoice.invLotNumber,
|
||||
};
|
||||
const updatedSaleInvoices = await SaleInvoice.tenant()
|
||||
const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.tenant()
|
||||
.query()
|
||||
.where('id', saleInvoiceId)
|
||||
.update({
|
||||
@@ -124,10 +126,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
changeCustomerBalanceOper,
|
||||
recordInventoryTransOper,
|
||||
]);
|
||||
|
||||
// Schedule sale invoice re-compute based on the item cost
|
||||
// method and starting date.
|
||||
await this.scheduleComputeInvoiceItemsCost(saleInvoice);
|
||||
await this.scheduleComputeInvoiceItemsCost(saleInvoiceId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,10 +314,51 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @return {Promise}
|
||||
*/
|
||||
static scheduleComputeInvoiceItemsCost(saleInvoice) {
|
||||
return this.scheduleComputeItemsCost(
|
||||
saleInvoice.entries.map((e) => e.item_id),
|
||||
saleInvoice.invoice_date,
|
||||
);
|
||||
static async scheduleComputeInvoiceItemsCost(saleInvoiceId: number, override?: boolean) {
|
||||
const saleInvoice: ISaleInvoice = await SaleInvoice.tenant()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
const inventoryItemsIds = chain(saleInvoice.entries)
|
||||
.filter((entry: IItemEntry) => entry.item.type === 'inventory')
|
||||
.map((entry: IItemEntry) => entry.itemId)
|
||||
.uniq().value();
|
||||
|
||||
if (inventoryItemsIds.length === 0) {
|
||||
await this.writeNonInventoryInvoiceJournals(saleInvoice, override);
|
||||
} else {
|
||||
await this.scheduleComputeItemsCost(
|
||||
inventoryItemsIds,
|
||||
saleInvoice.invoice_date,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the sale invoice journal entries.
|
||||
* @param {SaleInvoice} saleInvoice -
|
||||
*/
|
||||
static async writeNonInventoryInvoiceJournals(saleInvoice: ISaleInvoice, override: boolean) {
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
if (override) {
|
||||
const oldTransactions = await AccountTransaction.tenant()
|
||||
.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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,31 +9,27 @@ import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { ISaleInvoice, IItemEntry, IItem } from '@/interfaces';
|
||||
import { ISaleInvoice } 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 -
|
||||
* @param {number[]} itemIds - Inventory items ids.
|
||||
* @param {Date} startingDate - Starting compute cost date.
|
||||
* @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');
|
||||
static async scheduleComputeItemsCost(inventoryItemsIds: number[], startingDate: Date) {
|
||||
const asyncOpers: Promise<[]>[] = [];
|
||||
|
||||
inventoryItems.forEach((item: IItem) => {
|
||||
inventoryItemsIds.forEach((inventoryItemId: number) => {
|
||||
const oper: Promise<[]> = InventoryService.scheduleComputeItemCost(
|
||||
item.id,
|
||||
inventoryItemId,
|
||||
startingDate,
|
||||
);
|
||||
asyncOpers.push(oper);
|
||||
});
|
||||
const writeJEntriesOper: Promise<any> = this.scheduleWriteJournalEntries(startingDate);
|
||||
|
||||
return Promise.all([...asyncOpers, writeJEntriesOper]);
|
||||
return Promise.all([...asyncOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +59,6 @@ export default class SaleInvoicesCost {
|
||||
builder.withGraphFetched('entries.item')
|
||||
builder.withGraphFetched('costTransactions(groupedEntriesCost)');
|
||||
});
|
||||
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
@@ -79,62 +74,9 @@ export default class SaleInvoicesCost {
|
||||
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);
|
||||
}
|
||||
});
|
||||
salesInvoices.forEach((saleInvoice: ISaleInvoice) => {
|
||||
this.saleInvoiceJournal(saleInvoice, journal);
|
||||
});
|
||||
return Promise.all([
|
||||
journal.deleteEntries(),
|
||||
@@ -142,4 +84,66 @@ export default class SaleInvoicesCost {
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes journal entries for given sale invoice.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {JournalPoster} journal
|
||||
*/
|
||||
static saleInvoiceJournal(saleInvoice: ISaleInvoice, journal: JournalPoster) {
|
||||
let inventoryTotal: number = 0;
|
||||
const receivableAccount = { id: 10 };
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ export default class Voucher extends mixin(SystemModel) {
|
||||
filterActiveVoucher(query) {
|
||||
query.where('disabled', false);
|
||||
query.where('used', false);
|
||||
query.where('sent', false);
|
||||
},
|
||||
|
||||
// Find voucher by its code or id.
|
||||
|
||||
Reference in New Issue
Block a user