mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat(invoices|receipts|estimates|payments): auto-increment backend logic based.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { transactionIncrement } from 'utils';
|
||||
import { transactionIncrement, parseBoolean } from 'utils';
|
||||
|
||||
/**
|
||||
* Auto increment orders service.
|
||||
@@ -15,38 +15,18 @@ export default class AutoIncrementOrdersService {
|
||||
* @param {number} tenantId
|
||||
* @param {string} settingsGroup
|
||||
* @param {Function} getMaxTransactionNo
|
||||
* @return {Promise<[string, string]>}
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
async getNextTransactionNumber(
|
||||
tenantId: number,
|
||||
settingsGroup: string,
|
||||
getOrderTransaction: (prefix: string, number: string) => Promise<boolean>,
|
||||
getMaxTransactionNumber: (prefix: string, number: string) => Promise<string>
|
||||
): Promise<[string, string]> {
|
||||
getNextTransactionNumber(tenantId: number, settingsGroup: string): string {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const group = settingsGroup;
|
||||
|
||||
// Settings service transaction number and prefix.
|
||||
const settingNo = settings.get({ group, key: 'next_number' });
|
||||
const settingPrefix = settings.get({ group, key: 'number_prefix' });
|
||||
const autoIncrement = settings.get({ group, key: 'auto_increment' }, false);
|
||||
const settingNo = settings.get({ group, key: 'next_number' }, '');
|
||||
const settingPrefix = settings.get({ group, key: 'number_prefix' }, '');
|
||||
|
||||
let nextInvoiceNumber = settingNo;
|
||||
|
||||
const orderTransaction = await getOrderTransaction(
|
||||
settingPrefix,
|
||||
settingNo
|
||||
);
|
||||
if (orderTransaction) {
|
||||
// Retrieve the max invoice number in the given prefix.
|
||||
const maxInvoiceNo = await getMaxTransactionNumber(
|
||||
settingPrefix,
|
||||
settingNo
|
||||
);
|
||||
if (maxInvoiceNo) {
|
||||
nextInvoiceNumber = transactionIncrement(maxInvoiceNo);
|
||||
}
|
||||
}
|
||||
return [settingPrefix, nextInvoiceNumber];
|
||||
return parseBoolean(autoIncrement, false) ? `${settingPrefix}${settingNo}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,16 +35,17 @@ export default class AutoIncrementOrdersService {
|
||||
* @param {string} orderGroup - Order group.
|
||||
* @param {string} orderNumber -Order number.
|
||||
*/
|
||||
async incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
orderGroup: string,
|
||||
orderNumber: string
|
||||
) {
|
||||
async incrementSettingsNextNumber(tenantId: number, group: string) {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const settingNo = settings.get({ group, key: 'next_number' });
|
||||
const autoIncrement = settings.get({ group, key: 'auto_increment' });
|
||||
|
||||
// Can't continue if the auto-increment of the service was disabled.
|
||||
if (!autoIncrement) return;
|
||||
|
||||
settings.set(
|
||||
{ group: orderGroup, key: 'next_number' },
|
||||
transactionIncrement(orderNumber)
|
||||
{ group, key: 'next_number' },
|
||||
transactionIncrement(settingNo)
|
||||
);
|
||||
await settings.save();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import CustomersService from 'services/Contacts/CustomersService';
|
||||
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
|
||||
import JournalCommands from 'services/Accounting/JournalCommands';
|
||||
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
|
||||
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
|
||||
|
||||
const ERRORS = {
|
||||
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
|
||||
@@ -41,6 +42,7 @@ const ERRORS = {
|
||||
INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND',
|
||||
ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS',
|
||||
INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET',
|
||||
PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED'
|
||||
};
|
||||
/**
|
||||
* Payment receive service.
|
||||
@@ -66,6 +68,9 @@ export default class PaymentReceiveService {
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
autoIncrementOrdersService: AutoIncrementOrdersService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@@ -144,7 +149,8 @@ export default class PaymentReceiveService {
|
||||
/**
|
||||
* Validates the invoices IDs existance.
|
||||
* @param {number} tenantId -
|
||||
* @param {} paymentReceiveEntries -
|
||||
* @param {number} customerId -
|
||||
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries -
|
||||
*/
|
||||
async validateInvoicesIDsExistance(
|
||||
tenantId: number,
|
||||
@@ -225,6 +231,61 @@ export default class PaymentReceiveService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next unique payment receive number.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {string}
|
||||
*/
|
||||
getNextPaymentReceiveNumber(tenantId: number): string {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
tenantId,
|
||||
'payment_receives'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the payment receive next number.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
incrementNextPaymentReceiveNumber(tenantId: number) {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
'payment_receives'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive number require.
|
||||
* @param {IPaymentReceive} paymentReceiveObj
|
||||
*/
|
||||
validatePaymentReceiveNoRequire(paymentReceiveObj: IPaymentReceive) {
|
||||
if (!paymentReceiveObj.paymentReceiveNo) {
|
||||
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve estimate number to object model.
|
||||
* @param {number} tenantId
|
||||
* @param {IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO.
|
||||
* @param {IPaymentReceive} oldPaymentReceive - Old payment model object.
|
||||
*/
|
||||
transformPaymentNumberToModel(
|
||||
tenantId: number,
|
||||
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
||||
oldPaymentReceive?: IPaymentReceive
|
||||
): string {
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId);
|
||||
|
||||
if (paymentReceiveDTO.paymentReceiveNo) {
|
||||
return paymentReceiveDTO.paymentReceiveNo;
|
||||
}
|
||||
return oldPaymentReceive
|
||||
? oldPaymentReceive.paymentReceiveNo
|
||||
: autoNextNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive entries IDs existance.
|
||||
* @param {number} tenantId
|
||||
@@ -246,7 +307,6 @@ export default class PaymentReceiveService {
|
||||
'payment_receive_id',
|
||||
paymentReceiveId
|
||||
);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
@@ -255,6 +315,36 @@ export default class PaymentReceiveService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the create payment receive DTO to model object.
|
||||
* @param {number} tenantId
|
||||
* @param {IPaymentReceiveCreateDTO} paymentReceiveDTO
|
||||
*/
|
||||
transformPaymentReceiveDTOToModel(
|
||||
tenantId: number,
|
||||
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
||||
oldPaymentReceive?: IPaymentReceive
|
||||
): IPaymentReceive {
|
||||
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||
|
||||
// Retrieve the next payment receive number.
|
||||
const paymentReceiveNo = this.transformPaymentNumberToModel(
|
||||
tenantId,
|
||||
paymentReceiveDTO,
|
||||
oldPaymentReceive
|
||||
);
|
||||
return {
|
||||
amount: paymentAmount,
|
||||
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
|
||||
'paymentDate',
|
||||
]),
|
||||
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
|
||||
entries: paymentReceiveDTO.entries.map((entry) => ({
|
||||
...omit(entry, ['id']),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new payment receive and store it to the storage
|
||||
* with associated invoices payment and journal transactions.
|
||||
@@ -268,34 +358,36 @@ export default class PaymentReceiveService {
|
||||
authorizedUser: ISystemUser
|
||||
) {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||
|
||||
// Transformes the payment receive DTO to model.
|
||||
const paymentReceiveObj = this.transformPaymentReceiveDTOToModel(
|
||||
tenantId,
|
||||
paymentReceiveDTO
|
||||
);
|
||||
// Validate payment receive is required.
|
||||
this.validatePaymentReceiveNoRequire(paymentReceiveObj);
|
||||
|
||||
// Validate payment receive number uniquiness.
|
||||
if (paymentReceiveDTO.paymentReceiveNo) {
|
||||
await this.validatePaymentReceiveNoExistance(
|
||||
tenantId,
|
||||
paymentReceiveDTO.paymentReceiveNo
|
||||
);
|
||||
}
|
||||
await this.validatePaymentReceiveNoExistance(
|
||||
tenantId,
|
||||
paymentReceiveObj.paymentReceiveNo
|
||||
);
|
||||
// Validate customer existance.
|
||||
await this.customersService.getCustomerByIdOrThrowError(
|
||||
tenantId,
|
||||
paymentReceiveDTO.customerId
|
||||
);
|
||||
|
||||
// Validate the deposit account existance and type.
|
||||
await this.getDepositAccountOrThrowError(
|
||||
tenantId,
|
||||
paymentReceiveDTO.depositAccountId
|
||||
);
|
||||
|
||||
// Validate payment receive invoices IDs existance.
|
||||
await this.validateInvoicesIDsExistance(
|
||||
tenantId,
|
||||
paymentReceiveDTO.customerId,
|
||||
paymentReceiveDTO.entries
|
||||
);
|
||||
|
||||
// Validate invoice payment amount.
|
||||
await this.validateInvoicesPaymentsAmount(
|
||||
tenantId,
|
||||
@@ -304,15 +396,9 @@ export default class PaymentReceiveService {
|
||||
|
||||
this.logger.info('[payment_receive] inserting to the storage.');
|
||||
const paymentReceive = await PaymentReceive.query().insertGraphAndFetch({
|
||||
amount: paymentAmount,
|
||||
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
|
||||
'paymentDate',
|
||||
]),
|
||||
entries: paymentReceiveDTO.entries.map((entry) => ({
|
||||
...omit(entry, ['id']),
|
||||
})),
|
||||
...paymentReceiveObj,
|
||||
});
|
||||
|
||||
// Triggers `onPaymentReceiveCreated` event.
|
||||
await this.eventDispatcher.dispatch(events.paymentReceive.onCreated, {
|
||||
tenantId,
|
||||
paymentReceive,
|
||||
@@ -349,19 +435,26 @@ export default class PaymentReceiveService {
|
||||
authorizedUser: ISystemUser
|
||||
) {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||
|
||||
this.logger.info('[payment_receive] trying to edit payment receive.', {
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
paymentReceiveDTO,
|
||||
});
|
||||
|
||||
// Validate the payment receive existance.
|
||||
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
// Transformes the payment receive DTO to model.
|
||||
const paymentReceiveObj = this.transformPaymentReceiveDTOToModel(
|
||||
tenantId,
|
||||
paymentReceiveDTO,
|
||||
oldPaymentReceive
|
||||
);
|
||||
// Validate payment receive number existance.
|
||||
this.validatePaymentReceiveNoRequire(paymentReceiveObj);
|
||||
|
||||
// Validate payment receive number uniquiness.
|
||||
if (paymentReceiveDTO.paymentReceiveNo) {
|
||||
await this.validatePaymentReceiveNoExistance(
|
||||
@@ -375,7 +468,6 @@ export default class PaymentReceiveService {
|
||||
tenantId,
|
||||
paymentReceiveDTO.depositAccountId
|
||||
);
|
||||
|
||||
// Validate the entries ids existance on payment receive type.
|
||||
await this.validateEntriesIdsExistance(
|
||||
tenantId,
|
||||
@@ -397,11 +489,7 @@ export default class PaymentReceiveService {
|
||||
// Update the payment receive transaction.
|
||||
const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({
|
||||
id: paymentReceiveId,
|
||||
amount: paymentAmount,
|
||||
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
|
||||
'paymentDate',
|
||||
]),
|
||||
entries: paymentReceiveDTO.entries,
|
||||
...paymentReceiveObj,
|
||||
});
|
||||
|
||||
await this.eventDispatcher.dispatch(events.paymentReceive.onEdited, {
|
||||
|
||||
@@ -19,6 +19,7 @@ import events from 'subscribers/events';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import CustomersService from 'services/Contacts/CustomersService';
|
||||
import moment from 'moment';
|
||||
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
|
||||
|
||||
const ERRORS = {
|
||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
@@ -30,6 +31,7 @@ const ERRORS = {
|
||||
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
|
||||
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
|
||||
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
|
||||
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -56,6 +58,9 @@ export default class SaleEstimateService {
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
@Inject()
|
||||
autoIncrementOrdersService: AutoIncrementOrdersService;
|
||||
|
||||
/**
|
||||
* Retrieve sale estimate or throw service error.
|
||||
* @param {number} tenantId
|
||||
@@ -100,7 +105,7 @@ export default class SaleEstimateService {
|
||||
|
||||
/**
|
||||
* Validates the given sale estimate not already converted to invoice.
|
||||
* @param {ISaleEstimate} saleEstimate -
|
||||
* @param {ISaleEstimate} saleEstimate -
|
||||
*/
|
||||
validateEstimateNotConverted(saleEstimate: ISaleEstimate) {
|
||||
if (saleEstimate.isConvertedToInvoice) {
|
||||
@@ -108,6 +113,49 @@ export default class SaleEstimateService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next unique estimate number.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {string}
|
||||
*/
|
||||
getNextEstimateNumber(tenantId: number): string {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
tenantId,
|
||||
'sales_estimates'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the estimate next number.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
incrementNextEstimateNumber(tenantId: number) {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
'sales_estimates'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve estimate number to object model.
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleEstimateDTO} saleEstimateDTO
|
||||
* @param {ISaleEstimate} oldSaleEstimate
|
||||
*/
|
||||
transformEstimateNumberToModel(
|
||||
tenantId: number,
|
||||
saleEstimateDTO: ISaleEstimateDTO,
|
||||
oldSaleEstimate?: ISaleEstimate
|
||||
): string {
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.getNextEstimateNumber(tenantId);
|
||||
|
||||
if (saleEstimateDTO.estimateNumber) {
|
||||
return saleEstimateDTO.estimateNumber;
|
||||
}
|
||||
return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform DTO object ot model object.
|
||||
* @param {number} tenantId
|
||||
@@ -123,17 +171,24 @@ export default class SaleEstimateService {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e));
|
||||
|
||||
// Retreive the next estimate number.
|
||||
const estimateNumber = this.transformEstimateNumberToModel(
|
||||
tenantId,
|
||||
estimateDTO,
|
||||
oldSaleEstimate
|
||||
);
|
||||
|
||||
return {
|
||||
amount,
|
||||
...formatDateFields(omit(estimateDTO, ['delivered', 'entries']), [
|
||||
'estimateDate',
|
||||
'expirationDate',
|
||||
]),
|
||||
...(estimateNumber ? { estimateNumber } : {}),
|
||||
entries: estimateDTO.entries.map((entry) => ({
|
||||
reference_type: 'SaleEstimate',
|
||||
...omit(entry, ['total', 'amount', 'id']),
|
||||
})),
|
||||
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
...(estimateDTO.delivered &&
|
||||
!oldSaleEstimate?.deliveredAt && {
|
||||
@@ -141,6 +196,16 @@ export default class SaleEstimateService {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sale estimate number require.
|
||||
* @param {ISaleEstimate} saleInvoiceObj
|
||||
*/
|
||||
validateEstimateNoRequire(saleInvoiceObj: ISaleEstimate) {
|
||||
if (!saleInvoiceObj.estimateNumber) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
@@ -160,11 +225,14 @@ export default class SaleEstimateService {
|
||||
// Transform DTO object ot model object.
|
||||
const estimateObj = this.transformDTOToModel(tenantId, estimateDTO);
|
||||
|
||||
// Validate the sale estimate number require.
|
||||
this.validateEstimateNoRequire(estimateObj);
|
||||
|
||||
// Validate estimate number uniquiness on the storage.
|
||||
if (estimateDTO.estimateNumber) {
|
||||
if (estimateObj.estimateNumber) {
|
||||
await this.validateEstimateNumberExistance(
|
||||
tenantId,
|
||||
estimateDTO.estimateNumber
|
||||
estimateObj.estimateNumber
|
||||
);
|
||||
}
|
||||
// Retrieve the given customer or throw not found service error.
|
||||
@@ -221,6 +289,9 @@ export default class SaleEstimateService {
|
||||
estimateDTO,
|
||||
oldSaleEstimate
|
||||
);
|
||||
// Validate the sale estimate number require.
|
||||
this.validateEstimateNoRequire(estimateObj);
|
||||
|
||||
// Validate estimate number uniquiness on the storage.
|
||||
if (estimateDTO.estimateNumber) {
|
||||
await this.validateEstimateNumberExistance(
|
||||
|
||||
@@ -162,28 +162,44 @@ export default class SaleInvoicesService {
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {string}
|
||||
*/
|
||||
async getNextInvoiceNumber(tenantId: number): Promise<[string, string]> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the max invoice number in the given prefix.
|
||||
const getMaxInvoicesNo = (prefix, number) => {
|
||||
return SaleInvoice.query()
|
||||
.modify('maxInvoiceNo', prefix, number)
|
||||
.then((res) => res?.invNumber);
|
||||
};
|
||||
// Retrieve the order transaction number by number.
|
||||
const getTransactionNumber = (prefix, number) => {
|
||||
return SaleInvoice.query().modify('byPrefixAndNumber', prefix, number);
|
||||
};
|
||||
|
||||
getNextInvoiceNumber(tenantId: number): string {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
tenantId,
|
||||
'sales_invoices',
|
||||
getTransactionNumber,
|
||||
getMaxInvoicesNo
|
||||
'sales_invoices'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the invoice next number.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
incrementNextInvoiceNumber(tenantId: number) {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
'sales_invoices'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve invoice number to object model.
|
||||
* @param tenantId
|
||||
* @param saleInvoiceDTO
|
||||
* @param oldSaleInvoice
|
||||
*/
|
||||
transformInvoiceNumberToModel(
|
||||
tenantId: number,
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
|
||||
oldSaleInvoice?: ISaleInvoice
|
||||
): string {
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.getNextInvoiceNumber(tenantId);
|
||||
|
||||
if (saleInvoiceDTO.invoiceNo) {
|
||||
return saleInvoiceDTO.invoiceNo;
|
||||
}
|
||||
return oldSaleInvoice ? oldSaleInvoice.invoiceNo : autoNextNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform DTO object to model object.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
@@ -192,33 +208,37 @@ export default class SaleInvoicesService {
|
||||
transformDTOToModel(
|
||||
tenantId: number,
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
|
||||
oldSaleInvoice?: ISaleInvoice,
|
||||
autoNextNumber?: [string, string] // prefix, number
|
||||
oldSaleInvoice?: ISaleInvoice
|
||||
): ISaleInvoice {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
|
||||
ItemEntry.calcAmount(e)
|
||||
);
|
||||
|
||||
const invoiceNo = this.transformInvoiceNumberToModel(
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice
|
||||
);
|
||||
return {
|
||||
...formatDateFields(
|
||||
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
|
||||
['invoiceDate', 'dueDate']
|
||||
),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
balance,
|
||||
...(saleInvoiceDTO.delivered &&
|
||||
!oldSaleInvoice?.deliveredAt && {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
}),
|
||||
balance,
|
||||
paymentAmount: 0,
|
||||
...(saleInvoiceDTO.invoiceNo || autoNextNumber
|
||||
// Avoid add payment amount in edit mode.
|
||||
...(!oldSaleInvoice
|
||||
? {
|
||||
invoiceNo: saleInvoiceDTO.invoiceNo
|
||||
? saleInvoiceDTO.invoiceNo
|
||||
: join(autoNextNumber, ''),
|
||||
paymentAmount: 0,
|
||||
}
|
||||
: {}),
|
||||
...(invoiceNo ? { invoiceNo } : {}),
|
||||
entries: saleInvoiceDTO.entries.map((entry) => ({
|
||||
referenceType: 'SaleInvoice',
|
||||
...omit(entry, ['amount', 'id']),
|
||||
@@ -226,6 +246,16 @@ export default class SaleInvoicesService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the invoice number require.
|
||||
* @param {ISaleInvoice} saleInvoiceObj
|
||||
*/
|
||||
validateInvoiceNoRequire(saleInvoiceObj: ISaleInvoice) {
|
||||
if (!saleInvoiceObj.invoiceNo) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoices and store it to the storage
|
||||
* with associated to entries and journal transactions.
|
||||
@@ -241,28 +271,25 @@ export default class SaleInvoicesService {
|
||||
): Promise<ISaleInvoice> {
|
||||
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// The next invoice number automattically or manually.
|
||||
const autoNextNumber = !saleInvoiceDTO.invoiceNo
|
||||
? await this.getNextInvoiceNumber(tenantId)
|
||||
: null;
|
||||
|
||||
// Transform DTO object to model object.
|
||||
const saleInvoiceObj = this.transformDTOToModel(
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
null,
|
||||
autoNextNumber
|
||||
null
|
||||
);
|
||||
this.validateInvoiceNoRequire(saleInvoiceObj);
|
||||
|
||||
// Validate customer existance.
|
||||
await this.customersService.getCustomerByIdOrThrowError(
|
||||
tenantId,
|
||||
saleInvoiceDTO.customerId
|
||||
);
|
||||
|
||||
// Validate sale invoice number uniquiness.
|
||||
if (saleInvoiceDTO.invoiceNo) {
|
||||
if (saleInvoiceObj.invoiceNo) {
|
||||
await this.validateInvoiceNumberUnique(
|
||||
tenantId,
|
||||
saleInvoiceDTO.invoiceNo
|
||||
saleInvoiceObj.invoiceNo
|
||||
);
|
||||
}
|
||||
// Validate the from estimate id exists on the storage.
|
||||
@@ -296,7 +323,6 @@ export default class SaleInvoicesService {
|
||||
saleInvoiceDTO,
|
||||
saleInvoiceId: saleInvoice.id,
|
||||
authorizedUser,
|
||||
autoNextNumber,
|
||||
});
|
||||
this.logger.info('[sale_invoice] successfully inserted.', {
|
||||
tenantId,
|
||||
@@ -317,11 +343,12 @@ export default class SaleInvoicesService {
|
||||
public async editSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
saleInvoiceDTO: any,
|
||||
saleInvoiceDTO: ISaleInvoiceEditDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<ISaleInvoice> {
|
||||
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const oldSaleInvoice = await this.getInvoiceOrThrowError(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
|
||||
@@ -19,6 +19,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
|
||||
import { ItemEntry } from 'models';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
|
||||
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
|
||||
|
||||
const ERRORS = {
|
||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||
@@ -26,6 +27,7 @@ const ERRORS = {
|
||||
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
|
||||
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
|
||||
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
||||
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED'
|
||||
};
|
||||
|
||||
@Service()
|
||||
@@ -51,6 +53,9 @@ export default class SalesReceiptService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
autoIncrementOrdersService: AutoIncrementOrdersService;
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt exists on the storage.
|
||||
* @param {number} tenantId -
|
||||
@@ -130,6 +135,59 @@ export default class SalesReceiptService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sale receipt number require.
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
*/
|
||||
validateReceiptNoRequire(saleReceipt: ISaleReceipt) {
|
||||
if (!saleReceipt.receiptNumber) {
|
||||
throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next unique receipt number.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {string}
|
||||
*/
|
||||
getNextReceiptNumber(tenantId: number): string {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
tenantId,
|
||||
'sales_receipts'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the receipt next number.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
incrementNextReceiptNumber(tenantId: number) {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
'sales_receipts'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve estimate number to object model.
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleReceiptDTO} saleReceiptDTO - Sale receipt DTO.
|
||||
* @param {ISaleReceipt} oldSaleReceipt - Old receipt model object.
|
||||
*/
|
||||
transformReceiptNumberToModel(
|
||||
tenantId: number,
|
||||
saleReceiptDTO: ISaleReceiptDTO,
|
||||
oldSaleReceipt?: ISaleReceipt
|
||||
): string {
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.getNextReceiptNumber(tenantId);
|
||||
|
||||
if (saleReceiptDTO.receiptNumber) {
|
||||
return saleReceiptDTO.receiptNumber;
|
||||
}
|
||||
return oldSaleReceipt ? oldSaleReceipt.receiptNumber : autoNextNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform DTO object to model object.
|
||||
* @param {ISaleReceiptDTO} saleReceiptDTO -
|
||||
@@ -137,18 +195,26 @@ export default class SalesReceiptService {
|
||||
* @returns {ISaleReceipt}
|
||||
*/
|
||||
transformObjectDTOToModel(
|
||||
tenantId: number,
|
||||
saleReceiptDTO: ISaleReceiptDTO,
|
||||
oldSaleReceipt?: ISaleReceipt
|
||||
): ISaleReceipt {
|
||||
const amount = sumBy(saleReceiptDTO.entries, (e) =>
|
||||
ItemEntry.calcAmount(e)
|
||||
);
|
||||
// Retreive the receipt number.
|
||||
const receiptNumber = this.transformReceiptNumberToModel(
|
||||
tenantId,
|
||||
saleReceiptDTO,
|
||||
oldSaleReceipt
|
||||
);
|
||||
|
||||
return {
|
||||
amount,
|
||||
...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [
|
||||
'receiptDate',
|
||||
]),
|
||||
...(receiptNumber ? { receiptNumber } : {}),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
...(saleReceiptDTO.closed &&
|
||||
!oldSaleReceipt?.closedAt && {
|
||||
@@ -174,7 +240,12 @@ export default class SalesReceiptService {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
// Transform sale receipt DTO to model.
|
||||
const saleReceiptObj = this.transformObjectDTOToModel(saleReceiptDTO);
|
||||
const saleReceiptObj = this.transformObjectDTOToModel(
|
||||
tenantId,
|
||||
saleReceiptDTO
|
||||
);
|
||||
// Validate receipt number is required.
|
||||
this.validateReceiptNoRequire(saleReceiptObj);
|
||||
|
||||
// Validate receipt deposit account existance and type.
|
||||
await this.validateReceiptDepositAccountExistance(
|
||||
@@ -238,9 +309,13 @@ export default class SalesReceiptService {
|
||||
);
|
||||
// Transform sale receipt DTO to model.
|
||||
const saleReceiptObj = this.transformObjectDTOToModel(
|
||||
tenantId,
|
||||
saleReceiptDTO,
|
||||
oldSaleReceipt
|
||||
);
|
||||
// Validate receipt number is required.
|
||||
this.validateReceiptNoRequire(saleReceiptObj);
|
||||
|
||||
// Validate receipt deposit account existance and type.
|
||||
await this.validateReceiptDepositAccountExistance(
|
||||
tenantId,
|
||||
@@ -309,7 +384,7 @@ export default class SalesReceiptService {
|
||||
await this.eventDispatcher.dispatch(events.saleReceipt.onDeleted, {
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
oldSaleReceipt
|
||||
oldSaleReceipt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,7 +538,7 @@ export default class SalesReceiptService {
|
||||
'SaleReceipt',
|
||||
saleReceipt.receiptDate,
|
||||
'OUT',
|
||||
override,
|
||||
override
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ export const ERRORS = {
|
||||
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
|
||||
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
|
||||
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
||||
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user