This commit is contained in:
elforjani3
2021-03-22 11:52:18 +02:00
11 changed files with 265 additions and 289 deletions

View File

@@ -51,13 +51,6 @@ export default class ItemsController extends BaseController {
asyncMiddleware(this.editItem.bind(this)), asyncMiddleware(this.editItem.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
); );
router.delete(
'/',
[...this.validateBulkSelectSchema],
this.validationResult,
asyncMiddleware(this.bulkDeleteItems.bind(this)),
this.handlerServiceErrors
);
router.delete( router.delete(
'/:id', '/:id',
[...this.validateSpecificItemSchema], [...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. * Handles service errors.
* @param {Error} error * @param {Error} error

View File

@@ -1,32 +1,35 @@
import { Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { body, query } from 'express-validator'; import { body, query } from 'express-validator';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { IOptionDTO, IOptionsDTO } from 'interfaces'; import { IOptionDTO, IOptionsDTO } from 'interfaces';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import { import { getDefinedOptions, isDefinedOptionConfigurable } from 'utils';
getDefinedOptions, import SettingsService from 'services/Settings/SettingsService';
isDefinedOptionConfigurable,
} from 'utils';
@Service() @Service()
export default class SettingsController extends BaseController{ export default class SettingsController extends BaseController {
@Inject()
settingsService: SettingsService;
/** /**
* Router constructor. * Router constructor.
*/ */
router() { router() {
const router = Router(); const router = Router();
router.post('/', router.post(
'/',
this.saveSettingsValidationSchema, this.saveSettingsValidationSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.saveSettings.bind(this)), asyncMiddleware(this.saveSettings.bind(this))
); );
router.get('/', router.get(
'/',
this.getSettingsSchema, this.getSettingsSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.getSettings.bind(this)), asyncMiddleware(this.getSettings.bind(this))
); );
return router; return router;
} }
@@ -46,7 +49,7 @@ export default class SettingsController extends BaseController{
/** /**
* Retrieve the application options from the storage. * Retrieve the application options from the storage.
*/ */
private get getSettingsSchema() { private get getSettingsSchema() {
return [ return [
query('key').optional().trim().escape(), query('key').optional().trim().escape(),
query('group').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. * Saves the given options to the storage.
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
public async saveSettings(req: Request, res: Response, next) { public async saveSettings(req: Request, res: Response, next) {
const { Option } = req.models; const { tenantId } = req;
const optionsDTO: IOptionsDTO = this.matchedBodyData(req); const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
const { settings } = req; const { settings } = req;
const errorReasons: { type: string, code: number, keys: [] }[] = []; const errorReasons: { type: string; code: number; keys: [] }[] = [];
const notDefinedOptions = Option.validateDefined(optionsDTO.options); const notDefinedOptions = this.settingsService.validateNotDefinedSettings(
tenantId,
optionsDTO.options
);
if (notDefinedOptions.length) { if (notDefinedOptions.length) {
errorReasons.push({ errorReasons.push({
@@ -82,7 +88,7 @@ export default class SettingsController extends BaseController{
try { try {
await settings.save(); await settings.save();
return res.status(200).send({ return res.status(200).send({
type: 'success', type: 'success',
code: 'OPTIONS.SAVED.SUCCESSFULLY', code: 'OPTIONS.SAVED.SUCCESSFULLY',
message: 'Options have been saved successfully.', message: 'Options have been saved successfully.',
@@ -94,8 +100,8 @@ export default class SettingsController extends BaseController{
/** /**
* Retrieve settings. * Retrieve settings.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
public getSettings(req: Request, res: Response) { public getSettings(req: Request, res: Response) {
const { settings } = req; const { settings } = req;
@@ -103,4 +109,4 @@ export default class SettingsController extends BaseController{
return res.status(200).send({ settings: allSettings }); return res.status(200).send({ settings: allSettings });
} }
}; }

View File

@@ -1,166 +1,127 @@
export default { export default {
organization: [ organization: {
{ name: {
key: "name",
type: "string",
config: true,
},
{
key: "base_currency",
type: "string",
config: true,
},
{
key: "industry",
type: "string", type: "string",
}, },
{ base_currency: {
key: "location", type: 'string',
},
industry: {
type: "string", type: "string",
}, },
{ location: {
key: "fiscal_year",
type: "string",
// config: true,
},
{
key: "financial_date_start",
type: "string", type: "string",
}, },
{ fiscal_year: {
key: "language", type: 'string',
type: "string",
config: true,
}, },
{ financial_date_start: {
key: "time_zone", type: 'string',
type: "string",
}, },
{ language: {
key: "date_format", type: 'string',
type: "string",
}, },
{ time_zone: {
key: 'accounting_basis', type: 'string',
},
date_format: {
type: 'string',
},
accounting_basis: {
type: 'string', type: 'string',
} }
], },
manual_journals: [ manual_journals: {
{ next_number: {
key: "next_number", type: 'string',
type: "string",
}, },
{ number_prefix: {
key: "number_prefix", type: 'string',
type: "string",
}, },
{ auto_increment: {
key: "auto_increment", type: 'boolean',
type: "boolean",
} }
], },
bill_payments: [ bill_payments: {
{ withdrawal_account: {
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',
type: 'string' type: 'string'
}, },
{ },
key: 'advance_deposit', sales_estimates: {
key: 'string' next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: "boolean",
} }
], },
items: [ sales_receipts: {
{ next_number: {
key: "sell_account", type: "string",
type: "number",
}, },
{ number_prefix: {
key: "cost_account", type: "string",
type: "number",
}, },
{ auto_increment: {
key: "inventory_account", type: "boolean",
type: "number",
}, },
], preferred_deposit_account: {
expenses: [
{
key: "preferred_payment_account",
type: "number", type: "number",
}
},
sales_invoices: {
next_number: {
type: "string",
}, },
], number_prefix: {
accounts: [ type: "string",
{ },
key: 'account_code_required', auto_increment: {
type: "boolean",
},
},
payment_receives: {
next_number: {
type: "string",
},
number_prefix: {
type: "string",
},
auto_increment: {
type: 'boolean', type: 'boolean',
}, },
{ deposit_account: {
key: 'account_code_unique', 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', type: 'boolean',
}, },
] account_code_unique: {
type: 'boolean',
}
}
}; };

View File

@@ -61,14 +61,10 @@ export interface IItemDTO {
} }
export interface IItemsService { export interface IItemsService {
bulkDeleteItems(tenantId: number, itemsIds: number[]): Promise<void>;
getItem(tenantId: number, itemId: number): Promise<IItem>; getItem(tenantId: number, itemId: number): Promise<IItem>;
deleteItem(tenantId: number, itemId: number): Promise<void>; deleteItem(tenantId: number, itemId: number): Promise<void>;
editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise<IItem>; editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise<IItem>;
newItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem>; newItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem>;
itemsList(tenantId: number, itemsFilter: IItemsFilter): Promise<{items: IItem[]}>; itemsList(tenantId: number, itemsFilter: IItemsFilter): Promise<{items: IItem[]}>;
} }

View File

@@ -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');
}
}

View File

@@ -1,19 +1,19 @@
import { Model } from 'objection'; import { IMetadata, IMetableStoreStorage } from 'interfaces';
import {
IMetadata,
IMetableStoreStorage,
} from 'interfaces';
import MetableStore from './MetableStore'; import MetableStore from './MetableStore';
import { isBlank } from 'utils'; import { isBlank, parseBoolean } from 'utils';
import MetableConfig from './MetableConfig';
export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{ import config from 'data/options'
export default class MetableDBStore
extends MetableStore
implements IMetableStoreStorage {
repository: any; repository: any;
KEY_COLUMN: string; KEY_COLUMN: string;
VALUE_COLUMN: string; VALUE_COLUMN: string;
TYPE_COLUMN: string; TYPE_COLUMN: string;
extraQuery: Function; extraQuery: Function;
loaded: Boolean; loaded: Boolean;
config: MetableConfig;
/** /**
* Constructor method. * Constructor method.
*/ */
@@ -32,11 +32,12 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
...this.transfromMetaExtraColumns(meta), ...this.transfromMetaExtraColumns(meta),
}; };
}; };
this.config = new MetableConfig(config);
} }
/** /**
* Transformes meta query. * Transformes meta query.
* @param {IMetadata} meta * @param {IMetadata} meta
*/ */
private transfromMetaExtraColumns(meta: IMetadata) { private transfromMetaExtraColumns(meta: IMetadata) {
return this.extraColumns.reduce((obj, column) => { return this.extraColumns.reduce((obj, column) => {
@@ -56,10 +57,10 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
setRepository(repository) { setRepository(repository) {
this.repository = repository; this.repository = repository;
} }
/** /**
* Sets a extra query callback. * Sets a extra query callback.
* @param callback * @param callback
*/ */
setExtraQuery(callback) { setExtraQuery(callback) {
this.extraQuery = callback; this.extraQuery = callback;
@@ -84,17 +85,18 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
* @returns {Promise} * @returns {Promise}
*/ */
saveUpdated(metadata: IMetadata[]) { saveUpdated(metadata: IMetadata[]) {
const updated = metadata.filter((m) => (m._markAsUpdated === true)); const updated = metadata.filter((m) => m._markAsUpdated === true);
const opers = []; const opers = [];
updated.forEach((meta) => { updated.forEach((meta) => {
const updateOper = this.repository.update({ const updateOper = this.repository
[this.VALUE_COLUMN]: meta.value, .update(
}, { { [this.VALUE_COLUMN]: meta.value },
...this.extraQuery(meta), { ...this.extraQuery(meta) }
}).then(() => { )
meta._markAsUpdated = false; .then(() => {
}); meta._markAsUpdated = false;
});
opers.push(updateOper); opers.push(updateOper);
}); });
return Promise.all(opers); return Promise.all(opers);
@@ -106,16 +108,20 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
* @returns {Promise} * @returns {Promise}
*/ */
saveDeleted(metadata: IMetadata[]) { saveDeleted(metadata: IMetadata[]) {
const deleted = metadata.filter((m: IMetadata) => (m._markAsDeleted === true)); const deleted = metadata.filter(
(m: IMetadata) => m._markAsDeleted === true
);
const opers: Promise<void> = []; const opers: Promise<void> = [];
if (deleted.length > 0) { if (deleted.length > 0) {
deleted.forEach((meta) => { deleted.forEach((meta) => {
const deleteOper = this.repository.deleteBy({ const deleteOper = this.repository
...this.extraQuery(meta), .deleteBy({
}).then(() => { ...this.extraQuery(meta),
meta._markAsDeleted = false; })
}); .then(() => {
meta._markAsDeleted = false;
});
opers.push(deleteOper); opers.push(deleteOper);
}); });
} }
@@ -128,7 +134,9 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
* @returns {Promise} * @returns {Promise}
*/ */
saveInserted(metadata: IMetadata[]) { saveInserted(metadata: IMetadata[]) {
const inserted = metadata.filter((m: IMetadata) => (m._markAsInserted === true)); const inserted = metadata.filter(
(m: IMetadata) => m._markAsInserted === true
);
const opers: Promise<void> = []; const opers: Promise<void> = [];
inserted.forEach((meta) => { inserted.forEach((meta) => {
@@ -137,10 +145,9 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
[this.VALUE_COLUMN]: meta.value, [this.VALUE_COLUMN]: meta.value,
...this.transfromMetaExtraColumns(meta), ...this.transfromMetaExtraColumns(meta),
}; };
const insertOper = this.repository.create(insertData) const insertOper = this.repository.create(insertData).then(() => {
.then(() => { meta._markAsInserted = false;
meta._markAsInserted = false; });
});
opers.push(insertOper); opers.push(insertOper);
}); });
return Promise.all(opers); 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|Number|Boolean} value -
* @param {String} valueType - * @param {String} valueType -
* @return {String|Number|Boolean} - * @return {String|Number|Boolean} -
*/ */
static formatMetaValue(value: string|number|boolean, valueType: string|false) { static parseMetaValue(
let parsedValue: string|number|boolean; value: string,
valueType: string | false
): string | boolean | number {
let parsedValue: string | number | boolean;
switch (valueType) { switch (valueType) {
case 'number': case 'number':
parsedValue = `${value}`; parsedValue = parseFloat(value);
break; break;
case 'boolean': case 'boolean':
parsedValue = value ? '1' : '0'; parsedValue = parseBoolean(value, false);
break; break;
case 'json': case 'json':
parsedValue = JSON.stringify(parsedValue); parsedValue = JSON.stringify(parsedValue);
@@ -195,11 +205,15 @@ export default class MetableDBStore extends MetableStore implements IMetableStor
* @param {String} parseType - * @param {String} parseType -
*/ */
mapMetadata(metadata: IMetadata) { mapMetadata(metadata: IMetadata) {
const metaType = this.config.getMetaType(
metadata[this.KEY_COLUMN],
metadata['group'],
);
return { return {
key: metadata[this.KEY_COLUMN], key: metadata[this.KEY_COLUMN],
value: MetableDBStore.formatMetaValue( value: MetableDBStore.parseMetaValue(
metadata[this.VALUE_COLUMN], metadata[this.VALUE_COLUMN],
this.TYPE_COLUMN ? metadata[this.TYPE_COLUMN] : false, metaType
), ),
...this.extraColumns.reduce((obj, extraCol: string) => { ...this.extraColumns.reduce((obj, extraCol: string) => {
obj[extraCol] = metadata[extraCol] || null; 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. * Throw error in case the store is not loaded yet.
*/ */
private validateStoreIsLoaded() { private validateStoreIsLoaded() {
if (!this.loaded) { if (!this.loaded) {
throw new Error('You could not save the store before loaded from the storage.'); throw new Error(
'You could not save the store before loaded from the storage.'
);
} }
} }
} }

View File

@@ -20,7 +20,7 @@ export default class Contact extends TenantModel {
* Defined virtual attributes. * Defined virtual attributes.
*/ */
static get virtualAttributes() { 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() { get contactNormal() {
return Contact.getContactNormalByType(this.contactService); return Contact.getContactNormalByType(this.contactService);
} }
/**
* Retrieve formatted contact service.
*/
get formattedContactService() {
return Contact.getFormattedContactService(this.contactService);
}
/** /**
* Closing balance attribute. * Closing balance attribute.
*/ */
get closingBalance() { get closingBalance() {
return this.openingBalance + this.balance; return this.balance;
} }
/** /**

View File

@@ -48,7 +48,7 @@ export default class Customer extends TenantModel {
* Closing balance attribute. * Closing balance attribute.
*/ */
get closingBalance() { get closingBalance() {
return this.openingBalance + this.balance; return this.balance;
} }
/** /**

View File

@@ -47,7 +47,7 @@ export default class Vendor extends TenantModel {
* Closing balance attribute. * Closing balance attribute.
*/ */
get closingBalance() { get closingBalance() {
return this.openingBalance + this.balance; return this.balance;
} }
/** /**

View File

@@ -491,59 +491,6 @@ export default class ItemsService implements IItemsService {
return item; return item;
} }
/**
* Validates the given items IDs exists or throw not found service error.
* @param {number} tenantId -
* @param {number[]} itemsIDs -
* @return {Promise<void>}
*/
private async validateItemsIdsExists(
tenantId: number,
itemsIDs: number[]
): Promise<void> {
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. * Retrieve items datatable list.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -30,4 +30,24 @@ export default class SettingsService {
await settings.save(); 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;
}
} }