mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
Merge branch 'develop' into fix-spelling-a-char
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import composeAsync from 'async/compose';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceCreateDTO,
|
||||
ISaleInvoiceEditDTO,
|
||||
ICustomer,
|
||||
ITenantUser,
|
||||
} from '@/interfaces';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
|
||||
import { formatDateFields } from 'utils';
|
||||
|
||||
@Service()
|
||||
export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
@Inject()
|
||||
private invoiceIncrement: SaleInvoiceIncrement;
|
||||
|
||||
/**
|
||||
* Transformes the create DTO to invoice object model.
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
|
||||
* @return {ISaleInvoice}
|
||||
*/
|
||||
public async transformDTOToModel(
|
||||
tenantId: number,
|
||||
customer: ICustomer,
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
|
||||
authorizedUser: ITenantUser,
|
||||
oldSaleInvoice?: ISaleInvoice
|
||||
): Promise<ISaleInvoice> {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
|
||||
ItemEntry.calcAmount(e)
|
||||
);
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(tenantId);
|
||||
|
||||
// Invoice number.
|
||||
const invoiceNo =
|
||||
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
|
||||
|
||||
// Validate the invoice is required.
|
||||
this.validators.validateInvoiceNoRequire(invoiceNo);
|
||||
|
||||
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
|
||||
referenceType: 'SaleInvoice',
|
||||
...entry,
|
||||
}));
|
||||
const entries = await composeAsync(
|
||||
// Sets default cost and sell account to invoice items entries.
|
||||
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
|
||||
)(initialEntries);
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(
|
||||
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
|
||||
['invoiceDate', 'dueDate']
|
||||
),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
balance,
|
||||
currencyCode: customer.currencyCode,
|
||||
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
|
||||
...(saleInvoiceDTO.delivered &&
|
||||
!oldSaleInvoice?.deliveredAt && {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
}),
|
||||
// Avoid override payment amount in edit mode.
|
||||
...(!oldSaleInvoice && { paymentAmount: 0 }),
|
||||
...(invoiceNo ? { invoiceNo } : {}),
|
||||
entries,
|
||||
userId: authorizedUser.id,
|
||||
} as ISaleInvoice;
|
||||
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
||||
)(initialDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { SaleInvoice } from '@/models';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
@Service()
|
||||
export class CommandSaleInvoiceValidators {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Validates the given invoice is existance.
|
||||
* @param {SaleInvoice | undefined} invoice
|
||||
*/
|
||||
public validateInvoiceExistance(invoice: SaleInvoice | undefined) {
|
||||
if (!invoice) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice number unqiue on the storage.
|
||||
*/
|
||||
public async validateInvoiceNumberUnique(
|
||||
tenantId: number,
|
||||
invoiceNumber: string,
|
||||
notInvoiceId?: number
|
||||
) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findOne('invoice_no', invoiceNumber)
|
||||
.onBuild((builder) => {
|
||||
if (notInvoiceId) {
|
||||
builder.whereNot('id', notInvoiceId);
|
||||
}
|
||||
});
|
||||
|
||||
if (saleInvoice) {
|
||||
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the invoice amount is bigger than payment amount before edit the invoice.
|
||||
* @param {number} saleInvoiceAmount
|
||||
* @param {number} paymentAmount
|
||||
*/
|
||||
public validateInvoiceAmountBiggerPaymentAmount(
|
||||
saleInvoiceAmount: number,
|
||||
paymentAmount: number
|
||||
) {
|
||||
if (saleInvoiceAmount < paymentAmount) {
|
||||
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the invoice number require.
|
||||
* @param {ISaleInvoice} saleInvoiceObj
|
||||
*/
|
||||
public validateInvoiceNoRequire(invoiceNo: string) {
|
||||
if (!invoiceNo) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given customer has no sales invoices.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId - Customer id.
|
||||
*/
|
||||
public async validateCustomerHasNoInvoices(
|
||||
tenantId: number,
|
||||
customerId: number
|
||||
) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoices = await SaleInvoice.query().where('customer_id', customerId);
|
||||
|
||||
if (invoices.length > 0) {
|
||||
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts
Normal file
147
packages/server/src/services/Sales/Invoices/CreateSaleInvoice.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ICustomer,
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceCreateDTO,
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceCreatingPaylaod,
|
||||
ITenantUser,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer';
|
||||
import { SaleEstimateValidators } from '../Estimates/SaleEstimateValidators';
|
||||
|
||||
@Service()
|
||||
export class CreateSaleInvoice {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
@Inject()
|
||||
private transformerDTO: CommandSaleInvoiceDTOTransformer;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private commandEstimateValidators: SaleEstimateValidators;
|
||||
|
||||
/**
|
||||
* Creates a new sale invoices and store it to the storage
|
||||
* with associated to entries and journal transactions.
|
||||
* @async
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {ISaleInvoice} saleInvoiceDTO - Sale invoice object DTO.
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public createSaleInvoice = async (
|
||||
tenantId: number,
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO,
|
||||
authorizedUser: ITenantUser
|
||||
): Promise<ISaleInvoice> => {
|
||||
const { SaleInvoice, SaleEstimate, Contact } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
// Validate customer existance.
|
||||
const customer = await Contact.query()
|
||||
.modify('customer')
|
||||
.findById(saleInvoiceDTO.customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the from estimate id exists on the storage.
|
||||
if (saleInvoiceDTO.fromEstimateId) {
|
||||
const fromEstimate = await SaleEstimate.query()
|
||||
.findById(saleInvoiceDTO.fromEstimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the sale estimate is not already converted to invoice.
|
||||
this.commandEstimateValidators.validateEstimateNotConverted(fromEstimate);
|
||||
}
|
||||
// Validate items ids existance.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
// Validate items should be sellable items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
// Transform DTO object to model object.
|
||||
const saleInvoiceObj = await this.transformCreateDTOToModel(
|
||||
tenantId,
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
authorizedUser
|
||||
);
|
||||
// Validate sale invoice number uniquiness.
|
||||
if (saleInvoiceObj.invoiceNo) {
|
||||
await this.validators.validateInvoiceNumberUnique(
|
||||
tenantId,
|
||||
saleInvoiceObj.invoiceNo
|
||||
);
|
||||
}
|
||||
// Creates a new sale invoice and associated transactions under unit of work env.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
|
||||
saleInvoiceDTO,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ISaleInvoiceCreatingPaylaod);
|
||||
|
||||
// Create sale invoice graph to the storage.
|
||||
const saleInvoice = await SaleInvoice.query(trx).upsertGraph(
|
||||
saleInvoiceObj
|
||||
);
|
||||
const eventPayload: ISaleInvoiceCreatedPayload = {
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
saleInvoiceDTO,
|
||||
saleInvoiceId: saleInvoice.id,
|
||||
authorizedUser,
|
||||
trx,
|
||||
};
|
||||
// Triggers the event `onSaleInvoiceCreated`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onCreated,
|
||||
eventPayload
|
||||
);
|
||||
return saleInvoice;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes create DTO to model.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICustomer} customer -
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO -
|
||||
*/
|
||||
private transformCreateDTOToModel = async (
|
||||
tenantId: number,
|
||||
customer: ICustomer,
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO,
|
||||
authorizedUser: ITenantUser
|
||||
) => {
|
||||
return this.transformerDTO.transformDTOToModel(
|
||||
tenantId,
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
authorizedUser
|
||||
);
|
||||
};
|
||||
}
|
||||
154
packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts
Normal file
154
packages/server/src/services/Sales/Invoices/DeleteSaleInvoice.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISystemUser,
|
||||
ISaleInvoiceDeletePayload,
|
||||
ISaleInvoiceDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from './constants';
|
||||
import { UnlinkConvertedSaleEstimate } from '../Estimates/UnlinkConvertedSaleEstimate';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class DeleteSaleInvoice {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Validate the sale invoice has no payment entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
*/
|
||||
private async validateInvoiceHasNoPaymentEntries(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number
|
||||
) {
|
||||
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the sale invoice associated payment receive entries.
|
||||
const entries = await PaymentReceiveEntry.query().where(
|
||||
'invoice_id',
|
||||
saleInvoiceId
|
||||
);
|
||||
if (entries.length > 0) {
|
||||
throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sale invoice has no applied to credit note transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public validateInvoiceHasNoAppliedToCredit = async (
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<void> => {
|
||||
const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const appliedTransactions = await CreditNoteAppliedInvoice.query().where(
|
||||
'invoiceId',
|
||||
invoiceId
|
||||
);
|
||||
if (appliedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async getInvoiceOrThrowError(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number
|
||||
) {
|
||||
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const saleInvoice = await saleInvoiceRepository.findOneById(
|
||||
saleInvoiceId,
|
||||
'entries'
|
||||
);
|
||||
if (!saleInvoice) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
return saleInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given sale invoice with associated entries
|
||||
* and journal transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {Number} saleInvoiceId - The given sale invoice id.
|
||||
* @param {ISystemUser} authorizedUser -
|
||||
*/
|
||||
public async deleteSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<void> {
|
||||
const { ItemEntry, SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the given sale invoice with associated entries
|
||||
// or throw not found error.
|
||||
const oldSaleInvoice = await this.getInvoiceOrThrowError(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
// Validate the sale invoice has no associated payment entries.
|
||||
await this.validateInvoiceHasNoPaymentEntries(tenantId, saleInvoiceId);
|
||||
|
||||
// Validate the sale invoice has applied to credit note transaction.
|
||||
await this.validateInvoiceHasNoAppliedToCredit(tenantId, saleInvoiceId);
|
||||
|
||||
// Deletes sale invoice transaction and associate transactions with UOW env.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceDelete` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, {
|
||||
tenantId,
|
||||
saleInvoice: oldSaleInvoice,
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
} as ISaleInvoiceDeletePayload);
|
||||
|
||||
// Unlink the converted sale estimates from the given sale invoice.
|
||||
await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
trx
|
||||
);
|
||||
await ItemEntry.query(trx)
|
||||
.where('reference_id', saleInvoiceId)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.delete();
|
||||
|
||||
await SaleInvoice.query(trx).findById(saleInvoiceId).delete();
|
||||
|
||||
// Triggers `onSaleInvoiceDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, {
|
||||
tenantId,
|
||||
oldSaleInvoice,
|
||||
saleInvoiceId,
|
||||
authorizedUser,
|
||||
trx,
|
||||
} as ISaleInvoiceDeletedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Knex } from 'knex';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ISaleInvoiceDeliveringPayload,
|
||||
ISaleInvoiceEventDeliveredPayload,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { ERRORS } from './constants';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
|
||||
@Service()
|
||||
export class DeliverSaleInvoice {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
/**
|
||||
* Deliver the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deliverSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<void> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve details of the given sale invoice id.
|
||||
const oldSaleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
|
||||
|
||||
// Validates the given invoice existance.
|
||||
this.validators.validateInvoiceExistance(oldSaleInvoice);
|
||||
|
||||
// Throws error in case the sale invoice already published.
|
||||
if (oldSaleInvoice.isDelivered) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
|
||||
}
|
||||
// Update sale invoice transaction with assocaite transactions
|
||||
// under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceDelivering` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivering, {
|
||||
tenantId,
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceDeliveringPayload);
|
||||
|
||||
// Record the delivered at on the storage.
|
||||
const saleInvoice = await SaleInvoice.query(trx)
|
||||
.patchAndFetchById(saleInvoiceId, {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
})
|
||||
.withGraphFetched('entries');
|
||||
|
||||
// Triggers `onSaleInvoiceDelivered` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceEventDeliveredPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
165
packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts
Normal file
165
packages/server/src/services/Sales/Invoices/EditSaleInvoice.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
ICustomer,
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceEditDTO,
|
||||
ISaleInvoiceEditedPayload,
|
||||
ISaleInvoiceEditingPayload,
|
||||
ISystemUser,
|
||||
ITenantUser,
|
||||
} from '@/interfaces';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class EditSaleInvoice {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
@Inject()
|
||||
private transformerDTO: CommandSaleInvoiceDTOTransformer;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Edit the given sale invoice.
|
||||
* @async
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {Number} saleInvoiceId - Sale invoice id.
|
||||
* @param {ISaleInvoice} saleInvoice - Sale invoice DTO object.
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public async editSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
saleInvoiceDTO: ISaleInvoiceEditDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<ISaleInvoice> {
|
||||
const { SaleInvoice, Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const oldSaleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphJoined('entries');
|
||||
|
||||
// Validates the given invoice existance.
|
||||
this.validators.validateInvoiceExistance(oldSaleInvoice);
|
||||
|
||||
// Validate customer existance.
|
||||
const customer = await Contact.query()
|
||||
.findById(saleInvoiceDTO.customerId)
|
||||
.modify('customer')
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate items ids existance.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
// Validate non-sellable entries items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
// Validate the items entries existance.
|
||||
await this.itemsEntriesService.validateEntriesIdsExistance(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
'SaleInvoice',
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
// Transform DTO object to model object.
|
||||
const saleInvoiceObj = await this.tranformEditDTOToModel(
|
||||
tenantId,
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice,
|
||||
authorizedUser
|
||||
);
|
||||
// Validate sale invoice number uniquiness.
|
||||
if (saleInvoiceObj.invoiceNo) {
|
||||
await this.validators.validateInvoiceNumberUnique(
|
||||
tenantId,
|
||||
saleInvoiceObj.invoiceNo,
|
||||
saleInvoiceId
|
||||
);
|
||||
}
|
||||
// Validate the invoice amount is not smaller than the invoice payment amount.
|
||||
this.validators.validateInvoiceAmountBiggerPaymentAmount(
|
||||
saleInvoiceObj.balance,
|
||||
oldSaleInvoice.paymentAmount
|
||||
);
|
||||
// Edit sale invoice transaction in UOW envirment.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, {
|
||||
trx,
|
||||
oldSaleInvoice,
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
} as ISaleInvoiceEditingPayload);
|
||||
|
||||
// Upsert the the invoice graph to the storage.
|
||||
const saleInvoice: ISaleInvoice =
|
||||
await SaleInvoice.query().upsertGraphAndFetch({
|
||||
id: saleInvoiceId,
|
||||
...saleInvoiceObj,
|
||||
});
|
||||
// Edit event payload.
|
||||
const editEventPayload: ISaleInvoiceEditedPayload = {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
saleInvoice,
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice,
|
||||
authorizedUser,
|
||||
trx,
|
||||
};
|
||||
// Triggers `onSaleInvoiceEdited` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onEdited,
|
||||
editEventPayload
|
||||
);
|
||||
return saleInvoice;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes edit DTO to model.
|
||||
* @param {number} tennatId -
|
||||
* @param {ICustomer} customer -
|
||||
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO -
|
||||
* @param {ISaleInvoice} oldSaleInvoice
|
||||
*/
|
||||
private tranformEditDTOToModel = async (
|
||||
tenantId: number,
|
||||
customer: ICustomer,
|
||||
saleInvoiceDTO: ISaleInvoiceEditDTO,
|
||||
oldSaleInvoice: ISaleInvoice,
|
||||
authorizedUser: ITenantUser
|
||||
) => {
|
||||
return this.transformerDTO.transformDTOToModel(
|
||||
tenantId,
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
authorizedUser,
|
||||
oldSaleInvoice
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransactio
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export default class InvoicePaymentsService {
|
||||
export class GetInvoicePaymentsService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ISaleInvoice, ISystemUser } from '@/interfaces';
|
||||
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
|
||||
@Service()
|
||||
export class GetSaleInvoice {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice with associated entries.
|
||||
* @param {Number} saleInvoiceId -
|
||||
* @param {ISystemUser} authorizedUser -
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public async getSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<ISaleInvoice> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
new SaleInvoiceTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IFilterMeta,
|
||||
IPaginationMeta,
|
||||
ISaleInvoice,
|
||||
ISalesInvoicesFilter,
|
||||
} from '@/interfaces';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetSaleInvoices {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve sales invoices filterable and paginated list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async getSaleInvoices(
|
||||
tenantId: number,
|
||||
filterDTO: ISalesInvoicesFilter
|
||||
): Promise<{
|
||||
salesInvoices: ISaleInvoice[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Parses stringified filter roles.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
tenantId,
|
||||
SaleInvoice,
|
||||
filter
|
||||
);
|
||||
const { results, pagination } = await SaleInvoice.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('customer');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Retrieves the transformed sale invoices.
|
||||
const salesInvoices = await this.transformer.transform(
|
||||
tenantId,
|
||||
results,
|
||||
new SaleInvoiceTransformer()
|
||||
);
|
||||
|
||||
return {
|
||||
salesInvoices,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the sale invoice list filter DTO.
|
||||
* @param filterDTO
|
||||
* @returns
|
||||
*/
|
||||
private parseListFilterDTO(filterDTO) {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ISaleInvoice } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class GetSaleInvoicesPayable {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve due sales invoices.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
*/
|
||||
public async getPayableInvoices(
|
||||
tenantId: number,
|
||||
customerId?: number
|
||||
): Promise<ISaleInvoice> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const salesInvoices = await SaleInvoice.query().onBuild((query) => {
|
||||
query.modify('dueInvoices');
|
||||
query.modify('delivered');
|
||||
|
||||
if (customerId) {
|
||||
query.where('customer_id', customerId);
|
||||
}
|
||||
});
|
||||
return salesInvoices;
|
||||
}
|
||||
}
|
||||
@@ -88,8 +88,8 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the given invoice ledger.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getInvoiceGLedger = (
|
||||
@@ -103,7 +103,7 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL common entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {Partial<ILedgerEntry>}
|
||||
*/
|
||||
private getInvoiceGLCommonEntry = (
|
||||
@@ -131,8 +131,8 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieve receivable entry of the given invoice.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceReceivableEntry = (
|
||||
@@ -153,9 +153,9 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieve item income entry of the given invoice.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @param {IItemEntry} entry -
|
||||
* @param {number} index -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @param {IItemEntry} entry -
|
||||
* @param {number} index -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceItemEntry = R.curry(
|
||||
@@ -183,8 +183,8 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getInvoiceGLEntries = (
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { ISaleInvoice } from '@/interfaces';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
|
||||
@Service()
|
||||
export class InvoiceInventoryTransactions {
|
||||
@Inject()
|
||||
private itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
@Inject()
|
||||
private inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* Records the inventory transactions of the given sale invoice in case
|
||||
* the invoice has inventory entries only.
|
||||
*
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {SaleInvoice} saleInvoice - Sale invoice DTO.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {boolean} override - Allow to override old transactions.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async recordInventoryTranscactions(
|
||||
tenantId: number,
|
||||
saleInvoice: ISaleInvoice,
|
||||
override?: boolean,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
// Loads the inventory items entries of the given sale invoice.
|
||||
const inventoryEntries =
|
||||
await this.itemsEntriesService.filterInventoryEntries(
|
||||
tenantId,
|
||||
saleInvoice.entries
|
||||
);
|
||||
const transaction = {
|
||||
transactionId: saleInvoice.id,
|
||||
transactionType: 'SaleInvoice',
|
||||
transactionNumber: saleInvoice.invoiceNo,
|
||||
|
||||
exchangeRate: saleInvoice.exchangeRate,
|
||||
warehouseId: saleInvoice.warehouseId,
|
||||
|
||||
date: saleInvoice.invoiceDate,
|
||||
direction: 'OUT',
|
||||
entries: inventoryEntries,
|
||||
createdAt: saleInvoice.createdAt,
|
||||
};
|
||||
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
|
||||
tenantId,
|
||||
transaction,
|
||||
override,
|
||||
trx
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Reverting the inventory transactions once the invoice deleted.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} billId - Bill id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async revertInventoryTransactions(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
// Delete the inventory transaction of the given sale invoice.
|
||||
const { oldInventoryTransactions } =
|
||||
await this.inventoryService.deleteInventoryTransactions(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
'SaleInvoice',
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,11 @@ export class InvoicePaymentTransactionTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted payment date.
|
||||
* @param entry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentDate = (entry): string => {
|
||||
return this.formatDate(entry.payment.paymentDate);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import async from 'async';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { PaymentReceiveGLEntries } from '../PaymentReceives/PaymentReceiveGLEntries';
|
||||
|
||||
@Service()
|
||||
|
||||
@@ -64,7 +64,7 @@ export class SaleInvoiceCostGLEntries {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IInventoryLotCost} inventoryCostLot
|
||||
* @param {IInventoryLotCost} inventoryCostLot
|
||||
* @returns {}
|
||||
*/
|
||||
private getInvoiceCostGLCommonEntry = (
|
||||
@@ -91,7 +91,7 @@ export class SaleInvoiceCostGLEntries {
|
||||
|
||||
/**
|
||||
* Retrieves the inventory cost GL entry.
|
||||
* @param {IInventoryLotCost} inventoryLotCost
|
||||
* @param {IInventoryLotCost} inventoryLotCost
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getInventoryCostGLEntry = R.curry(
|
||||
@@ -127,10 +127,10 @@ export class SaleInvoiceCostGLEntries {
|
||||
|
||||
/**
|
||||
* Writes journal entries for given sale invoice.
|
||||
* -------
|
||||
* -----
|
||||
* - Cost of goods sold -> Debit -> YYYY
|
||||
* - Inventory assets -> Credit -> YYYY
|
||||
* --------
|
||||
*-----
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {JournalPoster} journal
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceIncrement {
|
||||
@Inject()
|
||||
private autoIncrementOrdersService: AutoIncrementOrdersService;
|
||||
|
||||
/**
|
||||
* Retrieves the next unique invoice number.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {string}
|
||||
*/
|
||||
public getNextInvoiceNumber(tenantId: number): string {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
tenantId,
|
||||
'sales_invoices'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the invoice next number.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
public incrementNextInvoiceNumber(tenantId: number) {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
'sales_invoices'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceSmsDetailsDTO,
|
||||
ISaleInvoiceSmsDetails,
|
||||
SMS_NOTIFICATION_KEY,
|
||||
InvoiceNotificationType,
|
||||
ICustomer,
|
||||
} from '@/interfaces';
|
||||
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
|
||||
import { formatSmsMessage, formatNumber } from 'utils';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import SaleNotifyBySms from '../SaleNotifyBySms';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { ERRORS } from './constants';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceNotifyBySms {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private smsNotificationsSettings: SmsNotificationsSettingsService;
|
||||
|
||||
@Inject()
|
||||
private saleSmsNotification: SaleNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
/**
|
||||
* Notify customer via sms about sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
*/
|
||||
public notifyBySms = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
invoiceNotificationType: InvoiceNotificationType
|
||||
) => {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('customer');
|
||||
|
||||
// Validates the givne invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
// Validate the customer phone number existance and number validation.
|
||||
this.saleSmsNotification.validateCustomerPhoneNumber(
|
||||
saleInvoice.customer.personalPhone
|
||||
);
|
||||
// Transformes the invoice notification key to sms notification key.
|
||||
const notificationKey = this.transformDTOKeyToNotificationKey(
|
||||
invoiceNotificationType
|
||||
);
|
||||
// Triggers `onSaleInvoiceNotifySms` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onNotifySms, {
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
});
|
||||
// Formattes the sms message and sends sms notification.
|
||||
await this.sendSmsNotification(tenantId, notificationKey, saleInvoice);
|
||||
|
||||
// Triggers `onSaleInvoiceNotifySms` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onNotifiedSms, {
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
});
|
||||
return saleInvoice;
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify invoice details by sms notification after invoice creation.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public notifyDetailsBySmsAfterCreation = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number
|
||||
): Promise<void> => {
|
||||
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
tenantId,
|
||||
SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
|
||||
);
|
||||
// Can't continue if the sms auto-notification is not enabled.
|
||||
if (!notification.isNotificationEnabled) return;
|
||||
|
||||
await this.notifyBySms(tenantId, saleInvoiceId, 'details');
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends SMS notification.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @param {ICustomer} customer
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private sendSmsNotification = async (
|
||||
tenantId: number,
|
||||
notificationType:
|
||||
| SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
|
||||
| SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
|
||||
invoice: ISaleInvoice & { customer: ICustomer }
|
||||
): Promise<void> => {
|
||||
const smsClient = this.tenancy.smsClient(tenantId);
|
||||
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Formates the given sms message.
|
||||
const message = this.formattedInvoiceDetailsMessage(
|
||||
tenantId,
|
||||
notificationType,
|
||||
invoice,
|
||||
tenantMetadata
|
||||
);
|
||||
const phoneNumber = invoice.customer.personalPhone;
|
||||
|
||||
// Run the send sms notification message job.
|
||||
await smsClient.sendMessageJob(phoneNumber, message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formates the invoice details sms message.
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @param {ICustomer} customer
|
||||
* @returns {string}
|
||||
*/
|
||||
private formattedInvoiceDetailsMessage = (
|
||||
tenantId: number,
|
||||
notificationKey:
|
||||
| SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
|
||||
| SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
|
||||
invoice: ISaleInvoice,
|
||||
tenantMetadata: TenantMetadata
|
||||
): string => {
|
||||
const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
tenantId,
|
||||
notificationKey
|
||||
);
|
||||
return this.formatInvoiceDetailsMessage(
|
||||
notification.smsMessage,
|
||||
invoice,
|
||||
tenantMetadata
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formattees the given invoice details sms message.
|
||||
* @param {string} smsMessage
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @param {ICustomer} customer
|
||||
* @param {TenantMetadata} tenantMetadata
|
||||
*/
|
||||
private formatInvoiceDetailsMessage = (
|
||||
smsMessage: string,
|
||||
invoice: ISaleInvoice & { customer: ICustomer },
|
||||
tenantMetadata: TenantMetadata
|
||||
) => {
|
||||
const formattedDueAmount = formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
const formattedAmount = formatNumber(invoice.balance, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
|
||||
return formatSmsMessage(smsMessage, {
|
||||
InvoiceNumber: invoice.invoiceNo,
|
||||
ReferenceNumber: invoice.referenceNo,
|
||||
CustomerName: invoice.customer.displayName,
|
||||
DueAmount: formattedDueAmount,
|
||||
DueDate: moment(invoice.dueDate).format('YYYY/MM/DD'),
|
||||
Amount: formattedAmount,
|
||||
CompanyName: tenantMetadata.name,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the SMS details of the given invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
*/
|
||||
public smsDetails = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO
|
||||
): Promise<ISaleInvoiceSmsDetails> => {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('customer');
|
||||
|
||||
// Validates the sale invoice existance.
|
||||
this.validateSaleInvoiceExistance(saleInvoice);
|
||||
|
||||
// Current tenant metadata.
|
||||
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Transformes the invoice notification key to sms notification key.
|
||||
const notificationKey = this.transformDTOKeyToNotificationKey(
|
||||
invoiceSmsDetailsDTO.notificationKey
|
||||
);
|
||||
// Formates the given sms message.
|
||||
const smsMessage = this.formattedInvoiceDetailsMessage(
|
||||
tenantId,
|
||||
notificationKey,
|
||||
saleInvoice,
|
||||
tenantMetadata
|
||||
);
|
||||
|
||||
return {
|
||||
customerName: saleInvoice.customer.displayName,
|
||||
customerPhoneNumber: saleInvoice.customer.personalPhone,
|
||||
smsMessage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the invoice notification key DTO to notification key.
|
||||
* @param {string} invoiceNotifKey
|
||||
* @returns {SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
|
||||
* | SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER}
|
||||
*/
|
||||
private transformDTOKeyToNotificationKey = (
|
||||
invoiceNotifKey: string
|
||||
):
|
||||
| SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
|
||||
| SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER => {
|
||||
const invoiceNotifKeyPairs = {
|
||||
details: SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS,
|
||||
reminder: SMS_NOTIFICATION_KEY.SALE_INVOICE_REMINDER,
|
||||
};
|
||||
return (
|
||||
invoiceNotifKeyPairs[invoiceNotifKey] ||
|
||||
SMS_NOTIFICATION_KEY.SALE_INVOICE_DETAILS
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the sale invoice existance.
|
||||
* @param {ISaleInvoice|null} saleInvoice
|
||||
*/
|
||||
private validateSaleInvoiceExistance(saleInvoice: ISaleInvoice | null) {
|
||||
if (!saleInvoice) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
pdfService: PdfService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
*/
|
||||
async saleInvoicePdf(tenantId: number, saleInvoice) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/invoice-regular', {
|
||||
organization,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
saleInvoice,
|
||||
...i18n,
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export class SaleInvoiceTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedInvoiceDate',
|
||||
'formattedDueDate',
|
||||
'formattedAmount',
|
||||
'formattedDueAmount',
|
||||
'formattedPaymentAmount',
|
||||
'formattedBalanceAmount',
|
||||
'formattedExchangeRate',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice date.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedInvoiceDate = (invoice): string => {
|
||||
return this.formatDate(invoice.invoiceDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted due date.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDueDate = (invoice): string => {
|
||||
return this.formatDate(invoice.dueDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (invoice): string => {
|
||||
return formatNumber(invoice.balance, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice due amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDueAmount = (invoice): string => {
|
||||
return formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted payment amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentAmount = (invoice): string => {
|
||||
return formatNumber(invoice.paymentAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted invoice balance.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedBalanceAmount = (invoice): string => {
|
||||
return formatNumber(invoice.balanceAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted exchange rate.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedExchangeRate = (invoice): string => {
|
||||
return formatNumber(invoice.exchangeRate, { money: false });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Service } from 'typedi';
|
||||
import { ISaleInvoice, AccountNormal, ILedgerEntry, ILedger } from '@/interfaces';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceWriteoffGLEntries {
|
||||
/**
|
||||
* Retrieves the invoice write-off common GL entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
*/
|
||||
private getInvoiceWriteoffGLCommonEntry = (saleInvoice: ISaleInvoice) => {
|
||||
return {
|
||||
date: saleInvoice.invoiceDate,
|
||||
|
||||
currencyCode: saleInvoice.currencyCode,
|
||||
exchangeRate: saleInvoice.exchangeRate,
|
||||
|
||||
transactionId: saleInvoice.id,
|
||||
transactionType: 'InvoiceWriteOff',
|
||||
transactionNumber: saleInvoice.invoiceNo,
|
||||
|
||||
referenceNo: saleInvoice.referenceNo,
|
||||
branchId: saleInvoice.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off receiveable GL entry.
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceWriteoffGLReceivableEntry = (
|
||||
ARAccountId: number,
|
||||
saleInvoice: ISaleInvoice
|
||||
): ILedgerEntry => {
|
||||
const commontEntry = this.getInvoiceWriteoffGLCommonEntry(saleInvoice);
|
||||
|
||||
return {
|
||||
...commontEntry,
|
||||
credit: saleInvoice.localWrittenoffAmount,
|
||||
accountId: ARAccountId,
|
||||
contactId: saleInvoice.customerId,
|
||||
debit: 0,
|
||||
index: 1,
|
||||
indexGroup: 300,
|
||||
accountNormal: saleInvoice.writtenoffExpenseAccount.accountNormal,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off expense GL entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceWriteoffGLExpenseEntry = (
|
||||
saleInvoice: ISaleInvoice
|
||||
): ILedgerEntry => {
|
||||
const commontEntry = this.getInvoiceWriteoffGLCommonEntry(saleInvoice);
|
||||
|
||||
return {
|
||||
...commontEntry,
|
||||
debit: saleInvoice.localWrittenoffAmount,
|
||||
accountId: saleInvoice.writtenoffExpenseAccountId,
|
||||
credit: 0,
|
||||
index: 2,
|
||||
indexGroup: 300,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off GL entries.
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getInvoiceWriteoffGLEntries = (
|
||||
ARAccountId: number,
|
||||
saleInvoice: ISaleInvoice
|
||||
): ILedgerEntry[] => {
|
||||
const creditEntry = this.getInvoiceWriteoffGLExpenseEntry(saleInvoice);
|
||||
const debitEntry = this.getInvoiceWriteoffGLReceivableEntry(
|
||||
ARAccountId,
|
||||
saleInvoice
|
||||
);
|
||||
return [debitEntry, creditEntry];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off ledger.
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
public getInvoiceWriteoffLedger = (
|
||||
ARAccountId: number,
|
||||
saleInvoice: ISaleInvoice
|
||||
): ILedger => {
|
||||
const entries = this.getInvoiceWriteoffGLEntries(ARAccountId, saleInvoice);
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Knex } from 'knex';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { SaleInvoiceWriteoffGLEntries } from './SaleInvoiceWriteoffGLEntries';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceWriteoffGLStorage {
|
||||
@Inject()
|
||||
private invoiceWriteoffLedger: SaleInvoiceWriteoffGLEntries;
|
||||
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Writes the invoice write-off GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public writeInvoiceWriteoffEntries = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Retrieves the sale invoice.
|
||||
const saleInvoice = await SaleInvoice.query(trx)
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('writtenoffExpenseAccount');
|
||||
|
||||
// Find or create the A/R account.
|
||||
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode,
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Retrieves the invoice write-off ledger.
|
||||
const ledger = this.invoiceWriteoffLedger.getInvoiceWriteoffLedger(
|
||||
ARAccount.id,
|
||||
saleInvoice
|
||||
);
|
||||
return this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the invoice write-off GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transactio} actiontrx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public rewriteInvoiceWriteoffEntries = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
await this.revertInvoiceWriteoffEntries(tenantId, saleInvoiceId, trx);
|
||||
|
||||
await this.writeInvoiceWriteoffEntries(tenantId, saleInvoiceId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the invoice write-off GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public revertInvoiceWriteoffEntries = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
'InvoiceWriteOff',
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ISaleInvoiceWriteoffCreatePayload,
|
||||
ISaleInvoiceWrittenOffCanceledPayload,
|
||||
} from '@/interfaces';
|
||||
import { SaleInvoiceWriteoffGLStorage } from './SaleInvoiceWriteoffGLStorage';
|
||||
|
||||
@Service()
|
||||
export default class SaleInvoiceWriteoffSubscriber {
|
||||
@Inject()
|
||||
writeGLStorage: SaleInvoiceWriteoffGLStorage;
|
||||
|
||||
/**
|
||||
* Attaches events.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onWrittenoff,
|
||||
this.writeJournalEntriesOnceWriteoffCreate
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onWrittenoffCanceled,
|
||||
this.revertJournalEntriesOnce
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Write the written-off sale invoice journal entries.
|
||||
* @param {ISaleInvoiceWriteoffCreatePayload}
|
||||
*/
|
||||
private writeJournalEntriesOnceWriteoffCreate = async ({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceWriteoffCreatePayload) => {
|
||||
await this.writeGLStorage.writeInvoiceWriteoffEntries(
|
||||
tenantId,
|
||||
saleInvoice.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the written-of sale invoice jounral entries.
|
||||
* @param {ISaleInvoiceWrittenOffCanceledPayload}
|
||||
*/
|
||||
private revertJournalEntriesOnce = async ({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceWrittenOffCanceledPayload) => {
|
||||
await this.writeGLStorage.revertInvoiceWriteoffEntries(
|
||||
tenantId,
|
||||
saleInvoice.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import {
|
||||
IFilterMeta,
|
||||
IPaginationMeta,
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceCreateDTO,
|
||||
ISaleInvoiceEditDTO,
|
||||
ISaleInvoiceSmsDetails,
|
||||
ISaleInvoiceSmsDetailsDTO,
|
||||
ISaleInvoiceWriteoffDTO,
|
||||
ISalesInvoicesFilter,
|
||||
ISystemUser,
|
||||
ITenantUser,
|
||||
InvoiceNotificationType,
|
||||
} from '@/interfaces';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { CreateSaleInvoice } from './CreateSaleInvoice';
|
||||
import { DeleteSaleInvoice } from './DeleteSaleInvoice';
|
||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||
import { EditSaleInvoice } from './EditSaleInvoice';
|
||||
import { GetSaleInvoices } from './GetSaleInvoices';
|
||||
import { DeliverSaleInvoice } from './DeliverSaleInvoice';
|
||||
import { GetSaleInvoicesPayable } from './GetSaleInvoicesPayable';
|
||||
import { WriteoffSaleInvoice } from './WriteoffSaleInvoice';
|
||||
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
||||
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
|
||||
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceApplication {
|
||||
@Inject()
|
||||
private createSaleInvoiceService: CreateSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private deleteSaleInvoiceService: DeleteSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoiceService: GetSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoicesService: GetSaleInvoices;
|
||||
|
||||
@Inject()
|
||||
private editSaleInvoiceService: EditSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private deliverSaleInvoiceService: DeliverSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private getReceivableSaleInvoicesService: GetSaleInvoicesPayable;
|
||||
|
||||
@Inject()
|
||||
private writeoffInvoiceService: WriteoffSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private getInvoicePaymentsService: GetInvoicePaymentsService;
|
||||
|
||||
@Inject()
|
||||
private pdfSaleInvoiceService: SaleInvoicePdf;
|
||||
|
||||
@Inject()
|
||||
private invoiceSms: SaleInvoiceNotifyBySms;
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice with associated GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO
|
||||
* @param {ITenantUser} authorizedUser
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public createSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO,
|
||||
authorizedUser: ITenantUser
|
||||
): Promise<ISaleInvoice> {
|
||||
return this.createSaleInvoiceService.createSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
authorizedUser
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given sale invoice with associated GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public editSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
saleInvoiceDTO: ISaleInvoiceEditDTO,
|
||||
authorizedUser: ISystemUser
|
||||
) {
|
||||
return this.editSaleInvoiceService.editSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
saleInvoiceDTO,
|
||||
authorizedUser
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given sale invoice with given associated GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<void> {
|
||||
return this.deleteSaleInvoiceService.deleteSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
authorizedUser
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given sale invoice details.
|
||||
* @param {number} tenantId
|
||||
* @param {ISalesInvoicesFilter} filterDTO
|
||||
* @returns
|
||||
*/
|
||||
public getSaleInvoices(
|
||||
tenantId: number,
|
||||
filterDTO: ISalesInvoicesFilter
|
||||
): Promise<{
|
||||
salesInvoices: ISaleInvoice[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
return this.getSaleInvoicesService.getSaleInvoices(tenantId, filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice details.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} saleInvoiceId -
|
||||
* @param {ISystemUser} authorizedUser -
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public getSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
) {
|
||||
return this.getSaleInvoiceService.getSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
authorizedUser
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given sale invoice as delivered.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {}
|
||||
*/
|
||||
public deliverSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
) {
|
||||
return this.deliverSaleInvoiceService.deliverSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
authorizedUser
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the receivable sale invoices of the given customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @returns
|
||||
*/
|
||||
public getReceivableSaleInvoices(tenantId: number, customerId?: number) {
|
||||
return this.getReceivableSaleInvoicesService.getPayableInvoices(
|
||||
tenantId,
|
||||
customerId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes-off the sale invoice on bad debt expense account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public writeOff = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
writeoffDTO: ISaleInvoiceWriteoffDTO
|
||||
): Promise<ISaleInvoice> => {
|
||||
return this.writeoffInvoiceService.writeOff(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
writeoffDTO
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels the written-off sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public cancelWrittenoff = (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number
|
||||
): Promise<ISaleInvoice> => {
|
||||
return this.writeoffInvoiceService.cancelWrittenoff(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the invoice assocaited payments transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
*/
|
||||
public getInvoicePayments = async (tenantId: number, invoiceId: number) => {
|
||||
return this.getInvoicePaymentsService.getInvoicePayments(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId ]
|
||||
* @param saleInvoice
|
||||
* @returns
|
||||
*/
|
||||
public saleInvoicePdf(tenantId: number, saleInvoice) {
|
||||
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {InvoiceNotificationType} invoiceNotificationType
|
||||
*/
|
||||
public notifySaleInvoiceBySms = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
invoiceNotificationType: InvoiceNotificationType
|
||||
) => {
|
||||
return this.invoiceSms.notifyBySms(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
invoiceNotificationType
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the SMS details of the given invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
*/
|
||||
public getSaleInvoiceSmsDetails = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
invoiceSmsDetailsDTO: ISaleInvoiceSmsDetailsDTO
|
||||
): Promise<ISaleInvoiceSmsDetails> => {
|
||||
return this.invoiceSms.smsDetails(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
invoiceSmsDetailsDTO
|
||||
);
|
||||
};
|
||||
}
|
||||
146
packages/server/src/services/Sales/Invoices/SalesInvoicesCost.ts
Normal file
146
packages/server/src/services/Sales/Invoices/SalesInvoicesCost.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import { chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Knex } from 'knex';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import {
|
||||
IInventoryCostLotsGLEntriesWriteEvent,
|
||||
IInventoryTransaction,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicesCost {
|
||||
@Inject()
|
||||
private inventoryService: InventoryService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Schedule sale invoice re-compute based on the item
|
||||
* cost method and starting date.
|
||||
* @param {number[]} itemIds - Inventory items ids.
|
||||
* @param {Date} startingDate - Starting compute cost date.
|
||||
* @return {Promise<Agenda>}
|
||||
*/
|
||||
async scheduleComputeCostByItemsIds(
|
||||
tenantId: number,
|
||||
inventoryItemsIds: number[],
|
||||
startingDate: Date
|
||||
): Promise<void> {
|
||||
const asyncOpers: Promise<[]>[] = [];
|
||||
|
||||
inventoryItemsIds.forEach((inventoryItemId: number) => {
|
||||
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
|
||||
tenantId,
|
||||
inventoryItemId,
|
||||
startingDate
|
||||
);
|
||||
asyncOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...asyncOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the max dated inventory transactions in the transactions that
|
||||
* have the same item id.
|
||||
* @param {IInventoryTransaction[]} inventoryTransactions
|
||||
* @return {IInventoryTransaction[]}
|
||||
*/
|
||||
getMaxDateInventoryTransactions(
|
||||
inventoryTransactions: IInventoryTransaction[]
|
||||
): IInventoryTransaction[] {
|
||||
return chain(inventoryTransactions)
|
||||
.reduce((acc: any, transaction) => {
|
||||
const compatatorDate = acc[transaction.itemId];
|
||||
|
||||
if (
|
||||
!compatatorDate ||
|
||||
moment(compatatorDate.date).isBefore(transaction.date)
|
||||
) {
|
||||
return {
|
||||
...acc,
|
||||
[transaction.itemId]: {
|
||||
...transaction,
|
||||
},
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
.values()
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes items costs by the given inventory transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {IInventoryTransaction[]} inventoryTransactions
|
||||
*/
|
||||
async computeItemsCostByInventoryTransactions(
|
||||
tenantId: number,
|
||||
inventoryTransactions: IInventoryTransaction[]
|
||||
) {
|
||||
const asyncOpers: Promise<[]>[] = [];
|
||||
const reducedTransactions = this.getMaxDateInventoryTransactions(
|
||||
inventoryTransactions
|
||||
);
|
||||
reducedTransactions.forEach((transaction) => {
|
||||
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
|
||||
tenantId,
|
||||
transaction.itemId,
|
||||
transaction.date
|
||||
);
|
||||
asyncOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...asyncOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule writing journal entries.
|
||||
* @param {Date} startingDate
|
||||
* @return {Promise<agenda>}
|
||||
*/
|
||||
scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) {
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
|
||||
startingDate,
|
||||
tenantId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes cost GL entries from the inventory cost lots.
|
||||
* @param {number} tenantId -
|
||||
* @param {Date} startingDate -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public writeCostLotsGLEntries = (tenantId: number, startingDate: Date) => {
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers event `onInventoryCostLotsGLEntriesBeforeWrite`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onCostLotsGLEntriesBeforeWrite,
|
||||
{
|
||||
tenantId,
|
||||
startingDate,
|
||||
trx,
|
||||
} as IInventoryCostLotsGLEntriesWriteEvent
|
||||
);
|
||||
// Triggers event `onInventoryCostLotsGLEntriesWrite`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onCostLotsGLEntriesWrite,
|
||||
{
|
||||
tenantId,
|
||||
startingDate,
|
||||
trx,
|
||||
} as IInventoryCostLotsGLEntriesWriteEvent
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceWriteoffCreatePayload,
|
||||
ISaleInvoiceWriteoffDTO,
|
||||
ISaleInvoiceWrittenOffCanceledPayload,
|
||||
ISaleInvoiceWrittenOffCancelPayload,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
@Service()
|
||||
export class WriteoffSaleInvoice {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
/**
|
||||
* Writes-off the sale invoice on bad debt expense account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public writeOff = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
writeoffDTO: ISaleInvoiceWriteoffDTO
|
||||
): Promise<ISaleInvoice> => {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
|
||||
|
||||
// Validates the given invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
// Validate the sale invoice whether already written-off.
|
||||
this.validateSaleInvoiceAlreadyWrittenoff(saleInvoice);
|
||||
|
||||
// Saves the invoice write-off transaction with associated transactions
|
||||
// under unit-of-work envirmenet.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
const eventPayload = {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
saleInvoice,
|
||||
writeoffDTO,
|
||||
trx,
|
||||
} as ISaleInvoiceWriteoffCreatePayload;
|
||||
|
||||
// Triggers `onSaleInvoiceWriteoff` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onWriteoff,
|
||||
eventPayload
|
||||
);
|
||||
// Mark the sale invoice as written-off.
|
||||
const newSaleInvoice = await SaleInvoice.query(trx)
|
||||
.patch({
|
||||
writtenoffExpenseAccountId: writeoffDTO.expenseAccountId,
|
||||
writtenoffAmount: saleInvoice.dueAmount,
|
||||
writtenoffAt: new Date(),
|
||||
})
|
||||
.findById(saleInvoiceId);
|
||||
|
||||
// Triggers `onSaleInvoiceWrittenoff` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onWrittenoff,
|
||||
eventPayload
|
||||
);
|
||||
return newSaleInvoice;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels the written-off sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public cancelWrittenoff = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number
|
||||
): Promise<ISaleInvoice> => {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
// Validate the sale invoice existance.
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId);
|
||||
|
||||
// Validate the sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
// Validate the sale invoice whether already written-off.
|
||||
this.validateSaleInvoiceNotWrittenoff(saleInvoice);
|
||||
|
||||
// Cancels the invoice written-off and removes the associated transactions.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceWrittenoffCancel` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onWrittenoffCancel,
|
||||
{
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceWrittenOffCancelPayload
|
||||
);
|
||||
// Mark the sale invoice as written-off.
|
||||
const newSaleInvoice = await SaleInvoice.query(trx)
|
||||
.patch({
|
||||
writtenoffAmount: null,
|
||||
writtenoffAt: null,
|
||||
})
|
||||
.findById(saleInvoiceId);
|
||||
|
||||
// Triggers `onSaleInvoiceWrittenoffCanceled`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onWrittenoffCanceled,
|
||||
{
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceWrittenOffCanceledPayload
|
||||
);
|
||||
return newSaleInvoice;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Should sale invoice not be written-off.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
*/
|
||||
private validateSaleInvoiceNotWrittenoff(saleInvoice: ISaleInvoice) {
|
||||
if (!saleInvoice.isWrittenoff) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_WRITTEN_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should sale invoice already written-off.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
*/
|
||||
private validateSaleInvoiceAlreadyWrittenoff(saleInvoice: ISaleInvoice) {
|
||||
if (saleInvoice.isWrittenoff) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_WRITTEN_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
packages/server/src/services/Sales/Invoices/constants.ts
Normal file
78
packages/server/src/services/Sales/Invoices/constants.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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',
|
||||
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
|
||||
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
||||
SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES:
|
||||
'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES',
|
||||
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
||||
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
|
||||
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Draft',
|
||||
slug: 'draft',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Delivered',
|
||||
slug: 'delivered',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'delivered',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Unpaid',
|
||||
slug: 'unpaid',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'unpaid' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Partially paid',
|
||||
slug: 'partially-paid',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'partially-paid',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Paid',
|
||||
slug: 'paid',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'paid' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user