fix(Journals): sync posting datetime with jorunal entries.

This commit is contained in:
a.bouhuolia
2021-03-29 10:50:44 +02:00
parent 40b2ba099e
commit 9a204282a2
38 changed files with 477 additions and 302 deletions

View File

@@ -106,7 +106,6 @@ export default class InventoryAdjustmentsController extends BaseController {
) { ) {
const { tenantId, user } = req; const { tenantId, user } = req;
const quickInventoryAdjustment = this.matchedBodyData(req); const quickInventoryAdjustment = this.matchedBodyData(req);
console.log(quickInventoryAdjustment);
try { try {
const inventoryAdjustment = await this.inventoryAdjustmentService.createQuickAdjustment( const inventoryAdjustment = await this.inventoryAdjustmentService.createQuickAdjustment(

View File

@@ -435,9 +435,7 @@ export default class SaleInvoicesController extends BaseController {
} }
if (error.errorType === 'SALE_INVOICE_NO_IS_REQUIRED') { if (error.errorType === 'SALE_INVOICE_NO_IS_REQUIRED') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [ errors: [{ type: 'SALE_INVOICE_NO_IS_REQUIRED', code: 1500 }],
{ type: 'SALE_INVOICE_NO_IS_REQUIRED', code: 1500 },
],
}); });
} }
} }

View File

@@ -15,9 +15,12 @@ exports.up = function(knex) {
table.integer('item_id').unsigned().nullable().index(); table.integer('item_id').unsigned().nullable().index();
table.string('note'); table.string('note');
table.integer('user_id').unsigned().index(); table.integer('user_id').unsigned().index();
table.integer('index').unsigned();
table.integer('index_group').unsigned().index();
table.integer('index').unsigned().index();
table.date('date').index(); table.date('date').index();
table.timestamps(); table.datetime('created_at').index();
}).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000'); }).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000');
}; };

View File

