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)),
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

View File

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

View File

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

View File

@@ -61,14 +61,10 @@ export interface IItemDTO {
}
export interface IItemsService {
bulkDeleteItems(tenantId: number, itemsIds: number[]): Promise<void>;
getItem(tenantId: number, itemId: number): Promise<IItem>;
deleteItem(tenantId: number, itemId: number): Promise<void>;
editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise<IItem>;
newItem(tenantId: number, itemDTO: IItemDTO): Promise<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 { 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<void> = [];
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<void> = [];
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.'
);
}
}
}

View File

@@ -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;
}
/**

View File

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

View File

@@ -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<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.
* @param {number} tenantId

View File

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