diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index e96822a9b..697addee5 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -51,13 +51,6 @@ export default class ItemsController extends BaseController { asyncMiddleware(this.editItem.bind(this)), this.handlerServiceErrors ); - router.delete( - '/', - [...this.validateBulkSelectSchema], - this.validationResult, - asyncMiddleware(this.bulkDeleteItems.bind(this)), - this.handlerServiceErrors - ); router.delete( '/:id', [...this.validateSpecificItemSchema], @@ -415,28 +408,6 @@ export default class ItemsController extends BaseController { } } - /** - * Deletes items in bulk. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async bulkDeleteItems(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { ids: itemsIds } = req.query; - - try { - await this.itemsService.bulkDeleteItems(tenantId, itemsIds); - - return res.status(200).send({ - ids: itemsIds, - message: 'Items have been deleted successfully.', - }); - } catch (error) { - next(error); - } - } - /** * Handles service errors. * @param {Error} error diff --git a/server/src/api/controllers/Settings.ts b/server/src/api/controllers/Settings.ts index a50876730..1760eb08f 100644 --- a/server/src/api/controllers/Settings.ts +++ b/server/src/api/controllers/Settings.ts @@ -1,32 +1,35 @@ -import { Service } from 'typedi'; +import { Inject, Service } from 'typedi'; import { Router, Request, Response } from 'express'; import { body, query } from 'express-validator'; import { pick } from 'lodash'; import { IOptionDTO, IOptionsDTO } from 'interfaces'; import BaseController from 'api/controllers/BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import { - getDefinedOptions, - isDefinedOptionConfigurable, -} from 'utils'; +import { getDefinedOptions, isDefinedOptionConfigurable } from 'utils'; +import SettingsService from 'services/Settings/SettingsService'; @Service() -export default class SettingsController extends BaseController{ +export default class SettingsController extends BaseController { + @Inject() + settingsService: SettingsService; + /** * Router constructor. */ router() { const router = Router(); - router.post('/', + router.post( + '/', this.saveSettingsValidationSchema, this.validationResult, - asyncMiddleware(this.saveSettings.bind(this)), + asyncMiddleware(this.saveSettings.bind(this)) ); - router.get('/', + router.get( + '/', this.getSettingsSchema, this.validationResult, - asyncMiddleware(this.getSettings.bind(this)), + asyncMiddleware(this.getSettings.bind(this)) ); return router; } @@ -46,7 +49,7 @@ export default class SettingsController extends BaseController{ /** * Retrieve the application options from the storage. */ - private get getSettingsSchema() { + private get getSettingsSchema() { return [ query('key').optional().trim().escape(), query('group').optional().trim().escape(), @@ -55,16 +58,19 @@ export default class SettingsController extends BaseController{ /** * Saves the given options to the storage. - * @param {Request} req - - * @param {Response} res - + * @param {Request} req - + * @param {Response} res - */ public async saveSettings(req: Request, res: Response, next) { - const { Option } = req.models; + const { tenantId } = req; const optionsDTO: IOptionsDTO = this.matchedBodyData(req); const { settings } = req; - const errorReasons: { type: string, code: number, keys: [] }[] = []; - const notDefinedOptions = Option.validateDefined(optionsDTO.options); + const errorReasons: { type: string; code: number; keys: [] }[] = []; + const notDefinedOptions = this.settingsService.validateNotDefinedSettings( + tenantId, + optionsDTO.options + ); if (notDefinedOptions.length) { errorReasons.push({ @@ -82,7 +88,7 @@ export default class SettingsController extends BaseController{ try { await settings.save(); - return res.status(200).send({ + return res.status(200).send({ type: 'success', code: 'OPTIONS.SAVED.SUCCESSFULLY', message: 'Options have been saved successfully.', @@ -94,8 +100,8 @@ export default class SettingsController extends BaseController{ /** * Retrieve settings. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ public getSettings(req: Request, res: Response) { const { settings } = req; @@ -103,4 +109,4 @@ export default class SettingsController extends BaseController{ return res.status(200).send({ settings: allSettings }); } -}; +} diff --git a/server/src/data/options.js b/server/src/data/options.js index b68ae3126..970742436 100644 --- a/server/src/data/options.js +++ b/server/src/data/options.js @@ -1,166 +1,127 @@ export default { - organization: [ - { - key: "name", - type: "string", - config: true, - }, - { - key: "base_currency", - type: "string", - config: true, - }, - { - key: "industry", + organization: { + name: { type: "string", }, - { - key: "location", + base_currency: { + type: 'string', + }, + industry: { type: "string", }, - { - key: "fiscal_year", - type: "string", - // config: true, - }, - { - key: "financial_date_start", + location: { type: "string", }, - { - key: "language", - type: "string", - config: true, + fiscal_year: { + type: 'string', }, - { - key: "time_zone", - type: "string", + financial_date_start: { + type: 'string', }, - { - key: "date_format", - type: "string", + language: { + type: 'string', }, - { - key: 'accounting_basis', + time_zone: { + type: 'string', + }, + date_format: { + type: 'string', + }, + accounting_basis: { type: 'string', } - ], - manual_journals: [ - { - key: "next_number", - type: "string", + }, + manual_journals: { + next_number: { + type: 'string', }, - { - key: "number_prefix", - type: "string", + number_prefix: { + type: 'string', }, - { - key: "auto_increment", - type: "boolean", + auto_increment: { + type: 'boolean', } - ], - bill_payments: [ - { - key: 'withdrawal_account', - type: 'string' - } - ], - sales_estimates: [ - { - key: "next_number", - type: "number", - }, - { - key: "number_prefix", - type: "string", - }, - { - key: "auto_increment", - type: "boolean", - } - ], - sales_receipts: [ - { - key: "next_number", - type: "number", - }, - { - key: "number_prefix", - type: "string", - }, - { - key: "auto_increment", - type: "boolean", - }, - { - key: "preferred_deposit_account", - type: "number", - }, - ], - sales_invoices: [ - { - key: "next_number", - type: "number", - }, - { - key: "number_prefix", - type: "string", - }, - { - key: "auto_increment", - type: "boolean", - } - ], - payment_receives: [ - { - key: "next_number", - type: "number", - }, - { - key: "number_prefix", - type: "string", - }, - { - key: "auto_increment", - type: "boolean", - }, - { - key: 'deposit_account', + }, + bill_payments: { + withdrawal_account: { type: 'string' }, - { - key: 'advance_deposit', - key: 'string' + }, + sales_estimates: { + next_number: { + type: 'string', + }, + number_prefix: { + type: 'string', + }, + auto_increment: { + type: "boolean", } - ], - items: [ - { - key: "sell_account", - type: "number", + }, + sales_receipts: { + next_number: { + type: "string", }, - { - key: "cost_account", - type: "number", + number_prefix: { + type: "string", }, - { - key: "inventory_account", - type: "number", + auto_increment: { + type: "boolean", }, - ], - expenses: [ - { - key: "preferred_payment_account", + preferred_deposit_account: { type: "number", + } + }, + sales_invoices: { + next_number: { + type: "string", }, - ], - accounts: [ - { - key: 'account_code_required', + number_prefix: { + type: "string", + }, + auto_increment: { + type: "boolean", + }, + }, + payment_receives: { + next_number: { + type: "string", + }, + number_prefix: { + type: "string", + }, + auto_increment: { type: 'boolean', }, - { - key: 'account_code_unique', + deposit_account: { + type: 'number', + }, + advance_deposit: { + type: 'number', + } + }, + items: { + sell_account: { + type: 'number', + }, + cost_account: { + type: 'number', + }, + inventory_account: { + type: 'number', + }, + }, + expenses: { + preferred_payment_account: { + type: "number", + } + }, + accounts: { + account_code_required: { type: 'boolean', }, - ] + account_code_unique: { + type: 'boolean', + } + } }; diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index ed2c0d147..a314c07b1 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -61,14 +61,10 @@ export interface IItemDTO { } export interface IItemsService { - bulkDeleteItems(tenantId: number, itemsIds: number[]): Promise; - getItem(tenantId: number, itemId: number): Promise; deleteItem(tenantId: number, itemId: number): Promise; editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise; - newItem(tenantId: number, itemDTO: IItemDTO): Promise; - itemsList(tenantId: number, itemsFilter: IItemsFilter): Promise<{items: IItem[]}>; } diff --git a/server/src/lib/Metable/MetableConfig.ts b/server/src/lib/Metable/MetableConfig.ts new file mode 100644 index 000000000..21febaffd --- /dev/null +++ b/server/src/lib/Metable/MetableConfig.ts @@ -0,0 +1,40 @@ +import { get } from 'lodash'; + +export default class MetableConfig { + readonly config: any; + + constructor(config) { + this.setConfig(config); + } + + /** + * Sets config. + */ + setConfig(config) { + this.config = config; + } + + /** + * + * @param {string} key + * @param {string} group + * @param {string} accessor + * @returns {object|string} + */ + getMetaConfig(key: string, group?: string, accessor?: string) { + const configGroup = get(this.config, group); + const config = get(configGroup, key); + + return accessor ? get(config, accessor) : config; + } + + /** + * + * @param {string} key + * @param {string} group + * @returns {string} + */ + getMetaType(key: string, group?: string) { + return this.getMetaConfig(key, group, 'type'); + } +} \ No newline at end of file diff --git a/server/src/lib/Metable/MetableStoreDB.ts b/server/src/lib/Metable/MetableStoreDB.ts index 830a07ca2..a4a63e4d5 100644 --- a/server/src/lib/Metable/MetableStoreDB.ts +++ b/server/src/lib/Metable/MetableStoreDB.ts @@ -1,19 +1,19 @@ -import { Model } from 'objection'; -import { - IMetadata, - IMetableStoreStorage, -} from 'interfaces'; +import { IMetadata, IMetableStoreStorage } from 'interfaces'; import MetableStore from './MetableStore'; -import { isBlank } from 'utils'; - -export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{ +import { isBlank, parseBoolean } from 'utils'; +import MetableConfig from './MetableConfig'; +import config from 'data/options' +export default class MetableDBStore + extends MetableStore + implements IMetableStoreStorage { repository: any; KEY_COLUMN: string; VALUE_COLUMN: string; TYPE_COLUMN: string; extraQuery: Function; loaded: Boolean; - + config: MetableConfig; + /** * Constructor method. */ @@ -32,11 +32,12 @@ export default class MetableDBStore extends MetableStore implements IMetableStor ...this.transfromMetaExtraColumns(meta), }; }; + this.config = new MetableConfig(config); } /** * Transformes meta query. - * @param {IMetadata} meta + * @param {IMetadata} meta */ private transfromMetaExtraColumns(meta: IMetadata) { return this.extraColumns.reduce((obj, column) => { @@ -56,10 +57,10 @@ export default class MetableDBStore extends MetableStore implements IMetableStor setRepository(repository) { this.repository = repository; } - + /** * Sets a extra query callback. - * @param callback + * @param callback */ setExtraQuery(callback) { this.extraQuery = callback; @@ -84,17 +85,18 @@ export default class MetableDBStore extends MetableStore implements IMetableStor * @returns {Promise} */ saveUpdated(metadata: IMetadata[]) { - const updated = metadata.filter((m) => (m._markAsUpdated === true)); + const updated = metadata.filter((m) => m._markAsUpdated === true); const opers = []; updated.forEach((meta) => { - const updateOper = this.repository.update({ - [this.VALUE_COLUMN]: meta.value, - }, { - ...this.extraQuery(meta), - }).then(() => { - meta._markAsUpdated = false; - }); + const updateOper = this.repository + .update( + { [this.VALUE_COLUMN]: meta.value }, + { ...this.extraQuery(meta) } + ) + .then(() => { + meta._markAsUpdated = false; + }); opers.push(updateOper); }); return Promise.all(opers); @@ -106,16 +108,20 @@ export default class MetableDBStore extends MetableStore implements IMetableStor * @returns {Promise} */ saveDeleted(metadata: IMetadata[]) { - const deleted = metadata.filter((m: IMetadata) => (m._markAsDeleted === true)); + const deleted = metadata.filter( + (m: IMetadata) => m._markAsDeleted === true + ); const opers: Promise = []; if (deleted.length > 0) { deleted.forEach((meta) => { - const deleteOper = this.repository.deleteBy({ - ...this.extraQuery(meta), - }).then(() => { - meta._markAsDeleted = false; - }); + const deleteOper = this.repository + .deleteBy({ + ...this.extraQuery(meta), + }) + .then(() => { + meta._markAsDeleted = false; + }); opers.push(deleteOper); }); } @@ -128,7 +134,9 @@ export default class MetableDBStore extends MetableStore implements IMetableStor * @returns {Promise} */ saveInserted(metadata: IMetadata[]) { - const inserted = metadata.filter((m: IMetadata) => (m._markAsInserted === true)); + const inserted = metadata.filter( + (m: IMetadata) => m._markAsInserted === true + ); const opers: Promise = []; inserted.forEach((meta) => { @@ -137,10 +145,9 @@ export default class MetableDBStore extends MetableStore implements IMetableStor [this.VALUE_COLUMN]: meta.value, ...this.transfromMetaExtraColumns(meta), }; - const insertOper = this.repository.create(insertData) - .then(() => { - meta._markAsInserted = false; - }); + const insertOper = this.repository.create(insertData).then(() => { + meta._markAsInserted = false; + }); opers.push(insertOper); }); return Promise.all(opers); @@ -164,20 +171,23 @@ export default class MetableDBStore extends MetableStore implements IMetableStor } /** - * Format the metadata before saving to the database. + * Parse the metadata values after fetching it from the storage. * @param {String|Number|Boolean} value - * @param {String} valueType - * @return {String|Number|Boolean} - */ - static formatMetaValue(value: string|number|boolean, valueType: string|false) { - let parsedValue: string|number|boolean; + static parseMetaValue( + value: string, + valueType: string | false + ): string | boolean | number { + let parsedValue: string | number | boolean; switch (valueType) { case 'number': - parsedValue = `${value}`; + parsedValue = parseFloat(value); break; case 'boolean': - parsedValue = value ? '1' : '0'; + parsedValue = parseBoolean(value, false); break; case 'json': parsedValue = JSON.stringify(parsedValue); @@ -195,11 +205,15 @@ export default class MetableDBStore extends MetableStore implements IMetableStor * @param {String} parseType - */ mapMetadata(metadata: IMetadata) { + const metaType = this.config.getMetaType( + metadata[this.KEY_COLUMN], + metadata['group'], + ); return { key: metadata[this.KEY_COLUMN], - value: MetableDBStore.formatMetaValue( + value: MetableDBStore.parseMetaValue( metadata[this.VALUE_COLUMN], - this.TYPE_COLUMN ? metadata[this.TYPE_COLUMN] : false, + metaType ), ...this.extraColumns.reduce((obj, extraCol: string) => { obj[extraCol] = metadata[extraCol] || null; @@ -220,8 +234,10 @@ export default class MetableDBStore extends MetableStore implements IMetableStor * Throw error in case the store is not loaded yet. */ private validateStoreIsLoaded() { - if (!this.loaded) { - throw new Error('You could not save the store before loaded from the storage.'); + if (!this.loaded) { + throw new Error( + 'You could not save the store before loaded from the storage.' + ); } } } diff --git a/server/src/models/Contact.js b/server/src/models/Contact.js index fe5166490..940351cad 100644 --- a/server/src/models/Contact.js +++ b/server/src/models/Contact.js @@ -20,7 +20,7 @@ export default class Contact extends TenantModel { * Defined virtual attributes. */ static get virtualAttributes() { - return ['contactNormal', 'closingBalance']; + return ['contactNormal', 'closingBalance', 'formattedContactService']; } /** @@ -35,17 +35,36 @@ export default class Contact extends TenantModel { } /** - * Retrieve the contact noraml; + * Retrieve the contact normal by the given contact service. + * @param {string} contactService + */ + static getFormattedContactService(contactService) { + const types = { + 'customer': 'Customer', + 'vendor': 'Vendor', + }; + return types[contactService]; + } + + /** + * Retrieve the contact normal. */ get contactNormal() { return Contact.getContactNormalByType(this.contactService); } + /** + * Retrieve formatted contact service. + */ + get formattedContactService() { + return Contact.getFormattedContactService(this.contactService); + } + /** * Closing balance attribute. */ get closingBalance() { - return this.openingBalance + this.balance; + return this.balance; } /** diff --git a/server/src/models/Customer.js b/server/src/models/Customer.js index 07ec3f429..024917888 100644 --- a/server/src/models/Customer.js +++ b/server/src/models/Customer.js @@ -48,7 +48,7 @@ export default class Customer extends TenantModel { * Closing balance attribute. */ get closingBalance() { - return this.openingBalance + this.balance; + return this.balance; } /** diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js index 91023b5b0..62eab7b4c 100644 --- a/server/src/models/Vendor.js +++ b/server/src/models/Vendor.js @@ -47,7 +47,7 @@ export default class Vendor extends TenantModel { * Closing balance attribute. */ get closingBalance() { - return this.openingBalance + this.balance; + return this.balance; } /** diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index c325f0241..8fa561e8a 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -491,59 +491,6 @@ export default class ItemsService implements IItemsService { return item; } - /** - * Validates the given items IDs exists or throw not found service error. - * @param {number} tenantId - - * @param {number[]} itemsIDs - - * @return {Promise} - */ - private async validateItemsIdsExists( - tenantId: number, - itemsIDs: number[] - ): Promise { - const { Item } = this.tenancy.models(tenantId); - - const storedItems = await Item.query().whereIn('id', itemsIDs); - const storedItemsIds = storedItems.map((t: IItem) => t.id); - - const notFoundItemsIds = difference(itemsIDs, storedItemsIds); - - if (notFoundItemsIds.length > 0) { - throw new ServiceError(ERRORS.ITEMS_NOT_FOUND, null, { - notFoundItemsIds, - }); - } - } - - /** - * Deletes items in bulk. - * @param {number} tenantId - * @param {number[]} itemsIds - Items ids. - */ - public async bulkDeleteItems(tenantId: number, itemsIds: number[]) { - const { Item } = this.tenancy.models(tenantId); - - this.logger.info('[items] trying to delete items in bulk.', { - tenantId, - itemsIds, - }); - // Validates the given items exist on the storage. - await this.validateItemsIdsExists(tenantId, itemsIds); - - // Validate the item has no associated inventory transactions. - await this.validateHasNoInventoryAdjustments(tenantId, itemsIds); - - // Validate the items have no associated invoices or bills. - await this.validateHasNoInvoicesOrBills(tenantId, itemsIds); - - await Item.query().whereIn('id', itemsIds).delete(); - - this.logger.info('[items] deleted successfully in bulk.', { - tenantId, - itemsIds, - }); - } - /** * Retrieve items datatable list. * @param {number} tenantId diff --git a/server/src/services/Settings/SettingsService.ts b/server/src/services/Settings/SettingsService.ts index 635ee3d96..34148a7d7 100644 --- a/server/src/services/Settings/SettingsService.ts +++ b/server/src/services/Settings/SettingsService.ts @@ -30,4 +30,24 @@ export default class SettingsService { await settings.save(); } } + + /** + * Validates the given options is defined or either not. + * @param {Array} options + * @return {Boolean} + */ + validateNotDefinedSettings(tenantId: number, options) { + const notDefined = []; + + const settings = this.tenancy.settings(tenantId); + + options.forEach((option) => { + const setting = settings.config.getMetaConfig(option.key, option.group); + + if (!setting) { + notDefined.push(option); + } + }); + return notDefined; + } }