fix: sync contacts balance with journal entries.

fix: edit invoice amount that has payment transactions.
This commit is contained in:
a.bouhuolia
2021-03-02 11:22:44 +02:00
parent b98d18f189
commit d51d9a5038
29 changed files with 477 additions and 401 deletions

View File

@@ -20,6 +20,7 @@
"agenda": "^3.1.0", "agenda": "^3.1.0",
"agendash": "^1.0.0", "agendash": "^1.0.0",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"async": "^3.2.0",
"axios": "^0.20.0", "axios": "^0.20.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"compression": "^1.7.4", "compression": "^1.7.4",

View File

@@ -5,7 +5,6 @@ import { mapKeysDeep } from 'utils'
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
export default class BaseController { export default class BaseController {
/** /**
* Converts plain object keys to cameCase style. * Converts plain object keys to cameCase style.
* @param {Object} data * @param {Object} data
@@ -75,6 +74,10 @@ export default class BaseController {
return response; return response;
} }
/**
* Async middleware.
* @param {function} callback
*/
asyncMiddleware(callback) { asyncMiddleware(callback) {
return asyncMiddleware(callback); return asyncMiddleware(callback);
} }

View File

@@ -245,7 +245,7 @@ export default class ManualJournalsController extends BaseController {
manualJournalId manualJournalId
); );
return res.status(200).send({ return res.status(200).send({
manual_journal: manualJournal manual_journal: manualJournal,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -449,14 +449,25 @@ export default class ManualJournalsController extends BaseController {
], ],
}); });
} }
if (error.errorType === 'CONTACTS_SHOULD_ASSIGN_WITH_VALID_ACCOUNT') {
return res.boom.badRequest('', {
errors: [
{
type: 'CONTACTS_SHOULD_ASSIGN_WITH_VALID_ACCOUNT',
code: 700,
meta: this.transfromToResponse(error.payload),
},
],
});
}
if (error.errorType === 'contacts_not_found') { if (error.errorType === 'contacts_not_found') {
return res.boom.badRequest('', { return res.boom.badRequest('', {
errors: [{ type: 'CONTACTS_NOT_FOUND', code: 700 }], errors: [{ type: 'CONTACTS_NOT_FOUND', code: 800 }],
}); });
} }
if (error.errorType === 'MANUAL_JOURNAL_ALREADY_PUBLISHED') { if (error.errorType === 'MANUAL_JOURNAL_ALREADY_PUBLISHED') {
return res.boom.badRequest('', { return res.boom.badRequest('', {
errors: [{ type: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', code: 800 }], errors: [{ type: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', code: 900 }],
}); });
} }
} }

View File

@@ -106,8 +106,14 @@ export default class SaleInvoicesController extends BaseController {
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toFloat(),
check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(), check('entries.*.discount')
check('entries.*.description').optional({ nullable: true }).trim().escape(), .optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
]; ];
} }
@@ -230,7 +236,11 @@ export default class SaleInvoicesController extends BaseController {
try { try {
// Deletes the sale invoice with associated entries and journal transaction. // Deletes the sale invoice with associated entries and journal transaction.
await this.saleInvoiceService.deleteSaleInvoice(tenantId, saleInvoiceId, user); await this.saleInvoiceService.deleteSaleInvoice(
tenantId,
saleInvoiceId,
user
);
return res.status(200).send({ return res.status(200).send({
id: saleInvoiceId, id: saleInvoiceId,
@@ -402,15 +412,23 @@ export default class SaleInvoicesController extends BaseController {
} }
if (error.errorType === 'SALE_ESTIMATE_NOT_FOUND') { if (error.errorType === 'SALE_ESTIMATE_NOT_FOUND') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [ errors: [{ type: 'FROM_SALE_ESTIMATE_NOT_FOUND', code: 1200 }],
{ type: 'FROM_SALE_ESTIMATE_NOT_FOUND', code: 1200 },
],
}); });
} }
if (error.errorType === 'SALE_ESTIMATE_CONVERTED_TO_INVOICE') { if (error.errorType === 'SALE_ESTIMATE_CONVERTED_TO_INVOICE') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [ errors: [
{ type: 'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE', code: 1300 }, {
type: 'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE',
code: 1300,
},
],
});
}
if (error.errorType === 'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT') {
return res.boom.badRequest(null, {
errors: [
{ type: 'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT', code: 1400 },
], ],
}); });
} }

View File

@@ -28,7 +28,6 @@ export default class LicensesController extends BaseController {
challenge: true, challenge: true,
}) })
); );
router.post( router.post(
'/generate', '/generate',
this.generateLicenseSchema, this.generateLicenseSchema,

View File

@@ -7,12 +7,10 @@ import 'subscribers/manualJournals';
import 'subscribers/expenses'; import 'subscribers/expenses';
import 'subscribers/Bills'; import 'subscribers/Bills';
import 'subscribers/Bills/SyncVendorsBalances';
import 'subscribers/Bills/WriteJournalEntries'; import 'subscribers/Bills/WriteJournalEntries';
import 'subscribers/Bills/WriteInventoryTransactions'; import 'subscribers/Bills/WriteInventoryTransactions';
import 'subscribers/SaleInvoices'; import 'subscribers/SaleInvoices';
import 'subscribers/SaleInvoices/SyncCustomersBalance';
import 'subscribers/SaleInvoices/WriteInventoryTransactions'; import 'subscribers/SaleInvoices/WriteInventoryTransactions';
import 'subscribers/SaleInvoices/WriteJournalEntries'; import 'subscribers/SaleInvoices/WriteJournalEntries';

View File

@@ -20,7 +20,25 @@ export default class Contact extends TenantModel {
* Defined virtual attributes. * Defined virtual attributes.
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return ['closingBalance']; return ['contactNormal', 'closingBalance'];
}
/**
* Retrieve the contact normal by the given contact type.
*/
static getContactNormalByType(contactType) {
const types = {
'vendor': 'credit',
'customer': 'debit',
};
return types[contactType];
}
/**
* Retrieve the contact noraml;
*/
get contactNormal() {
return Contact.getContactNormalByType(this.contactService);
} }
/** /**

View File

@@ -41,7 +41,7 @@ export default class Customer extends TenantModel {
* Defined virtual attributes. * Defined virtual attributes.
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return ['closingBalance']; return ['closingBalance', 'contactNormal'];
} }
/** /**
@@ -51,6 +51,13 @@ export default class Customer extends TenantModel {
return this.openingBalance + this.balance; return this.openingBalance + this.balance;
} }
/**
* Retrieve the contact noraml;
*/
get contactNormal() {
return 'debit';
}
/** /**
* Model modifiers. * Model modifiers.
*/ */

View File

@@ -40,7 +40,7 @@ export default class Vendor extends TenantModel {
* Defined virtual attributes. * Defined virtual attributes.
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return ['closingBalance']; return ['closingBalance', 'contactNormal'];
} }
/** /**
@@ -50,6 +50,13 @@ export default class Vendor extends TenantModel {
return this.openingBalance + this.balance; return this.openingBalance + this.balance;
} }
/**
* Retrieve the contact noraml;
*/
get contactNormal() {
return 'debit';
}
/** /**
* Model modifiers. * Model modifiers.
*/ */

