mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
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:
@@ -40,7 +40,7 @@
|
||||
"express-boom": "^3.0.0",
|
||||
"express-fileupload": "^1.1.7-alpha.3",
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-validator": "^6.2.0",
|
||||
"express-validator": "^6.8.0",
|
||||
"helmet": "^3.21.0",
|
||||
"i18n": "^0.8.5",
|
||||
"is-my-json-valid": "^2.20.5",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import MomentFormat from 'lib/MomentFormats';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class Ping {
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
}
|
||||
8
server/src/exceptions/ModelEntityNotFound.ts
Normal file
8
server/src/exceptions/ModelEntityNotFound.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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[],
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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: {
|
||||
|
||||
78
server/src/models/Customer.js
Normal file
78
server/src/models/Customer.js
Normal 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',
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
78
server/src/models/Vendor.js
Normal file
78
server/src/models/Vendor.js
Normal 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',
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
12
server/src/repositories/BillRepository.ts
Normal file
12
server/src/repositories/BillRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
231
server/src/repositories/EntityRepository.ts
Normal file
231
server/src/repositories/EntityRepository.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
12
server/src/repositories/ExpenseEntryRepository.ts
Normal file
12
server/src/repositories/ExpenseEntryRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
13
server/src/repositories/ItemRepository.ts
Normal file
13
server/src/repositories/ItemRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
12
server/src/repositories/PaymentReceiveEntryRepository.ts
Normal file
12
server/src/repositories/PaymentReceiveEntryRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { omit } from 'lodash';
|
||||
import { PaymentReceiveEntry } from 'models';
|
||||
import BaseModelRepository from 'repositories/BaseModelRepository';
|
||||
|
||||
export default class PaymentReceiveRepository extends BaseModelRepository {
|
||||
|
||||
}
|
||||
12
server/src/repositories/PaymentReceiveRepository.ts
Normal file
12
server/src/repositories/PaymentReceiveRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
|
||||
export default class SaleInvoiceRepository {
|
||||
|
||||
|
||||
|
||||
}
|
||||
12
server/src/repositories/SaleInvoiceRepository.ts
Normal file
12
server/src/repositories/SaleInvoiceRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
12
server/src/repositories/SettingRepository.ts
Normal file
12
server/src/repositories/SettingRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import ResourceRepository from './ResourceRepository';
|
||||
|
||||
export {
|
||||
ResourceRepository,
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user