fix: Date format in sales/purchases APIs.

fix: Algorithm FIFO cost calculate method.
This commit is contained in:
Ahmed Bouhuolia
2020-08-19 00:13:53 +02:00
parent a202a21df5
commit 52d01b4ed8
21 changed files with 291 additions and 133 deletions

View File

@@ -7,7 +7,7 @@
"build": "webpack", "build": "webpack",
"start": "npm-run-all --parallel watch:server watch:build", "start": "npm-run-all --parallel watch:server watch:build",
"watch:build": "webpack --watch", "watch:build": "webpack --watch",
"watch:server": "nodemon \"./dist/bundle.js\" --watch \"./dist\" ", "watch:server": "nodemon --inspect=\"9229\" \"./dist/bundle.js\" --watch \"./dist\" ",
"test": "cross-env NODE_ENV=test mocha-webpack --webpack-config webpack.config.js \"tests/**/*.test.js\"", "test": "cross-env NODE_ENV=test mocha-webpack --webpack-config webpack.config.js \"tests/**/*.test.js\"",
"test:watch": "cross-env NODE_ENV=test mocha-webpack --watch --webpack-config webpack.config.js --timeout=30000 tests/**/*.test.js" "test:watch": "cross-env NODE_ENV=test mocha-webpack --watch --webpack-config webpack.config.js --timeout=30000 tests/**/*.test.js"
}, },

View File