View File

@@ -17,7 +17,6 @@ import {
export default class JournalCommands { export default class JournalCommands {
journal: JournalPoster; journal: JournalPoster;
models: any; models: any;
repositories: any; repositories: any;
@@ -77,7 +76,6 @@ export default class JournalCommands {
credit: bill.amount, credit: bill.amount,
account: payableAccount.id, account: payableAccount.id,
contactId: bill.vendorId, contactId: bill.vendorId,
contactType: 'Vendor',
index: 1, index: 1,
}); });
this.journal.credit(payableEntry); this.journal.credit(payableEntry);
@@ -113,8 +111,8 @@ export default class JournalCommands {
) { ) {
const { accountRepository } = this.repositories; const { accountRepository } = this.repositories;
const openingBalanceAccount = await accountRepository.findOne({ const incomeAccount = await accountRepository.findOne({
slug: 'opening-balance', slug: 'other-income',
}); });
const receivableAccount = await accountRepository.findOne({ const receivableAccount = await accountRepository.findOne({
slug: 'accounts-receivable', slug: 'accounts-receivable',
@@ -123,8 +121,6 @@ export default class JournalCommands {
const commonEntry = { const commonEntry = {
referenceType: 'CustomerOpeningBalance', referenceType: 'CustomerOpeningBalance',
referenceId: customerId, referenceId: customerId,
contactType: 'Customer',
contactId: customerId,
date: openingBalanceAt, date: openingBalanceAt,
userId, userId,
}; };
@@ -133,13 +129,14 @@ export default class JournalCommands {
credit: 0, credit: 0,
debit: openingBalance, debit: openingBalance,
account: receivableAccount.id, account: receivableAccount.id,
contactId: customerId,
index: 1, index: 1,
}); });
const creditEntry = new JournalEntry({ const creditEntry = new JournalEntry({
...commonEntry, ...commonEntry,
credit: openingBalance, credit: openingBalance,
debit: 0, debit: 0,
account: openingBalanceAccount.id, account: incomeAccount.id,
index: 2, index: 2,
}); });
this.journal.debit(debitEntry); this.journal.debit(debitEntry);
@@ -171,8 +168,6 @@ export default class JournalCommands {
const commonEntry = { const commonEntry = {
referenceType: 'VendorOpeningBalance', referenceType: 'VendorOpeningBalance',
referenceId: vendorId, referenceId: vendorId,
contactType: 'Vendor',
contactId: vendorId,
date: openingBalanceAt, date: openingBalanceAt,
userId: authorizedUserId, userId: authorizedUserId,
}; };
@@ -182,6 +177,7 @@ export default class JournalCommands {
credit: openingBalance, credit: openingBalance,
debit: 0, debit: 0,
index: 1, index: 1,
contactId: vendorId,
}); });
const debitEntry = new JournalEntry({ const debitEntry = new JournalEntry({
...commonEntry, ...commonEntry,
@@ -297,7 +293,6 @@ export default class JournalCommands {
account: entry.accountId, account: entry.accountId,
referenceType: 'Journal', referenceType: 'Journal',
referenceId: manualJournalObj.id, referenceId: manualJournalObj.id,
contactType: entry.contactType,
contactId: entry.contactId, contactId: entry.contactId,
note: entry.note, note: entry.note,
date: manualJournalObj.date, date: manualJournalObj.date,
@@ -379,6 +374,7 @@ export default class JournalCommands {
...commonEntry, ...commonEntry,
debit: saleInvoice.balance, debit: saleInvoice.balance,
account: receivableAccountId, account: receivableAccountId,
contactId: saleInvoice.customerId,
index: 1, index: 1,
}); });
this.journal.debit(receivableEntry); this.journal.debit(receivableEntry);

View File

@@ -0,0 +1,74 @@
import async from 'async';
export default class JournalContacts {
saveContactBalanceQueue: any;
contactsBalanceTable: {
[key: number]: { credit: number; debit: number };
} = {};
constructor(journal) {
this.journal = journal;
this.saveContactBalanceQueue = async.queue(
this.saveContactBalanceChangeTask.bind(this),
10
);
}
/**
* Sets the contact balance change.
*/
private getContactsBalanceChanges(entry) {
if (!entry.contactId) {
return;
}
const change = {
debit: entry.debit,
credit: entry.credit,
};
if (!this.contactsBalanceTable[entry.contactId]) {
this.contactsBalanceTable[entry.contactId] = { credit: 0, debit: 0 };
}
if (change.credit) {
this.contactsBalanceTable[entry.contactId].credit += change.credit;
}
if (change.debit) {
this.contactsBalanceTable[entry.contactId].debit += change.debit;
}
}
/**
* Save contacts balance change.
*/
saveContactsBalance() {
const balanceChanges = Object.entries(
this.contactsBalanceTable
).map(([contactId, { credit, debit }]) => ({ contactId, credit, debit }));
return this.saveContactBalanceQueue.pushAsync(balanceChanges);
}
/**
* Saves contact balance change task.
* @param {number} contactId - Contact id.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
*/
async saveContactBalanceChangeTask({ contactId, credit, debit }, callback) {
const { contactRepository } = this.repositories;
const contact = await contactRepository.findOneById(contactId);
let balanceChange = 0;
if (contact.contactNormal === 'credit') {
balanceChange += credit - debit;
} else {
balanceChange += debit - credit;
}
// Contact change balance.
await contactRepository.changeNumber(
{ id: contactId },
'balance',
balanceChange
);
callback();
}
}

View File

@@ -14,6 +14,4 @@ export default class JournalFinancial {
this.journal = journal; this.journal = journal;
this.accountsDepGraph = this.journal.accountsDepGraph; this.accountsDepGraph = this.journal.accountsDepGraph;
} }
} }

