diff --git a/server/package.json b/server/package.json index a23578396..926c4a5f1 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/api/controllers/Ping.ts b/server/src/api/controllers/Ping.ts index bf7a9b608..0dd89f3f0 100644 --- a/server/src/api/controllers/Ping.ts +++ b/server/src/api/controllers/Ping.ts @@ -1,6 +1,4 @@ import { Router, Request, Response } from 'express'; -import MomentFormat from 'lib/MomentFormats'; -import moment from 'moment'; export default class Ping { /** diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 4920adf3b..dbea7503e 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -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); } diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 2006cd970..9be4fbb60 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -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. */ diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 29bbd53b0..7a49699fc 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -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); } diff --git a/server/src/api/middleware/SettingsMiddleware.ts b/server/src/api/middleware/SettingsMiddleware.ts index 408c20c5b..faa1055c0 100644 --- a/server/src/api/middleware/SettingsMiddleware.ts +++ b/server/src/api/middleware/SettingsMiddleware.ts @@ -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 () => { diff --git a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js index 646d61550..0b385262a 100644 --- a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js +++ b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js @@ -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(); }); }; diff --git a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js index 9fac2e9f2..8544cc0d2 100644 --- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js +++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js @@ -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(); }); }; diff --git a/server/src/database/migrations/20200719152005_create_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js index 67ab64378..8dc2ce1aa 100644 --- a/server/src/database/migrations/20200719152005_create_bills_table.js +++ b/server/src/database/migrations/20200719152005_create_bills_table.js @@ -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(); }); diff --git a/server/src/database/seeds/core/20200810121809_seed_settings.js b/server/src/database/seeds/core/20200810121809_seed_settings.js new file mode 100644 index 000000000..65f0b0ce4 --- /dev/null +++ b/server/src/database/seeds/core/20200810121809_seed_settings.js @@ -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) => { + +} \ No newline at end of file diff --git a/server/src/exceptions/ModelEntityNotFound.ts b/server/src/exceptions/ModelEntityNotFound.ts new file mode 100644 index 000000000..a7bd6dfe1 --- /dev/null +++ b/server/src/exceptions/ModelEntityNotFound.ts @@ -0,0 +1,8 @@ + +export default class ModelEntityNotFound extends Error { + + constructor(entityId, message?) { + message = message || `Entity with id ${entityId} does not exist`; + super(message); + } +} \ No newline at end of file diff --git a/server/src/interfaces/Bill.ts b/server/src/interfaces/Bill.ts index 4c47c97dc..df111a888 100644 --- a/server/src/interfaces/Bill.ts +++ b/server/src/interfaces/Bill.ts @@ -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[], }; diff --git a/server/src/interfaces/SaleEstimate.ts b/server/src/interfaces/SaleEstimate.ts index 89eb8dbe1..5c5c7d47e 100644 --- a/server/src/interfaces/SaleEstimate.ts +++ b/server/src/interfaces/SaleEstimate.ts @@ -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 { diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index 8fef7f84e..fd16ce2b4 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -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, diff --git a/server/src/lib/Metable/MetableStoreDB.ts b/server/src/lib/Metable/MetableStoreDB.ts index 243154302..d7c303a07 100644 --- a/server/src/lib/Metable/MetableStoreDB.ts +++ b/server/src/lib/Metable/MetableStoreDB.ts @@ -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) => { diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index e5f32d033..0babf0eed 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -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)); } \ No newline at end of file diff --git a/server/src/loaders/tenantRepositories.ts b/server/src/loaders/tenantRepositories.ts index ffe80e046..ad9003591 100644 --- a/server/src/loaders/tenantRepositories.ts +++ b/server/src/loaders/tenantRepositories.ts @@ -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), }; }; \ No newline at end of file diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index c01df5e91..f76d2b4be 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -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. */ diff --git a/server/src/models/Contact.js b/server/src/models/Contact.js index 35fbd255f..fb92f8471 100644 --- a/server/src/models/Contact.js +++ b/server/src/models/Contact.js @@ -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: { diff --git a/server/src/models/Customer.js b/server/src/models/Customer.js new file mode 100644 index 000000000..7f5d0a06a --- /dev/null +++ b/server/src/models/Customer.js @@ -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', + } + }; + } +} diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index 3369cd80e..2335ebbc7 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -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 { /** diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index 8a8d4c01b..a50cfbb34 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -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. */ diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index e05da70e0..ed63b41de 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -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', + }, + }, }; } diff --git a/server/src/models/TenantModel.js b/server/src/models/TenantModel.js index ed4301e4f..9fbc9739e 100644 --- a/server/src/models/TenantModel.js +++ b/server/src/models/TenantModel.js @@ -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 => { diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js new file mode 100644 index 000000000..09081fced --- /dev/null +++ b/server/src/models/Vendor.js @@ -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', + } + }; + } +} diff --git a/server/src/models/index.js b/server/src/models/index.js index a980b2156..8f8d065f2 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -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, }; \ No newline at end of file diff --git a/server/src/repositories/AccountRepository.ts b/server/src/repositories/AccountRepository.ts index 6e04cc5db..5083df5d8 100644 --- a/server/src/repositories/AccountRepository.ts +++ b/server/src/repositories/AccountRepository.ts @@ -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 { - 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 { - 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 { - 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 { - 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} */ async balanceChange(accountId: number, amount: number): Promise { - 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); - } } \ No newline at end of file diff --git a/server/src/repositories/AccountTransactionRepository.ts b/server/src/repositories/AccountTransactionRepository.ts index 28a17c800..9c8c28848 100644 --- a/server/src/repositories/AccountTransactionRepository.ts +++ b/server/src/repositories/AccountTransactionRepository.ts @@ -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') diff --git a/server/src/repositories/AccountTypeRepository.ts b/server/src/repositories/AccountTypeRepository.ts index 7ad3071c4..1551273ec 100644 --- a/server/src/repositories/AccountTypeRepository.ts +++ b/server/src/repositories/AccountTypeRepository.ts @@ -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} */ - 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 { + return super.findWhereIn('key', keys); } /** * Retrieve account tpy eof the given key. * @param {string} key - * @return {IAccountType} + * @return {Promise} */ - 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 { + return super.findOne({ key }); } /** * Retrieve accounts types of the given root type. * @param {string} rootType - * @return {IAccountType[]} + * @return {Promise} */ getByRootType(rootType: string): Promise { - 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} */ getByChildType(childType: string): Promise { - 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 }); } } \ No newline at end of file diff --git a/server/src/repositories/BaseModelRepository.ts b/server/src/repositories/BaseModelRepository.ts index 9b0fde83e..09d17a6e9 100644 --- a/server/src/repositories/BaseModelRepository.ts +++ b/server/src/repositories/BaseModelRepository.ts @@ -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; - } } \ No newline at end of file diff --git a/server/src/repositories/BillRepository.ts b/server/src/repositories/BillRepository.ts new file mode 100644 index 000000000..851290e3a --- /dev/null +++ b/server/src/repositories/BillRepository.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/repositories/CachableRepository.ts b/server/src/repositories/CachableRepository.ts index de88ff723..ad917de2b 100644 --- a/server/src/repositories/CachableRepository.ts +++ b/server/src/repositories/CachableRepository.ts @@ -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} - 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} - 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} - 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} + */ + 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} + */ + 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} - 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} 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} 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} 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); + } } \ No newline at end of file diff --git a/server/src/repositories/ContactRepository.ts b/server/src/repositories/ContactRepository.ts index a0bf5ccbb..fa413c8be 100644 --- a/server/src/repositories/ContactRepository.ts +++ b/server/src/repositories/ContactRepository.ts @@ -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} - */ - async deleteById(contactId: number): Promise { - 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; } } \ No newline at end of file diff --git a/server/src/repositories/CustomerRepository.ts b/server/src/repositories/CustomerRepository.ts index 0fff61b03..5cba7c34d 100644 --- a/server/src/repositories/CustomerRepository.ts +++ b/server/src/repositories/CustomerRepository.ts @@ -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( diff --git a/server/src/repositories/EntityRepository.ts b/server/src/repositories/EntityRepository.ts new file mode 100644 index 000000000..24898fe49 --- /dev/null +++ b/server/src/repositories/EntityRepository.ts @@ -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} - 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} - 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} - 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} + */ + 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} + */ + 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} - 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} 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} 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} 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)); + } +} \ No newline at end of file diff --git a/server/src/repositories/ExpenseEntryRepository.ts b/server/src/repositories/ExpenseEntryRepository.ts new file mode 100644 index 000000000..e5370de86 --- /dev/null +++ b/server/src/repositories/ExpenseEntryRepository.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/repositories/ExpenseRepository.ts b/server/src/repositories/ExpenseRepository.ts index d63be77bf..a93ad7e31 100644 --- a/server/src/repositories/ExpenseRepository.ts +++ b/server/src/repositories/ExpenseRepository.ts @@ -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} + * 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 { - 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 { - 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 { - 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 { - 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} */ - async bulkPublish(expensesIds: number): Promise { - const { Expense } = this.models; - await Expense.query().whereIn('id', expensesIds).patch({ + async whereIdInPublish(expensesIds: number): Promise { + await this.model.query().whereIn('id', expensesIds).patch({ publishedAt: moment().toMySqlDateTime(), }); this.flushCache(); } - - /** - * Flushes repository cache. - */ - flushCache() { - this.cache.delStartWith(`expense`); - } } \ No newline at end of file diff --git a/server/src/repositories/ItemEntryRepository.js b/server/src/repositories/ItemEntryRepository.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/src/repositories/ItemRepository.ts b/server/src/repositories/ItemRepository.ts new file mode 100644 index 000000000..3ac811c22 --- /dev/null +++ b/server/src/repositories/ItemRepository.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/repositories/JournalRepository.ts b/server/src/repositories/JournalRepository.ts index ad8fb5bc6..c7367a520 100644 --- a/server/src/repositories/JournalRepository.ts +++ b/server/src/repositories/JournalRepository.ts @@ -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; } } \ No newline at end of file diff --git a/server/src/repositories/PaymentReceiveEntryRepository.js b/server/src/repositories/PaymentReceiveEntryRepository.js deleted file mode 100644 index eb2935c2b..000000000 --- a/server/src/repositories/PaymentReceiveEntryRepository.js +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/server/src/repositories/PaymentReceiveEntryRepository.ts b/server/src/repositories/PaymentReceiveEntryRepository.ts new file mode 100644 index 000000000..de3325755 --- /dev/null +++ b/server/src/repositories/PaymentReceiveEntryRepository.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/repositories/PaymentReceiveRepository.js b/server/src/repositories/PaymentReceiveRepository.js deleted file mode 100644 index 833d07050..000000000 --- a/server/src/repositories/PaymentReceiveRepository.js +++ /dev/null @@ -1,7 +0,0 @@ -import { omit } from 'lodash'; -import { PaymentReceiveEntry } from 'models'; -import BaseModelRepository from 'repositories/BaseModelRepository'; - -export default class PaymentReceiveRepository extends BaseModelRepository { - -} diff --git a/server/src/repositories/PaymentReceiveRepository.ts b/server/src/repositories/PaymentReceiveRepository.ts new file mode 100644 index 000000000..60d06086e --- /dev/null +++ b/server/src/repositories/PaymentReceiveRepository.ts @@ -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; + } +} diff --git a/server/src/repositories/SaleInvoiceRepository.js b/server/src/repositories/SaleInvoiceRepository.js deleted file mode 100644 index 2e2c7235d..000000000 --- a/server/src/repositories/SaleInvoiceRepository.js +++ /dev/null @@ -1,7 +0,0 @@ - - -export default class SaleInvoiceRepository { - - - -} \ No newline at end of file diff --git a/server/src/repositories/SaleInvoiceRepository.ts b/server/src/repositories/SaleInvoiceRepository.ts new file mode 100644 index 000000000..525b9e25d --- /dev/null +++ b/server/src/repositories/SaleInvoiceRepository.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/repositories/SettingRepository.ts b/server/src/repositories/SettingRepository.ts new file mode 100644 index 000000000..584aec853 --- /dev/null +++ b/server/src/repositories/SettingRepository.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/repositories/TenantRepository.ts b/server/src/repositories/TenantRepository.ts index d692a24ec..a1d5ce847 100644 --- a/server/src/repositories/TenantRepository.ts +++ b/server/src/repositories/TenantRepository.ts @@ -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; - } } \ No newline at end of file diff --git a/server/src/repositories/VendorRepository.ts b/server/src/repositories/VendorRepository.ts index aa19a4249..d7e37e2ed 100644 --- a/server/src/repositories/VendorRepository.ts +++ b/server/src/repositories/VendorRepository.ts @@ -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( diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts index 288f7ad2d..6be4f0e37 100644 --- a/server/src/repositories/ViewRepository.ts +++ b/server/src/repositories/ViewRepository.ts @@ -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 { - const { View } = this.models; - const insertedView = await View.query().insertGraph({ ...view }); - this.flushCache(); - - return insertedView; - } - - async update(viewId: number, view: IView): Promise { - 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); } } \ No newline at end of file diff --git a/server/src/repositories/ViewRoleRepository.ts b/server/src/repositories/ViewRoleRepository.ts index a42ddb987..22a099a30 100644 --- a/server/src/repositories/ViewRoleRepository.ts +++ b/server/src/repositories/ViewRoleRepository.ts @@ -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); - }); - } } \ No newline at end of file diff --git a/server/src/repositories/index.js b/server/src/repositories/index.js deleted file mode 100644 index 7bf898a88..000000000 --- a/server/src/repositories/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import ResourceRepository from './ResourceRepository'; - -export { - ResourceRepository, -}; \ No newline at end of file diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 322180adb..0a5e15b13 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -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', diff --git a/server/src/services/Accounting/JournalPoster.ts b/server/src/services/Accounting/JournalPoster.ts index 896074630..3aa3c2572 100644 --- a/server/src/services/Accounting/JournalPoster.ts +++ b/server/src/services/Accounting/JournalPoster.ts @@ -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; diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index f50a1329d..97d0b828f 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -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'); diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 428afe164..97500d39a 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -27,10 +27,13 @@ export default class ContactsService { * @return {Promise} */ 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); } /** diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 509512dfe..9ee8b30ed 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -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} */ 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} */ 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); diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index a803aca8f..16982d750 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -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'); diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts index 66294da1d..669aae5c9 100644 --- a/server/src/services/DynamicListing/DynamicListService.ts +++ b/server/src/services/DynamicListing/DynamicListService.ts @@ -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); diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index f285bd580..27634afb5 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -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 { + 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} */ public async getExpense(tenantId: number, expenseId: number): Promise { - 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); } diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts index 20e16d7ce..3ed709422 100644 --- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts +++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts @@ -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. diff --git a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index 5544ae7df..5b74cc921 100644 --- a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts +++ b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts @@ -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. diff --git a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts index a9c88b0c0..af021cdda 100644 --- a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts +++ b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts @@ -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. diff --git a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts index 90287a38c..8f2a05755 100644 --- a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts +++ b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts @@ -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. diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index 893381135..ba892a6ed 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -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 }); diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 298cb3468..b729e0d83 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -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 }); diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index f861168e6..e880e8509 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -197,7 +197,8 @@ export default class ManualJournalsService implements IManualJournalsService { contactRequired: boolean = true, ): Promise { 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 && diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index 94c3f61e6..aeeec28c4 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -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 = { diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index c2a1aedea..fab143c62 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -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 { 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 { + 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(), + }); + } } \ No newline at end of file diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 190909a41..8f1a2261e 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -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); diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index e534c37d0..8ae4a77ef 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -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} */ - public async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise { - const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); + public async createEstimate( + tenantId: number, + estimateDTO: ISaleEstimateDTO + ): Promise { + 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 { - const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId); + public async editEstimate( + tenantId: number, + estimateId: number, + estimateDTO: ISaleEstimateDTO + ): Promise { + 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} + */ + async convertEstimateToInvoice( + tenantId: number, + estimateId: number, + invoiceId: number, + ): Promise { + 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} + */ + async unlinkConvertedEstimateFromInvoice( + tenantId: number, + invoiceId: number, + ): Promise { + 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 { + 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() + }); + } } \ No newline at end of file diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 1324ea271..8c334d4d5 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -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 { - const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId); - - const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); + public async createSaleInvoice( + tenantId: number, + saleInvoiceDTO: ISaleInvoiceCreateDTO + ): Promise { + 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} + */ + public async deliverSaleInvoice( + tenantId: number, + saleInvoiceId: number, + ): Promise { + 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() diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index 170bef443..3041b8b86 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -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); diff --git a/server/src/services/Settings/SettingsStore.ts b/server/src/services/Settings/SettingsStore.ts index 0f5889dcb..362674014 100644 --- a/server/src/services/Settings/SettingsStore.ts +++ b/server/src/services/Settings/SettingsStore.ts @@ -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); } } \ No newline at end of file diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index 73941ac42..057e6f53c 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -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); }); } diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts index b019ea168..5b882c069 100644 --- a/server/src/services/Views/ViewsService.ts +++ b/server/src/services/Views/ViewsService.ts @@ -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, diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts index 17ace00bd..5a7226cc8 100644 --- a/server/src/subscribers/saleInvoices.ts +++ b/server/src/subscribers/saleInvoices.ts @@ -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. */ diff --git a/server/src/utils/index.js b/server/src/utils/index.js index 40438b38f..ab6ddd696 100644 --- a/server/src/utils/index.js +++ b/server/src/utils/index.js @@ -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 };