mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
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:
@@ -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 {
|
||||
|
||||
16
server/src/services/Accounts/constants.ts
Normal file
16
server/src/services/Accounts/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
10
server/src/services/Purchases/constants.ts
Normal file
10
server/src/services/Purchases/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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
|
||||
|
||||
12
server/src/services/Sales/constants.ts
Normal file
12
server/src/services/Sales/constants.ts
Normal 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',
|
||||
};
|
||||
Reference in New Issue
Block a user