@@ -1,14 +1,23 @@
const { knexSnakeCaseMappers } = require("objection"); const { knexSnakeCaseMappers } = require('objection');
exports.up = function (knex) { exports.up = function (knex) {
return knex.schema.createTable('payment_receives', (table) => { return knex.schema.createTable('payment_receives', (table) => {
table.increments(); table.increments();
table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); table
.integer('customer_id')
.unsigned()
.index()
.references('id')
.inTable('contacts');
table.date('payment_date').index(); table.date('payment_date').index();
table.decimal('amount', 13, 3).defaultTo(0); table.decimal('amount', 13, 3).defaultTo(0);
table.string('currency_code', 3); table.string('currency_code', 3);
table.string('reference_no').index(); table.string('reference_no').index();
table.integer('deposit_account_id').unsigned().references('id').inTable('accounts'); table
.integer('deposit_account_id')
.unsigned()
.references('id')
.inTable('accounts');
table.string('payment_receive_no').nullable(); table.string('payment_receive_no').nullable();
table.text('statement'); table.text('statement');
table.integer('user_id').unsigned().index(); table.integer('user_id').unsigned().index();

View File

@@ -1,18 +1,17 @@
exports.up = function (knex) { exports.up = function (knex) {
return knex.schema.createTable('inventory_transactions', table => { return knex.schema.createTable('inventory_transactions', (table) => {
table.increments('id'); table.increments('id');
table.date('date').index(); table.date('date').index();
table.string('direction').index(); table.string('direction').index();
table
table.integer('item_id').unsigned().index().references('id').inTable('items'); .integer('item_id')
.unsigned()
.index()
.references('id')
.inTable('items');
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.integer('cost_account_id').unsigned().index().references('id').inTable('accounts');
table.string('transaction_type').index(); table.string('transaction_type').index();
table.integer('transaction_id').unsigned().index(); table.integer('transaction_id').unsigned().index();
@@ -21,6 +20,4 @@ exports.up = function(knex) {
}); });
}; };
exports.down = function(knex) { exports.down = function (knex) {};
};

View File

@@ -11,6 +11,10 @@ exports.up = function(knex) {
table.integer('discount').unsigned(); table.integer('discount').unsigned();
table.integer('quantity').unsigned(); table.integer('quantity').unsigned();
table.integer('rate').unsigned(); table.integer('rate').unsigned();
table.integer('sell_account_id').unsigned().references('id').inTable('accounts');
table.integer('cost_account_id').unsigned().references('id').inTable('accounts');
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -1,6 +1,5 @@
exports.up = function (knex) { 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').index(); table.date('date').index();
table.string('direction').index(); table.string('direction').index();
@@ -10,11 +9,12 @@ exports.up = function(knex) {
table.decimal('rate', 13, 3); table.decimal('rate', 13, 3);
table.integer('remaining'); table.integer('remaining');
table.decimal('cost', 13, 3); table.decimal('cost', 13, 3);
table.integer('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();
table.integer('entry_id').unsigned().index(); table.integer('entry_id').unsigned().index();
table.datetime('created_at').index();
}); });
}; };

View File

@@ -46,11 +46,13 @@ export interface IBill {
dueAmount: number, dueAmount: number,
overdueDays: number, overdueDays: number,
invLotNumber: string,
openedAt: Date | string, openedAt: Date | string,
entries: IItemEntry[], entries: IItemEntry[],
userId: number, userId: number,
createdAt: Date,
updateAt: Date,
}; };
export interface IBillsFilter extends IDynamicListFilterDTO { export interface IBillsFilter extends IDynamicListFilterDTO {

View File

@@ -1,3 +1,4 @@
import { LongDateFormatKey } from "moment";
export interface IBillPaymentEntry { export interface IBillPaymentEntry {
@@ -18,6 +19,8 @@ export interface IBillPayment {
userId: number, userId: number,
entries: IBillPaymentEntry[], entries: IBillPaymentEntry[],
statement: string, statement: string,
createdAt: Date,
updatedAt: Date,
} }
export interface IBillPaymentEntryDTO { export interface IBillPaymentEntryDTO {

View File

@@ -24,6 +24,7 @@ export interface IInventoryAdjustment {
entries: IInventoryAdjustmentEntry[]; entries: IInventoryAdjustmentEntry[];
userId: number; userId: number;
publishedAt?: Date|null; publishedAt?: Date|null;
createdAt?: Date,
} }
export interface IInventoryAdjustmentEntry { export interface IInventoryAdjustmentEntry {

View File

@@ -10,7 +10,6 @@ export interface IInventoryTransaction {
rate: number, rate: number,
transactionType: string, transactionType: string,
transactionId: number, transactionId: number,
lotNumber: number,
entryId: number, entryId: number,
createdAt?: Date, createdAt?: Date,
updatedAt?: Date, updatedAt?: Date,
@@ -25,10 +24,12 @@ export interface IInventoryLotCost {
rate: number, rate: number,
remaining: number, remaining: number,
cost: number, cost: number,
lotNumber: number,
transactionType: string, transactionType: string,
transactionId: number, transactionId: number,
entryId: number costAccountId: number,
sellAccountId: number,
entryId: number,
createdAt: Date,
}; };
export interface IItemsQuantityChanges { export interface IItemsQuantityChanges {

View File

@@ -14,6 +14,9 @@ export interface IItemEntry {
discount: number, discount: number,
quantity: number, quantity: number,
rate: number, rate: number,
sellAccountId: number,
costAccountId: number,
} }
export interface IItemEntryDTO { export interface IItemEntryDTO {

View File

@@ -13,6 +13,8 @@ export interface IManualJournal {
description: string; description: string;
userId: number; userId: number;
entries: IManualJournalEntry[]; entries: IManualJournalEntry[];
createdAt: Date;
updatedAt: Date;
} }
export interface IManualJournalEntry { export interface IManualJournalEntry {

View File

@@ -11,6 +11,8 @@ export interface IPaymentReceive {
statement: string; statement: string;
entries: IPaymentReceiveEntry[]; entries: IPaymentReceiveEntry[];
userId: number; userId: number;
createdAt: Date,
updatedAt: Date,
} }
export interface IPaymentReceiveCreateDTO { export interface IPaymentReceiveCreateDTO {
customerId: number; customerId: number;

View File

@@ -16,6 +16,7 @@ export interface ISaleInvoice {
entries: IItemEntry[]; entries: IItemEntry[];
deliveredAt: string | Date; deliveredAt: string | Date;
userId: number; userId: number;
createdAt: Date,
} }
export interface ISaleInvoiceDTO { export interface ISaleInvoiceDTO {

View File

@@ -14,6 +14,8 @@ export interface ISaleReceipt {
statement: string; statement: string;
closedAt: Date | string; closedAt: Date | string;
entries: any[]; entries: any[];
createdAt: Date,
updatedAt: Date,
} }
export interface ISalesReceiptsFilter {} export interface ISalesReceiptsFilter {}

View File

@@ -21,6 +21,9 @@ export default class WriteInvoicesJournalEntries {
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this)); agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));
} }
/**
* Handle the job execuation.
*/
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, tenantId } = job.attrs.data; const { startingDate, tenantId } = job.attrs.data;

View File

@@ -112,12 +112,10 @@ export default class AccountTransaction extends TenantModel {
filterContactIds(query, contactIds) { filterContactIds(query, contactIds) {
query.whereIn('contact_id', contactIds); query.whereIn('contact_id', contactIds);
}, },
openingBalance(query, fromDate) { openingBalance(query, fromDate) {
query.modify('filterDateRange', null, fromDate) query.modify('filterDateRange', null, fromDate)
query.modify('sumationCreditDebit') query.modify('sumationCreditDebit')
}, },
closingBalance(query, toDate) { closingBalance(query, toDate) {
query.modify('filterDateRange', null, toDate) query.modify('filterDateRange', null, toDate)
query.modify('sumationCreditDebit') query.modify('sumationCreditDebit')

View File

@@ -11,8 +11,10 @@ export default (Model) => {
const maybePromise = super.$beforeUpdate(opt, context); const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => { return Promise.resolve(maybePromise).then(() => {
if (this.timestamps[1]) { const key = this.timestamps[1];
this[this.timestamps[1]] = moment().format('YYYY/MM/DD HH:mm:ss');
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
} }
}); });
} }
@@ -21,8 +23,10 @@ export default (Model) => {
const maybePromise = super.$beforeInsert(context); const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => { return Promise.resolve(maybePromise).then(() => {
if (this.timestamps[0]) { const key = this.timestamps[0];
this[this.timestamps[0]] = moment().format('YYYY/MM/DD HH:mm:ss');
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
} }
}); });
} }

View File

@@ -46,12 +46,13 @@ export default class InventoryCostLotTracker extends TenantModel {
}; };
} }
/** /**
* Relationship mapping. * Relationship mapping.
*/ */
static get relationMappings() { static get relationMappings() {
const Item = require('models/Item'); const Item = require('models/Item');
const SaleInvoice = require('models/SaleInvoice');
const ItemEntry = require('models/ItemEntry');
return { return {
item: { item: {
@@ -62,6 +63,25 @@ export default class InventoryCostLotTracker extends TenantModel {
to: 'items.id', to: 'items.id',
}, },
}, },
invoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'inventory_cost_lot_tracker.transactionId',
to: 'sales_invoices.id',
},
filter(query) {
query.where('transaction_type', 'SaleInvoice');
},
},
itemEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'inventory_cost_lot_tracker.entryId',
to: 'items_entries.id',
},
}
}; };
} }
} }

View File