View File

@@ -1,6 +1,7 @@
import { omit, get } from 'lodash'; import { omit, get, chain } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Container } from 'typedi'; import { Container } from 'typedi';
import async from 'async';
import JournalEntry from 'services/Accounting/JournalEntry'; import JournalEntry from 'services/Accounting/JournalEntry';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { import {
@@ -11,6 +12,19 @@ import {
TEntryType, TEntryType,
} from 'interfaces'; } from 'interfaces';
const CONTACTS_CONFIG = [
{
accountBySlug: 'accounts-receivable',
contactService: 'customer',
assignRequired: true,
},
{
accountBySlug: 'accounts-payable',
contactService: 'vendor',
assignRequired: true,
},
];
export default class JournalPoster implements IJournalPoster { export default class JournalPoster implements IJournalPoster {
tenantId: number; tenantId: number;
tenancy: TenancyService; tenancy: TenancyService;
@@ -24,6 +38,10 @@ export default class JournalPoster implements IJournalPoster {
accountsDepGraph: IAccountsChange; accountsDepGraph: IAccountsChange;
accountsBalanceTable: { [key: number]: number } = {}; accountsBalanceTable: { [key: number]: number } = {};
contactsBalanceTable: {
[key: number]: { credit: number; debit: number }[];
} = {};
saveContactBalanceQueue: any;
/** /**
* Journal poster constructor. * Journal poster constructor.
@@ -39,6 +57,10 @@ export default class JournalPoster implements IJournalPoster {
if (accountsGraph) { if (accountsGraph) {
this.accountsDepGraph = accountsGraph; this.accountsDepGraph = accountsGraph;
} }
this.saveContactBalanceQueue = async.queue(
this.saveContactBalanceChangeTask.bind(this),
10
);
} }
/** /**
@@ -69,7 +91,7 @@ export default class JournalPoster implements IJournalPoster {
} }
/** /**
* * Detarmines the ledger is empty.
*/ */
public isEmpty() { public isEmpty() {
return this.entries.length === 0; return this.entries.length === 0;
@@ -85,6 +107,7 @@ export default class JournalPoster implements IJournalPoster {
} }
this.entries.push(entryModel.entry); this.entries.push(entryModel.entry);
this.setAccountBalanceChange(entryModel.entry); this.setAccountBalanceChange(entryModel.entry);
this.setContactBalanceChange(entryModel.entry);
} }
/** /**
@@ -97,6 +120,83 @@ export default class JournalPoster implements IJournalPoster {
} }
this.entries.push(entryModel.entry); this.entries.push(entryModel.entry);
this.setAccountBalanceChange(entryModel.entry); this.setAccountBalanceChange(entryModel.entry);
this.setContactBalanceChange(entryModel.entry);
}
/**
* Sets the contact balance change.
*/
private setContactBalanceChange(entry) {
if (!entry.contactId) {
return;
}
const change = {
debit: entry.debit || 0,
credit: entry.credit || 0,
account: entry.account,
};
if (!this.contactsBalanceTable[entry.contactId]) {
this.contactsBalanceTable[entry.contactId] = [];
}
this.contactsBalanceTable[entry.contactId].push(change);
}
/**
* Save contacts balance change.
*/
async saveContactsBalance() {
await this.initAccountsDepGraph();
const balanceChanges = Object.entries(this.contactsBalanceTable).map(
([contactId, entries]) => ({
contactId,
entries: entries.filter((entry) => {
const account = this.accountsDepGraph.getNodeData(entry.account);
return (
account &&
CONTACTS_CONFIG.some((config) => {
return config.accountBySlug === account.slug;
})
);
}),
})
);
const balanceEntries = chain(balanceChanges)
.map((change) => change.entries.map(entry => ({
...entry,
contactId: change.contactId
})))
.flatten()
.value();
return this.saveContactBalanceQueue.pushAsync(balanceEntries);
}
/**
* Saves contact balance change task.
* @param {number} contactId - Contact id.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
*/
async saveContactBalanceChangeTask({ contactId, credit, debit }) {
const { contactRepository } = this.repositories;
const contact = await contactRepository.findOneById(contactId);
let balanceChange = 0;
if (contact.contactNormal === 'credit') {
balanceChange += credit - debit;
} else {
balanceChange += debit - credit;
}
// Contact change balance.
await contactRepository.changeNumber(
{ id: contactId },
'balance',
balanceChange
);
} }
/** /**
@@ -321,7 +421,8 @@ export default class JournalPoster implements IJournalPoster {
entry.credit = -1 * entry.credit; entry.credit = -1 * entry.credit;
entry.debit = -1 * entry.debit; entry.debit = -1 * entry.debit;
this.setAccountBalanceChange(entry, entry.accountNormal); this.setAccountBalanceChange(entry);
this.setContactBalanceChange(entry);
}); });
this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id)); this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id));
} }

View File

@@ -240,7 +240,10 @@ export default class ContactsService {
journal.fromTransactions(contactsTransactions); journal.fromTransactions(contactsTransactions);
journal.removeEntries(); journal.removeEntries();
await Promise.all([journal.saveBalance(), journal.deleteEntries()]); await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
} }
/** /**

View File

@@ -268,7 +268,11 @@ export default class CustomersService {
openingBalanceAt, openingBalanceAt,
authorizedUserId authorizedUserId
); );
await Promise.all([journal.saveBalance(), journal.saveEntries()]); await Promise.all([
journal.saveBalance(),
journal.saveEntries(),
journal.saveContactsBalance(),
]);
} }
/** /**

View File

@@ -192,7 +192,11 @@ export default class VendorsService {
openingBalanceAt, openingBalanceAt,
user user
); );
await Promise.all([journal.saveBalance(), journal.saveEntries()]); await Promise.all([
journal.saveBalance(),
journal.saveEntries(),
journal.saveContactsBalance(),
]);
} }
/** /**

View File

@@ -10,29 +10,12 @@ 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 { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes' import {
ACCOUNT_PARENT_TYPE,
const ERRORS = { ACCOUNT_ROOT_TYPE,
NOT_FOUND: 'NOT_FOUND', ACCOUNT_TYPE,
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', } from 'data/AccountTypes';
import { ERRORS } from './constants';
ITEM_NAME_EXISTS: 'ITEM_NAME_EXISTS',
ITEM_CATEOGRY_NOT_FOUND: 'ITEM_CATEOGRY_NOT_FOUND',
COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS',
COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD',
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME',
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
};
@Service() @Service()
export default class ItemsService implements IItemsService { export default class ItemsService implements IItemsService {
@Inject() @Inject()
@@ -149,9 +132,7 @@ export default class ItemsService implements IItemsService {
tenantId: number, tenantId: number,
sellAccountId: number sellAccountId: number
) { ) {
const { const { accountRepository } = this.tenancy.repositories(tenantId);
accountRepository,
} = this.tenancy.repositories(tenantId);
this.logger.info('[items] validate sell account existance.', { this.logger.info('[items] validate sell account existance.', {
tenantId, tenantId,
@@ -166,7 +147,6 @@ export default class ItemsService implements IItemsService {
sellAccountId, sellAccountId,
}); });
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND); throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND);
} else if (!foundAccount.isParentType(ACCOUNT_ROOT_TYPE.INCOME)) { } else if (!foundAccount.isParentType(ACCOUNT_ROOT_TYPE.INCOME)) {
this.logger.info('[items] sell account not income type.', { this.logger.info('[items] sell account not income type.', {
tenantId, tenantId,
@@ -185,9 +165,7 @@ export default class ItemsService implements IItemsService {
tenantId: number, tenantId: number,
inventoryAccountId: number inventoryAccountId: number
) { ) {
const { const { accountRepository } = this.tenancy.repositories(tenantId);
accountRepository,
} = this.tenancy.repositories(tenantId);
this.logger.info('[items] validate inventory account existance.', { this.logger.info('[items] validate inventory account existance.', {
tenantId, tenantId,
@@ -203,7 +181,6 @@ export default class ItemsService implements IItemsService {
inventoryAccountId, inventoryAccountId,
}); });
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND); throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND);
} else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) { } else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) {
this.logger.info('[items] inventory account not inventory type.', { this.logger.info('[items] inventory account not inventory type.', {
tenantId, tenantId,

View File

@@ -0,0 +1,21 @@
export const ERRORS = {
NOT_FOUND: 'NOT_FOUND',
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
ITEM_NAME_EXISTS: 'ITEM_NAME_EXISTS',
ITEM_CATEOGRY_NOT_FOUND: 'ITEM_CATEOGRY_NOT_FOUND',
COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS',
COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD',
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME',
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
};

View File

@@ -9,6 +9,7 @@ import {
ISystemUser, ISystemUser,
IManualJournal, IManualJournal,
IPaginationMeta, IPaginationMeta,
IManualJournalEntry,
} from 'interfaces'; } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
@@ -20,30 +21,7 @@ import {
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService'; import JournalPosterService from 'services/Sales/JournalPosterService';
import { ERRORS } from './constants';
const ERRORS = {
NOT_FOUND: 'manual_journal_not_found',
CREDIT_DEBIT_NOT_EQUAL_ZERO: 'credit_debit_not_equal_zero',
CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal',
ACCCOUNTS_IDS_NOT_FOUND: 'acccounts_ids_not_found',
JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
CONTACTS_NOT_FOUND: 'contacts_not_found',
MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED',
};
const CONTACTS_CONFIG = [
{
accountBySlug: 'accounts-receivable',
contactService: 'customer',
assignRequired: false,
},
{
accountBySlug: 'accounts-payable',
contactService: 'vendor',
assignRequired: true,
},
];
@Service() @Service()
export default class ManualJournalsService implements IManualJournalsService { export default class ManualJournalsService implements IManualJournalsService {
@@ -159,8 +137,7 @@ export default class ManualJournalsService implements IManualJournalsService {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId); const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId);
const accounts = await Account.query() const accounts = await Account.query().whereIn('id', manualAccountsIds);
.whereIn('id', manualAccountsIds);
const storedAccountsIds = accounts.map((account) => account.id); const storedAccountsIds = accounts.map((account) => account.id);
@@ -203,29 +180,37 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {string} accountBySlug * @param {string} accountBySlug
* @param {string} contactType * @param {string} contactType
*/ */
private async validateAccountsWithContactType( private async validateAccountWithContactType(
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, entriesDTO: IManualJournalEntry[],
accountBySlug: string,
contactType: string,
contactRequired: boolean = true
): Promise<void> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const payableAccount = await accountRepository.findOne({
slug: accountBySlug,
});
const entriesHasNoVendorContact = manualJournalDTO.entries.filter( accountBySlug: string,
(e) => contactType: string
e.accountId === payableAccount.id && ): Promise<void> {
((!e.contactId && contactRequired) || e.contactType !== contactType) const { Account, Contact } = this.tenancy.models(tenantId);
// Retrieve account meta by the given account slug.
const account = await Account.query().findOne('slug', accountBySlug);
// Filter all entries of the given account.
const accountEntries = entriesDTO.filter(
(entry) => entry.accountId === account.id
); );
if (entriesHasNoVendorContact.length > 0) { // Can't continue if there is no entry that associate to the given account.
const indexes = entriesHasNoVendorContact.map((e) => e.index); if (accountEntries.length === 0) {
return;
}
// Filter entries that have no contact type or not equal the valid type.
const entriesNoContact = accountEntries.filter(
(entry) => !entry.contactType || entry.contactType !== contactType
);
// Throw error in case one of entries that has invalid contact type.
if (entriesNoContact.length > 0) {
const indexes = entriesNoContact.map(e => e.index);
throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', { throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', {
accountSlug: accountBySlug,
contactType, contactType,
accountBySlug,
indexes, indexes,
}); });
} }
@@ -238,19 +223,22 @@ export default class ManualJournalsService implements IManualJournalsService {
*/ */
private async dynamicValidateAccountsWithContactType( private async dynamicValidateAccountsWithContactType(
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO entriesDTO: IManualJournalEntry[]
): Promise<any> { ): Promise<any> {
return Promise.all( return Promise.all([
CONTACTS_CONFIG.map(({ accountBySlug, contactService, assignRequired }) => this.validateAccountWithContactType(
this.validateAccountsWithContactType(
tenantId, tenantId,
manualJournalDTO, entriesDTO,
accountBySlug, 'accounts-receivable',
contactService, 'customer'
assignRequired ),
) this.validateAccountWithContactType(
) tenantId,
); entriesDTO,
'accounts-payable',
'vendor'
),
]);
} }
/** /**
@@ -273,9 +261,9 @@ export default class ManualJournalsService implements IManualJournalsService {
const entriesContactsIds = entriesContactPairs.map( const entriesContactsIds = entriesContactPairs.map(
(entry) => entry.contactId (entry) => entry.contactId
); );
// Retrieve all stored contacts on the storage from contacts entries. // Retrieve all stored contacts on the storage from contacts entries.
const storedContacts = await contactRepository.findByIds( const storedContacts = await contactRepository.findWhereIn(
'id',
entriesContactsIds entriesContactsIds
); );
// Converts the stored contacts to map with id as key and entry as value. // Converts the stored contacts to map with id as key and entry as value.
@@ -287,8 +275,7 @@ export default class ManualJournalsService implements IManualJournalsService {
entriesContactPairs.forEach((contactEntry) => { entriesContactPairs.forEach((contactEntry) => {
const storedContact = storedContactsMap.get(contactEntry.contactId); const storedContact = storedContactsMap.get(contactEntry.contactId);
// in case the contact id not found or contact type no equals pushed to // in case the contact id not found.
// not found contacts.
if ( if (
!storedContact || !storedContact ||
storedContact.contactService !== contactEntry.contactType storedContact.contactService !== contactEntry.contactType
@@ -378,7 +365,7 @@ export default class ManualJournalsService implements IManualJournalsService {
// Validate accounts with contact type from the given config. // Validate accounts with contact type from the given config.
await this.dynamicValidateAccountsWithContactType( await this.dynamicValidateAccountsWithContactType(
tenantId, tenantId,
manualJournalDTO manualJournalDTO.entries
); );
this.logger.info( this.logger.info(
'[manual_journal] trying to save manual journal to the storage.', '[manual_journal] trying to save manual journal to the storage.',
@@ -446,7 +433,7 @@ export default class ManualJournalsService implements IManualJournalsService {
// Validate accounts with contact type from the given config. // Validate accounts with contact type from the given config.
await this.dynamicValidateAccountsWithContactType( await this.dynamicValidateAccountsWithContactType(
tenantId, tenantId,
manualJournalDTO manualJournalDTO.entries
); );
// Transform manual journal DTO to model. // Transform manual journal DTO to model.
const manualJournalObj = this.transformEditDTOToModel( const manualJournalObj = this.transformEditDTOToModel(
@@ -525,7 +512,7 @@ export default class ManualJournalsService implements IManualJournalsService {
tenantId: number, tenantId: number,
manualJournalsIds: number[] manualJournalsIds: number[]
): Promise<{ ): Promise<{
oldManualJournals: IManualJournal[] oldManualJournals: IManualJournal[];
}> { }> {
const { ManualJournal, ManualJournalEntry } = this.tenancy.models(tenantId); const { ManualJournal, ManualJournalEntry } = this.tenancy.models(tenantId);
@@ -817,6 +804,7 @@ export default class ManualJournalsService implements IManualJournalsService {
journal.saveBalance(), journal.saveBalance(),
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), journal.saveEntries(),
journal.saveContactsBalance(),
]); ]);
this.logger.info( this.logger.info(
'[manual_journal] the journal entries saved successfully.', '[manual_journal] the journal entries saved successfully.',

View File

@@ -0,0 +1,24 @@
export const ERRORS = {
NOT_FOUND: 'manual_journal_not_found',
CREDIT_DEBIT_NOT_EQUAL_ZERO: 'credit_debit_not_equal_zero',
CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal',
ACCCOUNTS_IDS_NOT_FOUND: 'acccounts_ids_not_found',
JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
CONTACTS_NOT_FOUND: 'contacts_not_found',
ENTRIES_CONTACTS_NOT_FOUND: 'ENTRIES_CONTACTS_NOT_FOUND',
MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED',
};
export const CONTACTS_CONFIG = [
{
accountBySlug: 'accounts-receivable',
contactService: 'customer',
assignRequired: true,
},
{
accountBySlug: 'accounts-payable',
contactService: 'vendor',
assignRequired: true,
},
];

View File

@@ -27,7 +27,6 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils'; import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
import PayableAgingSummaryService from 'services/FinancialStatements/AgingSummary/APAgingSummaryService';
const ERRORS = { const ERRORS = {
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
@@ -452,6 +451,7 @@ export default class BillPaymentsService {
.delete(); .delete();
await BillPayment.query().where('id', billPaymentId).delete(); await BillPayment.query().where('id', billPaymentId).delete();
// Triggers `onBillPaymentDeleted` event.
await this.eventDispatcher.dispatch(events.billPayment.onDeleted, { await this.eventDispatcher.dispatch(events.billPayment.onDeleted, {
tenantId, tenantId,
billPaymentId, billPaymentId,
@@ -490,7 +490,8 @@ export default class BillPaymentsService {
*/ */
public async recordJournalEntries( public async recordJournalEntries(
tenantId: number, tenantId: number,
billPayment: IBillPayment billPayment: IBillPayment,
override: boolean = false,
) { ) {
const { AccountTransaction } = this.tenancy.models(tenantId); const { AccountTransaction } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
@@ -498,7 +499,7 @@ export default class BillPaymentsService {
const paymentAmount = sumBy(billPayment.entries, 'paymentAmount'); const paymentAmount = sumBy(billPayment.entries, 'paymentAmount');
const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD'); const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
// Retrieve AP account from the storage. // Retrieve A/P account from the storage.
const payableAccount = await accountRepository.findOne({ const payableAccount = await accountRepository.findOne({
slug: 'accounts-payable', slug: 'accounts-payable',
}); });
@@ -511,26 +512,27 @@ export default class BillPaymentsService {
referenceType: 'BillPayment', referenceType: 'BillPayment',
date: formattedDate, date: formattedDate,
}; };
if (billPayment.id) { if (override) {
const transactions = await AccountTransaction.query() const transactions = await AccountTransaction.query()
.whereIn('reference_type', ['BillPayment']) .whereIn('reference_type', ['BillPayment'])
.where('reference_id', billPayment.id) .where('reference_id', billPayment.id)
.withGraphFetched('account'); .withGraphFetched('account');
journal.loadEntries(transactions); journal.fromTransactions(transactions);
journal.removeEntries(); journal.removeEntries();
} }
const debitReceivable = new JournalEntry({ const debitReceivable = new JournalEntry({
...commonJournal, ...commonJournal,
debit: paymentAmount, debit: paymentAmount,
contactType: 'Vendor',
contactId: billPayment.vendorId, contactId: billPayment.vendorId,
account: payableAccount.id, account: payableAccount.id,
index: 1,
}); });
const creditPaymentAccount = new JournalEntry({ const creditPaymentAccount = new JournalEntry({
...commonJournal, ...commonJournal,
credit: paymentAmount, credit: paymentAmount,
account: billPayment.paymentAccountId, account: billPayment.paymentAccountId,
index: 2,
}); });
journal.debit(debitReceivable); journal.debit(debitReceivable);
journal.credit(creditPaymentAccount); journal.credit(creditPaymentAccount);
@@ -539,6 +541,7 @@ export default class BillPaymentsService {
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), journal.saveEntries(),
journal.saveBalance(), journal.saveBalance(),
journal.saveContactsBalance(),
]); ]);
} }
@@ -554,7 +557,11 @@ export default class BillPaymentsService {
await journalCommands.revertJournalEntries(billPaymentId, 'BillPayment'); await journalCommands.revertJournalEntries(billPaymentId, 'BillPayment');
return Promise.all([journal.saveBalance(), journal.deleteEntries()]); return Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
journal.saveContactsBalance(),
]);
} }
/** /**

View File

@@ -527,6 +527,7 @@ export default class BillsService extends SalesInvoicesCost {
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), journal.saveEntries(),
journal.saveBalance(), journal.saveBalance(),
journal.saveContactsBalance(),
]); ]);
} }

View File

@@ -27,7 +27,8 @@ export default class JournalPosterService {
await Promise.all([ await Promise.all([
journal.deleteEntries(), journal.deleteEntries(),
journal.saveBalance() journal.saveBalance(),
journal.saveContactsBalance(),
]); ]);
} }
} }

View File

@@ -16,7 +16,6 @@ import {
IPaymentReceiveEntryDTO, IPaymentReceiveEntryDTO,
IPaymentReceivesFilter, IPaymentReceivesFilter,
ISaleInvoice, ISaleInvoice,
ISystemService,
ISystemUser, ISystemUser,
IPaymentReceivePageEntry, IPaymentReceivePageEntry,
} from 'interfaces'; } from 'interfaces';
@@ -496,7 +495,7 @@ export default class PaymentReceiveService {
paymentReceiveId: number, paymentReceiveId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{
paymentReceive: Omit<IPaymentReceive, "entries">; paymentReceive: Omit<IPaymentReceive, 'entries'>;
entries: IPaymentReceivePageEntry[]; entries: IPaymentReceivePageEntry[];
}> { }> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
@@ -665,7 +664,6 @@ export default class PaymentReceiveService {
const creditReceivable = new JournalEntry({ const creditReceivable = new JournalEntry({
...commonJournal, ...commonJournal,
credit: paymentAmount, credit: paymentAmount,
contactType: 'Customer',
contactId: paymentReceive.customerId, contactId: paymentReceive.customerId,
account: receivableAccount.id, account: receivableAccount.id,
index: 1, index: 1,
@@ -683,6 +681,7 @@ export default class PaymentReceiveService {
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), journal.saveEntries(),
journal.saveBalance(), journal.saveBalance(),
journal.saveContactsBalance(),
]); ]);
} }
@@ -705,7 +704,11 @@ export default class PaymentReceiveService {
await commands.revertJournalEntries(paymentReceiveId, 'PaymentReceive'); await commands.revertJournalEntries(paymentReceiveId, 'PaymentReceive');
await Promise.all([journal.saveBalance(), journal.deleteEntries()]); await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
journal.saveContactsBalance(),
]);
} }
/** /**

View File

@@ -29,6 +29,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService'; import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate'; import SaleEstimateService from 'services/Sales/SalesEstimate';
import JournalPosterService from './JournalPosterService'; import JournalPosterService from './JournalPosterService';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
const ERRORS = { const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE', INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
@@ -37,6 +38,8 @@ const ERRORS = {
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS', NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE', SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES: INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES', 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
}; };
@@ -130,6 +133,20 @@ export default class SaleInvoicesService {
return entries; return entries;
} }
/**
* Validate the invoice amount is bigger than payment amount before edit the invoice.
* @param {number} saleInvoiceAmount
* @param {number} paymentAmount
*/
validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceAmount: number,
paymentAmount: number
) {
if (saleInvoiceAmount < paymentAmount) {
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
}
}
/** /**
* Validate whether sale invoice exists on the storage. * Validate whether sale invoice exists on the storage.
* @param {Request} req * @param {Request} req
@@ -267,7 +284,7 @@ export default class SaleInvoicesService {
saleInvoiceDTO: any, saleInvoiceDTO: any,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<ISaleInvoice> { ): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const oldSaleInvoice = await this.getInvoiceOrThrowError( const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId, tenantId,
@@ -309,12 +326,16 @@ export default class SaleInvoicesService {
'SaleInvoice', 'SaleInvoice',
saleInvoiceDTO.entries saleInvoiceDTO.entries
); );
// Validate the invoice amount is not smaller than the invoice payment amount.
this.validateInvoiceAmountBiggerPaymentAmount(
saleInvoiceObj.balance,
oldSaleInvoice.paymentAmount
);
this.logger.info('[sale_invoice] trying to update sale invoice.'); this.logger.info('[sale_invoice] trying to update sale invoice.');
const saleInvoice: ISaleInvoice = await SaleInvoice.query().upsertGraphAndFetch( const saleInvoice: ISaleInvoice = await saleInvoiceRepository.update(
{ { ...omit(saleInvoiceObj, ['paymentAmount']) },
id: saleInvoiceId, { id: saleInvoiceId }
...saleInvoiceObj,
}
); );
// Triggers `onSaleInvoiceEdited` event. // Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, { await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
@@ -470,6 +491,7 @@ export default class SaleInvoicesService {
await Promise.all([ await Promise.all([
journal.deleteEntries(), journal.deleteEntries(),
journal.saveBalance(), journal.saveBalance(),
journal.saveContactsBalance(),
journal.saveEntries(), journal.saveEntries(),
]); ]);
} }

View File

@@ -1,68 +0,0 @@
import { Container } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
@EventSubscriber()
export default class BillSubscriber {
tenancy: TenancyService;
logger: any;
/**
* Constructor method.
*/
constructor() {
this.tenancy = Container.get(TenancyService);
this.logger = Container.get('logger');
}
/**
* Handles vendor balance increment once bill created.
*/
@On(events.bill.onCreated)
async handleVendorBalanceIncrement({ tenantId, billId, bill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Increments vendor balance.
this.logger.info('[bill] trying to increment vendor balance.', {
tenantId,
billId,
});
await vendorRepository.changeBalance(bill.vendorId, bill.amount);
}
/**
* Handles vendor balance decrement once bill deleted.
*/
@On(events.bill.onDeleted)
async handleVendorBalanceDecrement({ tenantId, billId, oldBill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Decrements vendor balance.
this.logger.info('[bill] trying to decrement vendor balance.', {
tenantId,
billId,
});
await vendorRepository.changeBalance(oldBill.vendorId, oldBill.amount * -1);
}
/**
* Handles vendor balance difference change.
*/
@On(events.bill.onEdited)
async handleVendorBalanceDiffChange({ tenantId, billId, oldBill, bill }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Changes the diff vendor balance between old and new amount.
this.logger.info('[bill[ change vendor the different balance.', {
tenantId,
billId,
});
await vendorRepository.changeDiffBalance(
bill.vendorId,
bill.amount,
oldBill.amount,
oldBill.vendorId
);
}
}

View File

@@ -1,81 +0,0 @@
import { Container } from 'typedi';
import { On, EventSubscriber } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
@EventSubscriber()
export default class SaleInvoiceSubscriber {
logger: any;
tenancy: TenancyService;
/**
* Constructor method.
*/
constructor() {
this.logger = Container.get('logger');
this.tenancy = Container.get(TenancyService);
}
/**
* Handles customer balance increment once sale invoice created.
*/
@On(events.saleInvoice.onCreated)
public async handleCustomerBalanceIncrement({
tenantId,
saleInvoice,
saleInvoiceId,
}) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[sale_invoice] trying to increment customer balance.', {
tenantId,
});
await customerRepository.changeBalance(
saleInvoice.customerId,
saleInvoice.balance
);
}
/**
* Handles customer balance diff balnace change once sale invoice edited.
*/
@On(events.saleInvoice.onEdited)
public async onSaleInvoiceEdited({
tenantId,
saleInvoice,
oldSaleInvoice,
saleInvoiceId,
}) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[sale_invoice] trying to change diff customer balance.', {
tenantId,
});
await customerRepository.changeDiffBalance(
saleInvoice.customerId,
saleInvoice.balance,
oldSaleInvoice.balance,
oldSaleInvoice.customerId
);
}
/**
* Handles customer balance decrement once sale invoice deleted.
*/
@On(events.saleInvoice.onDeleted)
public async handleCustomerBalanceDecrement({
tenantId,
saleInvoiceId,
oldSaleInvoice,
}) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[sale_invoice] trying to decrement customer balance.', {
tenantId,
});
await customerRepository.changeBalance(
oldSaleInvoice.customerId,
oldSaleInvoice.balance * -1
);
}
}

