This commit is contained in:
elforjani3
2021-01-11 09:42:17 +02:00
20 changed files with 433 additions and 180 deletions

View File

@@ -115,8 +115,8 @@ export default class InventoryAdjustmentsController extends BaseController {
tenantId, tenantId,
adjustmentId adjustmentId
); );
return res.status(200).send({ return res.status(200).send({
id: adjustmentId,
message: 'The inventory adjustment has been deleted successfully.', message: 'The inventory adjustment has been deleted successfully.',
}); });
} catch (error) { } catch (error) {

View File

@@ -25,14 +25,14 @@ export default class ItemsController extends BaseController {
router.post( router.post(
'/', '/',
[...this.validateItemSchema, ...this.validateNewItemSchema], this.validateItemSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.newItem.bind(this)), asyncMiddleware(this.newItem.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
); );
router.post( router.post(
'/:id/activate', '/:id/activate',
[...this.validateSpecificItemSchema], this.validateSpecificItemSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.activateItem.bind(this)), asyncMiddleware(this.activateItem.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
@@ -83,30 +83,6 @@ export default class ItemsController extends BaseController {
return router; return router;
} }
/**
* New item validation schema.
*/
get validateNewItemSchema(): ValidationChain[] {
return [
check('opening_quantity').default(0).isInt({ min: 0 }).toInt(),
check('opening_cost')
.if(body('opening_quantity').exists().isInt({ min: 1 }))
.exists()
.isFloat(),
check('opening_cost')
.optional({ nullable: true })
.isFloat({ min: 0 })
.toFloat(),
check('opening_date')
.if(
body('opening_quantity').exists().isFloat({ min: 1 }) ||
body('opening_cost').exists().isFloat({ min: 1 })
)
.exists(),
check('opening_date').optional({ nullable: true }).isISO8601().toDate(),
];
}
/** /**
* Validate item schema. * Validate item schema.
*/ */
@@ -503,6 +479,11 @@ export default class ItemsController extends BaseController {
errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }], errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
}); });
} }
if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') {
return res.status(400).send({
errors: [{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 }],
});
}
} }
next(error); next(error);
} }

View File