@@ -42,6 +42,7 @@ export default class InventoryTransaction extends TenantModel {
*/ */
static get relationMappings() { static get relationMappings() {
const Item = require('models/Item'); const Item = require('models/Item');
const ItemEntry = require('models/ItemEntry');
return { return {
item: { item: {
@@ -52,6 +53,14 @@ export default class InventoryTransaction extends TenantModel {
to: 'items.id', to: 'items.id',
}, },
}, },
itemEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'inventory_transactions.entryId',
to: 'items_entries.id',
},
}
}; };
} }
} }

View File

@@ -13,7 +13,8 @@ interface IJournalTransactionsFilter {
contactType?: string, contactType?: string,
referenceType?: string[], referenceType?: string[],
referenceId?: number[], referenceId?: number[],
index: number|number[] index: number|number[],
indexGroup: number|number[],
}; };
export default class AccountTransactionsRepository extends TenantRepository { export default class AccountTransactionsRepository extends TenantRepository {
@@ -58,6 +59,13 @@ export default class AccountTransactionsRepository extends TenantRepository {
query.where('index', filter.index); query.where('index', filter.index);
} }
} }
if (filter.indexGroup) {
if (Array.isArray(filter.indexGroup)) {
query.whereIn('index_group', filter.indexGroup);
} else {
query.where('index_group', filter.indexGroup);
}
}
}); });
}); });
} }

View File