@@ -15,6 +15,7 @@ exports.up = function(knex) {
table.decimal('balance', 13, 3); table.decimal('balance', 13, 3);
table.decimal('payment_amount', 13, 3); table.decimal('payment_amount', 13, 3);
table.string('inv_lot_number');
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -13,6 +13,7 @@ exports.up = function(knex) {
table.decimal('amount', 13, 3).defaultTo(0); table.decimal('amount', 13, 3).defaultTo(0);
table.decimal('payment_amount', 13, 3).defaultTo(0); table.decimal('payment_amount', 13, 3).defaultTo(0);
table.string('inv_lot_number');
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -10,6 +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.integer('lot_number');
table.string('transaction_type'); table.string('transaction_type');
table.integer('transaction_id'); table.integer('transaction_id');

View File

@@ -10,7 +10,7 @@ exports.up = function(knex) {
table.integer('quantity').unsigned(); table.integer('quantity').unsigned();
table.decimal('rate', 13, 3); table.decimal('rate', 13, 3);
table.integer('remaining'); table.integer('remaining');
table.string('lot_number'); table.integer('lot_number');
table.string('transaction_type'); table.string('transaction_type');
table.integer('transaction_id'); table.integer('transaction_id');

View File

@@ -0,0 +1,35 @@
import { camelCase, snakeCase } from 'lodash';
/**
* create a middleware to change json format from snake case to camelcase in request
* then change back to snake case in response
*
*/
export default function createMiddleware() {
return function (req, res, next) {
/**
* camelize req.body
*/
if (req.body && typeof req.body === 'object') {
req.body = camelCase(req.body);
}
/**
* camelize req.query
*/
if (req.query && typeof req.query === 'object') {
req.query = camelCase(req.query);
}
/**
* wrap res.json()
*/
const sendJson = res.json;
res.json = (data) => {
return sendJson.call(res, snakeCase(data));
}
return next();
}
}

View File

@@ -6,7 +6,14 @@ export default class ComputeItemCostJob {
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const { startingDate, itemId, costMethod } = job.attrs.data; const { startingDate, itemId, costMethod } = job.attrs.data;
try {
await InventoryService.computeItemCost(startingDate, itemId, costMethod); await InventoryService.computeItemCost(startingDate, itemId, costMethod);
Logger.log(`Compute item cost: ${job.attrs.data}`);
done(); done();
} catch(e) {
Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`);
done(e);
}
} }
} }

View File

@@ -12,7 +12,7 @@ export default class InventoryTransaction extends TenantModel {
/** /**
* Model timestamps. * Model timestamps.
*/ */
static get timestamps() { get timestamps() {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }

View File

@@ -1,5 +1,6 @@
import Customer from './Customer'; import Customer from './Customer';
import Vendor from './Vendor'; import Vendor from './Vendor';
import Option from './Option';
import SaleEstimate from './SaleEstimate'; import SaleEstimate from './SaleEstimate';
import SaleEstimateEntry from './SaleEstimateEntry'; import SaleEstimateEntry from './SaleEstimateEntry';
import SaleReceipt from './SaleReceipt'; import SaleReceipt from './SaleReceipt';
@@ -44,4 +45,5 @@ export {
InventoryTransaction, InventoryTransaction,
InventoryLotCostTracker, InventoryLotCostTracker,
AccountType, AccountType,
Option,
}; };

View File

@@ -45,8 +45,6 @@ export default class JournalCommands{
.map((groupedTrans: IInventoryTransaction[], transType: string) => [groupedTrans, transType]) .map((groupedTrans: IInventoryTransaction[], transType: string) => [groupedTrans, transType])
.value(); .value();
console.log(groupedInvTransactions);
return Promise.all( return Promise.all(
groupedInvTransactions.map(async (grouped: [IInventoryTransaction[], string]) => { groupedInvTransactions.map(async (grouped: [IInventoryTransaction[], string]) => {
const [invTransGroup, referenceType] = grouped; const [invTransGroup, referenceType] = grouped;
@@ -58,8 +56,6 @@ export default class JournalCommands{
.whereIn('reference_id', referencesIds) .whereIn('reference_id', referencesIds)
.withGraphFetched('account.type'); .withGraphFetched('account.type');
console.log(_transactions, referencesIds);
if (_transactions.length > 0) { if (_transactions.length > 0) {
this.journal.loadEntries(_transactions); this.journal.loadEntries(_transactions);
this.journal.removeEntries(_transactions.map((t: any) => t.id)); this.journal.removeEntries(_transactions.map((t: any) => t.id));

View File

@@ -1,9 +1,11 @@
import { import {
InventoryTransaction, InventoryTransaction,
Item Item,
Option,
} from '@/models'; } from '@/models';
import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost'; import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost';
import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker'; import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker';
import { option } from 'commander';
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
@@ -38,10 +40,7 @@ export default class InventoryService {
*/ */
static async recordInventoryTransactions( static async recordInventoryTransactions(
entries: [], entries: [],
date: Date, deleteOld: boolean,
transactionType: string,
transactionId: number,
direction: string,
) { ) {
const storedOpers: any = []; const storedOpers: any = [];
const entriesItemsIds = entries.map((e: any) => e.item_id); const entriesItemsIds = entries.map((e: any) => e.item_id);
@@ -56,19 +55,22 @@ export default class InventoryService {
const inventoryEntries = entries.filter( const inventoryEntries = entries.filter(
(entry: any) => inventoryItemsIds.indexOf(entry.item_id) !== -1 (entry: any) => inventoryItemsIds.indexOf(entry.item_id) !== -1
); );
inventoryEntries.forEach((entry: any) => { inventoryEntries.forEach(async (entry: any) => {
if (deleteOld) {
await this.deleteInventoryTransactions(
entry.transactionId,
entry.transactionType,
);
}
const oper = InventoryTransaction.tenant().query().insert({ const oper = InventoryTransaction.tenant().query().insert({
date, ...entry,
direction, lotNumber: entry.lotNumber,
item_id: entry.item_id,
quantity: entry.quantity,
rate: entry.rate,
transaction_type: transactionType,
transaction_id: transactionId,
}); });
storedOpers.push(oper); storedOpers.push(oper);
}); });
return Promise.all(storedOpers); return Promise.all([
...storedOpers,
]);
} }
/** /**
@@ -90,4 +92,24 @@ export default class InventoryService {
revertInventoryLotsCost(fromDate?: Date) { revertInventoryLotsCost(fromDate?: Date) {
} }
/**
* Retrieve the lot number after the increment.
*/
static async nextLotNumber() {
const LOT_NUMBER_KEY = 'lot_number_increment';
const effectRows = await Option.tenant().query()
.where('key', LOT_NUMBER_KEY)
.increment('value', 1);
if (effectRows) {
await Option.tenant().query()
.insert({
key: LOT_NUMBER_KEY,
value: 1,
});
}
const options = await Option.tenant().query();
return options.getMeta(LOT_NUMBER_KEY, 1);
}
} }

View File

@@ -1,5 +1,4 @@
import { omit, pick, chain } from 'lodash'; import { omit, pick, chain } from 'lodash';
import uniqid from 'uniqid';
import { import {
InventoryTransaction, InventoryTransaction,
InventoryLotCostTracker, InventoryLotCostTracker,
@@ -62,6 +61,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
.query() .query()
.where('date', '>=', this.startingDate) .where('date', '>=', this.startingDate)
.orderBy('date', 'ASC') .orderBy('date', 'ASC')
.orderBy('lot_number', 'ASC')
.where('item_id', this.itemId) .where('item_id', this.itemId)
.withGraphFetched('item'); .withGraphFetched('item');
@@ -70,6 +70,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
.query() .query()
.where('date', '<', this.startingDate) .where('date', '<', this.startingDate)
.orderBy('date', 'ASC') .orderBy('date', 'ASC')
.orderBy('lot_number', 'ASC')
.where('item_id', this.itemId) .where('item_id', this.itemId)
.where('direction', 'IN') .where('direction', 'IN')
.whereNot('remaining', 0); .whereNot('remaining', 0);
@@ -267,17 +268,16 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
...commonLotTransaction, ...commonLotTransaction,
decrement: 0, decrement: 0,
remaining: commonLotTransaction.remaining || commonLotTransaction.quantity, remaining: commonLotTransaction.remaining || commonLotTransaction.quantity,
lotNumber: commonLotTransaction.lotNumber || uniqid.time(),
}; };
costLotsTransactions.push(inventoryINTrans[id]); costLotsTransactions.push(inventoryINTrans[id]);
// Record inventory 'OUT' cost lots from 'IN' transactions. // Record inventory 'OUT' cost lots from 'IN' transactions.
} else if (transaction.direction === 'OUT') { } else if (transaction.direction === 'OUT') {
let invRemaining = transaction.quantity; let invRemaining = transaction.quantity;
const idsShouldDel: number[] = [];
inventoryByItem?.[itemId]?.some(( inventoryByItem?.[itemId]?.some((
_invTransactionId: number, _invTransactionId: number,
index: number,
) => { ) => {
const _invINTransaction = inventoryINTrans[_invTransactionId]; const _invINTransaction = inventoryINTrans[_invTransactionId];
if (invRemaining <= 0) { return true; } if (invRemaining <= 0) { return true; }
@@ -285,22 +285,23 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
// Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction. // Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction.
const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0; const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0;
const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining; const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining;
const maxDecrement = Math.min(decrement, invRemaining);
_invINTransaction.decrement += decrement; _invINTransaction.decrement += maxDecrement;
_invINTransaction.remaining = Math.max( _invINTransaction.remaining = Math.max(
_invINTransaction.remaining - decrement, _invINTransaction.remaining - maxDecrement,
0, 0,
); );
invRemaining = Math.max(invRemaining - decrement, 0); invRemaining = Math.max(invRemaining - maxDecrement, 0);
costLotsTransactions.push({ costLotsTransactions.push({
...commonLotTransaction, ...commonLotTransaction,
quantity: decrement, quantity: maxDecrement,
lotNumber: _invINTransaction.lotNumber, lotNumber: _invINTransaction.lotNumber,
}); });
// Pop the 'IN' lots that has zero remaining. // Pop the 'IN' lots that has zero remaining.
if (_invINTransaction.remaining === 0) { if (_invINTransaction.remaining === 0) {
inventoryByItem?.[itemId].splice(index, 1); idsShouldDel.push(_invTransactionId);
} }
return false; return false;
}); });
@@ -310,6 +311,9 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
quantity: invRemaining, quantity: invRemaining,
}); });
} }
// Remove the IN transactions that has zero remaining amount.
inventoryByItem[itemId] = inventoryByItem?.[itemId]
?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1);
} }
}); });
return costLotsTransactions; return costLotsTransactions;

View File

@@ -14,6 +14,7 @@ import AccountsService from '@/services/Accounts/AccountsService';
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry'; import JournalEntry from '@/services/Accounting/JournalEntry';
import JournalPosterService from '@/services/Sales/JournalPosterService'; import JournalPosterService from '@/services/Sales/JournalPosterService';
import { formatDateFields } from '@/utils';
/** /**
* Bill payments service. * Bill payments service.
@@ -32,14 +33,16 @@ export default class BillPaymentsService {
* - Decrement the vendor balance. * - Decrement the vendor balance.
* - Records payment journal entries. * - Records payment journal entries.
* *
* @param {IBillPayment} billPayment * @param {BillPaymentDTO} billPayment
*/ */
static async createBillPayment(billPayment) { static async createBillPayment(billPaymentDTO) {
const amount = sumBy(billPayment.entries, 'payment_amount'); const billPayment = {
amount: sumBy(billPaymentDTO.entries, 'payment_amount'),
...formatDateFields(billPaymentDTO, ['payment_date']),
}
const storedBillPayment = await BillPayment.tenant() const storedBillPayment = await BillPayment.tenant()
.query() .query()
.insert({ .insert({
amount,
...omit(billPayment, ['entries']), ...omit(billPayment, ['entries']),
}); });
const storeOpers = []; const storeOpers = [];
@@ -62,7 +65,7 @@ export default class BillPaymentsService {
// Decrement the vendor balance after bills payments. // Decrement the vendor balance after bills payments.
const vendorDecrementOper = Vendor.changeBalance( const vendorDecrementOper = Vendor.changeBalance(
billPayment.vendor_id, billPayment.vendor_id,
amount * -1, billPayment.amount * -1,
); );
// Records the journal transactions after bills payment // Records the journal transactions after bills payment
// and change diff acoount balance. // and change diff acoount balance.
@@ -92,24 +95,24 @@ export default class BillPaymentsService {
* - Update the diff bill payment amount. * - Update the diff bill payment amount.
* *
* @param {Integer} billPaymentId * @param {Integer} billPaymentId
* @param {IBillPayment} billPayment * @param {BillPaymentDTO} billPayment
* @param {IBillPayment} oldBillPayment * @param {IBillPayment} oldBillPayment
*/ */
static async editBillPayment(billPaymentId, billPayment, oldBillPayment) { static async editBillPayment(billPaymentId, billPaymentDTO, oldBillPayment) {
const amount = sumBy(billPayment.entries, 'payment_amount'); const billPayment = {
amount: sumBy(billPaymentDTO.entries, 'payment_amount'),
...formatDateFields(billPaymentDTO, ['payment_date']),
};
const updateBillPayment = await BillPayment.tenant() const updateBillPayment = await BillPayment.tenant()
.query() .query()
.where('id', billPaymentId) .where('id', billPaymentId)
.update({ .update({
amount,
...omit(billPayment, ['entries']), ...omit(billPayment, ['entries']),
}); });
const opers = []; const opers = [];
const entriesHasIds = billpayment.entries.filter((i) => i.id); const entriesHasIds = billPayment.entries.filter((i) => i.id);
const entriesHasNoIds = billPayment.entries.filter((e) => !e.id); const entriesHasNoIds = billPayment.entries.filter((e) => !e.id);
const entriesIds = entriesHasIds.map((e) => e.id);
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted(
oldBillPayment.entries, oldBillPayment.entries,
entriesHasIds entriesHasIds

View File

@@ -1,4 +1,4 @@
import { omit, sumBy } from 'lodash'; import { omit, sumBy, pick } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { import {
@@ -7,7 +7,6 @@ import {
Vendor, Vendor,
ItemEntry, ItemEntry,
Item, Item,
InventoryTransaction,
AccountTransaction, AccountTransaction,
} from '@/models'; } from '@/models';
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
@@ -16,14 +15,16 @@ import AccountsService from '@/services/Accounts/AccountsService';
import JournalPosterService from '@/services/Sales/JournalPosterService'; import JournalPosterService from '@/services/Sales/JournalPosterService';
import InventoryService from '@/services/Inventory/Inventory'; import InventoryService from '@/services/Inventory/Inventory';
import HasItemsEntries from '@/services/Sales/HasItemsEntries'; import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import { formatDateFields } from '@/utils';
/** /**
* Vendor bills services. * Vendor bills services.
* @service
*/ */
export default class BillsService { export default class BillsService {
/** /**
* Creates a new bill and stored it to the storage. * Creates a new bill and stored it to the storage.
*| *
* Precedures. * Precedures.
* ---- * ----
* - Insert bill transactions to the storage. * - Insert bill transactions to the storage.
@@ -35,16 +36,18 @@ export default class BillsService {
* @param {IBill} bill - * @param {IBill} bill -
* @return {void} * @return {void}
*/ */
static async createBill(bill) { static async createBill(billDTO) {
const agenda = Container.get('agenda'); const invLotNumber = await InventoryService.nextLotNumber();
const bill = {
const amount = sumBy(bill.entries, 'amount'); ...formatDateFields(billDTO, ['bill_date', 'due_date']),
amount: sumBy(billDTO.entries, 'amount'),
invLotNumber: billDTO.invLotNumber || invLotNumber
};
const saveEntriesOpers = []; const saveEntriesOpers = [];
const storedBill = await Bill.tenant() const storedBill = await Bill.tenant()
.query() .query()
.insert({ .insert({
amount,
...omit(bill, ['entries']), ...omit(bill, ['entries']),
}); });
bill.entries.forEach((entry) => { bill.entries.forEach((entry) => {
@@ -58,11 +61,11 @@ export default class BillsService {
saveEntriesOpers.push(oper); saveEntriesOpers.push(oper);
}); });
// Increments vendor balance. // Increments vendor balance.
const incrementOper = Vendor.changeBalance(bill.vendor_id, amount); const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount);
// Rewrite the inventory transactions for inventory items. // Rewrite the inventory transactions for inventory items.
const writeInvTransactionsOper = InventoryService.recordInventoryTransactions( const writeInvTransactionsOper = this.recordInventoryTransactions(
bill.entries, bill.bill_date, 'Bill', storedBill.id, 'IN', bill, storedBill.id
); );
// Writes the journal entries for the given bill transaction. // Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions({ const writeJEntriesOper = this.recordJournalTransactions({
@@ -75,7 +78,6 @@ export default class BillsService {
writeInvTransactionsOper, writeInvTransactionsOper,
writeJEntriesOper, writeJEntriesOper,
]); ]);
// Schedule bill re-compute based on the item cost // Schedule bill re-compute based on the item cost
// method and starting date. // method and starting date.
await this.scheduleComputeItemsCost(bill); await this.scheduleComputeItemsCost(bill);
@@ -83,7 +85,14 @@ export default class BillsService {
return storedBill; return storedBill;
} }
scheduleComputeItemCost(bill) { /**
* Schedule a job to re-compute the bill's items based on cost method
* of the each one.
* @param {Bill} bill
*/
static scheduleComputeItemsCost(bill) {
const agenda = Container.get('agenda');
return agenda.schedule('in 1 second', 'compute-item-cost', { return agenda.schedule('in 1 second', 'compute-item-cost', {
startingDate: bill.bill_date || bill.billDate, startingDate: bill.bill_date || bill.billDate,
itemId: bill.entries[0].item_id || bill.entries[0].itemId, itemId: bill.entries[0].item_id || bill.entries[0].itemId,
@@ -91,6 +100,25 @@ export default class BillsService {
}); });
} }
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill
* @param {number} billId
*/
static recordInventoryTransactions(bill, billId, override) {
const inventoryTransactions = bill.entries
.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate']),
lotNumber: bill.invLotNumber,
transactionType: 'Bill',
transactionId: billId,
direction: 'IN',
date: bill.bill_date,
}));
return InventoryService.recordInventoryTransactions(inventoryTransactions, override);
}
/** /**
* Edits details of the given bill id with associated entries. * Edits details of the given bill id with associated entries.
* *
@@ -106,19 +134,20 @@ export default class BillsService {
* @param {Integer} billId - The given bill id. * @param {Integer} billId - The given bill id.
* @param {IBill} bill - The given new bill details. * @param {IBill} bill - The given new bill details.
*/ */
static async editBill(billId, bill) { static async editBill(billId, billDTO) {
const oldBill = await Bill.tenant().query().findById(billId); const oldBill = await Bill.tenant().query().findById(billId);
const amount = sumBy(bill.entries, 'amount'); const bill = {
...formatDateFields(billDTO, ['bill_date', 'due_date']),
amount: sumBy(billDTO.entries, 'amount'),
invLotNumber: oldBill.invLotNumber,
};
// Update the bill transaction. // Update the bill transaction.
const updatedBill = await Bill.tenant() const updatedBill = await Bill.tenant()
.query() .query()
.where('id', billId) .where('id', billId)
.update({ .update({
amount, ...omit(bill, ['entries', 'invLotNumber'])
...omit(bill, ['entries'])
}); });
// Old stored entries. // Old stored entries.
const storedEntries = await ItemEntry.tenant() const storedEntries = await ItemEntry.tenant()
.query() .query()
@@ -133,17 +162,12 @@ export default class BillsService {
const changeVendorBalanceOper = Vendor.changeDiffBalance( const changeVendorBalanceOper = Vendor.changeDiffBalance(
bill.vendor_id, bill.vendor_id,
oldBill.vendorId, oldBill.vendorId,
amount, bill.amount,
oldBill.amount, oldBill.amount,
); );
// Re-write the inventory transactions for inventory items. // Re-write the inventory transactions for inventory items.
const writeInvTransactionsOper = InventoryService.recordInventoryTransactions( const writeInvTransactionsOper = this.recordInventoryTransactions(bill, billId, true);
bill.entries, bill.bill_date, 'Bill', billId, 'IN'
);
// Delete bill associated inventory transactions.
const deleteInventoryTransOper = InventoryService.deleteInventoryTransactions(
billId, 'Bill'
);
// Writes the journal entries for the given bill transaction. // Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions({ const writeJEntriesOper = this.recordJournalTransactions({
id: billId, id: billId,
@@ -154,10 +178,8 @@ export default class BillsService {
patchEntriesOper, patchEntriesOper,
changeVendorBalanceOper, changeVendorBalanceOper,
writeInvTransactionsOper, writeInvTransactionsOper,
deleteInventoryTransOper,
writeJEntriesOper, writeJEntriesOper,
]); ]);
// Schedule sale invoice re-compute based on the item cost // Schedule sale invoice re-compute based on the item cost
// method and starting date. // method and starting date.
await this.scheduleComputeItemsCost(bill); await this.scheduleComputeItemsCost(bill);

View File

@@ -15,6 +15,7 @@ import JournalPosterService from '@/services/Sales/JournalPosterService';
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries'; import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository'; import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository';
import CustomerRepository from '@/repositories/CustomerRepository'; import CustomerRepository from '@/repositories/CustomerRepository';
import { formatDateFields } from '@/utils';
/** /**
* Payment receive service. * Payment receive service.
@@ -33,7 +34,7 @@ export default class PaymentReceiveService {
.query() .query()
.insert({ .insert({
amount: paymentAmount, amount: paymentAmount,
...omit(paymentReceive, ['entries']), ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']),
}); });
const storeOpers: Array<any> = []; const storeOpers: Array<any> = [];
@@ -97,7 +98,7 @@ export default class PaymentReceiveService {
.where('id', paymentReceiveId) .where('id', paymentReceiveId)
.update({ .update({
amount: paymentAmount, amount: paymentAmount,
...omit(paymentReceive, ['entries']), ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']),
}); });
const opers = []; const opers = [];
const entriesIds = paymentReceive.entries.filter((i: any) => i.id); const entriesIds = paymentReceive.entries.filter((i: any) => i.id);

View File

@@ -2,21 +2,23 @@ import { omit, difference, sumBy, mixin } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { SaleEstimate, ItemEntry } from '@/models'; import { SaleEstimate, ItemEntry } from '@/models';
import HasItemsEntries from '@/services/Sales/HasItemsEntries'; import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import { formatDateFields } from '@/utils';
export default class SaleEstimateService { export default class SaleEstimateService {
/** /**
* Creates a new estimate with associated entries. * Creates a new estimate with associated entries.
* @async * @async
* @param {IEstimate} estimate * @param {EstimateDTO} estimate
* @return {void} * @return {void}
*/ */
static async createEstimate(estimate: any) { static async createEstimate(estimateDTO: any) {
const amount = sumBy(estimate.entries, 'amount'); const estimate = {
amount: sumBy(estimateDTO.entries, 'amount'),
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
};
const storedEstimate = await SaleEstimate.tenant() const storedEstimate = await SaleEstimate.tenant()
.query() .query()
.insert({ .insert({
amount,
...omit(estimate, ['entries']), ...omit(estimate, ['entries']),
}); });
const storeEstimateEntriesOpers: any[] = []; const storeEstimateEntriesOpers: any[] = [];
@@ -36,34 +38,21 @@ export default class SaleEstimateService {
return storedEstimate; return storedEstimate;
} }
/**
* Deletes the given estimate id with associated entries.
* @async
* @param {IEstimate} estimateId
* @return {void}
*/
static async deleteEstimate(estimateId: number) {
await ItemEntry.tenant()
.query()
.where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate')
.delete();
await SaleEstimate.tenant().query().where('id', estimateId).delete();
}
/** /**
* Edit details of the given estimate with associated entries. * Edit details of the given estimate with associated entries.
* @async * @async
* @param {Integer} estimateId * @param {Integer} estimateId
* @param {IEstimate} estimate * @param {EstimateDTO} estimate
* @return {void} * @return {void}
*/ */
static async editEstimate(estimateId: number, estimate: any) { static async editEstimate(estimateId: number, estimateDTO: any) {
const amount = sumBy(estimate.entries, 'amount'); const estimate = {
amount: sumBy(estimateDTO.entries, 'amount'),
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
};
const updatedEstimate = await SaleEstimate.tenant() const updatedEstimate = await SaleEstimate.tenant()
.query() .query()
.update({ .update({
amount,
...omit(estimate, ['entries']), ...omit(estimate, ['entries']),
}); });
const storedEstimateEntries = await ItemEntry.tenant() const storedEstimateEntries = await ItemEntry.tenant()
@@ -79,6 +68,26 @@ export default class SaleEstimateService {
]); ]);
} }
/**
* Deletes the given estimate id with associated entries.
* @async
* @param {IEstimate} estimateId
* @return {void}
*/
static async deleteEstimate(estimateId: number) {
await ItemEntry.tenant()
.query()
.where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate')
.delete();
await SaleEstimate.tenant()
.query()
.where('id', estimateId)
.delete();
}
/** /**
* Validates the given estimate ID exists. * Validates the given estimate ID exists.
* @async * @async

View File

@@ -1,4 +1,4 @@
import { omit, sumBy, difference } from 'lodash'; import { omit, sumBy, difference, pick } from 'lodash';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { import {
SaleInvoice, SaleInvoice,
@@ -12,6 +12,7 @@ import JournalPoster from '@/services/Accounting/JournalPoster';
import HasItemsEntries from '@/services/Sales/HasItemsEntries'; import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import CustomerRepository from '@/repositories/CustomerRepository'; import CustomerRepository from '@/repositories/CustomerRepository';
import InventoryService from '@/services/Inventory/Inventory'; import InventoryService from '@/services/Inventory/Inventory';
import { formatDateFields } from '@/utils';
/** /**
* Sales invoices service * Sales invoices service
@@ -25,14 +26,19 @@ export default class SaleInvoicesService {
* @param {ISaleInvoice} * @param {ISaleInvoice}
* @return {ISaleInvoice} * @return {ISaleInvoice}
*/ */
static async createSaleInvoice(saleInvoice: any) { static async createSaleInvoice(saleInvoiceDTO: any) {
const balance = sumBy(saleInvoice.entries, 'amount'); const balance = sumBy(saleInvoiceDTO.entries, 'amount');
const invLotNumber = await InventoryService.nextLotNumber();
const saleInvoice = {
...formatDateFields(saleInvoiceDTO, ['invoide_date', 'due_date']),
balance,
paymentAmount: 0,
invLotNumber,
};
const storedInvoice = await SaleInvoice.tenant() const storedInvoice = await SaleInvoice.tenant()
.query() .query()
.insert({ .insert({
...omit(saleInvoice, ['entries']), ...omit(saleInvoice, ['entries']),
balance,
payment_amount: 0,
}); });
const opers: Array<any> = []; const opers: Array<any> = [];
@@ -52,8 +58,8 @@ export default class SaleInvoicesService {
balance, balance,
); );
// Records the inventory transactions for inventory items. // Records the inventory transactions for inventory items.
const recordInventoryTransOpers = InventoryService.recordInventoryTransactions( const recordInventoryTransOpers = this.recordInventoryTranscactions(
saleInvoice.entries, saleInvoice.invoice_date, 'SaleInvoice', storedInvoice.id, 'OUT', saleInvoice, storedInvoice.id
); );
// Await all async operations. // Await all async operations.
await Promise.all([ await Promise.all([
@@ -79,11 +85,33 @@ export default class SaleInvoicesService {
} }
/**
* Records the inventory transactions from the givne sale invoice input.
* @param {SaleInvoice} saleInvoice -
* @param {number} saleInvoiceId -
* @param {boolean} override -
*/
static recordInventoryTranscactions(saleInvoice, saleInvoiceId: number, override?: boolean){
const inventortyTransactions = saleInvoice.entries
.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate']),
lotNumber: saleInvoice.invLotNumber,
transactionType: 'SaleInvoice',
transactionId: saleInvoiceId,
direction: 'OUT',
date: saleInvoice.invoice_date,
}));
return InventoryService.recordInventoryTransactions(
inventortyTransactions, override,
);
}
/** /**
* Schedule sale invoice re-compute based on the item * Schedule sale invoice re-compute based on the item
* cost method and starting date * cost method and starting date
* *
* @param saleInvoice * @param {SaleInvoice} saleInvoice -
* @return {Promise<Agenda>} * @return {Promise<Agenda>}
*/ */
static scheduleComputeItemsCost(saleInvoice) { static scheduleComputeItemsCost(saleInvoice) {
@@ -102,18 +130,22 @@ export default class SaleInvoicesService {
* @param {Number} saleInvoiceId - * @param {Number} saleInvoiceId -
* @param {ISaleInvoice} saleInvoice - * @param {ISaleInvoice} saleInvoice -
*/ */
static async editSaleInvoice(saleInvoiceId: number, saleInvoice: any) { static async editSaleInvoice(saleInvoiceId: number, saleInvoiceDTO: any) {
const balance = sumBy(saleInvoice.entries, 'amount'); const balance = sumBy(saleInvoiceDTO.entries, 'amount');
const oldSaleInvoice = await SaleInvoice.tenant().query() const oldSaleInvoice = await SaleInvoice.tenant().query()
.where('id', saleInvoiceId) .where('id', saleInvoiceId)
.first(); .first();
const saleInvoice = {
...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']),
balance,
invLotNumber: oldSaleInvoice.invLotNumber,
};
const updatedSaleInvoices = await SaleInvoice.tenant() const updatedSaleInvoices = await SaleInvoice.tenant()
.query() .query()
.where('id', saleInvoiceId) .where('id', saleInvoiceId)
.update({ .update({
balance, ...omit(saleInvoice, ['entries', 'invLotNumber']),
...omit(saleInvoice, ['entries']),
}); });
// Fetches the sale invoice items entries. // Fetches the sale invoice items entries.
const storedEntries = await ItemEntry.tenant() const storedEntries = await ItemEntry.tenant()
@@ -132,9 +164,14 @@ export default class SaleInvoicesService {
balance, balance,
oldSaleInvoice.balance, oldSaleInvoice.balance,
); );
// Records the inventory transactions for inventory items.
const recordInventoryTransOper = this.recordInventoryTranscactions(
saleInvoice, saleInvoiceId, true,
);
await Promise.all([ await Promise.all([
patchItemsEntriesOper, patchItemsEntriesOper,
changeCustomerBalanceOper, changeCustomerBalanceOper,
recordInventoryTransOper,
]); ]);
// Schedule sale invoice re-compute based on the item cost // Schedule sale invoice re-compute based on the item cost
@@ -221,7 +258,6 @@ export default class SaleInvoicesService {
const revertInventoryTransactionsOper = this.revertInventoryTransactions( const revertInventoryTransactionsOper = this.revertInventoryTransactions(
inventoryTransactions inventoryTransactions
); );
// Await all async operations. // Await all async operations.
await Promise.all([ await Promise.all([
journal.deleteEntries(), journal.deleteEntries(),

View File

@@ -7,6 +7,7 @@ import {
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalPosterService from '@/services/Sales/JournalPosterService'; import JournalPosterService from '@/services/Sales/JournalPosterService';
import HasItemEntries from '@/services/Sales/HasItemsEntries'; import HasItemEntries from '@/services/Sales/HasItemsEntries';
import { formatDateFields } from '@/utils';
export default class SalesReceipt { export default class SalesReceipt {
/** /**
@@ -15,12 +16,14 @@ export default class SalesReceipt {
* @param {ISaleReceipt} saleReceipt * @param {ISaleReceipt} saleReceipt
* @return {Object} * @return {Object}
*/ */
static async createSaleReceipt(saleReceipt: any) { static async createSaleReceipt(saleReceiptDTO: any) {
const amount = sumBy(saleReceipt.entries, 'amount'); const saleReceipt = {
amount: sumBy(saleReceiptDTO.entries, 'amount');
...formatDateFields(saleReceiptDTO, ['receipt_date'])
};
const storedSaleReceipt = await SaleReceipt.tenant() const storedSaleReceipt = await SaleReceipt.tenant()
.query() .query()
.insert({ .insert({
amount,
...omit(saleReceipt, ['entries']), ...omit(saleReceipt, ['entries']),
}); });
const storeSaleReceiptEntriesOpers: Array<any> = []; const storeSaleReceiptEntriesOpers: Array<any> = [];
@@ -39,29 +42,21 @@ export default class SalesReceipt {
return storedSaleReceipt; return storedSaleReceipt;
} }
/**
* Records journal transactions for sale receipt.
* @param {ISaleReceipt} saleReceipt
* @return {Promise}
*/
static async _recordJournalTransactions(saleReceipt: any) {
const accountsDepGraph = await Account.tenant().depGraph().query();
const journalPoster = new JournalPoster(accountsDepGraph);
}
/** /**
* Edit details sale receipt with associated entries. * Edit details sale receipt with associated entries.
* @param {Integer} saleReceiptId * @param {Integer} saleReceiptId
* @param {ISaleReceipt} saleReceipt * @param {ISaleReceipt} saleReceipt
* @return {void} * @return {void}
*/ */
static async editSaleReceipt(saleReceiptId: number, saleReceipt: any) { static async editSaleReceipt(saleReceiptId: number, saleReceiptDTO: any) {
const amount = sumBy(saleReceipt.entries, 'amount'); const saleReceipt = {
amount: sumBy(saleReceiptDTO.entries, 'amount'),
...formatDateFields(saleReceiptDTO, ['receipt_date'])
};
const updatedSaleReceipt = await SaleReceipt.tenant() const updatedSaleReceipt = await SaleReceipt.tenant()
.query() .query()
.where('id', saleReceiptId) .where('id', saleReceiptId)
.update({ .update({
amount,
...omit(saleReceipt, ['entries']), ...omit(saleReceipt, ['entries']),
}); });
const storedSaleReceiptEntries = await ItemEntry.tenant() const storedSaleReceiptEntries = await ItemEntry.tenant()
@@ -82,7 +77,11 @@ export default class SalesReceipt {
* @return {void} * @return {void}
*/ */
static async deleteSaleReceipt(saleReceiptId: number) { static async deleteSaleReceipt(saleReceiptId: number) {
const deleteSaleReceiptOper = SaleReceipt.tenant().query().where('id', saleReceiptId).delete(); const deleteSaleReceiptOper = SaleReceipt.tenant()
.query()
.where('id', saleReceiptId)
.delete();
const deleteItemsEntriesOper = ItemEntry.tenant() const deleteItemsEntriesOper = ItemEntry.tenant()
.query() .query()
.where('reference_id', saleReceiptId) .where('reference_id', saleReceiptId)
@@ -148,4 +147,14 @@ export default class SalesReceipt {
return saleReceipt; return saleReceipt;
} }
/**
* Records journal transactions for sale receipt.
* @param {ISaleReceipt} saleReceipt
* @return {Promise}
*/
static async _recordJournalTransactions(saleReceipt: any) {
const accountsDepGraph = await Account.tenant().depGraph().query();
const journalPoster = new JournalPoster(accountsDepGraph);
}
} }

View File

@@ -144,6 +144,17 @@ function applyMixins(derivedCtor, baseCtors) {
}); });
} }
const formatDateFields = (inputDTO, fields, format = 'YYYY-DD-MM') => {
const _inputDTO = { ...inputDTO };
fields.forEach((field) => {
if (_inputDTO[field]) {
_inputDTO[field] = moment(_inputDTO[field]).format(format);
}
});
return _inputDTO;
};
export { export {
hashPassword, hashPassword,
origin, origin,
@@ -156,4 +167,5 @@ export {
itemsStartWith, itemsStartWith,
getTotalDeep, getTotalDeep,
applyMixins, applyMixins,
formatDateFields,
}; };

View File

@@ -204,8 +204,6 @@ describe('routes: /accounts/', () => {
code: '123', code: '123',
}); });
console.log(res.body);
expect(res.status).equals(200); expect(res.status).equals(200);
}); });
}); });

View File

@@ -802,8 +802,6 @@ describe('routes: `/views`', () => {
value: '100', value: '100',
}], }],
}); });
// console.log(res.status, res.body);
const foundViewColumns = await ViewColumn.tenant().query().where('id', viewColumn.id); const foundViewColumns = await ViewColumn.tenant().query().where('id', viewColumn.id);
expect(foundViewColumns.length).equals(0); expect(foundViewColumns.length).equals(0);
}); });