feat(contacts): auto-complete contacts.

feat(items): auto-complete items.
feat(resources): resource columns feat.
feat(contacts): retrieve specific contact details.
This commit is contained in:
a.bouhuolia
2021-03-03 11:35:42 +02:00
parent d51d9a5038
commit ce875ccf4e
37 changed files with 693 additions and 219 deletions

View File

@@ -16,22 +16,7 @@ import {
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
import AccountTypesUtils from 'lib/AccountTypes';
const ERRORS = {
ACCOUNT_NOT_FOUND: 'account_not_found',
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
ACCOUNT_PREDEFINED: 'account_predefined',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
PREDEFINED_ACCOUNTS: 'predefined_accounts',
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE: 'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
}
import { ERRORS } from './constants';
@Service()
export default class AccountsService {

View File

@@ -0,0 +1,16 @@
export const ERRORS = {
ACCOUNT_NOT_FOUND: 'account_not_found',
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
ACCOUNT_PREDEFINED: 'account_predefined',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
PREDEFINED_ACCOUNTS: 'predefined_accounts',
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
};

View File

@@ -3,7 +3,13 @@ import { difference, upperFirst, omit } from 'lodash';
import moment from 'moment';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import { IContact, IContactNewDTO, IContactEditDTO } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import {
IContact,
IContactNewDTO,
IContactEditDTO,
IContactsAutoCompleteFilter,
} from 'interfaces';
import JournalPoster from '../Accounting/JournalPoster';
type TContactService = 'customer' | 'vendor';
@@ -17,6 +23,9 @@ export default class ContactsService {
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger')
logger: any;
@@ -166,11 +175,40 @@ export default class ContactsService {
async getContact(
tenantId: number,
contactId: number,
contactService: TContactService
contactService?: TContactService
) {
return this.getContactByIdOrThrowError(tenantId, contactId, contactService);
}
/**
* Retrieve auto-complete contacts list.
* @param {number} tenantId -
* @param {IContactsAutoCompleteFilter} contactsFilter -
* @return {IContactAutoCompleteItem}
*/
async autocompleteContacts(
tenantId: number,
contactsFilter: IContactsAutoCompleteFilter
) {
const { Contact } = this.tenancy.models(tenantId);
// Dynamic list.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Contact,
contactsFilter,
);
// Retrieve contacts list by the given query.
const contacts = await Contact.query().onBuild((builder) => {
if (contactsFilter.keyword) {
builder.where('display_name', 'LIKE', contactsFilter.keyword);
}
dynamicList.buildQuery()(builder);
builder.limit(contactsFilter.limit);
});
return contacts;
}
/**
* Retrieve contacts or throw not found error if one of ids were not found
* on the storage.
@@ -182,7 +220,7 @@ export default class ContactsService {
async getContactsOrThrowErrorNotFound(
tenantId: number,
contactsIds: number[],
contactService: TContactService,
contactService: TContactService
) {
const { Contact } = this.tenancy.models(tenantId);
const contacts = await Contact.query()
@@ -240,10 +278,7 @@ export default class ContactsService {
journal.fromTransactions(contactsTransactions);
journal.removeEntries();
await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
await Promise.all([journal.saveBalance(), journal.deleteEntries()]);
}
/**
@@ -268,7 +303,6 @@ export default class ContactsService {
contactId,
contactService
);
// Should the opening balance date be required.
if (!contact.openingBalanceAt && !openingBalanceAt) {
throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED);

View File

@@ -173,7 +173,6 @@ export default class CustomersService {
tenantId,
customerId,
});
// Retrieve the customer of throw not found service error.
await this.getCustomerByIdOrThrowError(tenantId, customerId);
@@ -375,7 +374,6 @@ export default class CustomersService {
const salesInvoice = await saleInvoiceRepository.find({
customer_id: customerId,
});
if (salesInvoice.length > 0) {
throw new ServiceError('customer_has_invoices');
}

View File

@@ -1,4 +1,4 @@
import { Service, Inject } from "typedi";
import { Service, Inject } from 'typedi';
import validator from 'is-my-json-valid';
import { Request, Response, NextFunction } from 'express';
import { ServiceError } from 'exceptions';
@@ -33,11 +33,15 @@ export default class DynamicListService implements IDynamicListService {
/**
* Retreive custom view or throws error not found.
* @param {number} tenantId
* @param {number} viewId
* @param {number} tenantId
* @param {number} viewId
* @return {Promise<IView>}
*/
private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
private async getCustomViewOrThrowError(
tenantId: number,
viewId: number,
model: IModel
) {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.findOneById(viewId, 'roles');
@@ -49,7 +53,7 @@ export default class DynamicListService implements IDynamicListService {
/**
* Validates the sort column whether exists.
* @param {IModel} model
* @param {IModel} model
* @param {string} columnSortBy - Sort column
* @throws {ServiceError}
*/
@@ -63,12 +67,18 @@ export default class DynamicListService implements IDynamicListService {
/**
* Validates existance the fields of filter roles.
* @param {IModel} model
* @param {IFilterRole[]} filterRoles
* @param {IModel} model
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles);
private validateRolesFieldsExistance(
model: IModel,
filterRoles: IFilterRole[]
) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(
model,
filterRoles
);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
@@ -77,7 +87,7 @@ export default class DynamicListService implements IDynamicListService {
/**
* Validates filter roles schema.
* @param {IFilterRole[]} filterRoles
* @param {IFilterRole[]} filterRoles
*/
private validateFilterRolesSchema(filterRoles: IFilterRole[]) {
const validate = validator({
@@ -100,17 +110,24 @@ export default class DynamicListService implements IDynamicListService {
/**
* Dynamic listing.
* @param {number} tenantId
* @param {IModel} model
* @param {IDynamicListFilterDTO} filter
* @param {number} tenantId - Tenant id.
* @param {IModel} model - Model.
* @param {IDynamicListFilterDTO} filter - Dynamic filter DTO.
*/
public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) {
public async dynamicList(
tenantId: number,
model: IModel,
filter: IDynamicListFilterDTO
) {
const dynamicFilter = new DynamicFilter(model);
// Custom view filter roles.
if (filter.customViewId) {
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model);
const view = await this.getCustomViewOrThrowError(
tenantId,
filter.customViewId,
model
);
const viewFilter = new DynamicFilterViews(view);
dynamicFilter.setFilter(viewFilter);
}
@@ -119,7 +136,8 @@ export default class DynamicListService implements IDynamicListService {
this.validateSortColumnExistance(model, filter.columnSortBy);
const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy, filter.sortOrder
filter.columnSortBy,
filter.sortOrder
);
dynamicFilter.setFilter(sortByFilter);
}
@@ -141,12 +159,17 @@ export default class DynamicListService implements IDynamicListService {
/**
* Middleware to catch services errors
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) {
public handlerErrorsToResponse(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, {
@@ -171,4 +194,4 @@ export default class DynamicListService implements IDynamicListService {
}
next(error);
}
}
}

View File

@@ -17,6 +17,7 @@ import {
import events from 'subscribers/events';
import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import HasTenancyService from 'services/Tenancy/TenancyService';
import InventoryService from './Inventory';
@@ -45,6 +46,9 @@ export default class InventoryAdjustmentService {
@Inject()
inventoryService: InventoryService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Transformes the quick inventory adjustment DTO to model object.
* @param {IQuickInventoryAdjustmentDTO} adjustmentDTO -
@@ -208,7 +212,7 @@ export default class InventoryAdjustmentService {
await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, {
tenantId,
inventoryAdjustmentId,
oldInventoryAdjustment
oldInventoryAdjustment,
});
this.logger.info(
'[inventory_adjustment] the adjustment deleted successfully.',
@@ -275,9 +279,18 @@ export default class InventoryAdjustmentService {
}> {
const { InventoryAdjustment } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
InventoryAdjustment,
adjustmentsFilter
);
const { results, pagination } = await InventoryAdjustment.query()
.withGraphFetched('entries.item')
.withGraphFetched('adjustmentAccount')
.onBuild((query) => {
query.withGraphFetched('entries.item');
query.withGraphFetched('adjustmentAccount');
dynamicFilter.buildQuery()(query);
})
.pagination(adjustmentsFilter.page - 1, adjustmentsFilter.pageSize);
return {

View File

@@ -5,7 +5,13 @@ import {
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
import {
IItemsFilter,
IItemsService,
IItemDTO,
IItem,
IItemsAutoCompleteFilter,
} from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
@@ -16,6 +22,7 @@ import {
ACCOUNT_TYPE,
} from 'data/AccountTypes';
import { ERRORS } from './constants';
@Service()
export default class ItemsService implements IItemsService {
@Inject()
@@ -496,6 +503,34 @@ export default class ItemsService implements IItemsService {
};
}
/**
* Retrieve auto-complete items list.
* @param {number} tenantId -
* @param {IItemsAutoCompleteFilter} itemsFilter -
*/
public async autocompleteItems(
tenantId: number,
itemsFilter: IItemsAutoCompleteFilter
) {
const { Item } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Item,
itemsFilter
);
const items = await Item.query().onBuild((builder) => {
builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder);
builder.limit(itemsFilter.limit);
});
// const autocompleteItems = this.transformAutoCompleteItems(items);
return items;
}
// transformAutoCompleteItems(item)
/**
* Validates the given item or items have no associated invoices or bills.
* @param {number} tenantId - Tenant id.

View File

@@ -591,6 +591,7 @@ export default class BillPaymentsService {
.onBuild((builder) => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
dynamicFilter.buildQuery()(builder);
})
.pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize);

View File

@@ -27,17 +27,7 @@ import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService';
const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
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',
};
import { ERRORS } from './constants';
/**
* Vendor bills services.

View File

@@ -0,0 +1,10 @@
export const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
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',
};

View File

@@ -29,20 +29,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import JournalPosterService from './JournalPosterService';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
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',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
};
import { ERRORS } from './constants';
/**
* Sales invoices service

View File

@@ -0,0 +1,12 @@
export 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',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
};