@@ -1,11 +1,13 @@
import { sumBy, chain } from 'lodash'; import moment from 'moment';
import moment, { LongDateFormatKey } from 'moment'; import {
import { IBill, IManualJournalEntry, ISaleReceipt, ISystemUser } from 'interfaces'; IBill,
IManualJournalEntry,
ISaleReceipt,
ISystemUser,
} from 'interfaces';
import JournalPoster from './JournalPoster'; import JournalPoster from './JournalPoster';
import JournalEntry from './JournalEntry'; import JournalEntry from './JournalEntry';
import { AccountTransaction } from 'models';
import { import {
IInventoryTransaction,
IManualJournal, IManualJournal,
IExpense, IExpense,
IExpenseCategory, IExpenseCategory,
@@ -14,6 +16,7 @@ import {
IInventoryLotCost, IInventoryLotCost,
IItemEntry, IItemEntry,
} from 'interfaces'; } from 'interfaces';
import { increment } from 'utils';
export default class JournalCommands { export default class JournalCommands {
journal: JournalPoster; journal: JournalPoster;
@@ -61,6 +64,8 @@ export default class JournalCommands {
referenceNumber: bill.referenceNo, referenceNumber: bill.referenceNo,
transactionNumber: bill.billNumber, transactionNumber: bill.billNumber,
createdAt: bill.createdAt,
}; };
// Overrides the old bill entries. // Overrides the old bill entries.
if (override) { if (override) {
@@ -90,9 +95,9 @@ export default class JournalCommands {
account: account:
['inventory'].indexOf(item.type) !== -1 ['inventory'].indexOf(item.type) !== -1
? item.inventoryAccountId ? item.inventoryAccountId
: item.costAccountId, : entry.costAccountId,
index: index + 2, index: index + 2,
itemId: entry.itemId itemId: entry.itemId,
}); });
this.journal.debit(debitEntry); this.journal.debit(debitEntry);
}); });
@@ -257,7 +262,7 @@ export default class JournalCommands {
const transactions = await transactionsRepository.journal({ const transactions = await transactionsRepository.journal({
fromDate: startingDate, fromDate: startingDate,
referenceType: ['SaleInvoice'], referenceType: ['SaleInvoice'],
index: [3, 4], indexGroup: 20
}); });
this.journal.fromTransactions(transactions); this.journal.fromTransactions(transactions);
this.journal.removeEntries(); this.journal.removeEntries();
@@ -267,9 +272,7 @@ export default class JournalCommands {
* Reverts sale invoice the income journal entries. * Reverts sale invoice the income journal entries.
* @param {number} saleInvoiceId * @param {number} saleInvoiceId
*/ */
async revertInvoiceIncomeEntries( async revertInvoiceIncomeEntries(saleInvoiceId: number) {
saleInvoiceId: number,
) {
const { transactionsRepository } = this.repositories; const { transactionsRepository } = this.repositories;
const transactions = await transactionsRepository.journal({ const transactions = await transactionsRepository.journal({
@@ -289,6 +292,7 @@ export default class JournalCommands {
const commonEntry = { const commonEntry = {
transaction_number: manualJournalObj.journalNumber, transaction_number: manualJournalObj.journalNumber,
reference_number: manualJournalObj.reference, reference_number: manualJournalObj.reference,
createdAt: manualJournalObj.createdAt,
}; };
manualJournalObj.entries.forEach((entry: IManualJournalEntry) => { manualJournalObj.entries.forEach((entry: IManualJournalEntry) => {
const jouranlEntry = new JournalEntry({ const jouranlEntry = new JournalEntry({
@@ -317,37 +321,51 @@ export default class JournalCommands {
* ------- * -------
* - Cost of goods sold -> Debit -> YYYY * - Cost of goods sold -> Debit -> YYYY
* - Inventory assets -> Credit -> YYYY * - Inventory assets -> Credit -> YYYY
* * --------
* @param {ISaleInvoice} saleInvoice * @param {ISaleInvoice} saleInvoice
* @param {JournalPoster} journal * @param {JournalPoster} journal
*/ */
saleInvoiceInventoryCost( saleInvoiceInventoryCost(
inventoryCostLot: IInventoryLotCost & { item: IItem } inventoryCostLots: IInventoryLotCost &
{ item: IItem; itemEntry: IItemEntry }[]
) { ) {
const getIndexIncrement = increment(0);
inventoryCostLots.forEach(
(
inventoryCostLot: IInventoryLotCost & {
item: IItem;
itemEntry: IItemEntry;
}
) => {
const commonEntry = { const commonEntry = {
referenceType: inventoryCostLot.transactionType, referenceType: inventoryCostLot.transactionType,
referenceId: inventoryCostLot.transactionId, referenceId: inventoryCostLot.transactionId,
date: inventoryCostLot.date, date: inventoryCostLot.date,
indexGroup: 20,
createdAt: inventoryCostLot.createdAt,
}; };
// XXX Debit - Cost account. // XXX Debit - Cost account.
const costEntry = new JournalEntry({ const costEntry = new JournalEntry({
...commonEntry, ...commonEntry,
debit: inventoryCostLot.cost, debit: inventoryCostLot.cost,
account: inventoryCostLot.item.costAccountId, account: inventoryCostLot.itemEntry.costAccountId,
index: 3, itemId: inventoryCostLot.itemId,
itemId: inventoryCostLot.itemId index: getIndexIncrement(),
}); });
// XXX Credit - Inventory account. // XXX Credit - Inventory account.
const inventoryEntry = new JournalEntry({ const inventoryEntry = new JournalEntry({
...commonEntry, ...commonEntry,
credit: inventoryCostLot.cost, credit: inventoryCostLot.cost,
account: inventoryCostLot.item.inventoryAccountId, account: inventoryCostLot.item.inventoryAccountId,
index: 4, itemId: inventoryCostLot.itemId,
itemId: inventoryCostLot.itemId index: getIndexIncrement(),
}); });
this.journal.credit(inventoryEntry); this.journal.credit(inventoryEntry);
this.journal.debit(costEntry); this.journal.debit(costEntry);
} }
);
}
/** /**
* Writes the sale invoice income journal entries. * Writes the sale invoice income journal entries.
@@ -373,6 +391,9 @@ export default class JournalCommands {
transactionNumber: saleInvoice.invoiceNo, transactionNumber: saleInvoice.invoiceNo,
referenceNumber: saleInvoice.referenceNo, referenceNumber: saleInvoice.referenceNo,
createdAt: saleInvoice.createdAt,
indexGroup: 10,
}; };
// XXX Debit - Receivable account. // XXX Debit - Receivable account.
const receivableEntry = new JournalEntry({ const receivableEntry = new JournalEntry({
@@ -384,22 +405,20 @@ export default class JournalCommands {
}); });
this.journal.debit(receivableEntry); this.journal.debit(receivableEntry);
saleInvoice.entries.forEach( saleInvoice.entries.forEach((entry: IItemEntry, index: number) => {
(entry: IItemEntry & { item: IItem }, index: number) => {
const income: number = entry.quantity * entry.rate; const income: number = entry.quantity * entry.rate;
// XXX Credit - Income account. // XXX Credit - Income account.
const incomeEntry = new JournalEntry({ const incomeEntry = new JournalEntry({
...commonEntry, ...commonEntry,
credit: income, credit: income,
account: entry.item.sellAccountId, account: entry.sellAccountId,
note: entry.description, note: entry.description,
index: index + 2, index: index + 2,
itemId: entry.itemId, itemId: entry.itemId,
}); });
this.journal.credit(incomeEntry); this.journal.credit(incomeEntry);
} });
);
} }
/** /**
@@ -415,7 +434,7 @@ export default class JournalCommands {
async saleReceiptIncomeEntries( async saleReceiptIncomeEntries(
saleReceipt: ISaleReceipt & { saleReceipt: ISaleReceipt & {
entries: IItemEntry & { item: IItem }; entries: IItemEntry & { item: IItem };
}, }
): Promise<void> { ): Promise<void> {
const commonEntry = { const commonEntry = {
referenceType: 'SaleReceipt', referenceType: 'SaleReceipt',
@@ -424,6 +443,7 @@ export default class JournalCommands {
userId: saleReceipt.userId, userId: saleReceipt.userId,
transactionNumber: saleReceipt.receiptNumber, transactionNumber: saleReceipt.receiptNumber,
referenceNumber: saleReceipt.referenceNo, referenceNumber: saleReceipt.referenceNo,
createdAt: saleReceipt.createdAt,
}; };
// XXX Debit - Deposit account. // XXX Debit - Deposit account.
const depositEntry = new JournalEntry({ const depositEntry = new JournalEntry({

View File

@@ -164,10 +164,12 @@ export default class JournalPoster implements IJournalPoster {
); );
const balanceEntries = chain(balanceChanges) const balanceEntries = chain(balanceChanges)
.map((change) => change.entries.map(entry => ({ .map((change) =>
change.entries.map((entry) => ({
...entry, ...entry,
contactId: change.contactId contactId: change.contactId,
}))) }))
)
.flatten() .flatten()
.value(); .value();
@@ -376,6 +378,8 @@ export default class JournalPoster implements IJournalPoster {
const { transactionsRepository } = this.repositories; const { transactionsRepository } = this.repositories;
const saveOperations: Promise<void>[] = []; const saveOperations: Promise<void>[] = [];
this.logger.info('[journal] trying to insert accounts transactions.');
this.entries.forEach((entry) => { this.entries.forEach((entry) => {
const oper = transactionsRepository.create({ const oper = transactionsRepository.create({
accountId: entry.account, accountId: entry.account,

View File

@@ -17,7 +17,6 @@ import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker'
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
@@ -35,20 +34,27 @@ export default class InventoryService {
/** /**
* Transforms the items entries to inventory transactions. * Transforms the items entries to inventory transactions.
*/ */
transformItemEntriesToInventory( transformItemEntriesToInventory(transaction: {
itemEntries: IItemEntry[], transactionId: number;
direction: TInventoryTransactionDirection, transactionType: IItemEntryTransactionType;
date: Date | string,
lotNumber: number date: Date | string;
): IInventoryTransaction[] { direction: TInventoryTransactionDirection;
return itemEntries.map((entry: IItemEntry) => ({ entries: IItemEntry[];
...pick(entry, ['itemId', 'quantity', 'rate']), createdAt: Date;
lotNumber, }): IInventoryTransaction[] {
transactionType: entry.referenceType, return transaction.entries.map((entry: IItemEntry) => ({
transactionId: entry.referenceId, ...pick(entry, [
direction, 'itemId',
date, 'quantity',
'rate',
]),
transactionType: transaction.transactionType,
transactionId: transaction.transactionId,
direction: transaction.direction,
date: transaction.date,
entryId: entry.id, entryId: entry.id,
createdAt: transaction.createdAt,
})); }));
} }
@@ -200,7 +206,6 @@ export default class InventoryService {
} }
return InventoryTransaction.query().insert({ return InventoryTransaction.query().insert({
...inventoryEntry, ...inventoryEntry,
lotNumber: inventoryEntry.lotNumber,
}); });
} }
@@ -215,31 +220,24 @@ export default class InventoryService {
*/ */
async recordInventoryTransactionsFromItemsEntries( async recordInventoryTransactionsFromItemsEntries(
tenantId: number, tenantId: number,
transactionId: number, transaction: {
transactionType: IItemEntryTransactionType, transactionId: number;
transactionDate: Date | string, transactionType: IItemEntryTransactionType;
transactionDirection: TInventoryTransactionDirection,
date: Date | string;
direction: TInventoryTransactionDirection;
entries: IItemEntry[];
createdAt: Date | string;
},
override: boolean = false override: boolean = false
): Promise<void> { ): Promise<void> {
// Retrieve the next inventory lot number.
const lotNumber = this.getNextLotNumber(tenantId);
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries = await this.itemsEntriesService.getInventoryEntries(
tenantId,
transactionType,
transactionId
);
// Can't continue if there is no entries has inventory items in the invoice. // Can't continue if there is no entries has inventory items in the invoice.
if (inventoryEntries.length <= 0) { if (transaction.entries.length <= 0) {
return; return;
} }
// Inventory transactions. // Inventory transactions.
const inventoryTranscations = this.transformItemEntriesToInventory( const inventoryTranscations = this.transformItemEntriesToInventory(
inventoryEntries, transaction
transactionDirection,
transactionDate,
lotNumber
); );
// Records the inventory transactions of the given sale invoice. // Records the inventory transactions of the given sale invoice.
await this.recordInventoryTransactions( await this.recordInventoryTransactions(
@@ -247,8 +245,6 @@ export default class InventoryService {
inventoryTranscations, inventoryTranscations,
override override
); );
// Increment and save the next lot number settings.
await this.incrementNextLotNumber(tenantId);
} }
/** /**
@@ -309,45 +305,6 @@ export default class InventoryService {
}); });
} }
/**
* Retrieve the lot number after the increment.
* @param {number} tenantId - Tenant id.
*/
getNextLotNumber(tenantId: number) {
const settings = this.tenancy.settings(tenantId);
const LOT_NUMBER_KEY = 'lot_number_increment';
const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY });
return storedLotNumber && storedLotNumber.value
? parseInt(storedLotNumber.value, 10)
: 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;
}
settings.set({ key: LOT_NUMBER_KEY }, lotNumber);
await settings.save();
return lotNumber;
}
/** /**
* Mark item cost computing is running. * Mark item cost computing is running.
* @param {number} tenantId - * @param {number} tenantId -
@@ -377,7 +334,7 @@ export default class InventoryService {
return settings.get({ return settings.get({
key: 'cost_compute_running', key: 'cost_compute_running',
group: 'inventory' group: 'inventory',
}); });
} }
} }

View File

@@ -20,6 +20,7 @@ import ItemsService from 'services/Items/ItemsService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import HasTenancyService from 'services/Tenancy/TenancyService'; import HasTenancyService from 'services/Tenancy/TenancyService';
import InventoryService from './Inventory'; import InventoryService from './Inventory';
import { increment } from 'utils';
const ERRORS = { const ERRORS = {
INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND', INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND',
@@ -310,14 +311,12 @@ export default class InventoryAdjustmentService {
inventoryAdjustment: IInventoryAdjustment, inventoryAdjustment: IInventoryAdjustment,
override: boolean = false override: boolean = false
): Promise<void> { ): Promise<void> {
// Gets the next inventory lot number.
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
const commonTransaction = { const commonTransaction = {
direction: inventoryAdjustment.inventoryDirection, direction: inventoryAdjustment.inventoryDirection,
date: inventoryAdjustment.date, date: inventoryAdjustment.date,
transactionType: 'InventoryAdjustment', transactionType: 'InventoryAdjustment',
transactionId: inventoryAdjustment.id, transactionId: inventoryAdjustment.id,
createdAt: inventoryAdjustment.createdAt
}; };
const inventoryTransactions = []; const inventoryTransactions = [];
@@ -327,7 +326,6 @@ export default class InventoryAdjustmentService {
itemId: entry.itemId, itemId: entry.itemId,
quantity: entry.quantity, quantity: entry.quantity,
rate: entry.cost, rate: entry.cost,
lotNumber,
}); });
}); });
// Saves the given inventory transactions to the storage. // Saves the given inventory transactions to the storage.
@@ -335,9 +333,7 @@ export default class InventoryAdjustmentService {
tenantId, tenantId,
inventoryTransactions, inventoryTransactions,
override override
); )
// Increment and save the next lot number settings.
await this.inventoryService.incrementNextLotNumber(tenantId);
} }
/** /**

View File

@@ -50,7 +50,7 @@ export default class InventoryAverageCostMethod
.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') .orderBy('createdAt', 'ASC')
.where('item_id', this.itemId) .where('item_id', this.itemId)
.withGraphFetched('item'); .withGraphFetched('item');
@@ -164,7 +164,9 @@ export default class InventoryAverageCostMethod
'entryId', 'entryId',
'transactionId', 'transactionId',
'transactionType', 'transactionType',
'lotNumber', 'createdAt',
'sellAccountId',
'costAccountId',
]), ]),
}; };
switch (invTransaction.direction) { switch (invTransaction.direction) {

View File

@@ -22,7 +22,9 @@ export default class InventoryCostMethod {
* @param {IInventoryLotCost[]} costLotsTransactions * @param {IInventoryLotCost[]} costLotsTransactions
* @return {Promise[]} * @return {Promise[]}
*/ */
public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> { public storeInventoryLotsCost(
costLotsTransactions: IInventoryLotCost[]
): Promise<object> {
const { InventoryCostLotTracker } = this.tenantModels; const { InventoryCostLotTracker } = this.tenantModels;
const opers: any = []; const opers: any = [];
@@ -32,10 +34,8 @@ export default class InventoryCostMethod {
.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 = InventoryCostLotTracker.query() const operation = InventoryCostLotTracker.query().insert({
.insert({
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']), ...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
}); });
opers.push(operation); opers.push(operation);

View File

@@ -50,6 +50,29 @@ export default class ItemsEntriesService {
return inventoryItemsEntries; return inventoryItemsEntries;
} }
/**
* Filter the given entries to inventory entries.
* @param {IItemEntry[]} entries -
* @returns {IItemEntry[]}
*/
public async filterInventoryEntries(
tenantId: number,
entries: IItemEntry[]
): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
// Retrieve entries inventory items.
const inventoryItems = await Item.query()
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');
const inventoryEntries = entries.filter((entry) =>
inventoryItems.some((item) => item.id === entry.itemId)
);
return inventoryEntries;
}
/** /**
* Validates the entries items ids. * Validates the entries items ids.
* @async * @async

View File

@@ -22,7 +22,6 @@ import {
ACCOUNT_TYPE, ACCOUNT_TYPE,
} from 'data/AccountTypes'; } from 'data/AccountTypes';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { AccountTransaction } from 'models';
@Service() @Service()
export default class ItemsService implements IItemsService { export default class ItemsService implements IItemsService {

View File

@@ -569,7 +569,12 @@ export default class BillPaymentsService implements IBillPaymentsService {
credit: 0, credit: 0,
referenceId: billPayment.id, referenceId: billPayment.id,
referenceType: 'BillPayment', referenceType: 'BillPayment',
transactionNumber: billPayment.paymentNumber,
referenceNumber: billPayment.reference,
date: formattedDate, date: formattedDate,
createdAt: billPayment.createdAt,
}; };
if (override) { if (override) {
const transactions = await AccountTransaction.query() const transactions = await AccountTransaction.query()

View File

@@ -1,6 +1,7 @@
import { omit, sumBy } from 'lodash'; import { omit, sumBy } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import composeAsync from 'async/compose';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -21,7 +22,8 @@ import {
IPaginationMeta, IPaginationMeta,
IFilterMeta, IFilterMeta,
IBillsFilter, IBillsFilter,
IBillsService IBillsService,
IItemEntry,
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
@@ -36,7 +38,9 @@ import { ERRORS } from './constants';
* @service * @service
*/ */
@Service('Bills') @Service('Bills')
export default class BillsService extends SalesInvoicesCost implements IBillsService { export default class BillsService
extends SalesInvoicesCost
implements IBillsService {
@Inject() @Inject()
inventoryService: InventoryService; inventoryService: InventoryService;
@@ -167,6 +171,29 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
} }
} }
/**
* Sets the default cost account to the bill entries.
*/
setBillEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
const items = await Item.query().whereIn('id', entriesItemsIds);
return entries.map((entry) => {
const item = items.find((i) => i.id === entry.itemId);
return {
...entry,
...(item.type !== 'inventory' && {
costAccountId: entry.costAccountId || item.costAccountId,
}),
};
});
};
}
/** /**
* Converts create bill DTO to model. * Converts create bill DTO to model.
* @param {number} tenantId * @param {number} tenantId
@@ -182,11 +209,7 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
) { ) {
const { ItemEntry } = this.tenancy.models(tenantId); const { ItemEntry } = this.tenancy.models(tenantId);
const entries = billDTO.entries.map((entry) => ({ const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
...entry,
amount: ItemEntry.calcAmount(entry),
}));
const amount = sumBy(entries, 'amount');
// Bill number from DTO or from auto-increment. // Bill number from DTO or from auto-increment.
const billNumber = billDTO.billNumber || oldBill?.billNumber; const billNumber = billDTO.billNumber || oldBill?.billNumber;
@@ -196,6 +219,15 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
tenantId, tenantId,
billDTO.vendorId billDTO.vendorId
); );
const initialEntries = billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}));
const entries = await composeAsync(
// Sets the default cost account to the bill entries.
this.setBillEntriesDefaultAccounts(tenantId)
)(initialEntries);
return { return {
...formatDateFields(omit(billDTO, ['open', 'entries']), [ ...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate', 'billDate',
@@ -204,10 +236,7 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
amount, amount,
currencyCode: vendor.currencyCode, currencyCode: vendor.currencyCode,
billNumber, billNumber,
entries: entries.map((entry) => ({ entries,
reference_type: 'Bill',
...omit(entry, ['amount']),
})),
// Avoid rewrite the open date in edit mode when already opened. // Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open && ...(billDTO.open &&
!oldBill?.openedAt && { !oldBill?.openedAt && {
@@ -239,15 +268,6 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
): Promise<IBill> { ): Promise<IBill> {
const { billRepository } = this.tenancy.repositories(tenantId); const { billRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[bill] trying to create a new bill', {
tenantId,
billDTO,
});
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
authorizedUser
);
// Retrieve vendor or throw not found service error. // Retrieve vendor or throw not found service error.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -264,8 +284,18 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
tenantId, tenantId,
billDTO.entries billDTO.entries
); );
this.logger.info('[bill] trying to create a new bill', {
tenantId,
billDTO,
});
// Transform the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
authorizedUser
);
// Inserts the bill graph object to the storage. // Inserts the bill graph object to the storage.
const bill = await billRepository.upsertGraph({ ...billObj }); const bill = await billRepository.upsertGraph(billObj);
// Triggers `onBillCreated` event. // Triggers `onBillCreated` event.
await this.eventDispatcher.dispatch(events.bill.onCreated, { await this.eventDispatcher.dispatch(events.bill.onCreated, {
@@ -309,13 +339,6 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
this.logger.info('[bill] trying to edit bill.', { tenantId, billId }); this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
const oldBill = await this.getBillOrThrowError(tenantId, billId); const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Transforms the bill DTO object to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
authorizedUser,
oldBill
);
// Retrieve vendor details or throw not found service error. // Retrieve vendor details or throw not found service error.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -340,6 +363,13 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
tenantId, tenantId,
billDTO.entries billDTO.entries
); );
// Transforms the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
authorizedUser,
oldBill
);
// Update the bill transaction. // Update the bill transaction.
const bill = await billRepository.upsertGraph({ const bill = await billRepository.upsertGraph({
id: billId, id: billId,
@@ -507,19 +537,25 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
bill: IBill, bill: IBill,
override?: boolean override?: boolean
): Promise<void> { ): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries(
tenantId,
bill.entries
);
const transaction = {
transactionId: bill.id,
transactionType: 'Bill',
date: bill.billDate,
direction: 'IN',
entries: inventoryEntries,
createdAt: bill.createdAt,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries( await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId, tenantId,
bill.id, transaction,
'Bill',
bill.billDate,
'IN',
override override
); );
// Triggers `onInventoryTransactionsCreated` event.
this.eventDispatcher.dispatch(events.bill.onInventoryTransactionsCreated, {
tenantId,
bill,
});
} }
/** /**
@@ -582,10 +618,7 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
* @param {number} tenantId * @param {number} tenantId
* @param {number} vendorId - Vendor id. * @param {number} vendorId - Vendor id.
*/ */
public async validateVendorHasNoBills( public async validateVendorHasNoBills(tenantId: number, vendorId: number) {
tenantId: number,
vendorId: number
) {
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().where('vendor_id', vendorId); const bills = await Bill.query().where('vendor_id', vendorId);

View File

@@ -678,8 +678,13 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
credit: 0, credit: 0,
referenceId: paymentReceive.id, referenceId: paymentReceive.id,
referenceType: 'PaymentReceive', referenceType: 'PaymentReceive',
transactionNumber: paymentReceive.paymentReceiveNo,
referenceNumber: paymentReceive.referenceNo,
date: paymentReceive.paymentDate, date: paymentReceive.paymentDate,
userId: authorizedUserId, userId: authorizedUserId,
createdAt: paymentReceive.createdAt,
}; };
if (override) { if (override) {
const transactions = await transactionsRepository.journal({ const transactions = await transactionsRepository.journal({

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { omit, sumBy, join } from 'lodash'; import { omit, sumBy, join, entries } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import composeAsync from 'async/compose';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -15,7 +16,7 @@ import {
ISystemUser, ISystemUser,
IItem, IItem,
IItemEntry, IItemEntry,
ISalesInvoicesService ISalesInvoicesService,
} from 'interfaces'; } from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
@@ -181,6 +182,30 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
); );
} }
/**
* Sets the cost/sell accounts to the invoice entries.
*/
setInvoiceEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
const items = await Item.query().whereIn('id', entriesItemsIds);
return entries.map((entry) => {
const item = items.find((i) => i.id === entry.itemId);
return {
...entry,
sellAccountId: entry.sellAccountId || item.sellAccountId,
...(item.type === 'inventory' && {
costAccountId: entry.costAccountId || item.costAccountId,
}),
};
});
};
}
/** /**
* Transformes the create DTO to invoice object model. * Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
@@ -212,6 +237,16 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
// Validate the invoice is required. // Validate the invoice is required.
this.validateInvoiceNoRequire(invoiceNo); this.validateInvoiceNoRequire(invoiceNo);
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
referenceType: 'SaleInvoice',
...entry,
}));
const entries = await composeAsync(
// Sets default cost and sell account to invoice items entries.
this.setInvoiceEntriesDefaultAccounts(tenantId)
)(initialEntries);
return { return {
...formatDateFields( ...formatDateFields(
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']), omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
@@ -227,10 +262,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
// Avoid override payment amount in edit mode. // Avoid override payment amount in edit mode.
...(!oldSaleInvoice && { paymentAmount: 0 }), ...(!oldSaleInvoice && { paymentAmount: 0 }),
...(invoiceNo ? { invoiceNo } : {}), ...(invoiceNo ? { invoiceNo } : {}),
entries: saleInvoiceDTO.entries.map((entry) => ({ entries,
referenceType: 'SaleInvoice',
...entry,
})),
}; };
} }
@@ -259,23 +291,11 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
): Promise<ISaleInvoice> { ): Promise<ISaleInvoice> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO
);
// Validate customer existance. // Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError( await this.customersService.getCustomerByIdOrThrowError(
tenantId, tenantId,
saleInvoiceDTO.customerId saleInvoiceDTO.customerId
); );
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo
);
}
// Validate the from estimate id exists on the storage. // Validate the from estimate id exists on the storage.
if (saleInvoiceDTO.fromEstimateId) { if (saleInvoiceDTO.fromEstimateId) {
const fromEstimate = await this.saleEstimatesService.getSaleEstimateOrThrowError( const fromEstimate = await this.saleEstimatesService.getSaleEstimateOrThrowError(
@@ -295,11 +315,21 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
tenantId, tenantId,
saleInvoiceDTO.entries saleInvoiceDTO.entries
); );
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceObj.invoiceNo
);
}
this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const saleInvoice = await saleInvoiceRepository.upsertGraph({ const saleInvoice = await saleInvoiceRepository.upsertGraph(saleInvoiceObj);
...saleInvoiceObj,
});
// Triggers the event `onSaleInvoiceCreated`. // Triggers the event `onSaleInvoiceCreated`.
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, { await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, {
tenantId, tenantId,
@@ -337,25 +367,11 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
tenantId, tenantId,
saleInvoiceId saleInvoiceId
); );
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
);
// Validate customer existance. // Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError( await this.customersService.getCustomerByIdOrThrowError(
tenantId, tenantId,
saleInvoiceDTO.customerId saleInvoiceDTO.customerId
); );
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo,
saleInvoiceId
);
}
// Validate items ids existance. // Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance( await this.itemsEntriesService.validateItemsIdsExistance(
tenantId, tenantId,
@@ -373,6 +389,20 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
'SaleInvoice', 'SaleInvoice',
saleInvoiceDTO.entries saleInvoiceDTO.entries
); );
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo,
saleInvoiceId
);
}
// Validate the invoice amount is not smaller than the invoice payment amount. // Validate the invoice amount is not smaller than the invoice payment amount.
this.validateInvoiceAmountBiggerPaymentAmount( this.validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceObj.balance, saleInvoiceObj.balance,
@@ -445,7 +475,8 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
const { ItemEntry } = this.tenancy.models(tenantId); const { ItemEntry } = this.tenancy.models(tenantId);
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given sale invoice with associated entries or throw not found error. // Retrieve the given sale invoice with associated entries
// or throw not found error.
const oldSaleInvoice = await this.getInvoiceOrThrowError( const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId, tenantId,
saleInvoiceId saleInvoiceId
@@ -497,12 +528,23 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
saleInvoice: ISaleInvoice, saleInvoice: ISaleInvoice,
override?: boolean override?: boolean
): Promise<void> { ): Promise<void> {
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleInvoice.entries
);
const transaction = {
transactionId: saleInvoice.id,
transactionType: 'SaleInvoice',
date: saleInvoice.invoiceDate,
direction: 'OUT',
entries: inventoryEntries,
createdAt: saleInvoice.created_at,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries( await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId, tenantId,
saleInvoice.id, transaction,
'SaleInvoice',
saleInvoice.invoiceDate,
'OUT',
override override
); );
} }

View File

@@ -1,5 +1,5 @@
import { Container, Service, Inject } from 'typedi'; import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash'; import { chain, groupBy } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
@@ -108,6 +108,20 @@ export default class SaleInvoicesCost {
}); });
} }
/**
* Grpups by transaction type and id the inventory transactions.
* @param {IInventoryTransaction} invTransactions
* @returns
*/
inventoryTransactionsGroupByType(
invTransactions: { transactionType: string; transactionId: number }[]
): { transactionType: string; transactionId: number }[][] {
return chain(invTransactions)
.groupBy((t) => `${t.transactionType}-${t.transactionId}`)
.values()
.value();
}
/** /**
* Writes journal entries from sales invoices. * Writes journal entries from sales invoices.
* @param {number} tenantId - The tenant id. * @param {number} tenantId - The tenant id.
@@ -124,25 +138,28 @@ export default class SaleInvoicesCost {
const inventoryCostLotTrans = await InventoryCostLotTracker.query() const inventoryCostLotTrans = await InventoryCostLotTracker.query()
.where('direction', 'OUT') .where('direction', 'OUT')
.modify('groupedEntriesCost')
.modify('filterDateRange', startingDate) .modify('filterDateRange', startingDate)
.orderBy('date', 'ASC') .orderBy('date', 'ASC')
.where('cost', '>', 0) .where('cost', '>', 0)
.withGraphFetched('item'); .withGraphFetched('item')
.withGraphFetched('itemEntry');
const accountsDepGraph = await accountRepository.getDependencyGraph(); const accountsDepGraph = await accountRepository.getDependencyGraph();
const journal = new JournalPoster(tenantId, accountsDepGraph); const journal = new JournalPoster(tenantId, accountsDepGraph);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
// Groups the inventory cost lots transactions.
const inventoryTransactions = this.inventoryTransactionsGroupByType(
inventoryCostLotTrans
);
if (override) { if (override) {
await journalCommands.revertInventoryCostJournalEntries(startingDate); await journalCommands.revertInventoryCostJournalEntries(startingDate);
} }
inventoryCostLotTrans.forEach( inventoryTransactions.forEach((inventoryLots) => {
(inventoryCostLot: IInventoryLotCost & { item: IItem }) => { journalCommands.saleInvoiceInventoryCost(inventoryLots);
journalCommands.saleInvoiceInventoryCost(inventoryCostLot); });
}
);
return Promise.all([ return Promise.all([
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), journal.saveEntries(),

View File

@@ -51,15 +51,9 @@ export default class SaleInvoiceSubscriber {
saleInvoice, saleInvoice,
authorizedUser, authorizedUser,
}) { }) {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const saleInvoiceWithItems = await saleInvoiceRepository.findOneById(
saleInvoiceId,
'entries.item'
);
await this.saleInvoicesService.writesIncomeJournalEntries( await this.saleInvoicesService.writesIncomeJournalEntries(
tenantId, tenantId,
saleInvoiceWithItems, saleInvoice,
true true
); );
} }

View File

@@ -313,7 +313,16 @@ export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1; return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1;
}; };
var increment = (n) => {
return () => {
n += 1;
return n;
};
};
export { export {
increment,
hashPassword, hashPassword,
origin, origin,
dateRangeCollection, dateRangeCollection,