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

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

View File

@@ -38,6 +38,14 @@ export default class BillsController extends BaseController {
asyncMiddleware(this.newBill.bind(this)),
this.handleServiceError,
);
router.post(
'/:id/open', [
...this.specificBillValidationSchema,
],
this.validationResult,
asyncMiddleware(this.openBill.bind(this)),
this.handleServiceError,
);
router.post(
'/:id', [
...this.billValidationSchema,
@@ -94,6 +102,8 @@ export default class BillsController extends BaseController {
check('due_date').optional().isISO8601(),
check('vendor_id').exists().isNumeric().toInt(),
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }),
check('entries.*.id').optional().isNumeric().toInt(),
@@ -117,6 +127,8 @@ export default class BillsController extends BaseController {
check('due_date').optional().isISO8601(),
check('vendor_id').exists().isNumeric().toInt(),
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }),
check('entries.*.id').optional().isNumeric().toInt(),
@@ -185,12 +197,13 @@ export default class BillsController extends BaseController {
* @param {Response} res
*/
async editBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params;
const { id: billId, user } = req.params;
const { tenantId } = req;
const billDTO: IBillEditDTO = this.matchedBodyData(req);
try {
const editedBill = await this.billsService.editBill(tenantId, billId, billDTO);
await this.billsService.editBill(tenantId, billId, billDTO, user);
return res.status(200).send({
id: billId,
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.
* @param {Request} req
@@ -339,6 +373,11 @@ export default class BillsController extends BaseController {
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);
}

View File

@@ -7,6 +7,7 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from "exceptions";
import { Request } from 'express-validator/src/base';
@Service()
export default class SalesEstimatesController extends BaseController {
@@ -30,6 +31,15 @@ export default class SalesEstimatesController extends BaseController {
asyncMiddleware(this.newEstimate.bind(this)),
this.handleServiceErrors,
);
router.post(
'/:id/deliver',
[
...this.validateSpecificEstimateSchema,
],
this.validationResult,
asyncMiddleware(this.deliverSaleEstimate.bind(this)),
this.handleServiceErrors,
);
router.post(
'/:id', [
...this.validateSpecificEstimateSchema,
@@ -75,6 +85,7 @@ export default class SalesEstimatesController extends BaseController {
check('expiration_date').optional().isISO8601(),
check('reference').optional(),
check('estimate_number').exists().trim().escape(),
check('delivered').default(false).isBoolean().toBoolean(),
check('entries').exists().isArray({ min: 1 }),
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.
*/

View File

@@ -28,11 +28,23 @@ export default class SaleInvoicesController extends BaseController{
router.post(
'/',
this.saleInvoiceValidationSchema,
[
...this.saleInvoiceValidationSchema,
check('from_estimate_id').optional().isNumeric().toInt(),
],
this.validationResult,
asyncMiddleware(this.newSaleInvoice.bind(this)),
this.handleServiceErrors,
);
router.post(
'/:id/deliver',
[
...this.specificSaleInvoiceValidation,
],
this.validationResult,
asyncMiddleware(this.deliverSaleInvoice.bind(this)),
this.handleServiceErrors,
)
router.post(
'/:id',
[
@@ -86,7 +98,7 @@ export default class SaleInvoicesController extends BaseController{
check('due_date').exists().isISO8601(),
check('invoice_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('terms_conditions').optional().trim().escape(),
@@ -172,6 +184,28 @@ export default class SaleInvoicesController extends BaseController{
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.
@@ -319,6 +353,11 @@ export default class SaleInvoicesController extends BaseController{
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);
}

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ exports.up = function(knex) {
table.decimal('amount', 13, 3).defaultTo(0);
table.decimal('payment_amount', 13, 3).defaultTo(0);
table.string('inv_lot_number').index();
table.date('opened_at').index();
table.integer('user_id').unsigned();
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,
amount: number,
paymentAmount: number,
open: boolean,
entries: IItemEntryDTO[],
};
@@ -24,6 +25,7 @@ export interface IBillEditDTO {
note: string,
amount: number,
paymentAmount: number,
open: boolean,
entries: IItemEntryDTO[],
};
@@ -41,6 +43,7 @@ export interface IBill {
paymentAmount: number,
invLotNumber: string,
openedAt: Date | string,
entries: IItemEntry[],
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,17 +7,25 @@ import ViewRepository from 'repositories/ViewRepository';
import ViewRoleRepository from 'repositories/ViewRoleRepository';
import ContactRepository from 'repositories/ContactRepository';
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 {
accountRepository: new AccountRepository(tenantId),
transactionsRepository: new AccountTransactionsRepository(tenantId),
accountTypeRepository: new AccountTypeRepository(tenantId),
customerRepository: new CustomerRepository(tenantId),
vendorRepository: new VendorRepository(tenantId),
contactRepository: new ContactRepository(tenantId),
expenseRepository: new ExpenseRepository(tenantId),
viewRepository: new ViewRepository(tenantId),
viewRoleRepository: new ViewRoleRepository(tenantId),
accountRepository: new AccountRepository(knex, cache),
transactionsRepository: new AccountTransactionsRepository(knex, cache),
accountTypeRepository: new AccountTypeRepository(knex, cache),
customerRepository: new CustomerRepository(knex, cache),
vendorRepository: new VendorRepository(knex, cache),
contactRepository: new ContactRepository(knex, cache),
expenseRepository: new ExpenseRepository(knex, cache),
expenseEntryRepository: new ExpenseEntryRepository(knex, cache),
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 moment from 'moment';
import { difference } from 'lodash';
import TenantModel from 'models/TenantModel';
export default class Bill extends TenantModel {
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['dueAmount'];
}
/**
* Table name
*/
@@ -36,6 +30,13 @@ export default class Bill extends TenantModel {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['dueAmount', 'isOpen', 'isPartiallyPaid', 'isFullyPaid', 'isPaid', 'remainingDays', 'overdueDays', 'isOverdue'];
}
/**
* Due amount of the given.
* @return {number}
@@ -44,6 +45,74 @@ export default class Bill extends TenantModel {
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.
*/

View File

@@ -26,7 +26,7 @@ export default class Contact extends TenantModel {
/**
* Closing balance attribute.
*/
closingBalance() {
get closingBalance() {
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() {
return {
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 TenantModel from "models/TenantModel";
import { viewRolesBuilder } from "lib/ViewRolesBuilder";
import Media from "./Media";
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 { defaultToTransform } from 'utils';
export default class SaleEstimate extends TenantModel {
/**
@@ -16,6 +18,41 @@ export default class SaleEstimate extends TenantModel {
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.
*/

View File

@@ -1,6 +1,7 @@
import { Model, raw } from 'objection';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils';
export default class SaleInvoice extends TenantModel {
/**
@@ -24,6 +25,90 @@ export default class SaleInvoice extends TenantModel {
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() {
return true;
}
@@ -67,6 +152,7 @@ export default class SaleInvoice extends TenantModel {
const ItemEntry = require('models/ItemEntry');
const Contact = require('models/Contact');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
return {
entries: {
@@ -115,7 +201,16 @@ export default class SaleInvoice extends TenantModel {
filter(builder) {
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) {
const Logger = Container.get('logger');
return super.query(...args).onBuildKnex(knexQueryBuilder => {
return super.query(...args).onBuildKnex((knexQueryBuilder) => {
const { userParams: { tenantId } } = knexQueryBuilder.client.config;
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 AccountType from './AccountType';
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 {
SaleEstimate,
@@ -40,4 +46,10 @@ export {
InventoryLotCostTracker,
AccountType,
Option,
Contact,
ExpenseCategory,
Expense,
ManualJournal,
Customer,
Vendor,
};

View File

@@ -1,124 +1,28 @@
import { Account } from 'models';
import TenantRepository from 'repositories/TenantRepository';
import { IAccount } from 'interfaces';
export default class AccountRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Account;
}
/**
* Retrieve accounts dependency graph.
* @returns {}
*/
async getDependencyGraph() {
const { Account } = this.models;
const accounts = await this.allAccounts();
const cacheKey = this.getCacheKey('accounts.depGraph');
async getDependencyGraph(withRelation) {
const accounts = await this.all(withRelation);
const cacheKey = this.getCacheKey('accounts.depGraph', withRelation);
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.
* @param {number} accountId
@@ -126,17 +30,9 @@ export default class AccountRepository extends TenantRepository {
* @return {Promise<void>}
*/
async balanceChange(accountId: number, amount: number): Promise<void> {
const { Account } = this.models;
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();
}
/**
* 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 hashObject from 'object-hash';
import TenantRepository from 'repositories/TenantRepository';
@@ -17,13 +14,19 @@ interface IJournalTransactionsFilter {
};
export default class AccountTransactionsRepository extends TenantRepository {
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = AccountTransaction;
}
journal(filter: IJournalTransactionsFilter) {
const { AccountTransaction } = this.models;
const cacheKey = this.getCacheKey('transactions.journal', filter);
return this.cache.get(cacheKey, () => {
return AccountTransaction.query()
return this.model.query()
.modify('filterAccounts', filter.accountsIds)
.modify('filterDateRange', filter.fromDate, filter.toDate)
.withGraphFetched('account.type')

View File

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

View File

@@ -2,10 +2,4 @@
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 EntityRepository from './EntityRepository';
export default class CachableRepository {
export default class CachableRepository extends EntityRepository{
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.
* @param {string} method
@@ -16,4 +27,197 @@ export default class CachableRepository {
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 { IContact } from 'interfaces';
import { Contact } from 'models'
export default class ContactRepository extends TenantRepository {
/**
* Retrieve the given contact model.
* @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.
* Constructor method.
*/
findByIds(contactIds: number[]): IContact[] {
const { Contact } = this.models;
return this.cache.get(`contacts.ids.${contactIds.join(',')}`, () => {
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`);
constructor(knex, cache) {
super(knex, cache);
this.model = Contact;
}
}

View File

@@ -1,75 +1,16 @@
import TenantRepository from "./TenantRepository";
import { Customer } from 'models'
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.
* @param {number} customerId - Customer id.
* Constructor method.
*/
getById(customerId: number) {
const { Contact } = this.models;
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');
constructor(knex, cache) {
super(knex, cache);
this.model = Customer;
}
changeBalance(vendorId: number, amount: number) {
const { Contact } = this.models;
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return Contact.query()
.where('id', vendorId)
[changeMethod]('balance', Math.abs(amount));
return super.changeNumber({ id: vendorId }, 'balance', amount);
}
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 { IExpense } from 'interfaces';
import moment from "moment";
import { Expense } from 'models';
export default class ExpenseRepository extends TenantRepository {
/**
* Retrieve the given expense by id.
* @param {number} expenseId
* @return {Promise<IExpense>}
* Constructor method.
*/
getById(expenseId: number) {
const { Expense } = this.models;
return this.cache.get(`expense.id.${expenseId}`, () => {
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();
constructor(knex, cache) {
super(knex, cache);
this.model = Expense;
}
/**
@@ -44,38 +15,10 @@ export default class ExpenseRepository extends TenantRepository {
* @param {number} expenseId
*/
async publish(expenseId: number): Promise<void> {
const { Expense } = this.models;
await Expense.query().findById(expenseId).patch({
super.update({
id: expenseId,
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
* @return {Promise<void>}
*/
async bulkPublish(expensesIds: number): Promise<void> {
const { Expense } = this.models;
await Expense.query().whereIn('id', expensesIds).patch({
async whereIdInPublish(expensesIds: number): Promise<void> {
await this.model.query().whereIn('id', expensesIds).patch({
publishedAt: moment().toMySqlDateTime(),
});
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';
export default class JournalRepository extends TenantRepository {
balanceSheet(query: IBalanceSheetQuery) {
// Accounts dependency graph.
const accountsGraph = Account.toDependencyGraph(balanceSheetAccounts);
// Load all entries that associated to the given accounts.
const journalEntriesCollected = Account.collectJournalEntries(balanceSheetAccounts);
const journalEntries = new JournalPoster(accountsGraph);
journalEntries.loadEntries(journalEntriesCollected);
/**
* Constructor method.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = ManualJournal;
}
}

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 {
repositoryName: string;
tenantId: number;
tenancy: TenancyService;
modelsInstance: any;
repositoriesInstance: any;
cacheInstance: any;
/**
* Constructor method.
* @param {number} tenantId
*/
constructor(tenantId: number) {
super();
this.tenantId = tenantId;
this.tenancy = Container.get(TenancyService);
constructor(knex, cache) {
super(knex, cache);
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";
export default class VendorRepository extends TenantRepository {
/**
* Retrieve vendor details of the given id.
* @param {number} vendorId - Vendor id.
* Constructor method.
*/
findById(vendorId: number) {
const { Contact } = this.models;
return Contact.query().findById(vendorId);
}
/**
* 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');
constructor(knex, cache) {
super(knex, cache);
this.model = Vendor;
}
changeBalance(vendorId: number, amount: number) {
const { Contact } = this.models;
const changeMethod = (amount > 0) ? 'increment' : 'decrement';
return Contact.query()
.where('id', vendorId)
[changeMethod]('balance', Math.abs(amount));
return super.changeNumber({ id: vendorId }, 'balance', amount);
}
async changeDiffBalance(

View File

@@ -1,60 +1,19 @@
import { IView } from 'interfaces';
import { View } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class ViewRepository extends TenantRepository {
/**
* Retrieve view model by the given id.
* @param {number} id -
* Constructor method.
*/
getById(id: number) {
const { View } = this.models;
return this.cache.get(`customView.id.${id}`, () => {
return View.query().findById(id)
.withGraphFetched('columns')
.withGraphFetched('roles');
});
constructor(knex, cache) {
super(knex, cache);
this.model = View;
}
/**
* Retrieve all views of the given resource id.
*/
allByResource(resourceModel: string) {
const { View } = this.models;
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');
allByResource(resourceModel: string, withRelations?) {
return super.find({ resource_mode: resourceModel }, withRelations);
}
}

View File

@@ -3,10 +3,4 @@ import TenantRepository from 'repositories/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) {
const { accountRepository } = this.repositories;
const openingBalanceAccount = await accountRepository.getBySlug('opening-balance');
const receivableAccount = await accountRepository.getBySlug('accounts-receivable');
const openingBalanceAccount = await accountRepository.findOne({ slug: 'opening-balance' });
const receivableAccount = await accountRepository.findOne({ slug: 'accounts-receivable' });
const commonEntry = {
referenceType: 'CustomerOpeningBalance',
@@ -98,8 +98,8 @@ export default class JournalCommands{
async vendorOpeningBalance(vendorId: number, openingBalance: number) {
const { accountRepository } = this.repositories;
const payableAccount = await accountRepository.getBySlug('accounts-payable');
const otherCost = await accountRepository.getBySlug('other-expenses');
const payableAccount = await accountRepository.findOne({ slug: 'accounts-payable' });
const otherCost = await accountRepository.findOne({ slug: 'other-expenses' });
const commonEntry = {
referenceType: 'VendorOpeningBalance',

View File

@@ -166,7 +166,7 @@ export default class JournalPoster implements IJournalPoster {
accountsIds.map(async (account: number) => {
const accountChange = accountsChange[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;
let change = 0;

View File

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

View File

@@ -27,10 +27,13 @@ export default class ContactsService {
* @return {Promise<IContact>}
*/
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 });
const contact = await Contact.query().findById(contactId).where('contact_service', contactService);
const contact = await contactRepository.findOne({
id: contactId,
contactService: contactService,
});
if (!contact) {
throw new ServiceError('contact_not_found');
@@ -70,7 +73,7 @@ export default class ContactsService {
const contactObj = this.transformContactObj(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 });
return contact;
@@ -84,13 +87,13 @@ export default class ContactsService {
* @param {IContactDTO} contactDTO
*/
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 contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
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);
this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId });
// Deletes contact of the given id.
await contactRepository.deleteById(contactId);
}
@@ -151,7 +156,7 @@ export default class ContactsService {
const { contactRepository } = this.tenancy.repositories(tenantId);
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
await contactRepository.bulkDelete(contactsIds);
await contactRepository.deleteWhereIdIn(contactsIds);
}
/**

View File

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

View File

@@ -194,8 +194,10 @@ export default class VendorsService {
* @param {number} vendorId
*/
private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
const { vendorRepository } = this.tenancy.repositories(tenantId);
const bills = await vendorRepository.getBills(vendorId);
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieve the bill that associated to the given vendor id.
const bills = await billRepository.find({ vendor_id: vendorId });
if (bills.length > 0) {
throw new ServiceError('vendor_has_bills')
@@ -209,14 +211,14 @@ export default class VendorsService {
* @throws {ServiceError}
*/
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);
const vendorsIdsWithBills = vendorsWithBills
.filter((vendor: IVendor) => vendor.bills.length > 0)
.map((vendor: IVendor) => vendor.id);
// Retrieves bills that assocaited to the given vendors.
const vendorsBills = await billRepository.findWhereIn('vendor_id', vendorsIds);
const billsVendorsIds = vendorsBills.map((bill) => bill.vendorId);
const vendorsHaveInvoices = difference(vendorsIds, vendorsIdsWithBills);
// The difference between the vendors ids and bills vendors ids.
const vendorsHaveInvoices = difference(vendorsIds, billsVendorsIds);
if (vendorsHaveInvoices.length > 0) {
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) {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.getById(viewId);
const view = await viewRepository.findOneById(viewId);
if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);

View File

@@ -15,6 +15,7 @@ import events from 'subscribers/events';
const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found',
EXPENSES_NOT_FOUND: 'EXPENSES_NOT_FOUND',
PAYMENT_ACCOUNT_NOT_FOUND: 'payment_account_not_found',
SOME_ACCOUNTS_NOT_FOUND: 'some_expenses_not_found',
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 });
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.findById(paymentAccountId)
const paymentAccount = await accountRepository.findOneById(paymentAccountId)
if (!paymentAccount) {
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[]) {
this.logger.info('[expenses] trying to get expenses accounts.', { tenantId, expenseAccountsIds });
const { Account } = this.tenancy.models(tenantId);
const storedExpenseAccounts = await Account.query().whereIn(
const { accountRepository } = this.tenancy.repositories(tenantId);
const storedExpenseAccounts = await accountRepository.findWhereIn(
'id', expenseAccountsIds,
);
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 });
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
// Retrieve accounts types of the given root type.
const expensesTypes = await accountTypeRepository.getByRootType('expense');
const expensesTypesIds = expensesTypes.map(t => t.id);
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 });
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
// Retrieve account tpy eof the given key.
const validAccountsType = await accountTypeRepository.getByKeys([
'current_asset', 'fixed_asset',
]);
@@ -200,7 +206,9 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
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) {
this.logger.info('[expense] the given expense not found.', { tenantId, expenseId });
@@ -209,8 +217,27 @@ export default class ExpensesService implements IExpensesService {
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.
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 });
return expenseModel;
@@ -348,7 +380,7 @@ export default class ExpensesService implements IExpensesService {
// 6. Save the expense to the storage.
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 });
@@ -394,7 +426,7 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
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 });
@@ -413,7 +445,7 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
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 });
@@ -432,7 +464,7 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
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 });
@@ -474,13 +506,13 @@ export default class ExpensesService implements IExpensesService {
* @return {Promise<IExpense>}
*/
public async getExpense(tenantId: number, expenseId: number): Promise<IExpense> {
const { Expense } = this.tenancy.models(tenantId);
const expense = await Expense.query().findById(expenseId)
.withGraphFetched('paymentAccount')
.withGraphFetched('media')
.withGraphFetched('categories.expenseAccount');
const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await expenseRepository.findOneById(expenseId, [
'paymentAccount',
'media',
'categories.expenseAccount',
]);
if (!expense) {
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 });
// Retrieve all accounts on the storage.
const accounts = await accountRepository.allAccounts('type');
const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph();
// 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' });
// Retrieve all accounts from the storage.
const accounts = await accountRepository.allAccounts('type');
const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph();
// 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' });
// Retrieve all accounts on the storage.
const accounts = await accountRepository.allAccounts('type');
const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph();
// 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 });
// Retrieve all accounts on the storage.
const accounts = await accountRepository.allAccounts('type');
const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph();
// 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 });
const incomeType = await accountTypeRepository.getByKey('income');
const foundAccount = await accountRepository.findById(sellAccountId);
const foundAccount = await accountRepository.findOneById(sellAccountId);
if (!foundAccount) {
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 });
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
const foundAccount = await accountRepository.findById(costAccountId)
const foundAccount = await accountRepository.findOneById(costAccountId)
if (!foundAccount) {
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 });
const otherAsset = await accountTypeRepository.getByKey('other_asset');
const foundAccount = await accountRepository.findById(inventoryAccountId);
const foundAccount = await accountRepository.findOneById(inventoryAccountId);
if (!foundAccount) {
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 });
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
const foundAccount = await accountRepository.findById(costAccountId)
const foundAccount = await accountRepository.findOneById(costAccountId)
if (!foundAccount) {
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 });
const incomeType = await accountTypeRepository.getByKey('income');
const foundAccount = await accountRepository.findById(sellAccountId);
const foundAccount = await accountRepository.findOneById(sellAccountId);
if (!foundAccount) {
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 });
const otherAsset = await accountTypeRepository.getByKey('other_asset');
const foundAccount = await accountRepository.findById(inventoryAccountId);
const foundAccount = await accountRepository.findOneById(inventoryAccountId);
if (!foundAccount) {
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,
): Promise<void> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const payableAccount = await accountRepository.getBySlug(accountBySlug);
const payableAccount = await accountRepository.findOne({ slug: accountBySlug });
const entriesHasNoVendorContact = manualJournalDTO.entries.filter(
(e) =>
e.accountId === payableAccount.id &&

View File

@@ -70,7 +70,9 @@ export default class BillPaymentsService {
*/
private async getVendorOrThrowError(tenantId: number, vendorId: number) {
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) {
throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND)
@@ -106,7 +108,7 @@ export default class BillPaymentsService {
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
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);
@@ -405,7 +407,9 @@ export default class BillPaymentsService {
const paymentAmount = sumBy(billPayment.entries, 'paymentAmount');
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 commonJournal = {

View File

@@ -23,13 +23,10 @@ import {
IPaginationMeta,
IFilterMeta,
IBillsFilter,
IBillPaymentEntry,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import { Bill } from 'models';
import PaymentMadesSubscriber from 'subscribers/paymentMades';
const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
@@ -39,6 +36,7 @@ const ERRORS = {
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
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);
this.logger.info('[bill] trying to get vendor.', { tenantId, vendorId });
const foundVendor = await vendorRepository.findById(vendorId);
const foundVendor = await vendorRepository.findOneById(vendorId);
if (!foundVendor) {
this.logger.info('[bill] the given vendor not found.', { tenantId, vendorId });
@@ -138,7 +136,12 @@ export default class BillsService extends SalesInvoicesCost {
*
* @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);
let invLotNumber = oldBill?.invLotNumber;
@@ -152,10 +155,22 @@ export default class BillsService extends SalesInvoicesCost {
const amount = sumBy(entries, 'amount');
return {
...formatDateFields(billDTO, ['billDate', 'dueDate']),
...formatDateFields(
omit(billDTO, ['open']),
['billDate', 'dueDate']
),
amount,
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);
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.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -197,15 +212,8 @@ export default class BillsService extends SalesInvoicesCost {
// Validate non-purchasable items.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
const bill = await Bill.query()
.insertGraph({
...omit(billObj, ['entries']),
userId: authorizedUser.id,
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount', 'id']),
})),
});
// Inserts the bill graph object to the storage.
const bill = await Bill.query().insertGraph({ ...billObj });
// Triggers `onBillCreated` event.
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.
* - Re-write the inventory transactions.
* - Re-write the bill journal transactions.
*
* ------
* @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id.
* @param {IBillEditDTO} billDTO - The given new bill details.
@@ -237,12 +245,15 @@ export default class BillsService extends SalesInvoicesCost {
tenantId: number,
billId: number,
billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
this.logger.info('[bill] trying to edit bill.', { 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.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -251,19 +262,19 @@ export default class BillsService extends SalesInvoicesCost {
if (billDTO.billNumber) {
await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId);
}
// Validate the entries ids existance.
await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, billId, 'Bill', billDTO.entries);
// Validate the items ids existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, billDTO.entries);
// Accept the purchasable items only.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(tenantId, billDTO.entries);
// Update the bill transaction.
const bill = await Bill.query().upsertGraphAndFetch({
id: billId,
...omit(billObj, ['entries', 'invLotNumber']),
entries: billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}))
...billObj,
});
// Triggers event `onBillEdited`.
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) {
const { Bill, ItemEntry } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Delete all associated bill entries.
@@ -340,7 +352,7 @@ export default class BillsService extends SalesInvoicesCost {
const storedItems = await Item.query().whereIn('id', entriesItemsIds);
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);
@@ -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 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);

View File

@@ -12,6 +12,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import events from 'subscribers/events';
import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService';
import moment from 'moment';
const ERRORS = {
@@ -19,6 +20,8 @@ const ERRORS = {
CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND',
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
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.
@@ -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.
* @async
@@ -87,16 +123,16 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
public async createEstimate(
tenantId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate } = this.tenancy.models(tenantId);
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
const estimateObj = {
amount,
...formatDateFields(estimateDTO, ['estimateDate', 'expirationDate']),
};
// Transform DTO object ot model object.
const estimateObj = this.transformDTOToModel(tenantId, estimateDTO);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
@@ -112,13 +148,7 @@ export default class SaleEstimateService {
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, estimateDTO.entries);
const saleEstimate = await SaleEstimate.query()
.upsertGraphAndFetch({
...omit(estimateObj, ['entries']),
entries: estimateObj.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount', 'id']),
}))
});
.upsertGraphAndFetch({ ...estimateObj });
this.logger.info('[sale_estimate] insert sale estimated success.');
await this.eventDispatcher.dispatch(events.saleEstimate.onCreated, {
@@ -136,15 +166,16 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate
* @return {void}
*/
public async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
public async editEstimate(
tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e));
const estimateObj = {
amount,
...formatDateFields(estimateDTO, ['estimateDate', 'expirationDate']),
};
// Transform DTO object ot model object.
const estimateObj = this.transformDTOToModel(tenantId, estimateDTO, oldSaleEstimate);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
@@ -166,11 +197,7 @@ export default class SaleEstimateService {
const saleEstimate = await SaleEstimate.query()
.upsertGraphAndFetch({
id: estimateId,
...omit(estimateObj, ['entries']),
entries: estimateObj.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount']),
})),
...estimateObj
});
await this.eventDispatcher.dispatch(events.saleEstimate.onEdited, {
@@ -194,6 +221,11 @@ export default class SaleEstimateService {
// Retrieve sale estimate or throw not found service error.
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.');
await ItemEntry.query()
.where('reference_id', estimateId)
@@ -254,4 +286,70 @@ export default class SaleEstimateService {
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 { omit, sumBy, difference, pick, chain } from 'lodash';
import { omit, sumBy, pick, chain } from 'lodash';
import moment from 'moment';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -10,7 +11,9 @@ import {
IItemEntry,
ISalesInvoicesFilter,
IPaginationMeta,
IFilterMeta
IFilterMeta,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
} from 'interfaces';
import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster';
@@ -23,11 +26,13 @@ import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
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',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
@@ -63,6 +68,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject()
customersService: CustomersService;
@Inject()
saleEstimatesService: SaleEstimateService;
/**
*
* Validate whether sale invoice number unqiue on the storage.
@@ -101,6 +109,33 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
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
* with associated to entries and journal transactions.
@@ -109,18 +144,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice}
*/
public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD): Promise<ISaleInvoice> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
public async createSaleInvoice(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invLotNumber = 1;
const saleInvoiceObj: ISaleInvoice = {
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']),
balance,
paymentAmount: 0,
// invLotNumber,
};
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
@@ -131,6 +164,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
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 oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const saleInvoiceObj = {
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']),
balance,
// invLotNumber: oldSaleInvoice.invLotNumber,
};
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO, oldSaleInvoice);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
@@ -203,10 +235,34 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId,
});
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
* and journal transactions.
@@ -218,6 +274,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
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.');
await SaleInvoice.query().where('id', saleInvoiceId).delete();
await ItemEntry.query()

View File

@@ -66,12 +66,12 @@ export default class SalesReceiptService {
*/
async validateReceiptDepositAccountExistance(tenantId: number, accountId: number) {
const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findById(accountId);
const depositAccount = await accountRepository.findOneById(accountId);
if (!depositAccount) {
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') {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);

View File

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

View File

@@ -69,7 +69,10 @@ export default class HasTenancyService {
*/
repositories(tenantId: number) {
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 { 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);
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) {
this.logger.info('[view] view not found.', { tenantId, viewId });
@@ -191,7 +191,7 @@ export default class ViewsService implements IViewsService {
}
// Save view details.
this.logger.info('[views] trying to insert to storage.', { tenantId, viewDTO })
const view = await viewRepository.insert({
const view = await viewRepository.create({
predefined: false,
name: viewDTO.name,
rolesLogicExpression: viewDTO.logicExpression,
@@ -245,7 +245,8 @@ export default class ViewsService implements IViewsService {
}
// Update view details.
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,
name: viewEditDTO.name,
rolesLogicExpression: viewEditDTO.logicExpression,

View File

@@ -3,17 +3,19 @@ import { On, EventSubscriber } from "event-dispatch";
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import SettingsService from 'services/Settings/SettingsService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
@EventSubscriber()
export default class SaleInvoiceSubscriber {
logger: any;
tenancy: TenancyService;
settingsService: SettingsService;
saleEstimatesService: SaleEstimateService;
constructor() {
this.logger = Container.get('logger');
this.tenancy = Container.get(TenancyService);
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);
}
/**
*
*/
@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.
*/

View File

@@ -227,6 +227,24 @@ const isBlank = (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 {
hashPassword,
origin,
@@ -246,5 +264,6 @@ export {
entriesAmountDiff,
convertEmptyStringToNull,
formatNumber,
isBlank
isBlank,
defaultToTransform
};