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 quickInventoryAdjustment = this.matchedBodyData(req);
console.log(quickInventoryAdjustment);
try {
const inventoryAdjustment = await this.inventoryAdjustmentService.createQuickAdjustment(

View File

@@ -324,7 +324,7 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req -
* @param {Response} res -
*/
async getPaymentReceiveEditPage(
async getPaymentReceiveEditPage(
req: Request,
res: Response,
next: NextFunction

View File

@@ -435,9 +435,7 @@ export default class SaleInvoicesController extends BaseController {
}
if (error.errorType === 'SALE_INVOICE_NO_IS_REQUIRED') {
return res.boom.badRequest(null, {
errors: [
{ type: 'SALE_INVOICE_NO_IS_REQUIRED', code: 1500 },
],
errors: [{ 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.string('note');
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.timestamps();
table.datetime('created_at').index();
}).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) => {
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.decimal('amount', 13, 3).defaultTo(0);
table.string('currency_code', 3);
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.text('statement');
table.integer('user_id').unsigned().index();
@@ -16,6 +25,6 @@ exports.up = function(knex) {
});
};
exports.down = function(knex) {
exports.down = function (knex) {
return knex.schema.dropTableIfExists('payment_receives');
};

View File

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

View File

@@ -1,6 +1,5 @@
exports.up = function(knex) {
return knex.schema.createTable('inventory_cost_lot_tracker', table => {
exports.up = function (knex) {
return knex.schema.createTable('inventory_cost_lot_tracker', (table) => {
table.increments();
table.date('date').index();
table.string('direction').index();
@@ -10,14 +9,15 @@ exports.up = function(knex) {
table.decimal('rate', 13, 3);
table.integer('remaining');
table.decimal('cost', 13, 3);
table.integer('lot_number').index();
table.string('transaction_type').index();
table.integer('transaction_id').unsigned().index();
table.integer('entry_id').unsigned().index();
});
table.datetime('created_at').index();
});
};
exports.down = function(knex) {
exports.down = function (knex) {
return knex.schema.dropTableIfExists('inventory_cost_lot_tracker');
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,9 @@ export default class WriteInvoicesJournalEntries {
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));
}
/**
* Handle the job execuation.
*/
public async handler(job, done: Function): Promise<void> {
const Logger = Container.get('logger');
const { startingDate, tenantId } = job.attrs.data;

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ export default class InventoryCostLotTracker extends TenantModel {
*/
static get modifiers() {
return {
groupedEntriesCost(query) {
groupedEntriesCost(query) {
query.select(['date', 'item_id', 'transaction_id', 'transaction_type']);
query.sum('cost as cost');
@@ -46,12 +46,13 @@ export default class InventoryCostLotTracker extends TenantModel {
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Item = require('models/Item');
const SaleInvoice = require('models/SaleInvoice');
const ItemEntry = require('models/ItemEntry');
return {
item: {
@@ -62,6 +63,25 @@ export default class InventoryCostLotTracker extends TenantModel {
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() {
const Item = require('models/Item');
const ItemEntry = require('models/ItemEntry');
return {
item: {
@@ -52,6 +53,14 @@ export default class InventoryTransaction extends TenantModel {
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,
referenceType?: string[],
referenceId?: number[],
index: number|number[]
index: number|number[],
indexGroup: number|number[],
};
export default class AccountTransactionsRepository extends TenantRepository {
@@ -58,6 +59,13 @@ export default class AccountTransactionsRepository extends TenantRepository {
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, { LongDateFormatKey } from 'moment';
import { IBill, IManualJournalEntry, ISaleReceipt, ISystemUser } from 'interfaces';
import moment from 'moment';
import {
IBill,
IManualJournalEntry,
ISaleReceipt,
ISystemUser,
} from 'interfaces';
import JournalPoster from './JournalPoster';
import JournalEntry from './JournalEntry';
import { AccountTransaction } from 'models';
import {
IInventoryTransaction,
IManualJournal,
IExpense,
IExpenseCategory,
@@ -14,6 +16,7 @@ import {
IInventoryLotCost,
IItemEntry,
} from 'interfaces';
import { increment } from 'utils';
export default class JournalCommands {
journal: JournalPoster;
@@ -61,6 +64,8 @@ export default class JournalCommands {
referenceNumber: bill.referenceNo,
transactionNumber: bill.billNumber,
createdAt: bill.createdAt,
};
// Overrides the old bill entries.
if (override) {
@@ -90,9 +95,9 @@ export default class JournalCommands {
account:
['inventory'].indexOf(item.type) !== -1
? item.inventoryAccountId
: item.costAccountId,
: entry.costAccountId,
index: index + 2,
itemId: entry.itemId
itemId: entry.itemId,
});
this.journal.debit(debitEntry);
});
@@ -257,7 +262,7 @@ export default class JournalCommands {
const transactions = await transactionsRepository.journal({
fromDate: startingDate,
referenceType: ['SaleInvoice'],
index: [3, 4],
indexGroup: 20
});
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
@@ -265,11 +270,9 @@ export default class JournalCommands {
/**
* Reverts sale invoice the income journal entries.
* @param {number} saleInvoiceId
* @param {number} saleInvoiceId
*/
async revertInvoiceIncomeEntries(
saleInvoiceId: number,
) {
async revertInvoiceIncomeEntries(saleInvoiceId: number) {
const { transactionsRepository } = this.repositories;
const transactions = await transactionsRepository.journal({
@@ -289,6 +292,7 @@ export default class JournalCommands {
const commonEntry = {
transaction_number: manualJournalObj.journalNumber,
reference_number: manualJournalObj.reference,
createdAt: manualJournalObj.createdAt,
};
manualJournalObj.entries.forEach((entry: IManualJournalEntry) => {
const jouranlEntry = new JournalEntry({
@@ -317,36 +321,50 @@ export default class JournalCommands {
* -------
* - Cost of goods sold -> Debit -> YYYY
* - Inventory assets -> Credit -> YYYY
*
* --------
* @param {ISaleInvoice} saleInvoice
* @param {JournalPoster} journal
*/
saleInvoiceInventoryCost(
inventoryCostLot: IInventoryLotCost & { item: IItem }
inventoryCostLots: IInventoryLotCost &
{ item: IItem; itemEntry: IItemEntry }[]
) {
const commonEntry = {
referenceType: inventoryCostLot.transactionType,
referenceId: inventoryCostLot.transactionId,
date: inventoryCostLot.date,
};
// XXX Debit - Cost account.
const costEntry = new JournalEntry({
...commonEntry,
debit: inventoryCostLot.cost,
account: inventoryCostLot.item.costAccountId,
index: 3,
itemId: inventoryCostLot.itemId
});
// XXX Credit - Inventory account.
const inventoryEntry = new JournalEntry({
...commonEntry,
credit: inventoryCostLot.cost,
account: inventoryCostLot.item.inventoryAccountId,
index: 4,
itemId: inventoryCostLot.itemId
});
this.journal.credit(inventoryEntry);
this.journal.debit(costEntry);
const getIndexIncrement = increment(0);
inventoryCostLots.forEach(
(
inventoryCostLot: IInventoryLotCost & {
item: IItem;
itemEntry: IItemEntry;
}
) => {
const commonEntry = {
referenceType: inventoryCostLot.transactionType,
referenceId: inventoryCostLot.transactionId,
date: inventoryCostLot.date,
indexGroup: 20,
createdAt: inventoryCostLot.createdAt,
};
// XXX Debit - Cost account.
const costEntry = new JournalEntry({
...commonEntry,
debit: inventoryCostLot.cost,
account: inventoryCostLot.itemEntry.costAccountId,
itemId: inventoryCostLot.itemId,
index: getIndexIncrement(),
});
// XXX Credit - Inventory account.
const inventoryEntry = new JournalEntry({
...commonEntry,
credit: inventoryCostLot.cost,
account: inventoryCostLot.item.inventoryAccountId,
itemId: inventoryCostLot.itemId,
index: getIndexIncrement(),
});
this.journal.credit(inventoryEntry);
this.journal.debit(costEntry);
}
);
}
/**
@@ -354,7 +372,7 @@ export default class JournalCommands {
* -----
* - Receivable accounts -> Debit -> XXXX
* - Income -> Credit -> XXXX
*
*
* @param {ISaleInvoice} saleInvoice
* @param {number} receivableAccountsId
* @param {number} authorizedUserId
@@ -373,6 +391,9 @@ export default class JournalCommands {
transactionNumber: saleInvoice.invoiceNo,
referenceNumber: saleInvoice.referenceNo,
createdAt: saleInvoice.createdAt,
indexGroup: 10,
};
// XXX Debit - Receivable account.
const receivableEntry = new JournalEntry({
@@ -384,22 +405,20 @@ export default class JournalCommands {
});
this.journal.debit(receivableEntry);
saleInvoice.entries.forEach(
(entry: IItemEntry & { item: IItem }, index: number) => {
const income: number = entry.quantity * entry.rate;
saleInvoice.entries.forEach((entry: IItemEntry, index: number) => {
const income: number = entry.quantity * entry.rate;
// XXX Credit - Income account.
const incomeEntry = new JournalEntry({
...commonEntry,
credit: income,
account: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
});
this.journal.credit(incomeEntry);
}
);
// XXX Credit - Income account.
const incomeEntry = new JournalEntry({
...commonEntry,
credit: income,
account: entry.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
});
this.journal.credit(incomeEntry);
});
}
/**
@@ -407,7 +426,7 @@ export default class JournalCommands {
* -----
* - Deposit account -> Debit -> XXXX
* - Income -> Credit -> XXXX
*
*
* @param {ISaleInvoice} saleInvoice
* @param {number} receivableAccountsId
* @param {number} authorizedUserId
@@ -415,7 +434,7 @@ export default class JournalCommands {
async saleReceiptIncomeEntries(
saleReceipt: ISaleReceipt & {
entries: IItemEntry & { item: IItem };
},
}
): Promise<void> {
const commonEntry = {
referenceType: 'SaleReceipt',
@@ -424,6 +443,7 @@ export default class JournalCommands {
userId: saleReceipt.userId,
transactionNumber: saleReceipt.receiptNumber,
referenceNumber: saleReceipt.referenceNo,
createdAt: saleReceipt.createdAt,
};
// XXX Debit - Deposit account.
const depositEntry = new JournalEntry({

View File

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

View File

@@ -17,7 +17,6 @@ import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker'
import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
@@ -35,20 +34,27 @@ export default class InventoryService {
/**
* Transforms the items entries to inventory transactions.
*/
transformItemEntriesToInventory(
itemEntries: IItemEntry[],
direction: TInventoryTransactionDirection,
date: Date | string,
lotNumber: number
): IInventoryTransaction[] {
return itemEntries.map((entry: IItemEntry) => ({
...pick(entry, ['itemId', 'quantity', 'rate']),
lotNumber,
transactionType: entry.referenceType,
transactionId: entry.referenceId,
direction,
date,
transformItemEntriesToInventory(transaction: {
transactionId: number;
transactionType: IItemEntryTransactionType;
date: Date | string;
direction: TInventoryTransactionDirection;
entries: IItemEntry[];
createdAt: Date;
}): IInventoryTransaction[] {
return transaction.entries.map((entry: IItemEntry) => ({
...pick(entry, [
'itemId',
'quantity',
'rate',
]),
transactionType: transaction.transactionType,
transactionId: transaction.transactionId,
direction: transaction.direction,
date: transaction.date,
entryId: entry.id,
createdAt: transaction.createdAt,
}));
}
@@ -200,7 +206,6 @@ export default class InventoryService {
}
return InventoryTransaction.query().insert({
...inventoryEntry,
lotNumber: inventoryEntry.lotNumber,
});
}
@@ -215,31 +220,24 @@ export default class InventoryService {
*/
async recordInventoryTransactionsFromItemsEntries(
tenantId: number,
transactionId: number,
transactionType: IItemEntryTransactionType,
transactionDate: Date | string,
transactionDirection: TInventoryTransactionDirection,
transaction: {
transactionId: number;
transactionType: IItemEntryTransactionType;
date: Date | string;
direction: TInventoryTransactionDirection;
entries: IItemEntry[];
createdAt: Date | string;
},
override: boolean = false
): 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.
if (inventoryEntries.length <= 0) {
if (transaction.entries.length <= 0) {
return;
}
// Inventory transactions.
const inventoryTranscations = this.transformItemEntriesToInventory(
inventoryEntries,
transactionDirection,
transactionDate,
lotNumber
transaction
);
// Records the inventory transactions of the given sale invoice.
await this.recordInventoryTransactions(
@@ -247,8 +245,6 @@ export default class InventoryService {
inventoryTranscations,
override
);
// Increment and save the next lot number settings.
await this.incrementNextLotNumber(tenantId);
}
/**
@@ -309,49 +305,10 @@ 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.
* @param {number} tenantId -
* @param {boolean} isRunning -
* @param {number} tenantId -
* @param {boolean} isRunning -
*/
async markItemsCostComputeRunning(
tenantId: number,
@@ -368,16 +325,16 @@ export default class InventoryService {
}
/**
*
* @param {number} tenantId
* @returns
*
* @param {number} tenantId
* @returns
*/
isItemsCostComputeRunning(tenantId) {
const settings = this.tenancy.settings(tenantId);
return settings.get({
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 HasTenancyService from 'services/Tenancy/TenancyService';
import InventoryService from './Inventory';
import { increment } from 'utils';
const ERRORS = {
INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND',
@@ -310,24 +311,21 @@ export default class InventoryAdjustmentService {
inventoryAdjustment: IInventoryAdjustment,
override: boolean = false
): Promise<void> {
// Gets the next inventory lot number.
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
const commonTransaction = {
direction: inventoryAdjustment.inventoryDirection,
date: inventoryAdjustment.date,
transactionType: 'InventoryAdjustment',
transactionId: inventoryAdjustment.id,
createdAt: inventoryAdjustment.createdAt
};
const inventoryTransactions = [];
inventoryAdjustment.entries.forEach((entry) => {
inventoryTransactions.push({
...commonTransaction,
itemId: entry.itemId,
quantity: entry.quantity,
rate: entry.cost,
lotNumber,
});
});
// Saves the given inventory transactions to the storage.
@@ -335,9 +333,7 @@ export default class InventoryAdjustmentService {
tenantId,
inventoryTransactions,
override
);
// Increment and save the next lot number settings.
await this.inventoryService.incrementNextLotNumber(tenantId);
)
}
/**

View File

@@ -50,10 +50,10 @@ export default class InventoryAverageCostMethod
.modify('filterDateRange', this.startingDate)
.orderBy('date', 'ASC')
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
.orderBy('lot_number', 'ASC')
.orderBy('createdAt', 'ASC')
.where('item_id', this.itemId)
.withGraphFetched('item');
// Tracking inventroy transactions and retrieve cost transactions based on
// average rate cost method.
const costTransactions = this.trackingCostTransactions(
@@ -164,7 +164,9 @@ export default class InventoryAverageCostMethod
'entryId',
'transactionId',
'transactionType',
'lotNumber',
'createdAt',
'sellAccountId',
'costAccountId',
]),
};
switch (invTransaction.direction) {

View File

@@ -19,10 +19,12 @@ export default class InventoryCostMethod {
/**
* Stores the inventory lots costs transactions in bulk.
* @param {IInventoryLotCost[]} costLotsTransactions
* @param {IInventoryLotCost[]} costLotsTransactions
* @return {Promise[]}
*/
public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> {
public storeInventoryLotsCost(
costLotsTransactions: IInventoryLotCost[]
): Promise<object> {
const { InventoryCostLotTracker } = this.tenantModels;
const opers: any = [];
@@ -32,15 +34,13 @@ export default class InventoryCostMethod {
.where('id', transaction.lotTransId)
.decrement('remaining', transaction.decrement);
opers.push(decrementOper);
} else if(!transaction.lotTransId) {
const operation = InventoryCostLotTracker.query()
.insert({
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
});
} else if (!transaction.lotTransId) {
const operation = InventoryCostLotTracker.query().insert({
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
});
opers.push(operation);
}
});
return Promise.all(opers);
}
}
}

View File

@@ -50,6 +50,29 @@ export default class ItemsEntriesService {
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.
* @async

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import composeAsync from 'async/compose';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -21,7 +22,8 @@ import {
IPaginationMeta,
IFilterMeta,
IBillsFilter,
IBillsService
IBillsService,
IItemEntry,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
@@ -36,7 +38,9 @@ import { ERRORS } from './constants';
* @service
*/
@Service('Bills')
export default class BillsService extends SalesInvoicesCost implements IBillsService {
export default class BillsService
extends SalesInvoicesCost
implements IBillsService {
@Inject()
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.
* @param {number} tenantId
@@ -182,11 +209,7 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
) {
const { ItemEntry } = this.tenancy.models(tenantId);
const entries = billDTO.entries.map((entry) => ({
...entry,
amount: ItemEntry.calcAmount(entry),
}));
const amount = sumBy(entries, 'amount');
const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
// Bill number from DTO or from auto-increment.
const billNumber = billDTO.billNumber || oldBill?.billNumber;
@@ -196,6 +219,15 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
tenantId,
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 {
...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate',
@@ -204,10 +236,7 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
amount,
currencyCode: vendor.currencyCode,
billNumber,
entries: entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
})),
entries,
// Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open &&
!oldBill?.openedAt && {
@@ -239,15 +268,6 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
): Promise<IBill> {
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.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -264,8 +284,18 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
tenantId,
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.
const bill = await billRepository.upsertGraph({ ...billObj });
const bill = await billRepository.upsertGraph(billObj);
// Triggers `onBillCreated` event.
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 });
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.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -340,6 +363,13 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
tenantId,
billDTO.entries
);
// Transforms the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
authorizedUser,
oldBill
);
// Update the bill transaction.
const bill = await billRepository.upsertGraph({
id: billId,
@@ -498,8 +528,8 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
@@ -507,19 +537,25 @@ export default class BillsService extends SalesInvoicesCost implements IBillsSer
bill: IBill,
override?: boolean
): 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(
tenantId,
bill.id,
'Bill',
bill.billDate,
'IN',
transaction,
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} vendorId - Vendor id.
*/
public async validateVendorHasNoBills(
tenantId: number,
vendorId: number
) {
public async validateVendorHasNoBills(tenantId: number, vendorId: number) {
const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().where('vendor_id', vendorId);

View File

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

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy, join } from 'lodash';
import { omit, sumBy, join, entries } from 'lodash';
import moment from 'moment';
import composeAsync from 'async/compose';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -15,7 +16,7 @@ import {
ISystemUser,
IItem,
IItemEntry,
ISalesInvoicesService
ISalesInvoicesService,
} from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
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.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
@@ -212,6 +237,16 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
// Validate the invoice is required.
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 {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
@@ -227,10 +262,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
// Avoid override payment amount in edit mode.
...(!oldSaleInvoice && { paymentAmount: 0 }),
...(invoiceNo ? { invoiceNo } : {}),
entries: saleInvoiceDTO.entries.map((entry) => ({
referenceType: 'SaleInvoice',
...entry,
})),
entries,
};
}
@@ -259,23 +291,11 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
): Promise<ISaleInvoice> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO
);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
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.
if (saleInvoiceDTO.fromEstimateId) {
const fromEstimate = await this.saleEstimatesService.getSaleEstimateOrThrowError(
@@ -295,11 +315,21 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
tenantId,
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.');
const saleInvoice = await saleInvoiceRepository.upsertGraph({
...saleInvoiceObj,
});
const saleInvoice = await saleInvoiceRepository.upsertGraph(saleInvoiceObj);
// Triggers the event `onSaleInvoiceCreated`.
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, {
tenantId,
@@ -337,25 +367,11 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
tenantId,
saleInvoiceId
);
// Transform DTO object to model object.
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo,
saleInvoiceId
);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
@@ -373,6 +389,20 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
'SaleInvoice',
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.
this.validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceObj.balance,
@@ -445,7 +475,8 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
const { ItemEntry } = this.tenancy.models(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(
tenantId,
saleInvoiceId
@@ -497,12 +528,23 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
saleInvoice: ISaleInvoice,
override?: boolean
): 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(
tenantId,
saleInvoice.id,
'SaleInvoice',
saleInvoice.invoiceDate,
'OUT',
transaction,
override
);
}

View File

@@ -1,5 +1,5 @@
import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash';
import { chain, groupBy } from 'lodash';
import moment from 'moment';
import JournalPoster from 'services/Accounting/JournalPoster';
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.
* @param {number} tenantId - The tenant id.
@@ -124,25 +138,28 @@ export default class SaleInvoicesCost {
const inventoryCostLotTrans = await InventoryCostLotTracker.query()
.where('direction', 'OUT')
.modify('groupedEntriesCost')
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.where('cost', '>', 0)
.withGraphFetched('item');
.withGraphFetched('item')
.withGraphFetched('itemEntry');
const accountsDepGraph = await accountRepository.getDependencyGraph();
const journal = new JournalPoster(tenantId, accountsDepGraph);
const journalCommands = new JournalCommands(journal);
// Groups the inventory cost lots transactions.
const inventoryTransactions = this.inventoryTransactionsGroupByType(
inventoryCostLotTrans
);
if (override) {
await journalCommands.revertInventoryCostJournalEntries(startingDate);
}
inventoryCostLotTrans.forEach(
(inventoryCostLot: IInventoryLotCost & { item: IItem }) => {
journalCommands.saleInvoiceInventoryCost(inventoryCostLot);
}
);
inventoryTransactions.forEach((inventoryLots) => {
journalCommands.saleInvoiceInventoryCost(inventoryLots);
});
return Promise.all([
journal.deleteEntries(),
journal.saveEntries(),

View File

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

View File

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