feat: rewrite repositories with base entity repository class.

feat: sales and purchases status.
feat: sales and purchases auto-increment number.
fix: settings find query with extra columns.
This commit is contained in:
Ahmed Bouhuolia
2020-12-13 19:50:59 +02:00
parent e9e4ddaee0
commit 188e411f02
78 changed files with 1634 additions and 869 deletions

View File

@@ -40,7 +40,7 @@
"express-boom": "^3.0.0", "express-boom": "^3.0.0",
"express-fileupload": "^1.1.7-alpha.3", "express-fileupload": "^1.1.7-alpha.3",
"express-oauth-server": "^2.0.0", "express-oauth-server": "^2.0.0",
"express-validator": "^6.2.0", "express-validator": "^6.8.0",
"helmet": "^3.21.0", "helmet": "^3.21.0",
"i18n": "^0.8.5", "i18n": "^0.8.5",
"is-my-json-valid": "^2.20.5", "is-my-json-valid": "^2.20.5",

View File

@@ -1,6 +1,4 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import MomentFormat from 'lib/MomentFormats';
import moment from 'moment';
export default class Ping { export default class Ping {
/** /**

View File

@@ -38,6 +38,14 @@ export default class BillsController extends BaseController {
asyncMiddleware(this.newBill.bind(this)), asyncMiddleware(this.newBill.bind(this)),
this.handleServiceError, this.handleServiceError,
); );
router.post(
'/:id/open', [
...this.specificBillValidationSchema,
],
this.validationResult,
asyncMiddleware(this.openBill.bind(this)),
this.handleServiceError,
);
router.post( router.post(
'/:id', [ '/:id', [
...this.billValidationSchema, ...this.billValidationSchema,
@@ -94,6 +102,8 @@ export default class BillsController extends BaseController {
check('due_date').optional().isISO8601(), check('due_date').optional().isISO8601(),
check('vendor_id').exists().isNumeric().toInt(), check('vendor_id').exists().isNumeric().toInt(),
check('note').optional().trim().escape(), check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }), check('entries').isArray({ min: 1 }),
check('entries.*.id').optional().isNumeric().toInt(), check('entries.*.id').optional().isNumeric().toInt(),
@@ -117,6 +127,8 @@ export default class BillsController extends BaseController {
check('due_date').optional().isISO8601(), check('due_date').optional().isISO8601(),
check('vendor_id').exists().isNumeric().toInt(), check('vendor_id').exists().isNumeric().toInt(),
check('note').optional().trim().escape(), check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }), check('entries').isArray({ min: 1 }),
check('entries.*.id').optional().isNumeric().toInt(), check('entries.*.id').optional().isNumeric().toInt(),
@@ -185,12 +197,13 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
*/ */
async editBill(req: Request, res: Response, next: NextFunction) { async editBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params; const { id: billId, user } = req.params;
const { tenantId } = req; const { tenantId } = req;
const billDTO: IBillEditDTO = this.matchedBodyData(req); const billDTO: IBillEditDTO = this.matchedBodyData(req);
try { try {
const editedBill = await this.billsService.editBill(tenantId, billId, billDTO); await this.billsService.editBill(tenantId, billId, billDTO, user);
return res.status(200).send({ return res.status(200).send({
id: billId, id: billId,
message: 'The bill has been edited successfully.', message: 'The bill has been edited successfully.',
@@ -200,6 +213,27 @@ export default class BillsController extends BaseController {
} }
} }
/**
* Open the given bill.
* @param {Request} req -
* @param {Response} res -
*/
async openBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params;
const { tenantId } = req;
try {
await this.billsService.openBill(tenantId, billId);
return res.status(200).send({
id: billId,
message: 'The bill has been opened successfully.',
});
} catch (error) {
next(error);
}
}
/** /**
* Retrieve the given bill details with associated item entries. * Retrieve the given bill details with associated item entries.
* @param {Request} req * @param {Request} req
@@ -339,6 +373,11 @@ export default class BillsController extends BaseController {
errors: [{ type: 'ITEMS_NOT_FOUND', code: 1000 }], errors: [{ type: 'ITEMS_NOT_FOUND', code: 1000 }],
}); });
} }
if (error.errorType === 'BILL_ALREADY_OPEN') {
return res.boom.badRequest(null, {
errors: [{ type: 'BILL_ALREADY_OPEN', code: 1100 }],
});
}
} }
next(error); next(error);
} }

View File

@@ -7,6 +7,7 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate'; import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from "exceptions"; import { ServiceError } from "exceptions";
import { Request } from 'express-validator/src/base';
@Service() @Service()
export default class SalesEstimatesController extends BaseController { export default class SalesEstimatesController extends BaseController {
@@ -30,6 +31,15 @@ export default class SalesEstimatesController extends BaseController {
asyncMiddleware(this.newEstimate.bind(this)), asyncMiddleware(this.newEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors,
); );
router.post(
'/:id/deliver',
[
...this.validateSpecificEstimateSchema,
],
this.validationResult,
asyncMiddleware(this.deliverSaleEstimate.bind(this)),
this.handleServiceErrors,
);
router.post( router.post(
'/:id', [ '/:id', [
...this.validateSpecificEstimateSchema, ...this.validateSpecificEstimateSchema,
@@ -75,6 +85,7 @@ export default class SalesEstimatesController extends BaseController {
check('expiration_date').optional().isISO8601(), check('expiration_date').optional().isISO8601(),
check('reference').optional(), check('reference').optional(),
check('estimate_number').exists().trim().escape(), check('estimate_number').exists().trim().escape(),
check('delivered').default(false).isBoolean().toBoolean(),
check('entries').exists().isArray({ min: 1 }), check('entries').exists().isArray({ min: 1 }),
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
@@ -170,6 +181,27 @@ export default class SalesEstimatesController extends BaseController {
} }
} }
/**
* Deliver the given sale estimate.
* @param {Request} req
* @param {Response} res
*/
async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
const { tenantId } = req;
try {
await this.saleEstimateService.deliverSaleEstimate(tenantId, estimateId);
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been delivered successfully.',
});
} catch (error) {
next(error);
}
}
/** /**
* Retrieve the given estimate with associated entries. * Retrieve the given estimate with associated entries.
*/ */

View File

@@ -28,11 +28,23 @@ export default class SaleInvoicesController extends BaseController{
router.post( router.post(
'/', '/',
this.saleInvoiceValidationSchema, [
...this.saleInvoiceValidationSchema,
check('from_estimate_id').optional().isNumeric().toInt(),
],
this.validationResult, this.validationResult,
asyncMiddleware(this.newSaleInvoice.bind(this)), asyncMiddleware(this.newSaleInvoice.bind(this)),
this.handleServiceErrors, this.handleServiceErrors,
); );
router.post(
'/:id/deliver',
[
...this.specificSaleInvoiceValidation,
],
this.validationResult,
asyncMiddleware(this.deliverSaleInvoice.bind(this)),
this.handleServiceErrors,
)
router.post( router.post(
'/:id', '/:id',
[ [
@@ -86,7 +98,7 @@ export default class SaleInvoicesController extends BaseController{
check('due_date').exists().isISO8601(), check('due_date').exists().isISO8601(),
check('invoice_no').optional().trim().escape(), check('invoice_no').optional().trim().escape(),
check('reference_no').optional().trim().escape(), check('reference_no').optional().trim().escape(),
check('status').exists().trim().escape(), check('delivered').default(false).isBoolean().toBoolean(),
check('invoice_message').optional().trim().escape(), check('invoice_message').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(), check('terms_conditions').optional().trim().escape(),
@@ -172,6 +184,28 @@ export default class SaleInvoicesController extends BaseController{
next(error); next(error);
} }
} }
/**
* Deliver the given sale invoice.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async deliverSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: saleInvoiceId } = req.params;
try {
await this.saleInvoiceService.deliverSaleInvoice(tenantId, saleInvoiceId);
return res.status(200).send({
id: saleInvoiceId,
message: 'The given sale invoice has been published successfully',
});
} catch (error) {
next(error);
}
}
/** /**
* Deletes the sale invoice with associated entries and journal transactions. * Deletes the sale invoice with associated entries and journal transactions.
@@ -319,6 +353,11 @@ export default class SaleInvoicesController extends BaseController{
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 200 }], errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 200 }],
}); });
} }
if (error.errorType === 'SALE_INVOICE_ALREADY_DELIVERED') {
return res.boom.badRequest(null, {
errors: [{ type: 'SALE_INVOICE_ALREADY_DELIVERED', code: 200 }],
});
}
} }
next(error); next(error);
} }

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import SettingsStore from 'services/Settings/SettingsStore'; import SettingsStore from 'services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => { export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user; const { tenantId } = req.user;
const { knex } = req; const { knex } = req;
@@ -10,16 +10,19 @@ export default async (req: Request, res: Response, next: NextFunction) => {
const tenantContainer = Container.of(`tenant-${tenantId}`); const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) { if (tenantContainer && !tenantContainer.has('settings')) {
const { settingRepository } = tenantContainer.get('repositories');
Logger.info('[settings_middleware] initialize settings store.'); Logger.info('[settings_middleware] initialize settings store.');
const settings = new SettingsStore(knex);
Logger.info('[settings_middleware] load settings from storage or cache.');
await settings.load();
const settings = new SettingsStore(settingRepository);
tenantContainer.set('settings', settings); tenantContainer.set('settings', settings);
} }
Logger.info('[settings_middleware] get settings instance from container.'); Logger.info('[settings_middleware] get settings instance from container.');
const settings = tenantContainer.get('settings'); const settings = tenantContainer.get('settings');
Logger.info('[settings_middleware] load settings from storage or cache.');
await settings.load();
req.settings = settings; req.settings = settings;
res.on('finish', async () => { res.on('finish', async () => {

View File

@@ -1,3 +1,4 @@
const { default: TrialBalanceSheet } = require("services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet");
exports.up = function(knex) { exports.up = function(knex) {
return knex.schema.createTable('sales_estimates', (table) => { return knex.schema.createTable('sales_estimates', (table) => {
@@ -11,7 +12,13 @@ exports.up = function(knex) {
table.text('note'); table.text('note');
table.text('terms_conditions'); table.text('terms_conditions');
table.text('send_to_email'); table.text('send_to_email');
table.date('delivered_at').index();
table.integer('user_id').unsigned().index(); table.integer('user_id').unsigned().index();
table.integer('converted_to_invoice_id').unsigned();
table.date('converted_to_invoice_at');
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -7,7 +7,6 @@ exports.up = function(knex) {
table.date('due_date'); table.date('due_date');
table.string('invoice_no').index(); table.string('invoice_no').index();
table.string('reference_no'); table.string('reference_no');
table.string('status').index();
table.text('invoice_message'); table.text('invoice_message');
table.text('terms_conditions'); table.text('terms_conditions');
@@ -16,6 +15,8 @@ exports.up = function(knex) {
table.decimal('payment_amount', 13, 3); table.decimal('payment_amount', 13, 3);
table.string('inv_lot_number').index(); table.string('inv_lot_number').index();
table.date('delivered_at').index();
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -12,6 +12,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').index(); table.string('inv_lot_number').index();
table.date('opened_at').index();
table.integer('user_id').unsigned(); table.integer('user_id').unsigned();
table.timestamps(); table.timestamps();
}); });

View File

@@ -0,0 +1,22 @@
exports.up = (knex) => {
// Inserts seed entries
return knex('settings').insert([
{ group: 'manual_journals', key: 'next_number', value: 1 },
{ group: 'sales_invoices', key: 'next_number', value: 1},
{ group: 'sales_invoices', key: 'number_prefix', value: 'INV' },
{ group: 'sales_receipts', key: 'next_number', value: 1 },
{ group: 'sales_receipts', key: 'number_prefix', value: 'REC' },
{ group: 'sales_estimates', key: 'next_number', value: 1 },
{ group: 'sales_estimates', key: 'number_prefix', value: 'EST' },
{ group: 'payment_receives', key: 'next_number', value: 1 },
]);
};
exports.down = (knex) => {
}

View File

@@ -0,0 +1,8 @@
export default class ModelEntityNotFound extends Error {
constructor(entityId, message?) {
message = message || `Entity with id ${entityId} does not exist`;
super(message);
}
}

View File

@@ -11,6 +11,7 @@ export interface IBillDTO {
note: string, note: string,
amount: number, amount: number,
paymentAmount: number, paymentAmount: number,
open: boolean,
entries: IItemEntryDTO[], entries: IItemEntryDTO[],
}; };
@@ -24,6 +25,7 @@ export interface IBillEditDTO {
note: string, note: string,
amount: number, amount: number,
paymentAmount: number, paymentAmount: number,
open: boolean,
entries: IItemEntryDTO[], entries: IItemEntryDTO[],
}; };
@@ -41,6 +43,7 @@ export interface IBill {
paymentAmount: number, paymentAmount: number,
invLotNumber: string, invLotNumber: string,
openedAt: Date | string,
entries: IItemEntry[], entries: IItemEntry[],
}; };

View File

@@ -13,6 +13,7 @@ export interface ISaleEstimate {
entries: IItemEntry[], entries: IItemEntry[],
sendToEmail: string, sendToEmail: string,
createdAt?: Date, createdAt?: Date,
deliveredAt: string|Date,
}; };
export interface ISaleEstimateDTO { export interface ISaleEstimateDTO {
customerId: number, customerId: number,
@@ -23,6 +24,7 @@ export interface ISaleEstimateDTO {
note: string, note: string,
termsConditions: string, termsConditions: string,
sendToEmail: string, sendToEmail: string,
delivered: boolean,
}; };
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO { export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {

View File

@@ -7,7 +7,9 @@ export interface ISaleInvoice {
invoiceDate: Date, invoiceDate: Date,
dueDate: Date, dueDate: Date,
dueAmount: number, dueAmount: number,
customerId: number,
entries: IItemEntry[], entries: IItemEntry[],
deliveredAt: string|Date,
} }
export interface ISaleInvoiceOTD { export interface ISaleInvoiceOTD {
@@ -19,8 +21,17 @@ export interface ISaleInvoiceOTD {
invoiceMessage: string, invoiceMessage: string,
termsConditions: string, termsConditions: string,
entries: IItemEntryDTO[], entries: IItemEntryDTO[],
delivered: boolean,
} }
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceOTD {
fromEstiamteId: number,
};
export interface ISaleInvoiceEditDTO extends ISaleInvoiceOTD {
};
export interface ISalesInvoicesFilter{ export interface ISalesInvoicesFilter{
page: number, page: number,
pageSize: number, pageSize: number,

View File

@@ -7,7 +7,7 @@ import MetableStore from './MetableStore';
import { isBlank } from 'utils'; import { isBlank } from 'utils';
export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{ export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{
model: Model; repository: any;
KEY_COLUMN: string; KEY_COLUMN: string;
VALUE_COLUMN: string; VALUE_COLUMN: string;
TYPE_COLUMN: string; TYPE_COLUMN: string;
@@ -24,14 +24,13 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
this.KEY_COLUMN = 'key'; this.KEY_COLUMN = 'key';
this.VALUE_COLUMN = 'value'; this.VALUE_COLUMN = 'value';
this.TYPE_COLUMN = 'type'; this.TYPE_COLUMN = 'type';
this.model = null; this.repository = null;
this.extraQuery = (query, meta) => { this.extraQuery = (meta) => {
const whereQuery = { return {
key: meta[this.KEY_COLUMN], key: meta[this.KEY_COLUMN],
...this.transfromMetaExtraColumns(meta), ...this.transfromMetaExtraColumns(meta),
}; };
query.where(whereQuery);
}; };
} }
@@ -51,11 +50,11 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
} }
/** /**
* Set model of this metadata collection. * Set repository entity of this metadata collection.
* @param {Object} model - * @param {Object} repository -
*/ */
setModel(model: Model) { setRepository(repository) {
this.model = model; this.repository = repository;
} }
/** /**
@@ -89,10 +88,10 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
const opers = []; const opers = [];
updated.forEach((meta) => { updated.forEach((meta) => {
const updateOper = this.model.query().onBuild((query) => { const updateOper = this.repository.update({
this.extraQuery(query, meta);
}).patch({
[this.VALUE_COLUMN]: meta.value, [this.VALUE_COLUMN]: meta.value,
}, {
...this.extraQuery(meta),
}).then(() => { }).then(() => {
meta._markAsUpdated = false; meta._markAsUpdated = false;
}); });
@@ -112,9 +111,9 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
if (deleted.length > 0) { if (deleted.length > 0) {
deleted.forEach((meta) => { deleted.forEach((meta) => {
const deleteOper = this.model.query().onBuild((query) => { const deleteOper = this.repository.deleteBy({
this.extraQuery(query, meta); ...this.extraQuery(meta),
}).delete().then(() => { }).then(() => {
meta._markAsDeleted = false; meta._markAsDeleted = false;
}); });
opers.push(deleteOper); opers.push(deleteOper);
@@ -138,9 +137,7 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
[this.VALUE_COLUMN]: meta.value, [this.VALUE_COLUMN]: meta.value,
...this.transfromMetaExtraColumns(meta), ...this.transfromMetaExtraColumns(meta),
}; };
const insertOper = this.repository.create(insertData)
const insertOper = this.model.query()
.insert(insertData)
.then(() => { .then(() => {
meta._markAsInserted = false; meta._markAsInserted = false;
}); });
@@ -155,7 +152,7 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
* @param {Boolean} force - * @param {Boolean} force -
*/ */
async load() { async load() {
const metadata = await this.model.query(); const metadata = await this.repository.all();
const mappedMetadata = this.mapMetadataCollection(metadata); const mappedMetadata = this.mapMetadataCollection(metadata);
mappedMetadata.forEach((meta: IMetadata) => { mappedMetadata.forEach((meta: IMetadata) => {

View File

@@ -10,7 +10,8 @@ import Bill from 'models/Bill';
import BillPayment from 'models/BillPayment'; import BillPayment from 'models/BillPayment';
import BillPaymentEntry from 'models/BillPaymentEntry'; import BillPaymentEntry from 'models/BillPaymentEntry';
import Currency from 'models/Currency'; import Currency from 'models/Currency';
import Contact from 'models/Contact'; import Vendor from 'models/Vendor';
import Customer from 'models/Customer';
import ExchangeRate from 'models/ExchangeRate'; import ExchangeRate from 'models/ExchangeRate';
import Expense from 'models/Expense'; import Expense from 'models/Expense';
import ExpenseCategory from 'models/ExpenseCategory'; import ExpenseCategory from 'models/ExpenseCategory';
@@ -66,7 +67,8 @@ export default (knex) => {
InventoryCostLotTracker, InventoryCostLotTracker,
Media, Media,
MediaLink, MediaLink,
Contact, Vendor,
Customer,
}; };
return mapValues(models, (model) => model.bindKnex(knex)); return mapValues(models, (model) => model.bindKnex(knex));
} }

View File

@@ -7,17 +7,25 @@ import ViewRepository from 'repositories/ViewRepository';
import ViewRoleRepository from 'repositories/ViewRoleRepository'; import ViewRoleRepository from 'repositories/ViewRoleRepository';
import ContactRepository from 'repositories/ContactRepository'; import ContactRepository from 'repositories/ContactRepository';
import AccountTransactionsRepository from 'repositories/AccountTransactionRepository'; import AccountTransactionsRepository from 'repositories/AccountTransactionRepository';
import SettingRepository from 'repositories/SettingRepository';
import ExpenseEntryRepository from 'repositories/ExpenseEntryRepository';
import BillRepository from 'repositories/BillRepository';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
export default (tenantId: number) => { export default (knex, cache) => {
return { return {
accountRepository: new AccountRepository(tenantId), accountRepository: new AccountRepository(knex, cache),
transactionsRepository: new AccountTransactionsRepository(tenantId), transactionsRepository: new AccountTransactionsRepository(knex, cache),
accountTypeRepository: new AccountTypeRepository(tenantId), accountTypeRepository: new AccountTypeRepository(knex, cache),
customerRepository: new CustomerRepository(tenantId), customerRepository: new CustomerRepository(knex, cache),
vendorRepository: new VendorRepository(tenantId), vendorRepository: new VendorRepository(knex, cache),
contactRepository: new ContactRepository(tenantId), contactRepository: new ContactRepository(knex, cache),
expenseRepository: new ExpenseRepository(tenantId), expenseRepository: new ExpenseRepository(knex, cache),
viewRepository: new ViewRepository(tenantId), expenseEntryRepository: new ExpenseEntryRepository(knex, cache),
viewRoleRepository: new ViewRoleRepository(tenantId), viewRepository: new ViewRepository(knex, cache),
viewRoleRepository: new ViewRoleRepository(knex, cache),
settingRepository: new SettingRepository(knex, cache),
billRepository: new BillRepository(knex, cache),
saleInvoiceRepository: new SaleInvoiceRepository(knex, cache),
}; };
}; };

View File

@@ -1,15 +1,9 @@
import { Model, raw } from 'objection'; import { Model, raw } from 'objection';
import moment from 'moment';
import { difference } from 'lodash'; import { difference } from 'lodash';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
export default class Bill extends TenantModel { export default class Bill extends TenantModel {
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['dueAmount'];
}
/** /**
* Table name * Table name
*/ */
@@ -36,6 +30,13 @@ export default class Bill extends TenantModel {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['dueAmount', 'isOpen', 'isPartiallyPaid', 'isFullyPaid', 'isPaid', 'remainingDays', 'overdueDays', 'isOverdue'];
}
/** /**
* Due amount of the given. * Due amount of the given.
* @return {number} * @return {number}
@@ -44,6 +45,74 @@ export default class Bill extends TenantModel {
return Math.max(this.amount - this.paymentAmount, 0); return Math.max(this.amount - this.paymentAmount, 0);
} }
/**
* Detarmine whether the bill is open.
* @return {boolean}
*/
get isOpen() {
return !!this.openedAt;
}
/**
* Deetarmine whether the bill paid partially.
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.amount && this.dueAmount > 0;
}
/**
* Deetarmine whether the bill paid fully.
* @return {boolean}
*/
get isFullyPaid() {
return this.dueAmount === 0;
}
/**
* Detarmines whether the bill paid fully or partially.
* @return {boolean}
*/
get isPaid() {
return this.isPartiallyPaid || this.isFullyPaid;
}
/**
* Retrieve the remaining days in number
* @return {number|null}
*/
get remainingDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
const date = moment();
const dueDate = moment(this.dueDate);
return Math.max(dueDate.diff(date, 'days'), 0);
}
/**
* Retrieve the overdue days in number.
* @return {number|null}
*/
get overdueDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
const date = moment();
const dueDate = moment(this.dueDate);
return Math.max(date.diff(dueDate, 'days'), 0);
}
/**
* Detarmines the due date is over.
* @return {boolean}
*/
get isOverdue() {
return this.overdueDays > 0;
}
/** /**
* Relationship mapping. * Relationship mapping.
*/ */

View File

@@ -26,7 +26,7 @@ export default class Contact extends TenantModel {
/** /**
* Closing balance attribute. * Closing balance attribute.
*/ */
closingBalance() { get closingBalance() {
return this.openingBalance + this.balance; return this.openingBalance + this.balance;
} }
@@ -77,66 +77,6 @@ export default class Contact extends TenantModel {
}; };
} }
/**
* Change vendor balance.
* @param {Integer} customerId
* @param {Numeric} amount
*/
static async changeBalance(customerId, amount) {
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return this.query()
.where('id', customerId)
[changeMethod]('balance', Math.abs(amount));
}
/**
* Increment the given customer balance.
* @param {Integer} customerId
* @param {Integer} amount
*/
static async incrementBalance(customerId, amount) {
return this.query()
.where('id', customerId)
.increment('balance', amount);
}
/**
* Decrement the given customer balance.
* @param {integer} customerId -
* @param {integer} amount -
*/
static async decrementBalance(customerId, amount) {
await this.query()
.where('id', customerId)
.decrement('balance', amount);
}
/**
*
* @param {number} customerId
* @param {number} oldCustomerId
* @param {number} amount
* @param {number} oldAmount
*/
static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) {
const diffAmount = amount - oldAmount;
const asyncOpers = [];
if (customerId != oldCustomerId) {
const oldCustomerOper = this.changeBalance(oldCustomerId, (oldAmount * -1));
const customerOper = this.changeBalance(customerId, amount);
asyncOpers.push(customerOper);
asyncOpers.push(oldCustomerOper);
} else {
const balanceChangeOper = this.changeBalance(customerId, diffAmount);
asyncOpers.push(balanceChangeOper);
}
return Promise.all(asyncOpers);
}
static get fields() { static get fields() {
return { return {
created_at: { created_at: {

View File

@@ -0,0 +1,78 @@
import { Model, QueryBuilder } from 'objection';
import TenantModel from 'models/TenantModel';
class CustomerQueryBuilder extends QueryBuilder {
constructor(...args) {
super(...args);
this.onBuild((builder) => {
if (builder.isFind() || builder.isDelete() || builder.isUpdate()) {
builder.where('contact_service', 'customer');
}
});
}
}
export default class Customer extends TenantModel {
/**
* Query builder.
*/
static get QueryBuilder() {
return CustomerQueryBuilder;
}
/**
* Table name
*/
static get tableName() {
return 'contacts';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Defined virtual attributes.
*/
static get virtualAttributes() {
return ['closingBalance'];
}
/**
* Closing balance attribute.
*/
get closingBalance() {
return this.openingBalance + this.balance;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleInvoice = require('models/SaleInvoice');
return {
salesInvoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
join: {
from: 'contacts.id',
to: 'sales_invoices.customerId',
},
},
};
}
static get fields() {
return {
created_at: {
column: 'created_at',
}
};
}
}

View File

@@ -1,7 +1,6 @@
import { Model } from "objection"; import { Model } from "objection";
import TenantModel from "models/TenantModel"; import TenantModel from "models/TenantModel";
import { viewRolesBuilder } from "lib/ViewRolesBuilder"; import { viewRolesBuilder } from "lib/ViewRolesBuilder";
import Media from "./Media";
export default class Expense extends TenantModel { export default class Expense extends TenantModel {
/** /**

View File

@@ -1,5 +1,7 @@
import { Model, mixin } from 'objection'; import moment from 'moment';
import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils';
export default class SaleEstimate extends TenantModel { export default class SaleEstimate extends TenantModel {
/** /**
@@ -16,6 +18,41 @@ export default class SaleEstimate extends TenantModel {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isDelivered', 'isExpired', 'isConvertedToInvoice'];
}
/**
* Detarmines whether the sale estimate converted to sale invoice.
* @return {boolean}
*/
get isConvertedToInvoice() {
return !!(this.convertedToInvoiceId && this.convertedToInvoiceAt);
}
/**
* Detarmines whether the estimate is delivered.
* @return {boolean}
*/
get isDelivered() {
return !!this.deliveredAt;
}
/**
* Detarmines whether the estimate is expired.
* @return {boolean}
*/
get isExpired() {
return defaultToTransform(
this.expirationDate,
moment().isAfter(this.expirationDate, 'day'),
false,
);
}
/** /**
* Allows to mark model as resourceable to viewable and filterable. * Allows to mark model as resourceable to viewable and filterable.
*/ */

View File

@@ -1,6 +1,7 @@
import { Model, raw } from 'objection'; import { Model, raw } from 'objection';
import moment from 'moment'; import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils';
export default class SaleInvoice extends TenantModel { export default class SaleInvoice extends TenantModel {
/** /**
@@ -24,6 +25,90 @@ export default class SaleInvoice extends TenantModel {
return ['created_at', 'updated_at']; return ['created_at', 'updated_at'];
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['dueAmount', 'isDelivered', 'isOverdue', 'isPartiallyPaid', 'isFullyPaid', 'isPaid', 'remainingDays', 'overdueDays'];
}
/**
* Detarmines whether the invoice is delivered.
* @return {boolean}
*/
get isDelivered() {
return !!this.deliveredAt;
}
/**
* Detarmines the due date is over.
* @return {boolean}
*/
get isOverdue() {
return this.overdueDays > 0;
}
/**
* Retrieve the invoice due amount.
* (Invoice amount - payment amount = Due amount)
* @return {boolean}
*/
dueAmount() {
return Math.max(this.balance - this.paymentAmount, 0);
}
/**
* Detarmine whether the invoice paid partially.
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.balance && this.dueAmount > 0;
}
/**
* Deetarmine whether the invoice paid fully.
* @return {boolean}
*/
get isFullyPaid() {
return this.dueAmount === 0;
}
/**
* Detarmines whether the invoice paid fully or partially.
* @return {boolean}
*/
get isPaid() {
return this.isPartiallyPaid || this.isFullyPaid;
}
/**
* Retrieve the remaining days in number
* @return {number|null}
*/
get remainingDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
const date = moment();
const dueDate = moment(this.dueDate);
return Math.max(dueDate.diff(date, 'days'), 0);
}
/**
* Retrieve the overdue days in number.
* @return {number|null}
*/
get overdueDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
const date = moment();
const dueDate = moment(this.dueDate);
return Math.max(date.diff(dueDate, 'days'), 0);
}
static get resourceable() { static get resourceable() {
return true; return true;
} }
@@ -67,6 +152,7 @@ export default class SaleInvoice extends TenantModel {
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const Contact = require('models/Contact'); const Contact = require('models/Contact');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker'); const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
return { return {
entries: { entries: {
@@ -115,7 +201,16 @@ export default class SaleInvoice extends TenantModel {
filter(builder) { filter(builder) {
builder.where('transaction_type', 'SaleInvoice'); builder.where('transaction_type', 'SaleInvoice');
}, },
} },
paymentEntries: {
relation: Model.HasManyRelation,
modelClass: PaymentReceiveEntry.default,
join: {
from: 'sales_invoices.id',
to: 'payment_receives_entries.invoice_id',
},
},
}; };
} }

View File

@@ -8,7 +8,8 @@ export default class TenantModel extends BaseModel {
*/ */
static query(...args) { static query(...args) {
const Logger = Container.get('logger'); const Logger = Container.get('logger');
return super.query(...args).onBuildKnex(knexQueryBuilder => {
return super.query(...args).onBuildKnex((knexQueryBuilder) => {
const { userParams: { tenantId } } = knexQueryBuilder.client.config; const { userParams: { tenantId } } = knexQueryBuilder.client.config;
knexQueryBuilder.on('query', queryData => { knexQueryBuilder.on('query', queryData => {

View File

@@ -0,0 +1,78 @@
import { Model, QueryBuilder } from 'objection';
import TenantModel from 'models/TenantModel';
class VendorQueryBuilder extends QueryBuilder {
constructor(...args) {
super(...args);
this.onBuild((builder) => {
if (builder.isFind() || builder.isDelete() || builder.isUpdate()) {
builder.where('contact_service', 'vendor');
}
});
}
}
export default class Vendor extends TenantModel {
/**
* Query builder.
*/
static get QueryBuilder() {
return VendorQueryBuilder;
}
/**
* Table name
*/
static get tableName() {
return 'contacts';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Defined virtual attributes.
*/
static get virtualAttributes() {
return ['closingBalance'];
}
/**
* Closing balance attribute.
*/
get closingBalance() {
return this.openingBalance + this.balance;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Bill = require('models/Bill');
return {
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'contacts.id',
to: 'bills.vendorId',
},
}
};
}
static get fields() {
return {
created_at: {
column: 'created_at',
}
};
}
}

View File

@@ -18,6 +18,12 @@ import ItemEntry from './ItemEntry';
import InventoryTransaction from './InventoryTransaction'; import InventoryTransaction from './InventoryTransaction';
import AccountType from './AccountType'; import AccountType from './AccountType';
import InventoryLotCostTracker from './InventoryCostLotTracker'; import InventoryLotCostTracker from './InventoryCostLotTracker';
import Customer from './Customer';
import Contact from './Contact';
import Vendor from './Vendor';
import ExpenseCategory from './ExpenseCategory';
import Expense from './Expense';
import ManualJournal from './ManualJournal';
export { export {
SaleEstimate, SaleEstimate,
@@ -40,4 +46,10 @@ export {
InventoryLotCostTracker, InventoryLotCostTracker,
AccountType, AccountType,
Option, Option,
Contact,
ExpenseCategory,
Expense,
ManualJournal,
Customer,
Vendor,
}; };

View File

@@ -1,124 +1,28 @@
import { Account } from 'models';
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
import { IAccount } from 'interfaces';
export default class AccountRepository extends TenantRepository { export default class AccountRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Account;
}
/** /**
* Retrieve accounts dependency graph. * Retrieve accounts dependency graph.
* @returns {} * @returns {}
*/ */
async getDependencyGraph() { async getDependencyGraph(withRelation) {
const { Account } = this.models; const accounts = await this.all(withRelation);
const accounts = await this.allAccounts(); const cacheKey = this.getCacheKey('accounts.depGraph', withRelation);
const cacheKey = this.getCacheKey('accounts.depGraph');
return this.cache.get(cacheKey, async () => { return this.cache.get(cacheKey, async () => {
return Account.toDependencyGraph(accounts); return this.model.toDependencyGraph(accounts);
}); });
} }
/**
* Retrieve all accounts on the storage.
* @return {IAccount[]}
*/
allAccounts(withRelations?: string|string[]): IAccount[] {
const { Account } = this.models;
const cacheKey = this.getCacheKey('accounts.depGraph', withRelations);
return this.cache.get(cacheKey, async () => {
return Account.query()
.withGraphFetched(withRelations);
});
}
/**
* Retrieve account of the given account slug.
* @param {string} slug
* @return {IAccount}
*/
getBySlug(slug: string): IAccount {
const { Account } = this.models;
const cacheKey = this.getCacheKey('accounts.slug', slug);
return this.cache.get(cacheKey, () => {
return Account.query().findOne('slug', slug);
});
}
/**
* Retrieve the account by the given id.
* @param {number} id - Account id.
* @return {IAccount}
*/
findById(id: number): IAccount {
const { Account } = this.models;
const cacheKey = this.getCacheKey('accounts.id', id);
return this.cache.get(cacheKey, () => {
return Account.query().findById(id);
});
}
/**
* Retrieve accounts by the given ids.
* @param {number[]} ids -
* @return {IAccount[]}
*/
findByIds(accountsIds: number[]) {
const { Account } = this.models;
const cacheKey = this.getCacheKey('accounts.id', accountsIds);
return this.cache.get(cacheKey, () => {
return Account.query().whereIn('id', accountsIds);
});
}
/**
* Activate the given account.
* @param {number} accountId -
* @return {void}
*/
async activate(accountId: number): Promise<void> {
const { Account } = this.models;
await Account.query().findById(accountId).patch({ active: 1 })
this.flushCache();
}
/**
* Inserts a new accounts to the storage.
* @param {IAccount} account
*/
async insert(accountInput: IAccount): Promise<void> {
const { Account } = this.models;
const account = await Account.query().insertAndFetch({ ...accountInput });
this.flushCache();
return account;
}
/**
* Updates account of the given account.
* @param {number} accountId - Account id.
* @param {IAccount} account
* @return {void}
*/
async edit(accountId: number, accountInput: IAccount): Promise<void> {
const { Account } = this.models;
const account = await Account.query().patchAndFetchById(accountId, { ...accountInput });
this.flushCache();
return account;
}
/**
* Deletes the given account by id.
* @param {number} accountId - Account id.
*/
async deleteById(accountId: number): Promise<void> {
const { Account } = this.models;
await Account.query().deleteById(accountId);
this.flushCache();
}
/** /**
* Changes account balance. * Changes account balance.
* @param {number} accountId * @param {number} accountId
@@ -126,17 +30,9 @@ export default class AccountRepository extends TenantRepository {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async balanceChange(accountId: number, amount: number): Promise<void> { async balanceChange(accountId: number, amount: number): Promise<void> {
const { Account } = this.models;
const method: string = (amount < 0) ? 'decrement' : 'increment'; const method: string = (amount < 0) ? 'decrement' : 'increment';
await Account.query().where('id', accountId)[method]('amount', amount); await this.model.query().where('id', accountId)[method]('amount', amount);
this.flushCache(); this.flushCache();
} }
/**
* Flush repository cache.
*/
flushCache(): void {
this.cache.delStartWith(this.repositoryName);
}
} }

View File

@@ -1,7 +1,4 @@
import { QueryBuilder } from 'knex';
import { AccountTransaction } from 'models'; import { AccountTransaction } from 'models';
import hashObject from 'object-hash';
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
@@ -17,13 +14,19 @@ interface IJournalTransactionsFilter {
}; };
export default class AccountTransactionsRepository extends TenantRepository { export default class AccountTransactionsRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = AccountTransaction;
}
journal(filter: IJournalTransactionsFilter) { journal(filter: IJournalTransactionsFilter) {
const { AccountTransaction } = this.models;
const cacheKey = this.getCacheKey('transactions.journal', filter); const cacheKey = this.getCacheKey('transactions.journal', filter);
return this.cache.get(cacheKey, () => { return this.cache.get(cacheKey, () => {
return AccountTransaction.query() return this.model.query()
.modify('filterAccounts', filter.accountsIds) .modify('filterAccounts', filter.accountsIds)
.modify('filterDateRange', filter.fromDate, filter.toDate) .modify('filterDateRange', filter.fromDate, filter.toDate)
.withGraphFetched('account.type') .withGraphFetched('account.type')

View File

@@ -1,81 +1,49 @@
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
import { IAccountType } from 'interfaces'; import { IAccountType } from 'interfaces';
import { AccountType } from 'models';
export default class AccountTypeRepository extends TenantRepository { export default class AccountTypeRepository extends TenantRepository {
/** /**
* Retrieve all accounts types. * Constructor method.
* @return {IAccountType[]}
*/ */
all() { constructor(knex, cache) {
const { AccountType } = this.models; super(knex, cache);
return this.cache.get('accountType.all', () => { this.model = AccountType;
return AccountType.query();
});
}
/**
* Retrieve account type meta.
* @param {number} accountTypeId
* @return {IAccountType}
*/
getTypeMeta(accountTypeId: number): IAccountType {
const { AccountType } = this.models;
return this.cache.get(`accountType.id.${accountTypeId}`, () => {
return AccountType.query().findById(accountTypeId);
});
} }
/** /**
* Retrieve accounts types of the given keys. * Retrieve accounts types of the given keys.
* @param {string[]} keys * @param {string[]} keys
* @return {IAccountType[]} * @return {Promise<IAccountType[]>}
*/ */
getByKeys(keys: string[]): IAccountType[] { getByKeys(keys: string[]): Promise<IAccountType[]> {
const { AccountType } = this.models; return super.findWhereIn('key', keys);
return this.cache.get(`accountType.keys.${keys.join(',')}`, () => {
return AccountType.query().whereIn('key', keys);
});
} }
/** /**
* Retrieve account tpy eof the given key. * Retrieve account tpy eof the given key.
* @param {string} key * @param {string} key
* @return {IAccountType} * @return {Promise<IAccountType>}
*/ */
getByKey(key: string): IAccountType { getByKey(key: string): Promise<IAccountType> {
const { AccountType } = this.models; return super.findOne({ key });
return this.cache.get(`accountType.key.${key}`, () => {
return AccountType.query().findOne('key', key);
});
} }
/** /**
* Retrieve accounts types of the given root type. * Retrieve accounts types of the given root type.
* @param {string} rootType * @param {string} rootType
* @return {IAccountType[]} * @return {Promise<IAccountType[]>}
*/ */
getByRootType(rootType: string): Promise<IAccountType[]> { getByRootType(rootType: string): Promise<IAccountType[]> {
const { AccountType } = this.models; return super.find({ root_type: rootType });
return this.cache.get(`accountType.rootType.${rootType}`, () => {
return AccountType.query().where('root_type', rootType);
});
} }
/** /**
* Retrieve accounts types of the given child type. * Retrieve accounts types of the given child type.
* @param {string} childType * @param {string} childType
* @return {Promise<IAccountType[]>}
*/ */
getByChildType(childType: string): Promise<IAccountType[]> { getByChildType(childType: string): Promise<IAccountType[]> {
const { AccountType } = this.models; return super.find({ child_type: childType });
return this.cache.get(`accountType.childType.${childType}`, () => {
return AccountType.query().where('child_type', childType);
});
}
/**
* Flush repository cache.
*/
flushCache() {
this.cache.delStartWith('accountType');
} }
} }

View File

@@ -2,10 +2,4 @@
export default class BaseModelRepository { export default class BaseModelRepository {
isExists(modelIdOrArray) {
const ids = Array.isArray(modelIdOrArray) ? modelIdOrArray : [modelIdOrArray];
const foundModels = this.model.tenant().query().whereIn('id', ids);
return foundModels.length > 0;
}
} }

View File

@@ -0,0 +1,12 @@
import { Bill } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class BillRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Bill;
}
}

View File

@@ -1,9 +1,20 @@
import hashObject from 'object-hash'; import hashObject from 'object-hash';
import EntityRepository from './EntityRepository';
export default class CachableRepository extends EntityRepository{
export default class CachableRepository {
repositoryName: string; repositoryName: string;
cache: any;
/**
* Constructor method.
* @param {Knex} knex
* @param {Cache} cache
*/
constructor(knex, cache) {
super(knex);
this.cache = cache;
}
/** /**
* Retrieve the cache key of the method name and arguments. * Retrieve the cache key of the method name and arguments.
* @param {string} method * @param {string} method
@@ -16,4 +27,197 @@ export default class CachableRepository {
return `${repositoryName}-${method}-${hashArgs}`; return `${repositoryName}-${method}-${hashArgs}`;
} }
/**
* Retrieve all entries with specified relations.
* @param withRelations
*/
all(withRelations?) {
const cacheKey = this.getCacheKey('all', withRelations);
return this.cache.get(cacheKey, () => {
return super.all(withRelations);
});
}
/**
* Finds list of entities with specified attributes
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve.
* @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
find(attributeValues = {}, withRelations?) {
const cacheKey = this.getCacheKey('find', attributeValues, withRelations);
return this.cache.get(cacheKey, () => {
return super.find(attributeValues, withRelations);
});
}
/**
* Finds list of entities with attribute values that are different from specified ones
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
findWhereNot(attributeValues = {}, withRelations?) {
const cacheKey = this.getCacheKey('findWhereNot', attributeValues, withRelations);
return this.cache.get(cacheKey, () => {
return super.findWhereNot(attributeValues, withRelations);
});
}
/**
* Finds list of entities with specified attributes (any of multiple specified values)
* Supports both ('attrName', ['value1', 'value2]) and ({attrName: ['value1', 'value2']} formats)
*
* @param {string|Object} searchParam - attribute name or search criteria object
* @param {*[]} [attributeValues] - attribute values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {PromiseLike<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
findWhereIn(searchParam, attributeValues, withRelations?) {
const cacheKey = this.getCacheKey('findWhereIn', attributeValues, withRelations);
return this.cache.get(cacheKey, () => {
return super.findWhereIn(searchParam, attributeValues, withRelations);
});
}
/**
* Finds first entity by given parameters
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object>}
*/
findOne(attributeValues = {}, withRelations?) {
const cacheKey = this.getCacheKey('findOne', attributeValues, withRelations);
return this.cache.get(cacheKey, () => {
return super.findOne(attributeValues, withRelations);
});
}
/**
* Finds first entity by given parameters
*
* @param {string || number} id - value of id column of the entity
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object>}
*/
findOneById(id, withRelations?) {
const cacheKey = this.getCacheKey('findOneById', id, withRelations);
return this.cache.get(cacheKey, () => {
return super.findOneById(id, withRelations);
});
}
/**
* Persists new entity or an array of entities.
* This method does not recursively persist related entities, use createRecursively (to be implemented) for that.
* Batch insert only works on PostgreSQL
* @param {Object} entity - model instance or parameters for a new entity
* @returns {Promise<Object>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
async create(entity) {
const result = await super.create(entity);
// Flushes the repository cache after insert operation.
this.flushCache();
return result;
}
/**
* Persists updated entity. If previously set fields are not present, performs an incremental update (does not remove fields unless explicitly set to null)
*
* @param {Object} entity - single entity instance
* @param {Object} [trx] - knex transaction instance. If not specified, new implicit transaction will be used.
* @returns {Promise<integer>} number of affected rows
*/
async update(entity, whereAttributes?) {
const result = await super.update(entity, whereAttributes);
// Flushes the repository cache after update operation.
this.flushCache();
return result;
}
/**
* @param {Object} attributeValues - values to filter deleted entities by
* @param {Object} [trx]
* @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
*/
async deleteBy(attributeValues) {
const result = await super.deleteBy(attributeValues);
this.flushCache();
return result;
}
/**
* @param {string || number} id - value of id column of the entity
* @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
*/
deleteById(id: number|string) {
const result = super.deleteById(id);
// Flushes the repository cache after insert operation.
this.flushCache();
return result;
}
/**
*
* @param {string|number[]} values -
*/
async deleteWhereIn(values: string | number[]) {
const result = await super.deleteWhereIdIn(values);
// Flushes the repository cache after delete operation.
this.flushCache();
return result;
}
/**
*
* @param graph
* @param options
*/
async upsertGraph(graph, options) {
const result = await super.upsertGraph(graph, options);
// Flushes the repository cache after insert operation.
this.flushCache();
return result;
}
/**
*
* @param {} whereAttributes
* @param {string} field
* @param {number} amount
*/
async changeNumber(whereAttributes, field: string, amount: number) {
const result = await super.changeNumber(whereAttributes, field, amount);
// Flushes the repository cache after update operation.
this.flushCache();
return result;
}
/**
* Flush repository cache.
*/
flushCache(): void {
this.cache.delStartWith(this.repositoryName);
}
} }

View File

@@ -1,76 +1,13 @@
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
import { IContact } from 'interfaces'; import { Contact } from 'models'
export default class ContactRepository extends TenantRepository { export default class ContactRepository extends TenantRepository {
/** /**
* Retrieve the given contact model. * Constructor method.
* @param {number} contactId
*/
findById(contactId: number): IContact {
const { Contact } = this.models;
return this.cache.get(`contacts.id.${contactId}`, () => {
return Contact.query().findById(contactId);
})
}
/**
* Retrieve the given contacts model.
* @param {number[]} contactIds - Contacts ids.
*/ */
findByIds(contactIds: number[]): IContact[] { constructor(knex, cache) {
const { Contact } = this.models; super(knex, cache);
return this.cache.get(`contacts.ids.${contactIds.join(',')}`, () => { this.model = Contact;
return Contact.query().whereIn('id', contactIds);
});
}
/**
* Inserts a new contact model.
* @param contact
*/
async insert(contactInput: IContact) {
const { Contact } = this.models;
const contact = await Contact.query().insert({ ...contactInput })
this.flushCache();
return contact;
}
/**
* Updates the contact details.
* @param {number} contactId - Contact id.
* @param {IContact} contact - Contact input.
*/
async update(contactId: number, contact: IContact) {
const { Contact } = this.models;
await Contact.query().findById(contactId).patch({ ...contact });
this.flushCache();
}
/**
* Deletes contact of the given id.
* @param {number} contactId -
* @return {Promise<void>}
*/
async deleteById(contactId: number): Promise<void> {
const { Contact } = this.models;
await Contact.query().where('id', contactId).delete();
this.flushCache();
}
/**
* Deletes contacts in bulk.
* @param {number[]} contactsIds
*/
async bulkDelete(contactsIds: number[]) {
const { Contact } = this.models;
await Contact.query().whereIn('id', contactsIds);
this.flushCache();
}
/**
* Flush contact repository cache.
*/
flushCache() {
this.cache.delStartWith(`contacts`);
} }
} }

View File

@@ -1,75 +1,16 @@
import TenantRepository from "./TenantRepository"; import TenantRepository from "./TenantRepository";
import { Customer } from 'models'
export default class CustomerRepository extends TenantRepository { export default class CustomerRepository extends TenantRepository {
all() {
const { Contact } = this.models;
return this.cache.get('customers', () => {
return Contact.query().modify('customer');
});
}
/** /**
* Retrieve customer details of the given id. * Constructor method.
* @param {number} customerId - Customer id.
*/ */
getById(customerId: number) { constructor(knex, cache) {
const { Contact } = this.models; super(knex, cache);
this.model = Customer;
return this.cache.get(`customers.id.${customerId}`, () => {
return Contact.query().modifier('customer').findById(customerId);
});
}
/**
* Detarmines the given customer exists.
* @param {number} customerId
* @returns {boolean}
*/
isExists(customerId: number) {
return !!this.getById(customerId);
}
/**
* Retrieve the sales invoices that assocaited to the given customer.
* @param {number} customerId
*/
getSalesInvoices(customerId: number) {
const { SaleInvoice } = this.models;
return this.cache.get(`customers.invoices.${customerId}`, () => {
return SaleInvoice.query().where('customer_id', customerId);
});
}
/**
* Retrieve customers details of the given ids.
* @param {number[]} customersIds - Customers ids.
* @return {IContact[]}
*/
customers(customersIds: number[]) {
const { Contact } = this.models;
return Contact.query().modifier('customer').whereIn('id', customersIds);
}
/**
* Retrieve customers of the given ids with associated sales invoices.
* @param {number[]} customersIds - Customers ids.
*/
customersWithSalesInvoices(customersIds: number[]) {
const { Contact } = this.models;
return Contact.query().modify('customer')
.whereIn('id', customersIds)
.withGraphFetched('salesInvoices');
} }
changeBalance(vendorId: number, amount: number) { changeBalance(vendorId: number, amount: number) {
const { Contact } = this.models; return super.changeNumber({ id: vendorId }, 'balance', amount);
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return Contact.query()
.where('id', vendorId)
[changeMethod]('balance', Math.abs(amount));
} }
async changeDiffBalance( async changeDiffBalance(

View File

@@ -0,0 +1,231 @@
import { cloneDeep, cloneDeepWith, forOwn, isString } from 'lodash';
import ModelEntityNotFound from 'exceptions/ModelEntityNotFound';
export default class EntityRepository {
modelInstance: any;
idColumn: string;
knex: any;
/**
* Constructor method.
* @param {Knex} knex
*/
constructor(knex) {
this.knex = knex;
this.idColumn = 'id';
}
/**
* Sets the model to the repository and bind it to knex instance.
*/
set model(model) {
if (!this.modelInstance) {
this.modelInstance = model.bindKnex(this.knex);
}
}
/**
* Retrieve the repository model binded it to knex instance.
*/
get model() {
return this.modelInstance;
}
/**
* Retrieve all entries with specified relations.
*
* @param withRelations
*/
all(withRelations?) {
return this.model.query().withGraphFetched(withRelations);
}
/**
* Finds list of entities with specified attributes
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve.
* @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
find(attributeValues = {}, withRelations?) {
return this.model
.query()
.where(attributeValues)
.withGraphFetched(withRelations);
}
/**
* Finds list of entities with attribute values that are different from specified ones
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {PromiseLike<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
findWhereNot(attributeValues = {}, withRelations?) {
return this.model
.query()
.whereNot(attributeValues)
.withGraphFetched(withRelations);
}
/**
* Finds list of entities with specified attributes (any of multiple specified values)
* Supports both ('attrName', ['value1', 'value2]) and ({attrName: ['value1', 'value2']} formats)
*
* @param {string|Object} searchParam - attribute name or search criteria object
* @param {*[]} [attributeValues] - attribute values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {PromiseLike<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
findWhereIn(searchParam, attributeValues, withRelations?) {
if (isString(searchParam)) {
return this.model
.query()
.whereIn(searchParam, attributeValues)
.withGraphFetched(withRelations);
} else {
const builder = this.model.query(this.knex).withGraphFetched(withRelations);
forOwn(searchParam, (value, key) => {
if (Array.isArray(value)) {
builder.whereIn(key, value);
} else {
builder.where(key, value);
}
});
return builder;
}
}
/**
* Finds first entity by given parameters
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object>}
*/
async findOne(attributeValues = {}, withRelations?) {
const results = await this.find(attributeValues, withRelations);
return results[0] || null;
}
/**
* Finds first entity by given parameters
*
* @param {string || number} id - value of id column of the entity
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object>}
*/
findOneById(id, withRelations?) {
return this.findOne({ [this.idColumn]: id }, withRelations);
}
/**
* Persists new entity or an array of entities.
* This method does not recursively persist related entities, use createRecursively (to be implemented) for that.
* Batch insert only works on PostgreSQL
*
* @param {Object} entity - model instance or parameters for a new entity
* @returns {Promise<Object>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
create(entity) {
// Keep the input parameter immutable
const instanceDTO = cloneDeep(entity);
return this.model.query().insert(instanceDTO);
}
/**
* Persists updated entity. If previously set fields are not present, performs an incremental update (does not remove fields unless explicitly set to null)
*
* @param {Object} entity - single entity instance
* @returns {Promise<integer>} number of affected rows
*/
async update(entity, whereAttributes?) {
const entityDto = cloneDeep(entity);
const identityClause = {};
if (Array.isArray(this.idColumn)) {
this.idColumn.forEach((idColumn) => (identityClause[idColumn] = entityDto[idColumn]));
} else {
identityClause[this.idColumn] = entityDto[this.idColumn];
}
const whereConditions = (whereAttributes || identityClause);
const modifiedEntitiesCount = await this.model
.query()
.where(whereConditions)
.update(entityDto);
if (modifiedEntitiesCount === 0) {
throw new ModelEntityNotFound(entityDto[this.idColumn]);
}
return modifiedEntitiesCount;
}
/**
*
* @param {Object} attributeValues - values to filter deleted entities by
* @param {Object} [trx]
* @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
*/
deleteBy(attributeValues) {
return this.model
.query()
.delete()
.where(attributeValues);
}
/**
* @param {string || number} id - value of id column of the entity
* @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
*/
deleteById(id: number|string) {
return this.deleteBy({
[this.idColumn]: id
});
}
/**
*
* @param {string} field -
* @param {number|string} values -
*/
deleteWhereIn(field: string, values: string|number[]) {
return this.model
.query()
.whereIn(field, values)
.delete();
}
/**
*
* @param {string|number[]} values
*/
deleteWhereIdIn(values: string|number[]) {
return this.deleteWhereIn(this.idColumn, values);
}
/**
*
* @param graph
* @param options
*/
upsertGraph(graph, options) {
// Keep the input grpah immutable
const graphCloned = cloneDeep(graph);
return this.model.upsertGraph(graphCloned)
}
/**
*
* @param {object} whereAttributes
* @param {string} field
* @param amount
*/
changeNumber(whereAttributes, field: string, amount: number) {
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return this.model.query()
.where(whereAttributes)
[changeMethod](field, Math.abs(amount));
}
}

View File

@@ -0,0 +1,12 @@
import TenantRepository from "./TenantRepository";
import { ExpenseCategory } from 'models';
export default class ExpenseEntyRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = ExpenseCategory;
}
}

View File

@@ -1,42 +1,13 @@
import TenantRepository from "./TenantRepository"; import TenantRepository from "./TenantRepository";
import { IExpense } from 'interfaces';
import moment from "moment"; import moment from "moment";
import { Expense } from 'models';
export default class ExpenseRepository extends TenantRepository { export default class ExpenseRepository extends TenantRepository {
/** /**
* Retrieve the given expense by id. * Constructor method.
* @param {number} expenseId
* @return {Promise<IExpense>}
*/ */
getById(expenseId: number) { constructor(knex, cache) {
const { Expense } = this.models; super(knex, cache);
return this.cache.get(`expense.id.${expenseId}`, () => { this.model = Expense;
return Expense.query().findById(expenseId).withGraphFetched('categories');
});
}
/**
* Inserts a new expense object.
* @param {IExpense} expense -
*/
async create(expenseInput: IExpense): Promise<void> {
const { Expense } = this.models;
const expense = await Expense.query().insertGraph({ ...expenseInput });
this.flushCache();
return expense;
}
/**
* Updates the given expense details.
* @param {number} expenseId
* @param {IExpense} expense
*/
async update(expenseId: number, expense: IExpense) {
const { Expense } = this.models;
await Expense.query().findById(expenseId).patch({ ...expense });
this.flushCache();
} }
/** /**
@@ -44,38 +15,10 @@ export default class ExpenseRepository extends TenantRepository {
* @param {number} expenseId * @param {number} expenseId
*/ */
async publish(expenseId: number): Promise<void> { async publish(expenseId: number): Promise<void> {
const { Expense } = this.models; super.update({
id: expenseId,
await Expense.query().findById(expenseId).patch({
publishedAt: moment().toMySqlDateTime(), publishedAt: moment().toMySqlDateTime(),
}); });
this.flushCache();
}
/**
* Deletes the given expense.
* @param {number} expenseId
*/
async delete(expenseId: number): Promise<void> {
const { Expense, ExpenseCategory } = this.models;
await ExpenseCategory.query().where('expense_id', expenseId).delete();
await Expense.query().where('id', expenseId).delete();
this.flushCache();
}
/**
* Deletes expenses in bulk.
* @param {number[]} expensesIds
*/
async bulkDelete(expensesIds: number[]): Promise<void> {
const { Expense, ExpenseCategory } = this.models;
await ExpenseCategory.query().whereIn('expense_id', expensesIds).delete();
await Expense.query().whereIn('id', expensesIds).delete();
this.flushCache();
} }
/** /**
@@ -83,18 +26,10 @@ export default class ExpenseRepository extends TenantRepository {
* @param {number[]} expensesIds * @param {number[]} expensesIds
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async bulkPublish(expensesIds: number): Promise<void> { async whereIdInPublish(expensesIds: number): Promise<void> {
const { Expense } = this.models; await this.model.query().whereIn('id', expensesIds).patch({
await Expense.query().whereIn('id', expensesIds).patch({
publishedAt: moment().toMySqlDateTime(), publishedAt: moment().toMySqlDateTime(),
}); });
this.flushCache(); this.flushCache();
} }
/**
* Flushes repository cache.
*/
flushCache() {
this.cache.delStartWith(`expense`);
}
} }

View File

@@ -0,0 +1,13 @@
import { Item } from "models";
import TenantRepository from "./TenantRepository";
export default class ItemRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Item;
}
}

View File

@@ -1,18 +1,12 @@
import { IBalanceSheetQuery } from 'interfaces'; import { ManualJournal } from 'models';
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
export default class JournalRepository extends TenantRepository { export default class JournalRepository extends TenantRepository {
/**
balanceSheet(query: IBalanceSheetQuery) { * Constructor method.
*/
// Accounts dependency graph. constructor(knex, cache) {
const accountsGraph = Account.toDependencyGraph(balanceSheetAccounts); super(knex, cache);
this.model = ManualJournal;
// Load all entries that associated to the given accounts.
const journalEntriesCollected = Account.collectJournalEntries(balanceSheetAccounts);
const journalEntries = new JournalPoster(accountsGraph);
journalEntries.loadEntries(journalEntriesCollected);
} }
} }

View File

@@ -1,55 +0,0 @@
import { omit } from 'lodash';
import BaseModelRepository from 'repositories/BaseModelRepository';
import { PaymentReceiveEntry } from 'models';
export default class PaymentReceiveEntryRepository extends BaseModelRepository {
/**
* Insert payment receive entries in bulk.
* @param {Array} entries
* @param {Integr} paymentReceiveId
* @return {Promise}
*/
static insertBulk(entries, paymentReceiveId) {
const opers = [];
entries.forEach((entry) => {
const insertOper = PaymentReceiveEntry.tenant()
.query()
.insert({
payment_receive_id: paymentReceiveId,
...entry,
});
opers.push(insertOper);
});
return Promise.all(opers);
}
/**
* Update payment receive entries in bulk.
* @param {Array} entries
* @return {Promise}
*/
static updateBulk(entries) {
const opers = [];
entries.forEach((entry) => {
const updateOper = PaymentReceiveEntry.tenant()
.query()
.patchAndFetchById(entry.id, {
...omit(entry, ['id', 'index']),
});
opers.push(updateOper);
});
return Promise.all(opers);
}
/**
* Deletes the given payment receive entries ids in bulk.
* @param {Array} entriesIds
* @return {Promise}
*/
static deleteBulk(entriesIds) {
return PaymentReceiveEntry.tenant()
.query()
.whereIn('id', entriesIds)
.delete();
}
}

View File

@@ -0,0 +1,12 @@
import { PaymentReceiveEntry } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class PaymentReceiveEntryRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = PaymentReceiveEntry;
}
}

View File

@@ -1,7 +0,0 @@
import { omit } from 'lodash';
import { PaymentReceiveEntry } from 'models';
import BaseModelRepository from 'repositories/BaseModelRepository';
export default class PaymentReceiveRepository extends BaseModelRepository {
}

View File

@@ -0,0 +1,12 @@
import { PaymentReceive } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class PaymentReceiveRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = PaymentReceive;
}
}

View File

@@ -1,7 +0,0 @@
export default class SaleInvoiceRepository {
}

View File

@@ -0,0 +1,12 @@
import { SaleInvoice } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class SaleInvoiceRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = SaleInvoice;
}
}

View File

@@ -0,0 +1,12 @@
import TenantRepository from 'repositories/TenantRepository';
import Setting from 'models/Setting';
export default class SettingRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Setting;
}
}

View File

@@ -4,42 +4,13 @@ import CachableRepository from './CachableRepository';
export default class TenantRepository extends CachableRepository { export default class TenantRepository extends CachableRepository {
repositoryName: string; repositoryName: string;
tenantId: number;
tenancy: TenancyService;
modelsInstance: any;
repositoriesInstance: any;
cacheInstance: any;
/** /**
* Constructor method. * Constructor method.
* @param {number} tenantId * @param {number} tenantId
*/ */
constructor(tenantId: number) { constructor(knex, cache) {
super(); super(knex, cache);
this.tenantId = tenantId;
this.tenancy = Container.get(TenancyService);
this.repositoryName = this.constructor.name; this.repositoryName = this.constructor.name;
} }
get models() {
if (!this.modelsInstance) {
this.modelsInstance = this.tenancy.models(this.tenantId);
}
return this.modelsInstance;
}
get repositories() {
if (!this.repositoriesInstance) {
this.repositoriesInstance = this.tenancy.repositories(this.tenantId);
}
return this.repositoriesInstance;
}
get cache() {
if (!this.cacheInstance) {
this.cacheInstance = this.tenancy.cache(this.tenantId);
}
return this.cacheInstance;
}
} }

View File

@@ -1,58 +1,17 @@
import { IVendor } from "interfaces"; import { Vendor } from "models";
import TenantRepository from "./TenantRepository"; import TenantRepository from "./TenantRepository";
export default class VendorRepository extends TenantRepository { export default class VendorRepository extends TenantRepository {
/** /**
* Retrieve vendor details of the given id. * Constructor method.
* @param {number} vendorId - Vendor id.
*/ */
findById(vendorId: number) { constructor(knex, cache) {
const { Contact } = this.models; super(knex, cache);
return Contact.query().findById(vendorId); this.model = Vendor;
}
/**
* Retrieve the bill that associated to the given vendor id.
* @param {number} vendorId - Vendor id.
*/
getBills(vendorId: number) {
const { Bill } = this.models;
return this.cache.get(`vendors.bills.${vendorId}`, () => {
return Bill.query().where('vendor_id', vendorId);
});
}
/**
* Retrieve all the given vendors.
* @param {numner[]} vendorsIds
* @return {IVendor}
*/
vendors(vendorsIds: number[]): IVendor[] {
const { Contact } = this.models;
return Contact.query().modifier('vendor').whereIn('id', vendorsIds);
}
/**
* Retrieve vendors with associated bills.
* @param {number[]} vendorIds
*/
vendorsWithBills(vendorIds: number[]) {
const { Contact } = this.models;
return Contact.query().modify('vendor')
.whereIn('id', vendorIds)
.withGraphFetched('bills');
} }
changeBalance(vendorId: number, amount: number) { changeBalance(vendorId: number, amount: number) {
const { Contact } = this.models; return super.changeNumber({ id: vendorId }, 'balance', amount);
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return Contact.query()
.where('id', vendorId)
[changeMethod]('balance', Math.abs(amount));
} }
async changeDiffBalance( async changeDiffBalance(

View File

@@ -1,60 +1,19 @@
import { IView } from 'interfaces'; import { View } from 'models';
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
export default class ViewRepository extends TenantRepository { export default class ViewRepository extends TenantRepository {
/** /**
* Retrieve view model by the given id. * Constructor method.
* @param {number} id -
*/ */
getById(id: number) { constructor(knex, cache) {
const { View } = this.models; super(knex, cache);
return this.cache.get(`customView.id.${id}`, () => { this.model = View;
return View.query().findById(id)
.withGraphFetched('columns')
.withGraphFetched('roles');
});
} }
/** /**
* Retrieve all views of the given resource id. * Retrieve all views of the given resource id.
*/ */
allByResource(resourceModel: string) { allByResource(resourceModel: string, withRelations?) {
const { View } = this.models; return super.find({ resource_mode: resourceModel }, withRelations);
return this.cache.get(`customView.resourceModel.${resourceModel}`, () => {
return View.query().where('resource_model', resourceModel)
.withGraphFetched('columns')
.withGraphFetched('roles');
});
}
/**
* Inserts a new view to the storage.
* @param {IView} view
*/
async insert(view: IView): Promise<IView> {
const { View } = this.models;
const insertedView = await View.query().insertGraph({ ...view });
this.flushCache();
return insertedView;
}
async update(viewId: number, view: IView): Promise<IView> {
const { View } = this.models;
const updatedView = await View.query().upsertGraph({
id: viewId,
...view
});
this.flushCache();
return updatedView;
}
/**
* Flushes repository cache.
*/
flushCache() {
this.cache.delStartWith('customView');
} }
} }

View File

@@ -3,10 +3,4 @@ import TenantRepository from 'repositories/TenantRepository';
export default class ViewRoleRepository extends TenantRepository { export default class ViewRoleRepository extends TenantRepository {
allByView(viewId: number) {
const { ViewRole } = this.models;
return this.cache.get(`viewRole.view.${viewId}`, async () => {
return ViewRole.query().where('view_id', viewId);
});
}
} }

View File

@@ -1,5 +0,0 @@
import ResourceRepository from './ResourceRepository';
export {
ResourceRepository,
};

View File

@@ -65,8 +65,8 @@ export default class JournalCommands{
async customerOpeningBalance(customerId: number, openingBalance: number) { async customerOpeningBalance(customerId: number, openingBalance: number) {
const { accountRepository } = this.repositories; const { accountRepository } = this.repositories;
const openingBalanceAccount = await accountRepository.getBySlug('opening-balance'); const openingBalanceAccount = await accountRepository.findOne({ slug: 'opening-balance' });
const receivableAccount = await accountRepository.getBySlug('accounts-receivable'); const receivableAccount = await accountRepository.findOne({ slug: 'accounts-receivable' });
const commonEntry = { const commonEntry = {
referenceType: 'CustomerOpeningBalance', referenceType: 'CustomerOpeningBalance',
@@ -98,8 +98,8 @@ export default class JournalCommands{
async vendorOpeningBalance(vendorId: number, openingBalance: number) { async vendorOpeningBalance(vendorId: number, openingBalance: number) {
const { accountRepository } = this.repositories; const { accountRepository } = this.repositories;
const payableAccount = await accountRepository.getBySlug('accounts-payable'); const payableAccount = await accountRepository.findOne({ slug: 'accounts-payable' });
const otherCost = await accountRepository.getBySlug('other-expenses'); const otherCost = await accountRepository.findOne({ slug: 'other-expenses' });
const commonEntry = { const commonEntry = {
referenceType: 'VendorOpeningBalance', referenceType: 'VendorOpeningBalance',

View File

@@ -166,7 +166,7 @@ export default class JournalPoster implements IJournalPoster {
accountsIds.map(async (account: number) => { accountsIds.map(async (account: number) => {
const accountChange = accountsChange[account]; const accountChange = accountsChange[account];
const accountNode = this.accountsDepGraph.getNodeData(account); const accountNode = this.accountsDepGraph.getNodeData(account);
const accountTypeMeta = await accountTypeRepository.getTypeMeta(accountNode.accountTypeId); const accountTypeMeta = await accountTypeRepository.findOneById(accountNode.accountTypeId);
const { normal }: { normal: TEntryType } = accountTypeMeta; const { normal }: { normal: TEntryType } = accountTypeMeta;
let change = 0; let change = 0;

View File

@@ -115,7 +115,7 @@ export default class AccountsService {
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating the account existance.', { tenantId, accountId }); this.logger.info('[accounts] validating the account existance.', { tenantId, accountId });
const account = await accountRepository.findById(accountId); const account = await accountRepository.findOneById(accountId);
if (!account) { if (!account) {
this.logger.info('[accounts] the given account not found.', { accountId }); this.logger.info('[accounts] the given account not found.', { accountId });
@@ -187,7 +187,7 @@ export default class AccountsService {
// Inherit active status from parent account. // Inherit active status from parent account.
accountDTO.active = parentAccount.active; accountDTO.active = parentAccount.active;
} }
const account = await accountRepository.insert({ const account = await accountRepository.create({
...accountDTO, ...accountDTO,
slug: kebabCase(accountDTO.name), slug: kebabCase(accountDTO.name),
}); });
@@ -231,7 +231,10 @@ export default class AccountsService {
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount); this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
} }
// Update the account on the storage. // Update the account on the storage.
const account = await accountRepository.edit(oldAccount.id, accountDTO); const account = await accountRepository.updateAndFetch({
id: oldAccount.id,
...accountDTO
});
this.logger.info('[account] account edited successfully.', { this.logger.info('[account] account edited successfully.', {
account, accountDTO, tenantId account, accountDTO, tenantId
}); });
@@ -545,8 +548,8 @@ export default class AccountsService {
this.throwErrorIfAccountPredefined(account); this.throwErrorIfAccountPredefined(account);
const accountType = await accountTypeRepository.getTypeMeta(account.accountTypeId); const accountType = await accountTypeRepository.findOneById(account.accountTypeId);
const toAccountType = await accountTypeRepository.getTypeMeta(toAccount.accountTypeId); const toAccountType = await accountTypeRepository.findOneById(toAccount.accountTypeId);
if (accountType.rootType !== toAccountType.rootType) { if (accountType.rootType !== toAccountType.rootType) {
throw new ServiceError('close_account_and_to_account_not_same_type'); throw new ServiceError('close_account_and_to_account_not_same_type');

View File

@@ -27,10 +27,13 @@ export default class ContactsService {
* @return {Promise<IContact>} * @return {Promise<IContact>}
*/ */
public async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) { public async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId); const { contactRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId }); this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId });
const contact = await Contact.query().findById(contactId).where('contact_service', contactService); const contact = await contactRepository.findOne({
id: contactId,
contactService: contactService,
});
if (!contact) { if (!contact) {
throw new ServiceError('contact_not_found'); throw new ServiceError('contact_not_found');
@@ -70,7 +73,7 @@ export default class ContactsService {
const contactObj = this.transformContactObj(contactDTO); const contactObj = this.transformContactObj(contactDTO);
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO }); this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
const contact = await contactRepository.insert({ contactService, ...contactObj }); const contact = await contactRepository.create({ contactService, ...contactObj });
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact }); this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
return contact; return contact;
@@ -84,13 +87,13 @@ export default class ContactsService {
* @param {IContactDTO} contactDTO * @param {IContactDTO} contactDTO
*/ */
async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) { async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId); const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO); const contactObj = this.transformContactObj(contactDTO);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO }); this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO });
await Contact.query().findById(contactId).patch({ ...contactObj }) await contactRepository.update({ ...contactObj }, { id: contactId });
} }
/** /**
@@ -105,6 +108,8 @@ export default class ContactsService {
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId }); this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId });
// Deletes contact of the given id.
await contactRepository.deleteById(contactId); await contactRepository.deleteById(contactId);
} }
@@ -151,7 +156,7 @@ export default class ContactsService {
const { contactRepository } = this.tenancy.repositories(tenantId); const { contactRepository } = this.tenancy.repositories(tenantId);
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService); this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
await contactRepository.bulkDelete(contactsIds); await contactRepository.deleteWhereIdIn(contactsIds);
} }
/** /**

View File

@@ -15,13 +15,15 @@ import {
ICustomersFilter, ICustomersFilter,
IContactNewDTO, IContactNewDTO,
IContactEditDTO, IContactEditDTO,
IContact IContact,
ISaleInvoice
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import moment from 'moment'; import moment from 'moment';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
@Service() @Service()
export default class CustomersService { export default class CustomersService {
@@ -68,6 +70,7 @@ export default class CustomersService {
} }
private transformContactToCustomer(contactModel: IContact) { private transformContactToCustomer(contactModel: IContact) {
console.log(contactModel);
return { return {
...omit(contactModel.toJSON(), ['contactService', 'contactType']), ...omit(contactModel.toJSON(), ['contactService', 'contactType']),
customerType: contactModel.contactType, customerType: contactModel.contactType,
@@ -263,8 +266,10 @@ export default class CustomersService {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
private async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) { private async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) {
const { customerRepository } = this.tenancy.repositories(tenantId); const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const salesInvoice = await customerRepository.getSalesInvoices(customerId);
// Retrieve the sales invoices that assocaited to the given customer.
const salesInvoice = await saleInvoiceRepository.find({ customer_id: customerId });
if (salesInvoice.length > 0) { if (salesInvoice.length > 0) {
throw new ServiceError('customer_has_invoices'); throw new ServiceError('customer_has_invoices');
@@ -279,14 +284,13 @@ export default class CustomersService {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
private async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) { private async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) {
const { customerRepository } = this.tenancy.repositories(tenantId); const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const customersWithInvoices = await customerRepository.customersWithSalesInvoices( const customersInvoices = await saleInvoiceRepository.findWhereIn(
customersIds, 'customer_id', customersIds
); );
const customersIdsWithInvoice = customersWithInvoices const customersIdsWithInvoice = customersInvoices
.filter((customer: ICustomer) => customer.salesInvoices.length > 0) .map((saleInvoice: ISaleInvoice) => saleInvoice.customerId);
.map((customer: ICustomer) => customer.id);
const customersHaveInvoices = difference(customersIds, customersIdsWithInvoice); const customersHaveInvoices = difference(customersIds, customersIdsWithInvoice);

View File

@@ -194,8 +194,10 @@ export default class VendorsService {
* @param {number} vendorId * @param {number} vendorId
*/ */
private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) { private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { billRepository } = this.tenancy.repositories(tenantId);
const bills = await vendorRepository.getBills(vendorId);
// Retrieve the bill that associated to the given vendor id.
const bills = await billRepository.find({ vendor_id: vendorId });
if (bills.length > 0) { if (bills.length > 0) {
throw new ServiceError('vendor_has_bills') throw new ServiceError('vendor_has_bills')
@@ -209,14 +211,14 @@ export default class VendorsService {
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) { private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { billRepository } = this.tenancy.repositories(tenantId);
const vendorsWithBills = await vendorRepository.vendorsWithBills(vendorsIds); // Retrieves bills that assocaited to the given vendors.
const vendorsIdsWithBills = vendorsWithBills const vendorsBills = await billRepository.findWhereIn('vendor_id', vendorsIds);
.filter((vendor: IVendor) => vendor.bills.length > 0) const billsVendorsIds = vendorsBills.map((bill) => bill.vendorId);
.map((vendor: IVendor) => vendor.id);
const vendorsHaveInvoices = difference(vendorsIds, vendorsIdsWithBills); // The difference between the vendors ids and bills vendors ids.
const vendorsHaveInvoices = difference(vendorsIds, billsVendorsIds);
if (vendorsHaveInvoices.length > 0) { if (vendorsHaveInvoices.length > 0) {
throw new ServiceError('some_vendors_have_bills'); throw new ServiceError('some_vendors_have_bills');

View File

@@ -39,7 +39,7 @@ export default class DynamicListService implements IDynamicListService {
*/ */
private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) { private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
const { viewRepository } = this.tenancy.repositories(tenantId); const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.getById(viewId); const view = await viewRepository.findOneById(viewId);
if (!view || view.resourceModel !== model.name) { if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND); throw new ServiceError(ERRORS.VIEW_NOT_FOUND);

View File

@@ -15,6 +15,7 @@ import events from 'subscribers/events';
const ERRORS = { const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found', EXPENSE_NOT_FOUND: 'expense_not_found',
EXPENSES_NOT_FOUND: 'EXPENSES_NOT_FOUND',
PAYMENT_ACCOUNT_NOT_FOUND: 'payment_account_not_found', PAYMENT_ACCOUNT_NOT_FOUND: 'payment_account_not_found',
SOME_ACCOUNTS_NOT_FOUND: 'some_expenses_not_found', SOME_ACCOUNTS_NOT_FOUND: 'some_expenses_not_found',
TOTAL_AMOUNT_EQUALS_ZERO: 'total_amount_equals_zero', TOTAL_AMOUNT_EQUALS_ZERO: 'total_amount_equals_zero',
@@ -48,7 +49,7 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId }); this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId });
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.findById(paymentAccountId) const paymentAccount = await accountRepository.findOneById(paymentAccountId)
if (!paymentAccount) { if (!paymentAccount) {
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId }); this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId });
@@ -68,8 +69,8 @@ export default class ExpensesService implements IExpensesService {
private async getExpensesAccountsOrThrowError(tenantId: number, expenseAccountsIds: number[]) { private async getExpensesAccountsOrThrowError(tenantId: number, expenseAccountsIds: number[]) {
this.logger.info('[expenses] trying to get expenses accounts.', { tenantId, expenseAccountsIds }); this.logger.info('[expenses] trying to get expenses accounts.', { tenantId, expenseAccountsIds });
const { Account } = this.tenancy.models(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
const storedExpenseAccounts = await Account.query().whereIn( const storedExpenseAccounts = await accountRepository.findWhereIn(
'id', expenseAccountsIds, 'id', expenseAccountsIds,
); );
const storedExpenseAccountsIds = storedExpenseAccounts.map((a: IAccount) => a.id); const storedExpenseAccountsIds = storedExpenseAccounts.map((a: IAccount) => a.id);
@@ -108,7 +109,10 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expenses] trying to validate expenses accounts type.', { tenantId, expensesAccounts }); this.logger.info('[expenses] trying to validate expenses accounts type.', { tenantId, expensesAccounts });
const { accountTypeRepository } = this.tenancy.repositories(tenantId); const { accountTypeRepository } = this.tenancy.repositories(tenantId);
// Retrieve accounts types of the given root type.
const expensesTypes = await accountTypeRepository.getByRootType('expense'); const expensesTypes = await accountTypeRepository.getByRootType('expense');
const expensesTypesIds = expensesTypes.map(t => t.id); const expensesTypesIds = expensesTypes.map(t => t.id);
const invalidExpenseAccounts: number[] = []; const invalidExpenseAccounts: number[] = [];
@@ -132,6 +136,8 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expenses] trying to validate payment account type.', { tenantId, paymentAccount }); this.logger.info('[expenses] trying to validate payment account type.', { tenantId, paymentAccount });
const { accountTypeRepository } = this.tenancy.repositories(tenantId); const { accountTypeRepository } = this.tenancy.repositories(tenantId);
// Retrieve account tpy eof the given key.
const validAccountsType = await accountTypeRepository.getByKeys([ const validAccountsType = await accountTypeRepository.getByKeys([
'current_asset', 'fixed_asset', 'current_asset', 'fixed_asset',
]); ]);
@@ -200,7 +206,9 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId }); this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId });
const expense = await expenseRepository.getById(expenseId);
// Retrieve the given expense by id.
const expense = await expenseRepository.findOneById(expenseId);
if (!expense) { if (!expense) {
this.logger.info('[expense] the given expense not found.', { tenantId, expenseId }); this.logger.info('[expense] the given expense not found.', { tenantId, expenseId });
@@ -209,8 +217,27 @@ export default class ExpensesService implements IExpensesService {
return expense; return expense;
} }
async getExpensesOrThrowError(tenantId: number, expensesIds: number[]) { /**
* Retrieve the give expenses models or throw not found service error.
* @param {number} tenantId -
* @param {number[]} expensesIds -
*/
async getExpensesOrThrowError(
tenantId: number,
expensesIds: number[]
): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId);
const storedExpenses = expenseRepository.findWhereIn('id', expensesIds);
const storedExpensesIds = storedExpenses.map((expense) => expense.id);
const notFoundExpenses = difference(expensesIds, storedExpensesIds);
if (notFoundExpenses.length > 0) {
this.logger.info('[expense] the give expenses ids not found.', { tenantId, expensesIds });
throw new ServiceError(ERRORS.EXPENSES_NOT_FOUND)
}
return storedExpenses;
} }
/** /**
@@ -301,7 +328,12 @@ export default class ExpensesService implements IExpensesService {
// - Update the expense on the storage. // - Update the expense on the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO); const expenseObj = this.expenseDTOToModel(expenseDTO);
const expenseModel = await expenseRepository.update(expenseId, expenseObj, null);
// - Upsert the expense object with expense entries.
const expenseModel = await expenseRepository.upsertGraph({
id: expenseId,
...expenseObj,
});
this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO }); this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO });
return expenseModel; return expenseModel;
@@ -348,7 +380,7 @@ export default class ExpensesService implements IExpensesService {
// 6. Save the expense to the storage. // 6. Save the expense to the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser); const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser);
const expenseModel = await expenseRepository.create(expenseObj); const expenseModel = await expenseRepository.upsertGraph(expenseObj);
this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO }); this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO });
@@ -394,7 +426,7 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId }); this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId });
await expenseRepository.delete(expenseId); await expenseRepository.deleteById(expenseId);
this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId }); this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId });
@@ -413,7 +445,7 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds }); this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds });
await expenseRepository.bulkDelete(expensesIds); await expenseRepository.deleteWhereIdIn(expensesIds);
this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds }); this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds });
@@ -432,7 +464,7 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds }); this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds });
await expenseRepository.bulkPublish(expensesIds); await expenseRepository.whereIdInPublish(expensesIds);
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds }); this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
@@ -474,13 +506,13 @@ export default class ExpensesService implements IExpensesService {
* @return {Promise<IExpense>} * @return {Promise<IExpense>}
*/ */
public async getExpense(tenantId: number, expenseId: number): Promise<IExpense> { public async getExpense(tenantId: number, expenseId: number): Promise<IExpense> {
const { Expense } = this.tenancy.models(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await Expense.query().findById(expenseId)
.withGraphFetched('paymentAccount')
.withGraphFetched('media')
.withGraphFetched('categories.expenseAccount');
const expense = await expenseRepository.findOneById(expenseId, [
'paymentAccount',
'media',
'categories.expenseAccount',
]);
if (!expense) { if (!expense) {
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND); throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
} }

View File

@@ -67,7 +67,7 @@ export default class BalanceSheetStatementService
this.logger.info('[balance_sheet] trying to calculate the report.', { filter, tenantId }); this.logger.info('[balance_sheet] trying to calculate the report.', { filter, tenantId });
// Retrieve all accounts on the storage. // Retrieve all accounts on the storage.
const accounts = await accountRepository.allAccounts('type'); const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all journal transactions based on the given query. // Retrieve all journal transactions based on the given query.

View File

@@ -80,7 +80,7 @@ export default class GeneralLedgerService {
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
// Retrieve all accounts from the storage. // Retrieve all accounts from the storage.
const accounts = await accountRepository.allAccounts('type'); const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
// Retreive journal transactions from/to the given date. // Retreive journal transactions from/to the given date.

View File

@@ -66,7 +66,7 @@ export default class ProfitLossSheetService {
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
// Retrieve all accounts on the storage. // Retrieve all accounts on the storage.
const accounts = await accountRepository.allAccounts('type'); const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all journal transactions based on the given query. // Retrieve all journal transactions based on the given query.

View File

@@ -58,7 +58,7 @@ export default class TrialBalanceSheetService {
this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { tenantId, filter }); this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { tenantId, filter });
// Retrieve all accounts on the storage. // Retrieve all accounts on the storage.
const accounts = await accountRepository.allAccounts('type'); const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all journal transactions based on the given query. // Retrieve all journal transactions based on the given query.

View File

@@ -120,7 +120,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId }); this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
const incomeType = await accountTypeRepository.getByKey('income'); const incomeType = await accountTypeRepository.getByKey('income');
const foundAccount = await accountRepository.findById(sellAccountId); const foundAccount = await accountRepository.findOneById(sellAccountId);
if (!foundAccount) { if (!foundAccount) {
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId }); this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
@@ -142,7 +142,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId }); this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
const foundAccount = await accountRepository.findById(costAccountId) const foundAccount = await accountRepository.findOneById(costAccountId)
if (!foundAccount) { if (!foundAccount) {
this.logger.info('[items] cost account not found.', { tenantId, costAccountId }); this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
@@ -164,7 +164,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId }); this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
const otherAsset = await accountTypeRepository.getByKey('other_asset'); const otherAsset = await accountTypeRepository.getByKey('other_asset');
const foundAccount = await accountRepository.findById(inventoryAccountId); const foundAccount = await accountRepository.findOneById(inventoryAccountId);
if (!foundAccount) { if (!foundAccount) {
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId }); this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });

View File

@@ -85,7 +85,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId }); this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
const foundAccount = await accountRepository.findById(costAccountId) const foundAccount = await accountRepository.findOneById(costAccountId)
if (!foundAccount) { if (!foundAccount) {
this.logger.info('[items] cost account not found.', { tenantId, costAccountId }); this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
@@ -106,7 +106,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId }); this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
const incomeType = await accountTypeRepository.getByKey('income'); const incomeType = await accountTypeRepository.getByKey('income');
const foundAccount = await accountRepository.findById(sellAccountId); const foundAccount = await accountRepository.findOneById(sellAccountId);
if (!foundAccount) { if (!foundAccount) {
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId }); this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
@@ -127,7 +127,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId }); this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
const otherAsset = await accountTypeRepository.getByKey('other_asset'); const otherAsset = await accountTypeRepository.getByKey('other_asset');
const foundAccount = await accountRepository.findById(inventoryAccountId); const foundAccount = await accountRepository.findOneById(inventoryAccountId);
if (!foundAccount) { if (!foundAccount) {
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId }); this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });

View File

@@ -197,7 +197,8 @@ export default class ManualJournalsService implements IManualJournalsService {
contactRequired: boolean = true, contactRequired: boolean = true,
): Promise<void> { ): Promise<void> {
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
const payableAccount = await accountRepository.getBySlug(accountBySlug); const payableAccount = await accountRepository.findOne({ slug: accountBySlug });
const entriesHasNoVendorContact = manualJournalDTO.entries.filter( const entriesHasNoVendorContact = manualJournalDTO.entries.filter(
(e) => (e) =>
e.accountId === payableAccount.id && e.accountId === payableAccount.id &&

View File

@@ -70,7 +70,9 @@ export default class BillPaymentsService {
*/ */
private async getVendorOrThrowError(tenantId: number, vendorId: number) { private async getVendorOrThrowError(tenantId: number, vendorId: number) {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { vendorRepository } = this.tenancy.repositories(tenantId);
const vendor = await vendorRepository.findById(vendorId);
// Retrieve vendor details of the given id.
const vendor = await vendorRepository.findOneById(vendorId);
if (!vendor) { if (!vendor) {
throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND) throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND)
@@ -106,7 +108,7 @@ export default class BillPaymentsService {
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset'); const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
const paymentAccount = await accountRepository.findById(paymentAccountId); const paymentAccount = await accountRepository.findOneById(paymentAccountId);
const currentAssetTypesIds = currentAssetTypes.map(type => type.id); const currentAssetTypesIds = currentAssetTypes.map(type => type.id);
@@ -405,7 +407,9 @@ 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');
const payableAccount = await accountRepository.getBySlug('accounts-payable');
// Retrieve AP account from the storage.
const payableAccount = await accountRepository.findOne({ slug: 'accounts-payable' });
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const commonJournal = { const commonJournal = {

View File

@@ -23,13 +23,10 @@ import {
IPaginationMeta, IPaginationMeta,
IFilterMeta, IFilterMeta,
IBillsFilter, IBillsFilter,
IBillPaymentEntry,
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import { Bill } from 'models';
import PaymentMadesSubscriber from 'subscribers/paymentMades';
const ERRORS = { const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND', BILL_NOT_FOUND: 'BILL_NOT_FOUND',
@@ -39,6 +36,7 @@ const ERRORS = {
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND', BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN'
}; };
/** /**
@@ -82,7 +80,7 @@ export default class BillsService extends SalesInvoicesCost {
const { vendorRepository } = this.tenancy.repositories(tenantId); const { vendorRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[bill] trying to get vendor.', { tenantId, vendorId }); this.logger.info('[bill] trying to get vendor.', { tenantId, vendorId });
const foundVendor = await vendorRepository.findById(vendorId); const foundVendor = await vendorRepository.findOneById(vendorId);
if (!foundVendor) { if (!foundVendor) {
this.logger.info('[bill] the given vendor not found.', { tenantId, vendorId }); this.logger.info('[bill] the given vendor not found.', { tenantId, vendorId });
@@ -138,7 +136,12 @@ export default class BillsService extends SalesInvoicesCost {
* *
* @returns {IBill} * @returns {IBill}
*/ */
private async billDTOToModel(tenantId: number, billDTO: IBillDTO | IBillEditDTO, oldBill?: IBill) { private async billDTOToModel(
tenantId: number,
billDTO: IBillDTO | IBillEditDTO,
oldBill?: IBill,
authorizedUser: ISystemUser,
) {
const { ItemEntry } = this.tenancy.models(tenantId); const { ItemEntry } = this.tenancy.models(tenantId);
let invLotNumber = oldBill?.invLotNumber; let invLotNumber = oldBill?.invLotNumber;
@@ -152,10 +155,22 @@ export default class BillsService extends SalesInvoicesCost {
const amount = sumBy(entries, 'amount'); const amount = sumBy(entries, 'amount');
return { return {
...formatDateFields(billDTO, ['billDate', 'dueDate']), ...formatDateFields(
omit(billDTO, ['open']),
['billDate', 'dueDate']
),
amount, amount,
invLotNumber, invLotNumber,
entries, entries: entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount', 'id']),
})),
// Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open && (!oldBill?.openedAt)) && ({
openedAt: moment().toMySqlDateTime(),
}),
userId: authorizedUser.id,
}; };
} }
@@ -182,7 +197,7 @@ export default class BillsService extends SalesInvoicesCost {
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO }); this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO });
const billObj = await this.billDTOToModel(tenantId, billDTO); const billObj = await this.billDTOToModel(tenantId, billDTO, null, authorizedUser);
// Retrieve vendor or throw not found service error. // Retrieve vendor or throw not found service error.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -197,15 +212,8 @@ export default class BillsService extends SalesInvoicesCost {
// Validate non-purchasable items. // Validate non-purchasable items.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
const bill = await Bill.query() // Inserts the bill graph object to the storage.
.insertGraph({ const bill = await Bill.query().insertGraph({ ...billObj });
...omit(billObj, ['entries']),
userId: authorizedUser.id,
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount', 'id']),
})),
});
// Triggers `onBillCreated` event. // Triggers `onBillCreated` event.
await this.eventDispatcher.dispatch(events.bill.onCreated, { await this.eventDispatcher.dispatch(events.bill.onCreated, {
@@ -227,7 +235,7 @@ export default class BillsService extends SalesInvoicesCost {
* - Increment the diff amount on the given vendor id. * - Increment the diff amount on the given vendor id.
* - Re-write the inventory transactions. * - Re-write the inventory transactions.
* - Re-write the bill journal transactions. * - Re-write the bill journal transactions.
* * ------
* @param {number} tenantId - The given tenant id. * @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id. * @param {Integer} billId - The given bill id.
* @param {IBillEditDTO} billDTO - The given new bill details. * @param {IBillEditDTO} billDTO - The given new bill details.
@@ -237,12 +245,15 @@ export default class BillsService extends SalesInvoicesCost {
tenantId: number, tenantId: number,
billId: number, billId: number,
billDTO: IBillEditDTO, billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> { ): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
this.logger.info('[bill] trying to edit bill.', { tenantId, billId }); this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
const oldBill = await this.getBillOrThrowError(tenantId, billId); const oldBill = await this.getBillOrThrowError(tenantId, billId);
const billObj = await this.billDTOToModel(tenantId, billDTO, oldBill);
// Transforms the bill DTO object to model object.
const billObj = await this.billDTOToModel(tenantId, billDTO, oldBill, authorizedUser);
// Retrieve vendor details or throw not found service error. // Retrieve vendor details or throw not found service error.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId); await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -251,19 +262,19 @@ export default class BillsService extends SalesInvoicesCost {
if (billDTO.billNumber) { if (billDTO.billNumber) {
await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId); await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId);
} }
// Validate the entries ids existance.
await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, billId, 'Bill', billDTO.entries); await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, billId, 'Bill', billDTO.entries);
// Validate the items ids existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries); await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries);
// Accept the purchasable items only.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries); await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
// Update the bill transaction. // Update the bill transaction.
const bill = await Bill.query().upsertGraphAndFetch({ const bill = await Bill.query().upsertGraphAndFetch({
id: billId, id: billId,
...omit(billObj, ['entries', 'invLotNumber']), ...billObj,
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}))
}); });
// Triggers event `onBillEdited`. // Triggers event `onBillEdited`.
await this.eventDispatcher.dispatch(events.bill.onEdited, { tenantId, billId, oldBill, bill }); await this.eventDispatcher.dispatch(events.bill.onEdited, { tenantId, billId, oldBill, bill });
@@ -280,6 +291,7 @@ export default class BillsService extends SalesInvoicesCost {
public async deleteBill(tenantId: number, billId: number) { public async deleteBill(tenantId: number, billId: number) {
const { Bill, ItemEntry } = this.tenancy.models(tenantId); const { Bill, ItemEntry } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId); const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Delete all associated bill entries. // Delete all associated bill entries.
@@ -340,7 +352,7 @@ export default class BillsService extends SalesInvoicesCost {
const storedItems = await Item.query().whereIn('id', entriesItemsIds); const storedItems = await Item.query().whereIn('id', entriesItemsIds);
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
const payableAccount = await accountRepository.getBySlug('accounts-payable'); const payableAccount = await accountRepository.find({ slug: 'accounts-payable' });
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
@@ -484,4 +496,28 @@ export default class BillsService extends SalesInvoicesCost {
); );
} }
} }
/**
* Mark the bill as open.
* @param {number} tenantId
* @param {number} billId
*/
public async openBill(
tenantId: number,
billId: number,
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
if (oldBill.isOpen) {
throw new ServiceError(ERRORS.BILL_ALREADY_OPEN);
}
// Record the bill opened at on the storage.
await Bill.query().findById(billId).patch({
openedAt: moment().toMySqlDateTime(),
});
}
} }

View File

@@ -119,7 +119,7 @@ export default class PaymentReceiveService {
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset'); const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
const depositAccount = await accountRepository.findById(depositAccountId); const depositAccount = await accountRepository.findOneById(depositAccountId);
const currentAssetTypesIds = currentAssetTypes.map(type => type.id); const currentAssetTypesIds = currentAssetTypes.map(type => type.id);

View File

@@ -12,6 +12,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService'; import CustomersService from 'services/Contacts/CustomersService';
import moment from 'moment';
const ERRORS = { const ERRORS = {
@@ -19,6 +20,8 @@ const ERRORS = {
CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND', CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND',
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE', SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS', ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS',
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE'
}; };
/** /**
* Sale estimate service. * Sale estimate service.
@@ -80,6 +83,39 @@ export default class SaleEstimateService {
} }
} }
/**
* Transform DTO object ot model object.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO
* @param {ISaleEstimate} oldSaleEstimate
* @return {ISaleEstimate}
*/
transformDTOToModel(
tenantId: number,
estimateDTO: ISaleEstimateDTO,
oldSaleEstimate?: ISaleEstimate,
): ISaleEstimate {
const { ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
return {
amount,
...formatDateFields(
omit(estimateDTO, ['delivered', 'entries']),
['estimateDate', 'expirationDate']
),
entries: estimateDTO.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount', 'id']),
})),
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered && (!oldSaleEstimate?.deliveredAt)) && ({
deliveredAt: moment().toMySqlDateTime(),
}),
};
}
/** /**
* Creates a new estimate with associated entries. * Creates a new estimate with associated entries.
* @async * @async
@@ -87,16 +123,16 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate * @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>} * @return {Promise<ISaleEstimate>}
*/ */
public async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> { public async createEstimate(
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); tenantId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate } = this.tenancy.models(tenantId);
this.logger.info('[sale_estimate] inserting sale estimate to the storage.'); this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e)); // Transform DTO object ot model object.
const estimateObj = { const estimateObj = this.transformDTOToModel(tenantId, estimateDTO);
amount,
...formatDateFields(estimateDTO, ['estimateDate', 'expirationDate']),
};
// Validate estimate number uniquiness on the storage. // Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) { if (estimateDTO.estimateNumber) {
@@ -112,13 +148,7 @@ export default class SaleEstimateService {
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, estimateDTO.entries); await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, estimateDTO.entries);
const saleEstimate = await SaleEstimate.query() const saleEstimate = await SaleEstimate.query()
.upsertGraphAndFetch({ .upsertGraphAndFetch({ ...estimateObj });
...omit(estimateObj, ['entries']),
entries: estimateObj.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount', 'id']),
}))
});
this.logger.info('[sale_estimate] insert sale estimated success.'); this.logger.info('[sale_estimate] insert sale estimated success.');
await this.eventDispatcher.dispatch(events.saleEstimate.onCreated, { await this.eventDispatcher.dispatch(events.saleEstimate.onCreated, {
@@ -136,15 +166,16 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate * @param {EstimateDTO} estimate
* @return {void} * @return {void}
*/ */
public async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> { public async editEstimate(
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId); const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e)); // Transform DTO object ot model object.
const estimateObj = { const estimateObj = this.transformDTOToModel(tenantId, estimateDTO, oldSaleEstimate);
amount,
...formatDateFields(estimateDTO, ['estimateDate', 'expirationDate']),
};
// Validate estimate number uniquiness on the storage. // Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) { if (estimateDTO.estimateNumber) {
@@ -166,11 +197,7 @@ export default class SaleEstimateService {
const saleEstimate = await SaleEstimate.query() const saleEstimate = await SaleEstimate.query()
.upsertGraphAndFetch({ .upsertGraphAndFetch({
id: estimateId, id: estimateId,
...omit(estimateObj, ['entries']), ...estimateObj
entries: estimateObj.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount']),
})),
}); });
await this.eventDispatcher.dispatch(events.saleEstimate.onEdited, { await this.eventDispatcher.dispatch(events.saleEstimate.onEdited, {
@@ -194,6 +221,11 @@ export default class SaleEstimateService {
// Retrieve sale estimate or throw not found service error. // Retrieve sale estimate or throw not found service error.
const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId); const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
// Throw error if the sale estimate converted to sale invoice.
if (oldSaleEstimate.convertedToInvoiceId) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
}
this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.'); this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.');
await ItemEntry.query() await ItemEntry.query()
.where('reference_id', estimateId) .where('reference_id', estimateId)
@@ -254,4 +286,70 @@ export default class SaleEstimateService {
filterMeta: dynamicFilter.getResponseMeta(), filterMeta: dynamicFilter.getResponseMeta(),
}; };
} }
/**
* Converts estimate to invoice.
* @param {number} tenantId -
* @param {number} estimateId -
* @return {Promise<void>}
*/
async convertEstimateToInvoice(
tenantId: number,
estimateId: number,
invoiceId: number,
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate.
const saleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
await SaleEstimate.query().where('id', estimateId).patch({
convertedToInvoiceId: invoiceId,
convertedToInvoiceAt: moment().toMySqlDateTime(),
});
}
/**
* Unlink the converted sale estimates from the given sale invoice.
* @param {number} tenantId -
* @param {number} invoiceId -
* @return {Promise<void>}
*/
async unlinkConvertedEstimateFromInvoice(
tenantId: number,
invoiceId: number,
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
await SaleEstimate.query().where({
convertedToInvoiceId: invoiceId,
}).patch({
convertedToInvoiceId: null,
convertedToInvoiceAt: null,
});
}
/**
* Mark the sale estimate as delivered.
* @param {number} tenantId - Tenant id.
* @param {number} saleEstimateId - Sale estimate id.
*/
public async deliverSaleEstimate(
tenantId: number,
saleEstimateId: number,
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const saleEstimate = await this.getSaleEstimateOrThrowError(tenantId, saleEstimateId);
// Throws error in case the sale estimate already published.
if (saleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await SaleEstimate.query().where('id', saleEstimateId).patch({
deliveredAt: moment().toMySqlDateTime()
});
}
} }

View File

@@ -1,5 +1,6 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { omit, sumBy, difference, pick, chain } from 'lodash'; import { omit, sumBy, pick, chain } from 'lodash';
import moment from 'moment';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -10,7 +11,9 @@ import {
IItemEntry, IItemEntry,
ISalesInvoicesFilter, ISalesInvoicesFilter,
IPaginationMeta, IPaginationMeta,
IFilterMeta IFilterMeta,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
} from 'interfaces'; } from 'interfaces';
import events from 'subscribers/events'; import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
@@ -23,11 +26,13 @@ import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService'; import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
const ERRORS = { const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE', INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND', SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
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'
@@ -63,6 +68,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject() @Inject()
customersService: CustomersService; customersService: CustomersService;
@Inject()
saleEstimatesService: SaleEstimateService;
/** /**
* *
* Validate whether sale invoice number unqiue on the storage. * Validate whether sale invoice number unqiue on the storage.
@@ -101,6 +109,33 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
return saleInvoice; return saleInvoice;
} }
/**
* Transform DTO object to model object.
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoiceOTD} saleInvoiceDTO - Sale invoice DTO.
*/
transformDTOToModel(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO|ISaleInvoiceEditDTO,
oldSaleInvoice?: ISaleInvoice
): ISaleInvoice {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
return {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered']),
['invoiceDate', 'dueDate']
),
// Avoid rewrite the deliver date in edit mode when already published.
...(saleInvoiceDTO.delivered && (!oldSaleInvoice?.deliveredAt)) && ({
deliveredAt: moment().toMySqlDateTime(),
}),
balance,
paymentAmount: 0,
}
}
/** /**
* Creates a new sale invoices and store it to the storage * Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions. * with associated to entries and journal transactions.
@@ -109,18 +144,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO - * @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice} * @return {ISaleInvoice}
*/ */
public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD): Promise<ISaleInvoice> { public async createSaleInvoice(
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId); tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); ): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invLotNumber = 1; const invLotNumber = 1;
const saleInvoiceObj: ISaleInvoice = { // Transform DTO object to model object.
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']), const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
balance,
paymentAmount: 0,
// invLotNumber,
};
// Validate customer existance. // Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId); await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
@@ -131,6 +164,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
} }
// Validate items ids existance. // Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries); await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries); await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.'); this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
@@ -165,11 +200,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId); const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const saleInvoiceObj = { // Transform DTO object to model object.
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']), const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO, oldSaleInvoice);
balance,
// invLotNumber: oldSaleInvoice.invLotNumber,
};
// Validate customer existance. // Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId); await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
@@ -203,10 +235,34 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, { await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId, saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId,
}); });
return saleInvoice; return saleInvoice;
} }
/**
* Deliver the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
): Promise<void> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve details of the given sale invoice id.
const saleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
// Throws error in case the sale invoice already published.
if (saleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await saleInvoiceRepository.update({
deliveredAt: moment().toMySqlDateTime()
}, { id: saleInvoiceId });
}
/** /**
* Deletes the given sale invoice with associated entries * Deletes the given sale invoice with associated entries
* and journal transactions. * and journal transactions.
@@ -218,6 +274,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId); const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
// Unlink the converted sale estimates from the given sale invoice.
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
tenantId,
saleInvoiceId,
);
this.logger.info('[sale_invoice] delete sale invoice with entries.'); this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete(); await SaleInvoice.query().where('id', saleInvoiceId).delete();
await ItemEntry.query() await ItemEntry.query()

View File

@@ -66,12 +66,12 @@ export default class SalesReceiptService {
*/ */
async validateReceiptDepositAccountExistance(tenantId: number, accountId: number) { async validateReceiptDepositAccountExistance(tenantId: number, accountId: number) {
const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId); const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findById(accountId); const depositAccount = await accountRepository.findOneById(accountId);
if (!depositAccount) { if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
} }
const depositAccountType = await accountTypeRepository.getTypeMeta(depositAccount.accountTypeId); const depositAccountType = await accountTypeRepository.findOneById(depositAccount.accountTypeId);
if (!depositAccountType || depositAccountType.childRoot === 'current_asset') { if (!depositAccountType || depositAccountType.childRoot === 'current_asset') {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET); throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);