View File

@@ -21,12 +21,20 @@ export default class PaymentMadesSubscriber {
*/ */
@On(events.billPayment.onCreated) @On(events.billPayment.onCreated)
@On(events.billPayment.onEdited) @On(events.billPayment.onEdited)
async handleBillsIncrementPaymentAmount({ tenantId, billPayment, oldBillPayment, billPaymentId }) { async handleBillsIncrementPaymentAmount({
this.logger.info('[payment_made] trying to change bills payment amount.', { tenantId, billPaymentId }); tenantId,
billPayment,
oldBillPayment,
billPaymentId,
}) {
this.logger.info('[payment_made] trying to change bills payment amount.', {
tenantId,
billPaymentId,
});
this.billPaymentsService.saveChangeBillsPaymentAmount( this.billPaymentsService.saveChangeBillsPaymentAmount(
tenantId, tenantId,
billPayment.entries, billPayment.entries,
oldBillPayment?.entries || null, oldBillPayment?.entries || null
); );
} }
@@ -34,27 +42,19 @@ export default class PaymentMadesSubscriber {
* Handle revert bill payment amount once bill payment deleted. * Handle revert bill payment amount once bill payment deleted.
*/ */
@On(events.billPayment.onDeleted) @On(events.billPayment.onDeleted)
handleBillDecrementPaymentAmount({ tenantId, billPaymentId, oldBillPayment }) { handleBillDecrementPaymentAmount({
this.logger.info('[payment_made] tring to revert bill payment amount.', { tenantId, billPaymentId }); tenantId,
billPaymentId,
oldBillPayment,
}) {
this.logger.info('[payment_made] tring to revert bill payment amount.', {
tenantId,
billPaymentId,
});
this.billPaymentsService.saveChangeBillsPaymentAmount( this.billPaymentsService.saveChangeBillsPaymentAmount(
tenantId, tenantId,
oldBillPayment.entries.map((entry) => ({ ...entry, paymentAmount: 0 })), oldBillPayment.entries.map((entry) => ({ ...entry, paymentAmount: 0 })),
oldBillPayment.entries, oldBillPayment.entries
);
}
/**
* Handle vendor balance increment once payment made created.
*/
@On(events.billPayment.onCreated)
async handleVendorIncrement({ tenantId, billPayment, billPaymentId }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Increment the vendor balance after bills payments.
this.logger.info('[bill_payment] trying to increment vendor balance.', { tenantId });
await vendorRepository.changeBalance(
billPayment.vendorId,
billPayment.amount * -1,
); );
} }
@@ -65,20 +65,30 @@ export default class PaymentMadesSubscriber {
async handleWriteJournalEntries({ tenantId, billPayment }) { async handleWriteJournalEntries({ tenantId, billPayment }) {
// Records the journal transactions after bills payment // Records the journal transactions after bills payment
// and change diff acoount balance. // and change diff acoount balance.
this.logger.info('[bill_payment] trying to write journal entries.', { tenantId, billPaymentId: billPayment.id }); this.logger.info('[bill_payment] trying to write journal entries.', {
tenantId,
billPaymentId: billPayment.id,
});
await this.billPaymentsService.recordJournalEntries(tenantId, billPayment); await this.billPaymentsService.recordJournalEntries(tenantId, billPayment);
} }
/** /**
* Decrements the vendor balance once bill payment deleted. * Handle bill payment re-writing journal entries once the payment transaction be edited.
*/ */
@On(events.billPayment.onDeleted) @On(events.billPayment.onEdited)
async handleVendorDecrement({ tenantId, paymentMadeId, oldPaymentMade }) { async handleRewriteJournalEntriesOncePaymentEdited({
const { vendorRepository } = this.tenancy.repositories(tenantId); tenantId,
billPayment,
await vendorRepository.changeBalance( }) {
oldPaymentMade.vendorId, // Records the journal transactions after bills payment be edited.
oldPaymentMade.amount, this.logger.info('[bill_payment] trying to rewrite journal entries.', {
tenantId,
billPaymentId: billPayment.id,
});
await this.billPaymentsService.recordJournalEntries(
tenantId,
billPayment,
true
); );
} }
@@ -88,24 +98,8 @@ export default class PaymentMadesSubscriber {
@On(events.billPayment.onDeleted) @On(events.billPayment.onDeleted)
async handleRevertJournalEntries({ tenantId, billPaymentId }) { async handleRevertJournalEntries({ tenantId, billPaymentId }) {
await this.billPaymentsService.revertJournalEntries( await this.billPaymentsService.revertJournalEntries(
tenantId, billPaymentId, tenantId,
); billPaymentId
}
/**
* Change the vendor balance different between old and new once
* bill payment edited.
*/
@On(events.billPayment.onEdited)
async handleVendorChangeDiffBalance({ tenantId, paymentMadeId, billPayment, oldBillPayment }) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
// Change the different vendor balance between the new and old one.
await vendorRepository.changeDiffBalance(
billPayment.vendorId,
oldBillPayment.vendorId,
billPayment.amount * -1,
oldBillPayment.amount * -1,
); );
} }
} }

View File

@@ -39,61 +39,6 @@ export default class PaymentReceivesSubscriber {
); );
} }
/**
* Handle customer balance decrement once payment receive created.
*/
@On(events.paymentReceive.onCreated)
async handleCustomerBalanceDecrement({
tenantId,
paymentReceiveId,
paymentReceive,
}) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[payment_receive] trying to decrement customer balance.');
await customerRepository.changeBalance(
paymentReceive.customerId,
paymentReceive.amount * -1
);
}
/**
* Handle customer balance increment once payment receive deleted.
*/
@On(events.paymentReceive.onDeleted)
async handleCustomerBalanceIncrement({
tenantId,
paymentReceiveId,
oldPaymentReceive,
}) {
const { customerRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[payment_receive] trying to increment customer balance.');
await customerRepository.changeBalance(
oldPaymentReceive.customerId,
oldPaymentReceive.amount
);
}
/**
* Handles customer balance diff balance change once payment receive edited.
*/
@On(events.paymentReceive.onEdited)
async handleCustomerBalanceDiffChange({
tenantId,
paymentReceiveId,
paymentReceive,
oldPaymentReceive,
}) {
const { customerRepository } = this.tenancy.repositories(tenantId);
await customerRepository.changeDiffBalance(
paymentReceive.customerId,
paymentReceive.amount * -1,
oldPaymentReceive.amount * -1,
);
}
/** /**
* Handle journal entries writing once the payment receive edited. * Handle journal entries writing once the payment receive edited.
*/ */