@@ -18,10 +18,6 @@ exports.up = function (knex) {
table.text('purchase_description').nullable(); table.text('purchase_description').nullable();
table.integer('quantity_on_hand'); table.integer('quantity_on_hand');
table.integer('opening_quantity');
table.decimal('opening_cost', 13, 3);
table.date('opening_date');
table.text('note').nullable(); table.text('note').nullable();
table.boolean('active'); table.boolean('active');
table.integer('category_id').unsigned().index().references('id').inTable('items_categories'); table.integer('category_id').unsigned().index().references('id').inTable('items_categories');

View File

@@ -10,7 +10,8 @@ exports.up = function(knex) {
table.integer('quantity').unsigned(); table.integer('quantity').unsigned();
table.decimal('rate', 13, 3).unsigned(); table.decimal('rate', 13, 3).unsigned();
table.string('lot_number').index(); 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();

View File

@@ -1,5 +1,5 @@
type IAdjustmentTypes = 'increment' | 'decrement' | 'value_adjustment'; type IAdjustmentTypes = 'increment' | 'decrement';
export interface IQuickInventoryAdjustmentDTO { export interface IQuickInventoryAdjustmentDTO {
date: Date | string; date: Date | string;
@@ -20,6 +20,7 @@ export interface IInventoryAdjustment {
reason: string; reason: string;
description: string; description: string;
referenceNo: string; referenceNo: string;
inventoryDirection?: 'IN' | 'OUT',
entries: IInventoryAdjustmentEntry[]; entries: IInventoryAdjustmentEntry[];
userId: number; userId: number;
}; };

View File

@@ -3,14 +3,14 @@ export type TInventoryTransactionDirection = 'IN' | 'OUT';
export interface IInventoryTransaction { export interface IInventoryTransaction {
id?: number, id?: number,
date: Date, date: Date|string,
direction: TInventoryTransactionDirection, direction: TInventoryTransactionDirection,
itemId: number, itemId: number,
quantity: number, quantity: number,
rate: number, rate: number,
transactionType: string, transactionType: string,
transactionId: number, transactionId: number,
lotNumber: string, lotNumber: number,
entryId: number, entryId: number,
createdAt?: Date, createdAt?: Date,
updatedAt?: Date, updatedAt?: Date,
@@ -25,7 +25,7 @@ export interface IInventoryLotCost {
rate: number, rate: number,
remaining: number, remaining: number,
cost: number, cost: number,
lotNumber: string|number, lotNumber: number,
transactionType: string, transactionType: string,
transactionId: number, transactionId: number,
entryId: number entryId: number

View File

@@ -22,10 +22,6 @@ export interface IItem{
quantityOnHand: number, quantityOnHand: number,
openingQuantity: number,
openingCost: number,
openingDate: Date,
note: string, note: string,
active: boolean, active: boolean,
@@ -58,10 +54,6 @@ export interface IItemDTO {
quantityOnHand: number, quantityOnHand: number,
openingQuantity?: number,
openingCost?: number,
openingDate?: Date,
note: string, note: string,
active: boolean, active: boolean,

View File

@@ -23,10 +23,12 @@ import 'subscribers/SaleReceipt/SyncItemsQuantity';
import 'subscribers/SaleReceipt/WriteInventoryTransactions'; import 'subscribers/SaleReceipt/WriteInventoryTransactions';
import 'subscribers/SaleReceipt/WriteJournalEntries'; import 'subscribers/SaleReceipt/WriteJournalEntries';
import 'subscribers/Inventory/Inventory';
import 'subscribers/Inventory/InventoryAdjustment';
import 'subscribers/customers'; import 'subscribers/customers';
import 'subscribers/vendors'; import 'subscribers/vendors';
import 'subscribers/paymentMades'; import 'subscribers/paymentMades';
import 'subscribers/paymentReceives'; import 'subscribers/paymentReceives';
import 'subscribers/saleEstimates'; import 'subscribers/saleEstimates';
import 'subscribers/inventory';
import 'subscribers/items'; import 'subscribers/items';

View File

@@ -35,6 +35,8 @@ import ManualJournal from 'models/ManualJournal';
import ManualJournalEntry from 'models/ManualJournalEntry'; import ManualJournalEntry from 'models/ManualJournalEntry';
import Media from 'models/Media'; import Media from 'models/Media';
import MediaLink from 'models/MediaLink'; import MediaLink from 'models/MediaLink';
import InventoryAdjustment from 'models/InventoryAdjustment';
import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry';
export default (knex) => { export default (knex) => {
const models = { const models = {
@@ -73,6 +75,8 @@ export default (knex) => {
Vendor, Vendor,
Customer, Customer,
Contact, Contact,
InventoryAdjustment,
InventoryAdjustmentEntry,
}; };
return mapValues(models, (model) => model.bindKnex(knex)); return mapValues(models, (model) => model.bindKnex(knex));
} }

View File

@@ -17,6 +17,34 @@ export default class AccountTransaction extends TenantModel {
return ['createdAt']; return ['createdAt'];
} }
static get virtualAttributes() {
return ['referenceTypeFormatted'];
}
/**
* Retrieve formatted reference type.
* @return {string}
*/
get referenceTypeFormatted() {
return AccountTransaction.getReferenceTypeFormatted(this.referenceType);
}
/**
* Reference type formatted.
*/
static getReferenceTypeFormatted(referenceType) {
const mapped = {
'SaleInvoice': 'Sale invoice',
'SaleReceipt': 'Sale receipt',
'PaymentReceive': 'Payment receive',
'BillPayment': 'Payment made',
'VendorOpeningBalance': 'Vendor opening balance',
'CustomerOpeningBalance': 'Customer opening balance',
'InventoryAdjustment': 'Inventory adjustment'
};
return mapped[referenceType] || '';
}
/** /**
* Model modifiers. * Model modifiers.
*/ */

View File

@@ -16,6 +16,28 @@ export default class InventoryAdjustment extends TenantModel {
return ['created_at']; return ['created_at'];
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['inventoryDirection'];
}
/**
* Retrieve formatted reference type.
*/
get inventoryDirection() {
return InventoryAdjustment.getInventoryDirection(this.type);
}
static getInventoryDirection(type) {
const directions = {
'increment': 'IN',
'decrement': 'OUT',
};
return directions[type] || '';
}
/** /**
* Relationship mapping. * Relationship mapping.
*/ */

View File

@@ -231,7 +231,6 @@ export default class JournalCommands {
referenceId: expense.id, referenceId: expense.id,
date: expense.paymentDate, date: expense.paymentDate,
userId, userId,
draft: !expense.publishedAt,
}; };
const paymentJournalEntry = new JournalEntry({ const paymentJournalEntry = new JournalEntry({
credit: expense.totalAmount, credit: expense.totalAmount,
@@ -330,7 +329,6 @@ export default class JournalCommands {
note: entry.note, note: entry.note,
date: manualJournalObj.date, date: manualJournalObj.date,
userId: manualJournalObj.userId, userId: manualJournalObj.userId,
draft: !manualJournalObj.status,
index: entry.index, index: entry.index,
}); });
if (entry.debit) { if (entry.debit) {
@@ -354,7 +352,7 @@ export default class JournalCommands {
inventoryCostLot: IInventoryLotCost & { item: IItem } inventoryCostLot: IInventoryLotCost & { item: IItem }
) { ) {
const commonEntry = { const commonEntry = {
referenceType: 'SaleInvoice', referenceType: inventoryCostLot.transactionType,
referenceId: inventoryCostLot.transactionId, referenceId: inventoryCostLot.transactionId,
date: inventoryCostLot.date, date: inventoryCostLot.date,
}; };

View File

@@ -164,7 +164,7 @@ export default class AccountsService {
* @param {number} accountId * @param {number} accountId
* @return {IAccount} * @return {IAccount}
*/ */
private async getAccountOrThrowError(tenantId: number, accountId: number) { public async getAccountOrThrowError(tenantId: number, accountId: number) {
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating the account existance.', { this.logger.info('[accounts] validating the account existance.', {

View File

@@ -38,17 +38,26 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {IExchangeRateDTO} exchangeRateDTO * @param {IExchangeRateDTO} exchangeRateDTO
* @returns {Promise<IExchangeRate>} * @returns {Promise<IExchangeRate>}
*/ */
public async newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise<IExchangeRate> { public async newExchangeRate(
tenantId: number,
exchangeRateDTO: IExchangeRateDTO
): Promise<IExchangeRate> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying to insert new exchange rate.', { tenantId, exchangeRateDTO }); this.logger.info('[exchange_rates] trying to insert new exchange rate.', {
tenantId,
exchangeRateDTO,
});
await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO); await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO);
const exchangeRate = await ExchangeRate.query().insertAndFetch({ const exchangeRate = await ExchangeRate.query().insertAndFetch({
...exchangeRateDTO, ...exchangeRateDTO,
date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'), date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'),
}); });
this.logger.info('[exchange_rates] inserted successfully.', { tenantId, exchangeRateDTO }); this.logger.info('[exchange_rates] inserted successfully.', {
tenantId,
exchangeRateDTO,
});
return exchangeRate; return exchangeRate;
} }
@@ -58,14 +67,28 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {number} exchangeRateId - Exchange rate id. * @param {number} exchangeRateId - Exchange rate id.
* @param {IExchangeRateEditDTO} editExRateDTO - Edit exchange rate DTO. * @param {IExchangeRateEditDTO} editExRateDTO - Edit exchange rate DTO.
*/ */
public async editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise<void> { public async editExchangeRate(
tenantId: number,
exchangeRateId: number,
editExRateDTO: IExchangeRateEditDTO
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying to edit exchange rate.', { tenantId, exchangeRateId, editExRateDTO }); this.logger.info('[exchange_rates] trying to edit exchange rate.', {
tenantId,
exchangeRateId,
editExRateDTO,
});
await this.validateExchangeRateExistance(tenantId, exchangeRateId); await this.validateExchangeRateExistance(tenantId, exchangeRateId);
await ExchangeRate.query().where('id', exchangeRateId).update({ ...editExRateDTO }); await ExchangeRate.query()
this.logger.info('[exchange_rates] exchange rate edited successfully.', { tenantId, exchangeRateId, editExRateDTO }); .where('id', exchangeRateId)
.update({ ...editExRateDTO });
this.logger.info('[exchange_rates] exchange rate edited successfully.', {
tenantId,
exchangeRateId,
editExRateDTO,
});
} }
/** /**
@@ -73,7 +96,10 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {number} exchangeRateId - Exchange rate id. * @param {number} exchangeRateId - Exchange rate id.
*/ */
public async deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise<void> { public async deleteExchangeRate(
tenantId: number,
exchangeRateId: number
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
await this.validateExchangeRateExistance(tenantId, exchangeRateId); await this.validateExchangeRateExistance(tenantId, exchangeRateId);
@@ -85,10 +111,15 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter. * @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter.
*/ */
public async listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise<void> { public async listExchangeRates(
tenantId: number,
exchangeRateFilter: IExchangeRateFilter
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
const exchangeRates = await ExchangeRate.query() const exchangeRates = await ExchangeRate.query().pagination(
.pagination(exchangeRateFilter.page - 1, exchangeRateFilter.pageSize); exchangeRateFilter.page - 1,
exchangeRateFilter.pageSize
);
return exchangeRates; return exchangeRates;
} }
@@ -98,14 +129,23 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {number} tenantId * @param {number} tenantId
* @param {number[]} exchangeRatesIds * @param {number[]} exchangeRatesIds
*/ */
public async deleteBulkExchangeRates(tenantId: number, exchangeRatesIds: number[]): Promise<void> { public async deleteBulkExchangeRates(
tenantId: number,
exchangeRatesIds: number[]
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying delete in bulk.', { tenantId, exchangeRatesIds }); this.logger.info('[exchange_rates] trying delete in bulk.', {
tenantId,
exchangeRatesIds,
});
await this.validateExchangeRatesIdsExistance(tenantId, exchangeRatesIds); await this.validateExchangeRatesIdsExistance(tenantId, exchangeRatesIds);
await ExchangeRate.query().whereIn('id', exchangeRatesIds).delete(); await ExchangeRate.query().whereIn('id', exchangeRatesIds).delete();
this.logger.info('[exchange_rates] deleted successfully.', { tenantId, exchangeRatesIds }); this.logger.info('[exchange_rates] deleted successfully.', {
tenantId,
exchangeRatesIds,
});
} }
/** /**
@@ -114,16 +154,23 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {IExchangeRateDTO} exchangeRateDTO - Exchange rate DTO. * @param {IExchangeRateDTO} exchangeRateDTO - Exchange rate DTO.
* @return {Promise<void>} * @return {Promise<void>}
*/ */
private async validateExchangeRatePeriodExistance(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise<void> { private async validateExchangeRatePeriodExistance(
tenantId: number,
exchangeRateDTO: IExchangeRateDTO
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying to validate period existance.', { tenantId }); this.logger.info('[exchange_rates] trying to validate period existance.', {
tenantId,
});
const foundExchangeRate = await ExchangeRate.query() const foundExchangeRate = await ExchangeRate.query()
.where('currency_code', exchangeRateDTO.currencyCode) .where('currency_code', exchangeRateDTO.currencyCode)
.where('date', exchangeRateDTO.date); .where('date', exchangeRateDTO.date);
if (foundExchangeRate.length > 0) { if (foundExchangeRate.length > 0) {
this.logger.info('[exchange_rates] given exchange rate period exists.', { tenantId }); this.logger.info('[exchange_rates] given exchange rate period exists.', {
tenantId,
});
throw new ServiceError(ERRORS.EXCHANGE_RATE_PERIOD_EXISTS); throw new ServiceError(ERRORS.EXCHANGE_RATE_PERIOD_EXISTS);
} }
} }
@@ -134,14 +181,25 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {number} exchangeRateId - Exchange rate id. * @param {number} exchangeRateId - Exchange rate id.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateExchangeRateExistance(tenantId: number, exchangeRateId: number) { private async validateExchangeRateExistance(
tenantId: number,
exchangeRateId: number
) {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying to validate exchange rate id existance.', { tenantId, exchangeRateId }); this.logger.info(
const foundExchangeRate = await ExchangeRate.query().findById(exchangeRateId); '[exchange_rates] trying to validate exchange rate id existance.',
{ tenantId, exchangeRateId }
);
const foundExchangeRate = await ExchangeRate.query().findById(
exchangeRateId
);
if (!foundExchangeRate) { if (!foundExchangeRate) {
this.logger.info('[exchange_rates] exchange rate not found.', { tenantId, exchangeRateId }); this.logger.info('[exchange_rates] exchange rate not found.', {
tenantId,
exchangeRateId,
});
throw new ServiceError(ERRORS.EXCHANGE_RATE_NOT_FOUND); throw new ServiceError(ERRORS.EXCHANGE_RATE_NOT_FOUND);
} }
} }
@@ -152,12 +210,23 @@ export default class ExchangeRatesService implements IExchangeRatesService {
* @param {number[]} exchangeRatesIds - Exchange rates ids. * @param {number[]} exchangeRatesIds - Exchange rates ids.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateExchangeRatesIdsExistance(tenantId: number, exchangeRatesIds: number[]): Promise<void> { private async validateExchangeRatesIdsExistance(
tenantId: number,
exchangeRatesIds: number[]
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId); const { ExchangeRate } = this.tenancy.models(tenantId);
const storedExchangeRates = await ExchangeRate.query().whereIn('id', exchangeRatesIds); const storedExchangeRates = await ExchangeRate.query().whereIn(
const storedExchangeRatesIds = storedExchangeRates.map((category) => category.id); 'id',
const notFoundExRates = difference(exchangeRatesIds, storedExchangeRatesIds); exchangeRatesIds
);
const storedExchangeRatesIds = storedExchangeRates.map(
(category) => category.id
);
const notFoundExRates = difference(
exchangeRatesIds,
storedExchangeRatesIds
);
if (notFoundExRates.length > 0) { if (notFoundExRates.length > 0) {
throw new ServiceError(ERRORS.NOT_FOUND_EXCHANGE_RATES); throw new ServiceError(ERRORS.NOT_FOUND_EXCHANGE_RATES);

View File

@@ -39,7 +39,7 @@ export default class InventoryService {
direction: TInventoryTransactionDirection, direction: TInventoryTransactionDirection,
date: Date | string, date: Date | string,
lotNumber: number lotNumber: number
) { ): IInventoryTransaction[] {
return itemEntries.map((entry: IItemEntry) => ({ return itemEntries.map((entry: IItemEntry) => ({
...pick(entry, ['itemId', 'quantity', 'rate']), ...pick(entry, ['itemId', 'quantity', 'rate']),
lotNumber, lotNumber,
@@ -150,12 +150,29 @@ export default class InventoryService {
*/ */
async recordInventoryTransactions( async recordInventoryTransactions(
tenantId: number, tenantId: number,
inventoryEntries: IInventoryTransaction[], transactions: IInventoryTransaction[],
deleteOld: boolean override: boolean = false
): Promise<void> { ): Promise<void> {
inventoryEntries.forEach(async (entry: IInventoryTransaction) => { const bulkInsertOpers = [];
await this.recordInventoryTransaction(tenantId, entry, deleteOld);
transactions.forEach((transaction: IInventoryTransaction) => {
const oper = this.recordInventoryTransaction(
tenantId,
transaction,
override
);
bulkInsertOpers.push(oper);
}); });
const inventoryTransactions = await Promise.all(bulkInsertOpers);
// Triggers `onInventoryTransactionsCreated` event.
this.eventDispatcher.dispatch(
events.inventory.onInventoryTransactionsCreated,
{
tenantId,
inventoryTransactions,
}
);
} }
/** /**
@@ -203,7 +220,7 @@ export default class InventoryService {
transactionDirection: TInventoryTransactionDirection, transactionDirection: TInventoryTransactionDirection,
override: boolean = false override: boolean = false
): Promise<void> { ): Promise<void> {
// Gets the next inventory lot number. // Retrieve the next inventory lot number.
const lotNumber = this.getNextLotNumber(tenantId); const lotNumber = this.getNextLotNumber(tenantId);
// Loads the inventory items entries of the given sale invoice. // Loads the inventory items entries of the given sale invoice.
@@ -231,20 +248,6 @@ export default class InventoryService {
); );
// Increment and save the next lot number settings. // Increment and save the next lot number settings.
await this.incrementNextLotNumber(tenantId); await this.incrementNextLotNumber(tenantId);
// Triggers `onInventoryTransactionsCreated` event.
this.eventDispatcher.dispatch(
events.inventory.onInventoryTransactionsCreated,
{
tenantId,
inventoryEntries,
transactionId,
transactionType,
transactionDate,
transactionDirection,
override,
}
);
} }
/** /**
@@ -253,7 +256,7 @@ export default class InventoryService {
* @param {string} transactionType * @param {string} transactionType
* @param {number} transactionId * @param {number} transactionId
* @return {Promise<{ * @return {Promise<{
* oldInventoryTransactions: IInventoryTransaction[] * oldInventoryTransactions: IInventoryTransaction[]
* }>} * }>}
*/ */
async deleteInventoryTransactions( async deleteInventoryTransactions(
@@ -261,7 +264,9 @@ export default class InventoryService {
transactionId: number, transactionId: number,
transactionType: string transactionType: string
): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> { ): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> {
const { inventoryTransactionRepository } = this.tenancy.repositories(tenantId); const { inventoryTransactionRepository } = this.tenancy.repositories(
tenantId
);
// Retrieve the inventory transactions of the given sale invoice. // Retrieve the inventory transactions of the given sale invoice.
const oldInventoryTransactions = await inventoryTransactionRepository.find({ const oldInventoryTransactions = await inventoryTransactionRepository.find({

View File

@@ -11,11 +11,13 @@ import {
IPaginationMeta, IPaginationMeta,
IInventoryAdjustmentsFilter, IInventoryAdjustmentsFilter,
ISystemUser, ISystemUser,
IInventoryTransaction,
} from 'interfaces'; } from 'interfaces';
import events from 'subscribers/events'; import events from 'subscribers/events';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import HasTenancyService from 'services/Tenancy/TenancyService'; import HasTenancyService from 'services/Tenancy/TenancyService';
import InventoryService from './Inventory';
const ERRORS = { const ERRORS = {
INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND', INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND',
@@ -39,6 +41,9 @@ export default class InventoryAdjustmentService {
@EventDispatcher() @EventDispatcher()
eventDispatcher: EventDispatcherInterface; eventDispatcher: EventDispatcherInterface;
@Inject()
inventoryService: InventoryService;
/** /**
* Transformes the quick inventory adjustment DTO to model object. * Transformes the quick inventory adjustment DTO to model object.
* @param {IQuickInventoryAdjustmentDTO} adjustmentDTO - * @param {IQuickInventoryAdjustmentDTO} adjustmentDTO -
@@ -55,14 +60,15 @@ export default class InventoryAdjustmentService {
{ {
index: 1, index: 1,
itemId: adjustmentDTO.itemId, itemId: adjustmentDTO.itemId,
...(['increment', 'decrement'].indexOf(adjustmentDTO.type) !== -1
? {
quantity: adjustmentDTO.quantity,
}
: {}),
...('increment' === adjustmentDTO.type ...('increment' === adjustmentDTO.type
? { ? {
cost: adjustmentDTO.cost, quantity: adjustmentDTO.quantity,
cost: adjustmentDTO.cost,
}
: {}),
...('decrement' === adjustmentDTO.type
? {
quantity: adjustmentDTO.quantity,
} }
: {}), : {}),
}, },
@@ -138,6 +144,7 @@ export default class InventoryAdjustmentService {
quickAdjustmentDTO, quickAdjustmentDTO,
authorizedUser authorizedUser
); );
// Saves the inventory adjustment with assocaited entries to the storage.
const inventoryAdjustment = await InventoryAdjustment.query().upsertGraph({ const inventoryAdjustment = await InventoryAdjustment.query().upsertGraph({
...invAdjustmentObject, ...invAdjustmentObject,
}); });
@@ -146,6 +153,8 @@ export default class InventoryAdjustmentService {
events.inventoryAdjustment.onQuickCreated, events.inventoryAdjustment.onQuickCreated,
{ {
tenantId, tenantId,
inventoryAdjustment,
inventoryAdjustmentId: inventoryAdjustment.id,
} }
); );
this.logger.info( this.logger.info(
@@ -192,6 +201,7 @@ export default class InventoryAdjustmentService {
// Triggers `onInventoryAdjustmentDeleted` event. // Triggers `onInventoryAdjustmentDeleted` event.
await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, { await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, {
tenantId, tenantId,
inventoryAdjustmentId,
}); });
this.logger.info( this.logger.info(
'[inventory_adjustment] the adjustment deleted successfully.', '[inventory_adjustment] the adjustment deleted successfully.',
@@ -226,4 +236,61 @@ export default class InventoryAdjustmentService {
pagination, pagination,
}; };
} }
/**
* Writes the inventory transactions from the inventory adjustment transaction.
* @param {number} tenantId
* @param {IInventoryAdjustment} inventoryAdjustment
* @return {Promise<void>}
*/
async writeInventoryTransactions(
tenantId: number,
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,
};
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.
this.inventoryService.recordInventoryTransactions(
tenantId,
inventoryTransactions,
override
);
// Increment and save the next lot number settings.
await this.inventoryService.incrementNextLotNumber(tenantId);
}
/**
* Reverts the inventory transactions from the inventory adjustment transaction.
* @param {number} tenantId
* @param {number} inventoryAdjustmentId
*/
async revertInventoryTransactions(
tenantId: number,
inventoryAdjustmentId: number
): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> {
return this.inventoryService.deleteInventoryTransactions(
tenantId,
inventoryAdjustmentId,
'InventoryAdjustment'
);
}
} }

View File

@@ -10,6 +10,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry';
const ERRORS = { const ERRORS = {
NOT_FOUND: 'NOT_FOUND', NOT_FOUND: 'NOT_FOUND',
@@ -27,6 +28,8 @@ const ERRORS = {
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
}; };
@Service() @Service()
@@ -52,7 +55,7 @@ export default class ItemsService implements IItemsService {
* @param {number} itemId * @param {number} itemId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
private async getItemOrThrowError( public async getItemOrThrowError(
tenantId: number, tenantId: number,
itemId: number itemId: number
): Promise<void> { ): Promise<void> {
@@ -235,16 +238,10 @@ export default class ItemsService implements IItemsService {
* @return {IItem} * @return {IItem}
*/ */
private transformNewItemDTOToModel(itemDTO: IItemDTO) { private transformNewItemDTOToModel(itemDTO: IItemDTO) {
const inventoryAttrs = ['openingQuantity', 'openingCost', 'openingDate'];
return { return {
...omit(itemDTO, inventoryAttrs), ...itemDTO,
...(itemDTO.type === 'inventory' ? pick(itemDTO, inventoryAttrs) : {}),
active: defaultTo(itemDTO.active, 1), active: defaultTo(itemDTO.active, 1),
quantityOnHand: quantityOnHand: itemDTO.type === 'inventory' ? 0 : null,
itemDTO.type === 'inventory'
? defaultTo(itemDTO.openingQuantity, 0)
: null,
}; };
} }
@@ -282,7 +279,7 @@ export default class ItemsService implements IItemsService {
); );
} }
const item = await Item.query().insertAndFetch({ const item = await Item.query().insertAndFetch({
...this.transformNewItemDTOToModel(itemDTO) ...this.transformNewItemDTOToModel(itemDTO),
}); });
this.logger.info('[items] item inserted successfully.', { this.logger.info('[items] item inserted successfully.', {
tenantId, tenantId,
@@ -297,46 +294,6 @@ export default class ItemsService implements IItemsService {
return item; return item;
} }
/**
* Records the opening items inventory transaction.
* @param {number} tenantId -
* @param itemId -
* @param openingQuantity -
* @param openingCost -
* @param openingDate -
*/
public async recordOpeningItemsInventoryTransaction(
tenantId: number,
itemId: number,
openingQuantity: number,
openingCost: number,
openingDate: Date
): Promise<void> {
// Gets the next inventory lot number.
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
// Records the inventory transaction.
const inventoryTransaction = await this.inventoryService.recordInventoryTransaction(
tenantId,
{
date: openingDate,
quantity: openingQuantity,
rate: openingCost,
direction: 'IN',
transactionType: 'OpeningItem',
itemId,
lotNumber,
}
);
// Records the inventory cost lot transaction.
await this.inventoryService.recordInventoryCostLotTransaction(tenantId, {
...omit(inventoryTransaction, ['updatedAt', 'createdAt']),
cost: openingQuantity * openingCost,
remaining: 0,
});
await this.inventoryService.incrementNextLotNumber(tenantId);
}
/** /**
* Edits the item metadata. * Edits the item metadata.
* @param {number} tenantId * @param {number} tenantId
@@ -398,16 +355,19 @@ export default class ItemsService implements IItemsService {
*/ */
public async deleteItem(tenantId: number, itemId: number) { public async deleteItem(tenantId: number, itemId: number) {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
this.logger.info('[items] trying to delete item.', { tenantId, itemId }); this.logger.info('[items] trying to delete item.', { tenantId, itemId });
// Retreive the given item or throw not found service error. // Retreive the given item or throw not found service error.
await this.getItemOrThrowError(tenantId, itemId); await this.getItemOrThrowError(tenantId, itemId);
// Validate the item has no associated inventory transactions.
await this.validateHasNoInventoryAdjustments(tenantId, itemId);
// Validate the item has no associated invoices or bills. // Validate the item has no associated invoices or bills.
await this.validateHasNoInvoicesOrBills(tenantId, itemId); await this.validateHasNoInvoicesOrBills(tenantId, itemId);
await Item.query().findById(itemId).delete(); await Item.query().findById(itemId).delete();
this.logger.info('[items] deleted successfully.', { tenantId, itemId }); this.logger.info('[items] deleted successfully.', { tenantId, itemId });
} }
@@ -518,6 +478,9 @@ export default class ItemsService implements IItemsService {
// Validates the given items exist on the storage. // Validates the given items exist on the storage.
await this.validateItemsIdsExists(tenantId, itemsIds); await this.validateItemsIdsExists(tenantId, itemsIds);
// Validate the item has no associated inventory transactions.
await this.validateHasNoInventoryAdjustments(tenantId, itemsIds);
// Validate the items have no associated invoices or bills. // Validate the items have no associated invoices or bills.
await this.validateHasNoInvoicesOrBills(tenantId, itemsIds); await this.validateHasNoInvoicesOrBills(tenantId, itemsIds);
@@ -541,7 +504,6 @@ export default class ItemsService implements IItemsService {
Item, Item,
itemsFilter itemsFilter
); );
const { results, pagination } = await Item.query() const { results, pagination } = await Item.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('inventoryAccount'); builder.withGraphFetched('inventoryAccount');
@@ -584,4 +546,24 @@ export default class ItemsService implements IItemsService {
); );
} }
} }
/**
* Validates the given item has no associated inventory adjustment transactions.
* @param {number} tenantId -
* @param {number} itemId -
*/
private async validateHasNoInventoryAdjustments(
tenantId: number,
itemId: number[] | number,
): Promise<void> {
const { InventoryAdjustmentEntry } = this.tenancy.models(tenantId);
const itemsIds = Array.isArray(itemId) ? itemId : [itemId];
const inventoryAdjEntries = await InventoryAdjustmentEntry.query()
.whereIn('item_id', itemsIds);
if (inventoryAdjEntries.length > 0) {
throw new ServiceError(ERRORS.ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT);
}
}
} }

View File

@@ -1,8 +1,10 @@
import { Container, Service, Inject } from 'typedi'; import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash';
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';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { IInventoryLotCost, IItem } from 'interfaces'; import { IInventoryLotCost, IInventoryTransaction, IItem } from 'interfaces';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
@Service() @Service()
@@ -24,7 +26,7 @@ export default class SaleInvoicesCost {
tenantId: number, tenantId: number,
inventoryItemsIds: number[], inventoryItemsIds: number[],
startingDate: Date startingDate: Date
) { ): Promise<void> {
const asyncOpers: Promise<[]>[] = []; const asyncOpers: Promise<[]>[] = [];
inventoryItemsIds.forEach((inventoryItemId: number) => { inventoryItemsIds.forEach((inventoryItemId: number) => {
@@ -35,7 +37,61 @@ export default class SaleInvoicesCost {
); );
asyncOpers.push(oper); asyncOpers.push(oper);
}); });
return Promise.all([...asyncOpers]); await Promise.all([...asyncOpers]);
}
/**
* Retrieve the max dated inventory transactions in the transactions that
* have the same item id.
* @param {IInventoryTransaction[]} inventoryTransactions
* @return {IInventoryTransaction[]}
*/
getMaxDateInventoryTransactions(
inventoryTransactions: IInventoryTransaction[]
): IInventoryTransaction[] {
return chain(inventoryTransactions)
.reduce((acc: any, transaction) => {
const compatatorDate = acc[transaction.itemId];
if (
!compatatorDate ||
moment(compatatorDate.date).isBefore(transaction.date)
) {
return {
...acc,
[transaction.itemId]: {
...transaction,
},
};
}
return acc;
}, {})
.values()
.value();
}
/**
* Computes items costs by the given inventory transaction.
* @param {number} tenantId
* @param {IInventoryTransaction[]} inventoryTransactions
*/
async computeItemsCostByInventoryTransactions(
tenantId: number,
inventoryTransactions: IInventoryTransaction[]
) {
const asyncOpers: Promise<[]>[] = [];
const reducedTransactions = this.getMaxDateInventoryTransactions(
inventoryTransactions
);
reducedTransactions.forEach((transaction) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
transaction.itemId,
transaction.date
);
asyncOpers.push(oper);
});
await Promise.all([...asyncOpers]);
} }
/** /**
@@ -90,7 +146,7 @@ export default class SaleInvoicesCost {
return Promise.all([ return Promise.all([
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), journal.saveEntries(),
journal.saveBalance() journal.saveBalance(),
]); ]);
} }
} }

View File

@@ -43,19 +43,13 @@ export class InventorySubscriber {
@On(events.inventory.onInventoryTransactionsCreated) @On(events.inventory.onInventoryTransactionsCreated)
async handleScheduleItemsCostOnInventoryTransactionsCreated({ async handleScheduleItemsCostOnInventoryTransactionsCreated({
tenantId, tenantId,
inventoryEntries, inventoryTransactions
transactionId,
transactionType,
transactionDate,
transactionDirection,
override
}) { }) {
const inventoryItemsIds = map(inventoryEntries, 'itemId'); const inventoryItemsIds = map(inventoryTransactions, 'itemId');
await this.saleInvoicesCost.scheduleComputeCostByItemsIds( await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
tenantId, tenantId,
inventoryItemsIds, inventoryTransactions
transactionDate,
); );
} }
@@ -69,6 +63,12 @@ export class InventorySubscriber {
transactionId, transactionId,
oldInventoryTransactions oldInventoryTransactions
}) { }) {
// Ignore compute item cost with theses transaction types.
const ignoreWithTransactionTypes = ['OpeningItem'];
if (ignoreWithTransactionTypes.indexOf(transactionType) !== -1) {
return;
}
const inventoryItemsIds = map(oldInventoryTransactions, 'itemId'); const inventoryItemsIds = map(oldInventoryTransactions, 'itemId');
const startingDates = map(oldInventoryTransactions, 'date'); const startingDates = map(oldInventoryTransactions, 'date');
const startingDate = head(startingDates); const startingDate = head(startingDates);

View File

@@ -0,0 +1,49 @@
import { Container } from 'typedi';
import { On, EventSubscriber } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import InventoryAdjustmentService from 'services/Inventory/InventoryAdjustmentService';
@EventSubscriber()
export default class InventoryAdjustmentsSubscriber {
logger: any;
tenancy: TenancyService;
inventoryAdjustment: InventoryAdjustmentService;
/**
* Constructor method.
*/
constructor() {
this.logger = Container.get('logger');
this.tenancy = Container.get(TenancyService);
this.inventoryAdjustment = Container.get(InventoryAdjustmentService);
}
/**
* Handles writing inventory transactions once the quick adjustment created.
*/
@On(events.inventoryAdjustment.onQuickCreated)
async handleWriteInventoryTransactionsOnceQuickCreated({
tenantId,
inventoryAdjustment,
}) {
await this.inventoryAdjustment.writeInventoryTransactions(
tenantId,
inventoryAdjustment
)
}
/**
* Handles reverting invetory transactions once the inventory adjustment deleted.
*/
@On(events.inventoryAdjustment.onDeleted)
async handleRevertInventoryTransactionsOnceDeleted({
tenantId,
inventoryAdjustmentId
}) {
await this.inventoryAdjustment.revertInventoryTransactions(
tenantId,
inventoryAdjustmentId,
);
}
}