View File

@@ -7,9 +7,9 @@ export default class SettingsStore extends MetableStoreDB {
* Constructor method. * Constructor method.
* @param {number} tenantId * @param {number} tenantId
*/ */
constructor(knex: Knex) { constructor(repository) {
super(); super();
this.setExtraColumns(['group']); this.setExtraColumns(['group']);
this.setModel(Setting.bindKnex(knex)); this.setRepository(repository);
} }
} }

View File

@@ -69,7 +69,10 @@ export default class HasTenancyService {
*/ */
repositories(tenantId: number) { repositories(tenantId: number) {
return this.singletonService(tenantId, 'repositories', () => { return this.singletonService(tenantId, 'repositories', () => {
return tenantRepositoriesLoader(tenantId); const cache = this.cache(tenantId);
const knex = this.knex(tenantId);
return tenantRepositoriesLoader(knex, cache);
}); });
} }

View File

@@ -48,7 +48,7 @@ export default class ViewsService implements IViewsService {
const resourceModel = this.getResourceModelOrThrowError(tenantId, resourceModelName); const resourceModel = this.getResourceModelOrThrowError(tenantId, resourceModelName);
const { viewRepository } = this.tenancy.repositories(tenantId); const { viewRepository } = this.tenancy.repositories(tenantId);
return viewRepository.allByResource(resourceModel.name); return viewRepository.allByResource(resourceModel.name, ['columns', 'roles']);
} }
/** /**
@@ -104,7 +104,7 @@ export default class ViewsService implements IViewsService {
const { viewRepository } = this.tenancy.repositories(tenantId); const { viewRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
const view = await viewRepository.getById(viewId); const view = await viewRepository.findOneById(viewId);
if (!view) { if (!view) {
this.logger.info('[view] view not found.', { tenantId, viewId }); this.logger.info('[view] view not found.', { tenantId, viewId });
@@ -191,7 +191,7 @@ export default class ViewsService implements IViewsService {
} }
// Save view details. // Save view details.
this.logger.info('[views] trying to insert to storage.', { tenantId, viewDTO }) this.logger.info('[views] trying to insert to storage.', { tenantId, viewDTO })
const view = await viewRepository.insert({ const view = await viewRepository.create({
predefined: false, predefined: false,
name: viewDTO.name, name: viewDTO.name,
rolesLogicExpression: viewDTO.logicExpression, rolesLogicExpression: viewDTO.logicExpression,
@@ -245,7 +245,8 @@ export default class ViewsService implements IViewsService {
} }
// Update view details. // Update view details.
this.logger.info('[views] trying to update view details.', { tenantId, viewId }); this.logger.info('[views] trying to update view details.', { tenantId, viewId });
const view = await viewRepository.update(viewId, { const view = await viewRepository.upsertGraph({
id: viewId,
predefined: false, predefined: false,
name: viewEditDTO.name, name: viewEditDTO.name,
rolesLogicExpression: viewEditDTO.logicExpression, rolesLogicExpression: viewEditDTO.logicExpression,

View File

@@ -3,17 +3,19 @@ import { On, EventSubscriber } from "event-dispatch";
import events from 'subscribers/events'; import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import SettingsService from 'services/Settings/SettingsService'; import SettingsService from 'services/Settings/SettingsService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
@EventSubscriber() @EventSubscriber()
export default class SaleInvoiceSubscriber { export default class SaleInvoiceSubscriber {
logger: any; logger: any;
tenancy: TenancyService; tenancy: TenancyService;
settingsService: SettingsService; settingsService: SettingsService;
saleEstimatesService: SaleEstimateService;
constructor() { constructor() {
this.logger = Container.get('logger'); this.logger = Container.get('logger');
this.tenancy = Container.get(TenancyService); this.tenancy = Container.get(TenancyService);
this.settingsService = Container.get(SettingsService); this.settingsService = Container.get(SettingsService);
this.saleEstimatesService = Container.get(SaleEstimateService);
} }
/** /**
@@ -27,6 +29,20 @@ export default class SaleInvoiceSubscriber {
await customerRepository.changeBalance(saleInvoice.customerId, saleInvoice.balance); await customerRepository.changeBalance(saleInvoice.customerId, saleInvoice.balance);
} }
/**
*
*/
@On(events.saleInvoice.onCreated)
public async handleMarkEstimateConvert({ tenantId, saleInvoice, saleInvoiceId }) {
if (saleInvoice.fromEstiamteId) {
this.saleEstimatesService.convertEstimateToInvoice(
tenantId,
saleInvoice.fromEstiamteId,
saleInvoiceId,
);
}
}
/** /**
* Handles customer balance diff balnace change once sale invoice edited. * Handles customer balance diff balnace change once sale invoice edited.
*/ */

View File

@@ -227,6 +227,24 @@ const isBlank = (value) => {
return _.isEmpty(value) && !_.isNumber(value) || _.isNaN(value); return _.isEmpty(value) && !_.isNumber(value) || _.isNaN(value);
} }
function defaultToTransform(
value,
defaultOrTransformedValue,
defaultValue,
) {
const _defaultValue =
typeof defaultValue === 'undefined'
? defaultOrTransformedValue
: defaultValue;
const _transfromedValue =
typeof defaultValue === 'undefined' ? value : defaultOrTransformedValue;
return value == null || value !== value || value === ''
? _defaultValue
: _transfromedValue;
}
export { export {
hashPassword, hashPassword,
origin, origin,
@@ -246,5 +264,6 @@ export {
entriesAmountDiff, entriesAmountDiff,
convertEmptyStringToNull, convertEmptyStringToNull,
formatNumber, formatNumber,
isBlank isBlank,
defaultToTransform
}; };