mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
800 lines
23 KiB
TypeScript
800 lines
23 KiB
TypeScript
import { Service, Inject } from 'typedi';
|
|
import { omit, sumBy } from 'lodash';
|
|
import * as R from 'ramda';
|
|
import moment from 'moment';
|
|
import { Knex } from 'knex';
|
|
import composeAsync from 'async/compose';
|
|
import {
|
|
ISaleInvoice,
|
|
ISaleInvoiceCreateDTO,
|
|
ISaleInvoiceEditDTO,
|
|
ISalesInvoicesFilter,
|
|
IPaginationMeta,
|
|
IFilterMeta,
|
|
ISystemUser,
|
|
ISalesInvoicesService,
|
|
ISaleInvoiceCreatedPayload,
|
|
ISaleInvoiceDeletePayload,
|
|
ISaleInvoiceDeletedPayload,
|
|
ISaleInvoiceEventDeliveredPayload,
|
|
ISaleInvoiceEditedPayload,
|
|
ISaleInvoiceCreatingPaylaod,
|
|
ISaleInvoiceEditingPayload,
|
|
ISaleInvoiceDeliveringPayload,
|
|
ICustomer,
|
|
ITenantUser,
|
|
} from '@/interfaces';
|
|
import events from '@/subscribers/events';
|
|
import InventoryService from '@/services/Inventory/Inventory';
|
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
|
import { formatDateFields } from 'utils';
|
|
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
|
import { ServiceError } from '@/exceptions';
|
|
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
|
import SaleEstimateService from '@/services/Sales/SalesEstimate';
|
|
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
|
|
import { ERRORS } from './constants';
|
|
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
|
|
import UnitOfWork from '@/services/UnitOfWork';
|
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
|
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
|
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
|
|
|
/**
|
|
* Sales invoices service
|
|
* @service
|
|
*/
|
|
@Service('SalesInvoices')
|
|
export default class SaleInvoicesService implements ISalesInvoicesService {
|
|
@Inject()
|
|
tenancy: TenancyService;
|
|
|
|
@Inject()
|
|
inventoryService: InventoryService;
|
|
|
|
@Inject()
|
|
itemsEntriesService: ItemsEntriesService;
|
|
|
|
@Inject('logger')
|
|
logger: any;
|
|
|
|
@Inject()
|
|
dynamicListService: DynamicListingService;
|
|
|
|
@Inject()
|
|
private saleEstimatesService: SaleEstimateService;
|
|
|
|
@Inject()
|
|
private autoIncrementOrdersService: AutoIncrementOrdersService;
|
|
|
|
@Inject()
|
|
private eventPublisher: EventPublisher;
|
|
|
|
@Inject()
|
|
private uow: UnitOfWork;
|
|
|
|
@Inject()
|
|
private branchDTOTransform: BranchTransactionDTOTransform;
|
|
|
|
@Inject()
|
|
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
|
|
|
@Inject()
|
|
private transformer: TransformerInjectable;
|
|
|
|
/**
|
|
* Validate whether sale invoice number unqiue on the storage.
|
|
*/
|
|
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 sale invoice has no payment entries.
|
|
* @param {number} tenantId
|
|
* @param {number} saleInvoiceId
|
|
*/
|
|
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 invoice amount is bigger than payment amount before edit the invoice.
|
|
* @param {number} saleInvoiceAmount
|
|
* @param {number} paymentAmount
|
|
*/
|
|
validateInvoiceAmountBiggerPaymentAmount(
|
|
saleInvoiceAmount: number,
|
|
paymentAmount: number
|
|
) {
|
|
if (saleInvoiceAmount < paymentAmount) {
|
|
throw new ServiceError(ERRORS.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate whether sale invoice exists on the storage.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {Function} next
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the next unique invoice number.
|
|
* @param {number} tenantId - Tenant id.
|
|
* @return {string}
|
|
*/
|
|
getNextInvoiceNumber(tenantId: number): string {
|
|
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
|
tenantId,
|
|
'sales_invoices'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Increment the invoice next number.
|
|
* @param {number} tenantId -
|
|
*/
|
|
incrementNextInvoiceNumber(tenantId: number) {
|
|
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
|
tenantId,
|
|
'sales_invoices'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.transformDTOToModel(
|
|
tenantId,
|
|
customer,
|
|
saleInvoiceDTO,
|
|
authorizedUser,
|
|
oldSaleInvoice
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 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.transformDTOToModel(
|
|
tenantId,
|
|
customer,
|
|
saleInvoiceDTO,
|
|
authorizedUser
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Transformes the create DTO to invoice object model.
|
|
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
|
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
|
|
* @return {ISaleInvoice}
|
|
*/
|
|
private 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.getNextInvoiceNumber(tenantId);
|
|
|
|
// Invoice number.
|
|
const invoiceNo =
|
|
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
|
|
|
|
// Validate the invoice is required.
|
|
this.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);
|
|
}
|
|
|
|
/**
|
|
* Validate the invoice number require.
|
|
* @param {ISaleInvoice} saleInvoiceObj
|
|
*/
|
|
validateInvoiceNoRequire(invoiceNo: string) {
|
|
if (!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.
|
|
* @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, 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 this.saleEstimatesService.getSaleEstimateOrThrowError(
|
|
tenantId,
|
|
saleInvoiceDTO.fromEstimateId
|
|
);
|
|
// Validate the sale estimate is not already converted to invoice.
|
|
this.saleEstimatesService.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.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;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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 this.getInvoiceOrThrowError(
|
|
tenantId,
|
|
saleInvoiceId
|
|
);
|
|
// 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.validateInvoiceNumberUnique(
|
|
tenantId,
|
|
saleInvoiceObj.invoiceNo,
|
|
saleInvoiceId
|
|
);
|
|
}
|
|
// Validate the invoice amount is not smaller than the invoice payment amount.
|
|
this.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;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 this.getInvoiceOrThrowError(
|
|
tenantId,
|
|
saleInvoiceId
|
|
);
|
|
// 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)
|
|
.where({ id: saleInvoiceId })
|
|
.update({ deliveredAt: moment().toMySqlDateTime() });
|
|
|
|
// Triggers `onSaleInvoiceDelivered` event.
|
|
await this.eventPublisher.emitAsync(events.saleInvoice.onDelivered, {
|
|
tenantId,
|
|
saleInvoiceId,
|
|
saleInvoice,
|
|
} as ISaleInvoiceEventDeliveredPayload);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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.saleEstimatesService.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);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
|
|
return this.transformer.transform(
|
|
tenantId,
|
|
saleInvoice,
|
|
new SaleInvoiceTransformer()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parses the sale invoice list filter DTO.
|
|
* @param filterDTO
|
|
* @returns
|
|
*/
|
|
private parseListFilterDTO(filterDTO) {
|
|
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
|
}
|
|
|
|
/**
|
|
* Retrieve sales invoices filterable and paginated list.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {NextFunction} next
|
|
*/
|
|
public async salesInvoicesList(
|
|
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(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
};
|
|
}
|