mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ItemsEntriesService } from '../Items/ItemsEntries.service';
|
||||
import { ModelObject } from 'objection';
|
||||
import { SaleInvoice } from './models/SaleInvoice';
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceInventoryTransactions {
|
||||
constructor(
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
// private readonly 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(
|
||||
saleInvoice: ModelObject<SaleInvoice>,
|
||||
override?: boolean,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Loads the inventory items entries of the given sale invoice.
|
||||
const inventoryEntries =
|
||||
await this.itemsEntriesService.filterInventoryEntries(
|
||||
saleInvoice.entries,
|
||||
trx,
|
||||
);
|
||||
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(
|
||||
// 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(
|
||||
// saleInvoiceId,
|
||||
// 'SaleInvoice',
|
||||
// trx,
|
||||
// );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Knex } from 'knex';
|
||||
import async from 'async';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PaymentReceivedGLEntries } from '../PaymentReceived/commands/PaymentReceivedGLEntries';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { PaymentReceivedEntry } from '../PaymentReceived/models/PaymentReceivedEntry';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicePaymentsGLEntriesRewrite {
|
||||
constructor(
|
||||
private readonly paymentGLEntries: PaymentReceivedGLEntries,
|
||||
|
||||
@Inject(PaymentReceivedEntry.name)
|
||||
private readonly paymentReceivedEntryModel: TenantModelProxy<typeof PaymentReceivedEntry>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Rewrites the payment GL entries task.
|
||||
* @param {{ tenantId: number, paymentId: number, trx: Knex?.Transaction }}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public rewritePaymentsGLEntriesTask = async ({
|
||||
paymentId,
|
||||
trx,
|
||||
}) => {
|
||||
await this.paymentGLEntries.rewritePaymentGLEntries(
|
||||
paymentId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the payment GL entries of the given payments ids.
|
||||
* @param {number[]} paymentsIds
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewritePaymentsGLEntriesQueue = async (
|
||||
paymentsIds: number[],
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Initiate a new queue for accounts balance mutation.
|
||||
const rewritePaymentGL = async.queue(this.rewritePaymentsGLEntriesTask, 10);
|
||||
|
||||
paymentsIds.forEach((paymentId: number) => {
|
||||
rewritePaymentGL.push({ paymentId, trx });
|
||||
});
|
||||
if (paymentsIds.length > 0) {
|
||||
await rewritePaymentGL.drain();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the payments GL entries that associated to the given invoice.
|
||||
* @param {number} invoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @ {Promise<void>}
|
||||
*/
|
||||
public invoicePaymentsGLEntriesRewrite = async (
|
||||
invoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const invoicePaymentEntries = await this.paymentReceivedEntryModel()
|
||||
.query()
|
||||
.where('invoiceId', invoiceId);
|
||||
|
||||
const paymentsIds = invoicePaymentEntries.map((e) => e.paymentReceiveId);
|
||||
|
||||
await this.rewritePaymentsGLEntriesQueue(paymentsIds, trx);
|
||||
};
|
||||
}
|
||||
316
packages/server/src/modules/SaleInvoices/SaleInvoice.types.ts
Normal file
316
packages/server/src/modules/SaleInvoices/SaleInvoice.types.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IItemEntryDTO } from '../TransactionItemEntry/ItemEntry.types';
|
||||
import { AttachmentLinkDTO } from '../Attachments/Attachments.types';
|
||||
import { SaleInvoice } from './models/SaleInvoice';
|
||||
import { IDynamicListFilter } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import {
|
||||
CommonMailOptions,
|
||||
CommonMailOptionsDTO,
|
||||
} from '../MailNotification/MailNotification.types';
|
||||
import { TenantJobPayload } from '@/interfaces/Tenant';
|
||||
import { CreateSaleInvoiceDto, EditSaleInvoiceDto } from './dtos/SaleInvoice.dto';
|
||||
|
||||
export interface PaymentIntegrationTransactionLink {
|
||||
id: number;
|
||||
enable: true;
|
||||
paymentIntegrationId: number;
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
}
|
||||
|
||||
export interface PaymentIntegrationTransactionLinkEventPayload {
|
||||
tenantId: number;
|
||||
enable: true;
|
||||
paymentIntegrationId: number;
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
saleInvoiceId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
|
||||
tenantId: number;
|
||||
enable: true;
|
||||
paymentIntegrationId: number;
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
oldSaleInvoiceId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDTO {
|
||||
invoiceDate: Date;
|
||||
dueDate: Date;
|
||||
referenceNo: string;
|
||||
invoiceNo: string;
|
||||
customerId: number;
|
||||
exchangeRate?: number;
|
||||
invoiceMessage: string;
|
||||
termsConditions: string;
|
||||
isTaxExclusive: boolean;
|
||||
entries: IItemEntryDTO[];
|
||||
delivered: boolean;
|
||||
|
||||
warehouseId?: number | null;
|
||||
projectId?: number;
|
||||
branchId?: number | null;
|
||||
|
||||
isInclusiveTax?: boolean;
|
||||
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {
|
||||
fromEstimateId: number;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO {}
|
||||
|
||||
export interface ISalesInvoicesFilter extends IDynamicListFilter {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
searchKeyword?: string;
|
||||
filterQuery?: (q: Knex.QueryBuilder) => void;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceWriteoffDTO {
|
||||
expenseAccountId: number;
|
||||
date: Date;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export type InvoiceNotificationType = 'details' | 'reminder';
|
||||
|
||||
export interface ISaleInvoiceCreatedPayload {
|
||||
saleInvoice: SaleInvoice;
|
||||
saleInvoiceDTO: CreateSaleInvoiceDto;
|
||||
saleInvoiceId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceCreatingPaylaod {
|
||||
tenantId: number;
|
||||
saleInvoiceDTO: CreateSaleInvoiceDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceEditedPayload {
|
||||
saleInvoice: SaleInvoice;
|
||||
oldSaleInvoice: SaleInvoice;
|
||||
saleInvoiceDTO: EditSaleInvoiceDto;
|
||||
saleInvoiceId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceEditingPayload {
|
||||
saleInvoiceDTO: EditSaleInvoiceDto;
|
||||
oldSaleInvoice: SaleInvoice;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDeletePayload {
|
||||
// tenantId: number;
|
||||
oldSaleInvoice: SaleInvoice;
|
||||
saleInvoiceId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDeletingPayload {
|
||||
oldSaleInvoice: SaleInvoice;
|
||||
saleInvoiceId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDeletedPayload {
|
||||
oldSaleInvoice: SaleInvoice;
|
||||
saleInvoiceId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceWriteoffCreatePayload {
|
||||
saleInvoiceId: number;
|
||||
saleInvoice: SaleInvoice;
|
||||
writeoffDTO: ISaleInvoiceWriteoffDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceWriteoffCreatedPayload {
|
||||
saleInvoiceId: number;
|
||||
saleInvoice: SaleInvoice;
|
||||
writeoffDTO: ISaleInvoiceCreatedPayload;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceWrittenOffCancelPayload {
|
||||
saleInvoice: SaleInvoice;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceWrittenOffCanceledPayload {
|
||||
saleInvoice: SaleInvoice;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceEventDeliveredPayload {
|
||||
saleInvoiceId: number;
|
||||
saleInvoice: SaleInvoice;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDeliveringPayload {
|
||||
oldSaleInvoice: SaleInvoice;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export enum SaleInvoiceAction {
|
||||
Create = 'Create',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
Writeoff = 'Writeoff',
|
||||
NotifyBySms = 'NotifyBySms',
|
||||
}
|
||||
|
||||
export interface SaleInvoiceMailOptions extends CommonMailOptions {
|
||||
attachInvoice?: boolean;
|
||||
formatArgs?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SaleInvoiceMailState extends SaleInvoiceMailOptions {
|
||||
invoiceNo: string;
|
||||
|
||||
invoiceDate: string;
|
||||
invoiceDateFormatted: string;
|
||||
|
||||
dueDate: string;
|
||||
dueDateFormatted: string;
|
||||
|
||||
total: number;
|
||||
totalFormatted: string;
|
||||
|
||||
subtotal: number;
|
||||
subtotalFormatted: number;
|
||||
|
||||
companyName: string;
|
||||
companyLogoUri: string;
|
||||
|
||||
customerName: string;
|
||||
|
||||
// # Invoice entries
|
||||
entries?: Array<{ label: string; total: string; quantity: string | number }>;
|
||||
}
|
||||
|
||||
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
|
||||
attachInvoice?: boolean;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceNotifyPayload {
|
||||
saleInvoiceId: number;
|
||||
messageDTO: SendInvoiceMailDTO;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceMailSend {
|
||||
saleInvoiceId: number;
|
||||
messageOptions: SendInvoiceMailDTO;
|
||||
// formattedMessageOptions: SaleInvoiceMailOptions;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceMailSent {
|
||||
saleInvoiceId: number;
|
||||
messageOptions: SendInvoiceMailDTO;
|
||||
}
|
||||
|
||||
// Invoice Pdf Document
|
||||
export interface InvoicePdfLine {
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface InvoicePdfTax {
|
||||
label: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface InvoicePdfTemplateAttributes {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
|
||||
companyName: string;
|
||||
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
|
||||
dueDate: string;
|
||||
dueDateLabel: string;
|
||||
showDueDate: boolean;
|
||||
|
||||
dateIssue: string;
|
||||
dateIssueLabel: string;
|
||||
showDateIssue: boolean;
|
||||
|
||||
invoiceNumberLabel: string;
|
||||
invoiceNumber: string;
|
||||
showInvoiceNumber: boolean;
|
||||
|
||||
// Customer Address
|
||||
showCustomerAddress: boolean;
|
||||
customerAddress: string;
|
||||
|
||||
// Company address
|
||||
showCompanyAddress: boolean;
|
||||
companyAddress: string;
|
||||
billedToLabel: string;
|
||||
|
||||
lineItemLabel: string;
|
||||
lineDescriptionLabel: string;
|
||||
lineRateLabel: string;
|
||||
lineTotalLabel: string;
|
||||
|
||||
totalLabel: string;
|
||||
subtotalLabel: string;
|
||||
discountLabel: string;
|
||||
paymentMadeLabel: string;
|
||||
|
||||
showTotal: boolean;
|
||||
showSubtotal: boolean;
|
||||
showDiscount: boolean;
|
||||
showTaxes: boolean;
|
||||
showPaymentMade: boolean;
|
||||
|
||||
total: string;
|
||||
subtotal: string;
|
||||
discount: string;
|
||||
paymentMade: string;
|
||||
|
||||
// Due Amount
|
||||
dueAmount: string;
|
||||
showDueAmount: boolean;
|
||||
dueAmountLabel: string;
|
||||
|
||||
termsConditionsLabel: string;
|
||||
showTermsConditions: boolean;
|
||||
termsConditions: string;
|
||||
|
||||
lines: InvoicePdfLine[];
|
||||
taxes: InvoicePdfTax[];
|
||||
|
||||
statementLabel: string;
|
||||
showStatement: boolean;
|
||||
statement: string;
|
||||
}
|
||||
|
||||
export interface ISaleInvocieState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
|
||||
export interface SaleInvoiceSendMailData {
|
||||
saleInvoiceId: number;
|
||||
messageOptions: SendInvoiceMailDTO;
|
||||
}
|
||||
|
||||
export interface SendSaleInvoiceMailJobPayload extends TenantJobPayload {
|
||||
messageOptions: any;
|
||||
saleInvoiceId: number;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { InventoryCostLotTracker } from '../InventoryCost/models/InventoryCostLotTracker';
|
||||
import { LedgerStorageService } from '../Ledger/LedgerStorage.service';
|
||||
import { groupInventoryTransactionsByTypeId } from '../InventoryCost/utils';
|
||||
import { Ledger } from '../Ledger/Ledger';
|
||||
import { AccountNormal } from '@/interfaces/Account';
|
||||
import { ILedgerEntry } from '../Ledger/types/Ledger.types';
|
||||
import { increment } from '@/utils/increment';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceCostGLEntries {
|
||||
constructor(
|
||||
private readonly ledgerStorage: LedgerStorageService,
|
||||
|
||||
@Inject(InventoryCostLotTracker.name)
|
||||
private readonly inventoryCostLotTracker: TenantModelProxy<
|
||||
typeof InventoryCostLotTracker
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes journal entries from sales invoices.
|
||||
* @param {Date} startingDate - Starting date.
|
||||
* @param {boolean} override
|
||||
*/
|
||||
public writeInventoryCostJournalEntries = async (
|
||||
startingDate: Date,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
const inventoryCostLotTrans = await this.inventoryCostLotTracker()
|
||||
.query()
|
||||
.where('direction', 'OUT')
|
||||
.where('transaction_type', 'SaleInvoice')
|
||||
.where('cost', '>', 0)
|
||||
.modify('filterDateRange', startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.withGraphFetched('invoice')
|
||||
.withGraphFetched('item');
|
||||
|
||||
const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
|
||||
|
||||
// Commit the ledger to the storage.
|
||||
await this.ledgerStorage.commit(ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the inventory cost lots ledger.
|
||||
* @param {IInventoryLotCost[]} inventoryCostLots
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
private getInventoryCostLotsLedger = (
|
||||
inventoryCostLots: InventoryCostLotTracker[],
|
||||
) => {
|
||||
// Groups the inventory cost lots transactions.
|
||||
const inventoryTransactions =
|
||||
groupInventoryTransactionsByTypeId(inventoryCostLots);
|
||||
|
||||
const entries = inventoryTransactions
|
||||
.map(this.getSaleInvoiceCostGLEntries)
|
||||
.flat();
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IInventoryLotCost} inventoryCostLot
|
||||
* @returns {}
|
||||
*/
|
||||
private getInvoiceCostGLCommonEntry = (
|
||||
inventoryCostLot: InventoryCostLotTracker,
|
||||
) => {
|
||||
return {
|
||||
currencyCode: inventoryCostLot.invoice.currencyCode,
|
||||
exchangeRate: inventoryCostLot.invoice.exchangeRate,
|
||||
|
||||
transactionType: inventoryCostLot.transactionType,
|
||||
transactionId: inventoryCostLot.transactionId,
|
||||
|
||||
date: inventoryCostLot.date,
|
||||
indexGroup: 20,
|
||||
costable: true,
|
||||
createdAt: inventoryCostLot.createdAt,
|
||||
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
branchId: inventoryCostLot.invoice.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the inventory cost GL entry.
|
||||
* @param {IInventoryLotCost} inventoryLotCost
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getInventoryCostGLEntry = R.curry(
|
||||
(
|
||||
getIndexIncrement,
|
||||
inventoryCostLot: InventoryCostLotTracker,
|
||||
): ILedgerEntry[] => {
|
||||
const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
|
||||
const costAccountId =
|
||||
inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
|
||||
|
||||
// XXX Debit - Cost account.
|
||||
const costEntry = {
|
||||
...commonEntry,
|
||||
debit: inventoryCostLot.cost,
|
||||
accountId: costAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
itemId: inventoryCostLot.itemId,
|
||||
index: getIndexIncrement(),
|
||||
};
|
||||
// XXX Credit - Inventory account.
|
||||
const inventoryEntry = {
|
||||
...commonEntry,
|
||||
credit: inventoryCostLot.cost,
|
||||
accountId: inventoryCostLot.item.inventoryAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
itemId: inventoryCostLot.itemId,
|
||||
index: getIndexIncrement(),
|
||||
};
|
||||
return [costEntry, inventoryEntry];
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes journal entries for given sale invoice.
|
||||
* -----
|
||||
* - Cost of goods sold -> Debit -> YYYY
|
||||
* - Inventory assets -> Credit -> YYYY
|
||||
*-----
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {JournalPoster} journal
|
||||
*/
|
||||
public getSaleInvoiceCostGLEntries = (
|
||||
inventoryCostLots: InventoryCostLotTracker[],
|
||||
): ILedgerEntry[] => {
|
||||
const getIndexIncrement = increment(0);
|
||||
const getInventoryLotEntry =
|
||||
this.getInventoryCostGLEntry(getIndexIncrement);
|
||||
|
||||
return inventoryCostLots.map((t) => getInventoryLotEntry(t)).flat();
|
||||
};
|
||||
}
|
||||
@@ -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,205 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service';
|
||||
import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service';
|
||||
import { GetSaleInvoice } from './queries/GetSaleInvoice.service';
|
||||
import { EditSaleInvoice } from './commands/EditSaleInvoice.service';
|
||||
import { DeliverSaleInvoice } from './commands/DeliverSaleInvoice.service';
|
||||
import { GetSaleInvoicesPayable } from './queries/GetSaleInvoicesPayable.service';
|
||||
import { WriteoffSaleInvoice } from './commands/WriteoffSaleInvoice.service';
|
||||
import { SaleInvoicePdf } from './queries/SaleInvoicePdf.service';
|
||||
import { GetInvoicePaymentsService } from './queries/GetInvoicePayments.service';
|
||||
import { GetSaleInvoiceState } from './queries/GetSaleInvoiceState.service';
|
||||
import { GetSaleInvoiceMailState } from './queries/GetSaleInvoiceMailState.service';
|
||||
import {
|
||||
ISaleInvoiceWriteoffDTO,
|
||||
ISalesInvoicesFilter,
|
||||
SaleInvoiceMailState,
|
||||
SendInvoiceMailDTO,
|
||||
} from './SaleInvoice.types';
|
||||
import { GetSaleInvoicesService } from './queries/GetSaleInvoices';
|
||||
import { SendSaleInvoiceMail } from './commands/SendSaleInvoiceMail';
|
||||
import {
|
||||
CreateSaleInvoiceDto,
|
||||
EditSaleInvoiceDto,
|
||||
} from './dtos/SaleInvoice.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceApplication {
|
||||
constructor(
|
||||
private createSaleInvoiceService: CreateSaleInvoice,
|
||||
private deleteSaleInvoiceService: DeleteSaleInvoice,
|
||||
private getSaleInvoiceService: GetSaleInvoice,
|
||||
private getSaleInvoicesService: GetSaleInvoicesService,
|
||||
private editSaleInvoiceService: EditSaleInvoice,
|
||||
private deliverSaleInvoiceService: DeliverSaleInvoice,
|
||||
private getReceivableSaleInvoicesService: GetSaleInvoicesPayable,
|
||||
private writeoffInvoiceService: WriteoffSaleInvoice,
|
||||
private getInvoicePaymentsService: GetInvoicePaymentsService,
|
||||
private pdfSaleInvoiceService: SaleInvoicePdf,
|
||||
private getSaleInvoiceStateService: GetSaleInvoiceState,
|
||||
private sendSaleInvoiceMailService: SendSaleInvoiceMail,
|
||||
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice with associated GL entries.
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public createSaleInvoice(saleInvoiceDTO: CreateSaleInvoiceDto) {
|
||||
return this.createSaleInvoiceService.createSaleInvoice(saleInvoiceDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given sale invoice with associated GL entries.
|
||||
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public editSaleInvoice(
|
||||
saleInvoiceId: number,
|
||||
saleInvoiceDTO: EditSaleInvoiceDto,
|
||||
) {
|
||||
return this.editSaleInvoiceService.editSaleInvoice(
|
||||
saleInvoiceId,
|
||||
saleInvoiceDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given sale invoice with given associated GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteSaleInvoice(saleInvoiceId: number) {
|
||||
return this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given sale invoice details.
|
||||
* @param {ISalesInvoicesFilter} filterDTO
|
||||
* @returns {Promise<{ salesInvoices: SaleInvoice[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }>}
|
||||
*/
|
||||
public getSaleInvoices(filterDTO: ISalesInvoicesFilter) {
|
||||
return this.getSaleInvoicesService.getSaleInvoices(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice details.
|
||||
* @param {number} saleInvoiceId -
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public getSaleInvoice(saleInvoiceId: number) {
|
||||
return this.getSaleInvoiceService.getSaleInvoice(saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sale invoice state.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns
|
||||
*/
|
||||
public getSaleInvoiceState() {
|
||||
return this.getSaleInvoiceStateService.getSaleInvoiceState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given sale invoice as delivered.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {}
|
||||
*/
|
||||
public deliverSaleInvoice(saleInvoiceId: number) {
|
||||
return this.deliverSaleInvoiceService.deliverSaleInvoice(saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the receivable sale invoices of the given customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} customerId
|
||||
* @returns
|
||||
*/
|
||||
public getReceivableSaleInvoices(customerId?: number) {
|
||||
return this.getReceivableSaleInvoicesService.getPayableInvoices(customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes-off the sale invoice on bad debt expense account.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO - Writeoff data.
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public async writeOff(
|
||||
saleInvoiceId: number,
|
||||
writeoffDTO: ISaleInvoiceWriteoffDTO,
|
||||
) {
|
||||
return this.writeoffInvoiceService.writeOff(saleInvoiceId, writeoffDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the written-off sale invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @returns {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public cancelWrittenoff(saleInvoiceId: number) {
|
||||
return this.writeoffInvoiceService.cancelWrittenoff(saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice assocaited payments transactions.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
*/
|
||||
public getInvoicePayments = async (invoiceId: number) => {
|
||||
return this.getInvoicePaymentsService.getInvoicePayments(invoiceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the pdf buffer of the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoice
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public saleInvoicePdf(saleInvoiceId: number) {
|
||||
return this.pdfSaleInvoiceService.getSaleInvoicePdf(saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the html content of the given sale invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public saleInvoiceHtml(saleInvoiceId: number): Promise<string> {
|
||||
return this.pdfSaleInvoiceService.getSaleInvoiceHtml(saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the invoice mail of the given sale invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {SendInvoiceMailDTO} messageDTO - Message data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public sendSaleInvoiceMail(
|
||||
saleInvoiceId: number,
|
||||
messageDTO: SendInvoiceMailDTO,
|
||||
) {
|
||||
return this.sendSaleInvoiceMailService.triggerMail(
|
||||
saleInvoiceId,
|
||||
messageDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale invoice.
|
||||
* @param {number} saleInvoiceid - Sale invoice id.
|
||||
* @returns {Promise<SaleInvoiceMailState>}
|
||||
*/
|
||||
public getSaleInvoiceMailState(
|
||||
saleInvoiceid: number,
|
||||
): Promise<SaleInvoiceMailState> {
|
||||
return this.getSaleInvoiceMailStateService.getInvoiceMailState(
|
||||
saleInvoiceid,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ISaleInvoiceWriteoffDTO,
|
||||
ISalesInvoicesFilter,
|
||||
SaleInvoiceMailState,
|
||||
SendInvoiceMailDTO,
|
||||
} from './SaleInvoice.types';
|
||||
import { SaleInvoiceApplication } from './SaleInvoices.application';
|
||||
import {
|
||||
ApiHeader,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
CreateSaleInvoiceDto,
|
||||
EditSaleInvoiceDto,
|
||||
} from './dtos/SaleInvoice.dto';
|
||||
|
||||
@Controller('sale-invoices')
|
||||
@ApiTags('sale-invoices')
|
||||
@ApiHeader({
|
||||
name: 'organization-id',
|
||||
description: 'The organization id',
|
||||
required: true,
|
||||
})
|
||||
@ApiHeader({
|
||||
name: 'x-access-token',
|
||||
description: 'The authentication token',
|
||||
required: true,
|
||||
})
|
||||
export class SaleInvoicesController {
|
||||
constructor(private saleInvoiceApplication: SaleInvoiceApplication) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new sale invoice.' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Sale invoice created successfully',
|
||||
})
|
||||
createSaleInvoice(@Body() saleInvoiceDTO: CreateSaleInvoiceDto) {
|
||||
return this.saleInvoiceApplication.createSaleInvoice(saleInvoiceDTO);
|
||||
}
|
||||
|
||||
@Put(':id/mail')
|
||||
@ApiOperation({ summary: 'Send the sale invoice mail.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale invoice mail sent successfully',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
sendSaleInvoiceMail(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() messageDTO: SendInvoiceMailDTO,
|
||||
) {
|
||||
return this.saleInvoiceApplication.sendSaleInvoiceMail(id, messageDTO);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Edit the given sale invoice.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale invoice edited successfully',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
editSaleInvoice(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() saleInvoiceDTO: EditSaleInvoiceDto,
|
||||
) {
|
||||
return this.saleInvoiceApplication.editSaleInvoice(id, saleInvoiceDTO);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete the given sale invoice.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The sale invoice has been successfully deleted.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
deleteSaleInvoice(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.deleteSaleInvoice(id);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoices.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The sale invoices have been successfully retrieved.',
|
||||
})
|
||||
getSaleInvoices(@Query() filterDTO: ISalesInvoicesFilter) {
|
||||
return this.saleInvoiceApplication.getSaleInvoices(filterDTO);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoice details.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The sale invoice details have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
getSaleInvoice(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.getSaleInvoice(id);
|
||||
}
|
||||
|
||||
@Get(':id/state')
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoice state.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The sale invoice state has been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
getSaleInvoiceState() {
|
||||
return this.saleInvoiceApplication.getSaleInvoiceState();
|
||||
}
|
||||
|
||||
@Put(':id/deliver')
|
||||
@ApiOperation({ summary: 'Deliver the given sale invoice.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The sale invoice has been successfully marked asdelivered.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
@HttpCode(200)
|
||||
deliverSaleInvoice(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.deliverSaleInvoice(id);
|
||||
}
|
||||
|
||||
@Get('receivable/:customerId?')
|
||||
@ApiOperation({ summary: 'Retrieves the receivable sale invoices.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The receivable sale invoices have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The customer not found.' })
|
||||
@ApiParam({
|
||||
name: 'customerId',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'The customer id',
|
||||
})
|
||||
getReceivableSaleInvoices(@Param('customerId') customerId?: number) {
|
||||
return this.saleInvoiceApplication.getReceivableSaleInvoices(customerId);
|
||||
}
|
||||
|
||||
@Post(':id/writeoff')
|
||||
@ApiOperation({ summary: 'Write off the given sale invoice.' })
|
||||
@HttpCode(200)
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The sale invoice has been successfully written off.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
writeOff(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() writeoffDTO: ISaleInvoiceWriteoffDTO,
|
||||
) {
|
||||
return this.saleInvoiceApplication.writeOff(id, writeoffDTO);
|
||||
}
|
||||
|
||||
@Post(':id/cancel-writeoff')
|
||||
@ApiOperation({ summary: 'Cancel the written off sale invoice.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The sale invoice has been successfully marked as not written off.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
@HttpCode(200)
|
||||
cancelWrittenoff(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.cancelWrittenoff(id);
|
||||
}
|
||||
|
||||
@Get(':id/payments')
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoice payments.' })
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
getInvoicePayments(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.getInvoicePayments(id);
|
||||
}
|
||||
|
||||
@Get(':id/pdf')
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoice PDF.' })
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
saleInvoicePdf(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.saleInvoicePdf(id);
|
||||
}
|
||||
|
||||
@Get(':id/html')
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoice HTML.' })
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
saleInvoiceHtml(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.saleInvoiceApplication.saleInvoiceHtml(id);
|
||||
}
|
||||
|
||||
@Get(':id/mail-state')
|
||||
@ApiOperation({ summary: 'Retrieves the sale invoice mail state.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sale invoice mail state retrieved successfully',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The sale invoice id',
|
||||
})
|
||||
getSaleInvoiceMailState(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
): Promise<SaleInvoiceMailState> {
|
||||
return this.saleInvoiceApplication.getSaleInvoiceMailState(id);
|
||||
}
|
||||
}
|
||||
124
packages/server/src/modules/SaleInvoices/SaleInvoices.module.ts
Normal file
124
packages/server/src/modules/SaleInvoices/SaleInvoices.module.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service';
|
||||
import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service';
|
||||
import { DeliverSaleInvoice } from './commands/DeliverSaleInvoice.service';
|
||||
import { EditSaleInvoice } from './commands/EditSaleInvoice.service';
|
||||
import { GenerateShareLink } from './commands/GenerateInvoicePaymentLink.service';
|
||||
import { SaleInvoiceIncrement } from './commands/SaleInvoiceIncrement.service';
|
||||
import { GetInvoicePaymentMail } from './queries/GetInvoicePaymentMail.service';
|
||||
import { GetSaleInvoice } from './queries/GetSaleInvoice.service';
|
||||
import { GetSaleInvoicesPayable } from './queries/GetSaleInvoicesPayable.service';
|
||||
import { GetSaleInvoiceState } from './queries/GetSaleInvoiceState.service';
|
||||
import { SaleInvoicePdf } from './queries/SaleInvoicePdf.service';
|
||||
import { SaleInvoiceApplication } from './SaleInvoices.application';
|
||||
import { ItemsEntriesService } from '../Items/ItemsEntries.service';
|
||||
import { CommandSaleInvoiceValidators } from './commands/CommandSaleInvoiceValidators.service';
|
||||
import { CommandSaleInvoiceDTOTransformer } from './commands/CommandSaleInvoiceDTOTransformer.service';
|
||||
import { SaleEstimateValidators } from '../SaleEstimates/commands/SaleEstimateValidators.service';
|
||||
import { UnlinkConvertedSaleEstimate } from '../SaleEstimates/commands/UnlinkConvertedSaleEstimate.service';
|
||||
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
||||
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
|
||||
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
|
||||
import { SaleInvoicePdfTemplate } from './queries/SaleInvoicePdfTemplate.service';
|
||||
import { WriteoffSaleInvoice } from './commands/WriteoffSaleInvoice.service';
|
||||
import { GetInvoicePaymentsService } from './queries/GetInvoicePayments.service';
|
||||
import { BranchesModule } from '../Branches/Branches.module';
|
||||
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
||||
import { TaxRatesModule } from '../TaxRates/TaxRate.module';
|
||||
import { SaleInvoicesController } from './SaleInvoices.controller';
|
||||
import { InvoiceGLEntriesSubscriber } from './subscribers/InvoiceGLEntriesSubscriber';
|
||||
import { SaleInvoiceGLEntries } from './ledger/InvoiceGLEntries';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
import { SaleInvoiceWriteoffSubscriber } from './subscribers/SaleInvoiceWriteoffSubscriber';
|
||||
import { SaleInvoiceWriteoffGLStorage } from './commands/writeoff/SaleInvoiceWriteoffGLStorage';
|
||||
import { InvoiceInventoryTransactions } from './commands/inventory/InvoiceInventoryTransactions';
|
||||
import { MailModule } from '../Mail/Mail.module';
|
||||
import { GetSaleInvoicesService } from './queries/GetSaleInvoices';
|
||||
import { SendSaleInvoiceMail } from './commands/SendSaleInvoiceMail';
|
||||
import { GetSaleInvoiceMailState } from './queries/GetSaleInvoiceMailState.service';
|
||||
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
||||
import { SendSaleInvoiceMailCommon } from './commands/SendInvoiceInvoiceMailCommon.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { MailNotificationModule } from '../MailNotification/MailNotification.module';
|
||||
import { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { SendSaleInvoiceQueue } from './constants';
|
||||
import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber';
|
||||
import { InvoiceChangeStatusOnMailSentSubscriber } from './subscribers/InvoiceChangeStatusOnMailSentSubscriber';
|
||||
import { InvoiceCostGLEntriesSubscriber } from './subscribers/InvoiceCostGLEntriesSubscriber';
|
||||
import { InvoicePaymentGLRewriteSubscriber } from './subscribers/InvoicePaymentGLRewriteSubscriber';
|
||||
import { SaleInvoiceWriteInventoryTransactionsSubscriber } from './subscribers/InvoiceWriteInventoryTransactions';
|
||||
import { SaleInvoiceCostGLEntries } from './SaleInvoiceCostGLEntries';
|
||||
import { InvoicePaymentsGLEntriesRewrite } from './InvoicePaymentsGLRewrite';
|
||||
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
|
||||
import { SaleInvoicesCost } from './SalesInvoicesCost';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TenancyDatabaseModule,
|
||||
PdfTemplatesModule,
|
||||
AutoIncrementOrdersModule,
|
||||
ChromiumlyTenancyModule,
|
||||
BranchesModule,
|
||||
WarehousesModule,
|
||||
TaxRatesModule,
|
||||
PaymentsReceivedModule,
|
||||
LedgerModule,
|
||||
AccountsModule,
|
||||
MailModule,
|
||||
MailNotificationModule,
|
||||
forwardRef(() => InventoryCostModule),
|
||||
DynamicListModule,
|
||||
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
|
||||
],
|
||||
controllers: [SaleInvoicesController],
|
||||
providers: [
|
||||
CreateSaleInvoice,
|
||||
EditSaleInvoice,
|
||||
DeleteSaleInvoice,
|
||||
GetSaleInvoicesPayable,
|
||||
DeliverSaleInvoice,
|
||||
GenerateShareLink,
|
||||
GetInvoicePaymentMail,
|
||||
SaleInvoiceIncrement,
|
||||
GetSaleInvoiceState,
|
||||
GetSaleInvoice,
|
||||
GetInvoicePaymentMail,
|
||||
SaleInvoicePdf,
|
||||
SaleInvoiceApplication,
|
||||
TenancyContext,
|
||||
TransformerInjectable,
|
||||
ItemsEntriesService,
|
||||
CommandSaleInvoiceValidators,
|
||||
CommandSaleInvoiceDTOTransformer,
|
||||
SaleEstimateValidators,
|
||||
UnlinkConvertedSaleEstimate,
|
||||
SaleInvoicePdfTemplate,
|
||||
WriteoffSaleInvoice,
|
||||
GetInvoicePaymentsService,
|
||||
SaleInvoiceGLEntries,
|
||||
InvoiceGLEntriesSubscriber,
|
||||
SaleInvoiceWriteoffGLStorage,
|
||||
SaleInvoiceWriteoffSubscriber,
|
||||
InvoiceInventoryTransactions,
|
||||
SendSaleInvoiceMail,
|
||||
GetSaleInvoicesService,
|
||||
GetSaleInvoiceMailState,
|
||||
SendSaleInvoiceMailCommon,
|
||||
SendSaleInvoiceMailProcessor,
|
||||
SaleInvoiceCostGLEntries,
|
||||
InvoicePaymentIntegrationSubscriber,
|
||||
InvoiceChangeStatusOnMailSentSubscriber,
|
||||
InvoiceCostGLEntriesSubscriber,
|
||||
InvoicePaymentGLRewriteSubscriber,
|
||||
SaleInvoiceWriteInventoryTransactionsSubscriber,
|
||||
InvoicePaymentsGLEntriesRewrite,
|
||||
SaleInvoicesCost,
|
||||
],
|
||||
exports: [GetSaleInvoice, SaleInvoicesCost, SaleInvoicePdf],
|
||||
})
|
||||
export class SaleInvoicesModule {}
|
||||
150
packages/server/src/modules/SaleInvoices/SalesInvoicesCost.ts
Normal file
150
packages/server/src/modules/SaleInvoices/SalesInvoicesCost.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ModelObject } from 'objection';
|
||||
import { InventoryTransaction } from '../InventoryCost/models/InventoryTransaction';
|
||||
import { IInventoryCostLotsGLEntriesWriteEvent } from '../InventoryCost/types/InventoryCost.types';
|
||||
import { InventoryComputeCostService } from '../InventoryCost/commands/InventoryComputeCost.service';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoicesCost {
|
||||
constructor(
|
||||
private readonly inventoryService: InventoryComputeCostService,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
inventoryItemsIds: number[],
|
||||
startingDate: Date,
|
||||
): Promise<void> {
|
||||
const mutex = new Mutex();
|
||||
const asyncOpers = inventoryItemsIds.map(
|
||||
async (inventoryItemId: number) => {
|
||||
// @todo refactor the lock acquire to be distrbuted using Redis
|
||||
// and run the cost schedule job after running invoice transaction.
|
||||
const release = await mutex.acquire();
|
||||
|
||||
try {
|
||||
await this.inventoryService.scheduleComputeItemCost(
|
||||
inventoryItemId,
|
||||
startingDate,
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
},
|
||||
);
|
||||
await Promise.all(asyncOpers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the max dated inventory transactions in the transactions that
|
||||
* have the same item id.
|
||||
* @param {ModelObject<InventoryTransaction>[]} inventoryTransactions
|
||||
* @return {ModelObject<InventoryTransaction>[]}
|
||||
*/
|
||||
getMaxDateInventoryTransactions(
|
||||
inventoryTransactions: ModelObject<InventoryTransaction>[],
|
||||
): ModelObject<InventoryTransaction>[] {
|
||||
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(
|
||||
inventoryTransactions: ModelObject<InventoryTransaction>[],
|
||||
) {
|
||||
const mutex = new Mutex();
|
||||
const reducedTransactions = this.getMaxDateInventoryTransactions(
|
||||
inventoryTransactions,
|
||||
);
|
||||
const asyncOpers = reducedTransactions.map(async (transaction) => {
|
||||
const release = await mutex.acquire();
|
||||
|
||||
try {
|
||||
await this.inventoryService.scheduleComputeItemCost(
|
||||
transaction.itemId,
|
||||
transaction.date,
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
await Promise.all([...asyncOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule writing journal entries.
|
||||
* @param {Date} startingDate - Starting date.
|
||||
* @return {Promise<agenda>}
|
||||
*/
|
||||
scheduleWriteJournalEntries(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 = (startingDate: Date) => {
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers event `onInventoryCostLotsGLEntriesBeforeWrite`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onCostLotsGLEntriesBeforeWrite,
|
||||
{
|
||||
startingDate,
|
||||
trx,
|
||||
} as IInventoryCostLotsGLEntriesWriteEvent,
|
||||
);
|
||||
// Triggers event `onInventoryCostLotsGLEntriesWrite`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onCostLotsGLEntriesWrite,
|
||||
{
|
||||
startingDate,
|
||||
trx,
|
||||
} as IInventoryCostLotsGLEntriesWriteEvent,
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import * as moment from 'moment';
|
||||
import '../../../utils/moment-mysql';
|
||||
import * as composeAsync from 'async/compose';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
|
||||
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement.service';
|
||||
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
|
||||
import { formatDateFields } from '@/utils/format-date-fields';
|
||||
import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import {
|
||||
CreateSaleInvoiceDto,
|
||||
EditSaleInvoiceDto,
|
||||
} from '../dtos/SaleInvoice.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CommandSaleInvoiceDTOTransformer {
|
||||
/**
|
||||
* @param {BranchTransactionDTOTransformer} branchDTOTransform - Branch transaction DTO transformer.
|
||||
* @param {WarehouseTransactionDTOTransform} warehouseDTOTransform - Warehouse transaction DTO transformer.
|
||||
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
|
||||
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
|
||||
* @param {SaleInvoiceIncrement} invoiceIncrement - Sale invoice increment.
|
||||
* @param {ItemEntriesTaxTransactions} taxDTOTransformer - Item entries tax transactions.
|
||||
* @param {BrandingTemplateDTOTransformer} brandingTemplatesTransformer - Branding template DTO transformer.
|
||||
* @param {TenancyContext} tenancyContext - Tenancy context.
|
||||
* @param {SaleInvoice} saleInvoiceModel - Sale invoice model.
|
||||
*/
|
||||
constructor(
|
||||
private branchDTOTransform: BranchTransactionDTOTransformer,
|
||||
private warehouseDTOTransform: WarehouseTransactionDTOTransform,
|
||||
private itemsEntriesService: ItemsEntriesService,
|
||||
private validators: CommandSaleInvoiceValidators,
|
||||
private invoiceIncrement: SaleInvoiceIncrement,
|
||||
private taxDTOTransformer: ItemEntriesTaxTransactions,
|
||||
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
|
||||
private tenancyContext: TenancyContext,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
customer: Customer,
|
||||
saleInvoiceDTO: CreateSaleInvoiceDto | EditSaleInvoiceDto,
|
||||
oldSaleInvoice?: SaleInvoice,
|
||||
): Promise<SaleInvoice> {
|
||||
const entriesModels = this.transformDTOEntriesToModels(saleInvoiceDTO);
|
||||
const amount = this.getDueBalanceItemEntries(entriesModels);
|
||||
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = await this.invoiceIncrement.getNextInvoiceNumber();
|
||||
|
||||
// Retrieve the authorized user.
|
||||
const authorizedUser = await this.tenancyContext.getSystemUser();
|
||||
|
||||
// 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',
|
||||
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
|
||||
...entry,
|
||||
}));
|
||||
const asyncEntries = await composeAsync(
|
||||
// Associate tax rate from tax id to entries.
|
||||
this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries,
|
||||
// Associate tax rate id from tax code to entries.
|
||||
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries,
|
||||
// Sets default cost and sell account to invoice items entries.
|
||||
this.itemsEntriesService.setItemsEntriesDefaultAccounts,
|
||||
)(initialEntries);
|
||||
|
||||
const entries = R.compose(
|
||||
// Remove tax code from entries.
|
||||
R.map(R.omit(['taxCode'])),
|
||||
|
||||
// Associate the default index for each item entry lin.
|
||||
assocItemEntriesDefaultIndex,
|
||||
)(asyncEntries);
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(
|
||||
omit(saleInvoiceDTO, [
|
||||
'delivered',
|
||||
'entries',
|
||||
'fromEstimateId',
|
||||
'attachments',
|
||||
]),
|
||||
['invoiceDate', 'dueDate'],
|
||||
),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
balance: amount,
|
||||
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 SaleInvoice;
|
||||
|
||||
const initialAsyncDTO = await composeAsync(
|
||||
this.branchDTOTransform.transformDTO<SaleInvoice>,
|
||||
this.warehouseDTOTransform.transformDTO<SaleInvoice>,
|
||||
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
'SaleInvoice',
|
||||
),
|
||||
)(initialDTO);
|
||||
|
||||
return R.compose(this.taxDTOTransformer.assocTaxAmountWithheldFromEntries)(
|
||||
initialAsyncDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the DTO entries to invoice entries models.
|
||||
* @param {ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO} entries
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
private transformDTOEntriesToModels = (
|
||||
saleInvoiceDTO: CreateSaleInvoiceDto | EditSaleInvoiceDto,
|
||||
): ItemEntry[] => {
|
||||
return saleInvoiceDTO.entries.map((entry) => {
|
||||
return ItemEntry.fromJson({
|
||||
...entry,
|
||||
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the due balance from the invoice entries.
|
||||
* @param {IItemEntry[]} entries
|
||||
* @returns {number}
|
||||
*/
|
||||
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
|
||||
return sumBy(entries, (e) => e.amount);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ERRORS } from '../constants';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class CommandSaleInvoiceValidators {
|
||||
constructor(
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} invoiceNumber -
|
||||
* @param {number} notInvoiceId -
|
||||
*/
|
||||
public async validateInvoiceNumberUnique(
|
||||
invoiceNumber: string,
|
||||
notInvoiceId?: number,
|
||||
) {
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.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} customerId - Customer id.
|
||||
*/
|
||||
public async validateCustomerHasNoInvoices(customerId: number) {
|
||||
const invoices = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.where('customer_id', customerId);
|
||||
|
||||
if (invoices.length > 0) {
|
||||
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_INVOICES);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceCreatingPaylaod,
|
||||
} from '../SaleInvoice.types';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
|
||||
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { SaleEstimateValidators } from '@/modules/SaleEstimates/commands/SaleEstimateValidators.service';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CreateSaleInvoiceDto } from '../dtos/SaleInvoice.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreateSaleInvoice {
|
||||
/**
|
||||
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
|
||||
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
|
||||
* @param {CommandSaleInvoiceDTOTransformer} transformerDTO - Command sale invoice DTO transformer.
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {SaleEstimateValidators} commandEstimateValidators - Command sale estimate validators.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
|
||||
* @param {TenantModelProxy<typeof SaleEstimate>} saleEstimateModel - Sale estimate model.
|
||||
* @param {TenantModelProxy<typeof Customer>} customerModel - Customer model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
private readonly validators: CommandSaleInvoiceValidators,
|
||||
private readonly transformerDTO: CommandSaleInvoiceDTOTransformer,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly commandEstimateValidators: SaleEstimateValidators,
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
|
||||
@Inject(SaleEstimate.name)
|
||||
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
|
||||
|
||||
@Inject(Customer.name)
|
||||
private readonly customerModel: TenantModelProxy<typeof Customer>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
saleInvoiceDTO: CreateSaleInvoiceDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<SaleInvoice> => {
|
||||
// Validate customer existance.
|
||||
const customer = await this.customerModel()
|
||||
.query()
|
||||
.findById(saleInvoiceDTO.customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the from estimate id exists on the storage.
|
||||
if (saleInvoiceDTO.fromEstimateId) {
|
||||
const fromEstimate = await this.saleEstimateModel()
|
||||
.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(
|
||||
saleInvoiceDTO.entries,
|
||||
);
|
||||
// Validate items should be sellable items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
saleInvoiceDTO.entries,
|
||||
);
|
||||
// Transform DTO object to model object.
|
||||
const saleInvoiceObj = await this.transformCreateDTOToModel(
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
// authorizedUser,
|
||||
);
|
||||
// Validate sale invoice number uniquiness.
|
||||
if (saleInvoiceObj.invoiceNo) {
|
||||
await this.validators.validateInvoiceNumberUnique(
|
||||
saleInvoiceObj.invoiceNo,
|
||||
);
|
||||
}
|
||||
// Creates a new sale invoice and associated transactions under unit of work env.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
|
||||
saleInvoiceDTO,
|
||||
trx,
|
||||
} as ISaleInvoiceCreatingPaylaod);
|
||||
|
||||
// Create sale invoice graph to the storage.
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query(trx)
|
||||
.upsertGraph(saleInvoiceObj);
|
||||
|
||||
const eventPayload: ISaleInvoiceCreatedPayload = {
|
||||
saleInvoice,
|
||||
saleInvoiceDTO,
|
||||
saleInvoiceId: saleInvoice.id,
|
||||
trx,
|
||||
};
|
||||
// Triggers the event `onSaleInvoiceCreated`.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onCreated,
|
||||
eventPayload,
|
||||
);
|
||||
return saleInvoice;
|
||||
}, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes create DTO to model.
|
||||
* @param {Customer} customer -
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO -
|
||||
*/
|
||||
private transformCreateDTOToModel = async (
|
||||
customer: Customer,
|
||||
saleInvoiceDTO: CreateSaleInvoiceDto,
|
||||
) => {
|
||||
return this.transformerDTO.transformDTOToModel(customer, saleInvoiceDTO);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISaleInvoiceDeletePayload,
|
||||
ISaleInvoiceDeletedPayload,
|
||||
ISaleInvoiceDeletingPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { UnlinkConvertedSaleEstimate } from '@/modules/SaleEstimates/commands/UnlinkConvertedSaleEstimate.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ERRORS } from '../constants';
|
||||
import { events } from '@/common/events/events';
|
||||
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
|
||||
import { CreditNoteAppliedInvoice } from '@/modules/CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteSaleInvoice {
|
||||
/**
|
||||
* @param {UnlinkConvertedSaleEstimate} unlockEstimateFromInvoice - Unlink converted sale estimate service.
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {TenantModelProxy<typeof PaymentReceivedEntry>} paymentReceivedEntryModel - Payment received entry model.
|
||||
* @param {TenantModelProxy<typeof CreditNoteAppliedInvoice>} creditNoteAppliedInvoiceModel - Credit note applied invoice model.
|
||||
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
|
||||
*/
|
||||
constructor(
|
||||
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate,
|
||||
private eventPublisher: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
|
||||
@Inject(PaymentReceivedEntry.name)
|
||||
private paymentReceivedEntryModel: TenantModelProxy<
|
||||
typeof PaymentReceivedEntry
|
||||
>,
|
||||
|
||||
@Inject(CreditNoteAppliedInvoice.name)
|
||||
private creditNoteAppliedInvoiceModel: TenantModelProxy<
|
||||
typeof CreditNoteAppliedInvoice
|
||||
>,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
|
||||
@Inject(ItemEntry.name)
|
||||
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate the sale invoice has no payment entries.
|
||||
* @param {number} saleInvoiceId
|
||||
*/
|
||||
private async validateInvoiceHasNoPaymentEntries(saleInvoiceId: number) {
|
||||
// Retrieve the sale invoice associated payment receive entries.
|
||||
const entries = await this.paymentReceivedEntryModel()
|
||||
.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} invoiceId - Invoice id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public validateInvoiceHasNoAppliedToCredit = async (
|
||||
invoiceId: number,
|
||||
): Promise<void> => {
|
||||
const appliedTransactions = await this.creditNoteAppliedInvoiceModel()
|
||||
.query()
|
||||
.where('invoiceId', invoiceId);
|
||||
|
||||
if (appliedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given sale invoice with associated entries
|
||||
* and journal transactions.
|
||||
* @param {Number} saleInvoiceId - The given sale invoice id.
|
||||
* @param {ISystemUser} authorizedUser -
|
||||
*/
|
||||
public async deleteSaleInvoice(saleInvoiceId: number): Promise<void> {
|
||||
// Retrieve the given sale invoice with associated entries
|
||||
// or throw not found error.
|
||||
const oldSaleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries')
|
||||
.withGraphFetched('paymentMethods')
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the sale invoice has no associated payment entries.
|
||||
await this.validateInvoiceHasNoPaymentEntries(saleInvoiceId);
|
||||
|
||||
// Validate the sale invoice has applied to credit note transaction.
|
||||
await this.validateInvoiceHasNoAppliedToCredit(saleInvoiceId);
|
||||
|
||||
// Triggers `onSaleInvoiceDelete` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDelete, {
|
||||
oldSaleInvoice,
|
||||
saleInvoiceId,
|
||||
} as ISaleInvoiceDeletePayload);
|
||||
|
||||
// Deletes sale invoice transaction and associate transactions with UOW env.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleting, {
|
||||
oldSaleInvoice,
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
} as ISaleInvoiceDeletingPayload);
|
||||
|
||||
// Unlink the converted sale estimates from the given sale invoice.
|
||||
await this.unlockEstimateFromInvoice.unlinkConvertedEstimateFromInvoice(
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
);
|
||||
await this.itemEntryModel()
|
||||
.query(trx)
|
||||
.where('reference_id', saleInvoiceId)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.delete();
|
||||
|
||||
await this.saleInvoiceModel().query(trx).findById(saleInvoiceId).delete();
|
||||
|
||||
// Triggers `onSaleInvoiceDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, {
|
||||
oldSaleInvoice,
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
} as ISaleInvoiceDeletedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
ISaleInvoiceDeliveringPayload,
|
||||
ISaleInvoiceEventDeliveredPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ERRORS } from '../constants';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeliverSaleInvoice {
|
||||
/**
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
|
||||
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
|
||||
*/
|
||||
constructor(
|
||||
private eventEmitter: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private validators: CommandSaleInvoiceValidators,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deliver the given sale invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deliverSaleInvoice(saleInvoiceId: number): Promise<void> {
|
||||
// Retrieve details of the given sale invoice id.
|
||||
const oldSaleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId);
|
||||
|
||||
// Validates the given invoice existence.
|
||||
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 associate transactions
|
||||
// under unit-of-work environment.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceDelivering` event.
|
||||
await this.eventEmitter.emitAsync(events.saleInvoice.onDelivering, {
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceDeliveringPayload);
|
||||
|
||||
// Record the delivered at on the storage.
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query(trx)
|
||||
.patchAndFetchById(saleInvoiceId, {
|
||||
deliveredAt: moment().toMySqlDateTime(),
|
||||
})
|
||||
.withGraphFetched('entries');
|
||||
|
||||
// Triggers `onSaleInvoiceDelivered` event.
|
||||
await this.eventEmitter.emitAsync(events.saleInvoice.onDelivered, {
|
||||
saleInvoiceId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceEventDeliveredPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ISaleInvoiceEditDTO,
|
||||
ISaleInvoiceEditedPayload,
|
||||
ISaleInvoiceEditingPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
|
||||
import { CommandSaleInvoiceDTOTransformer } from './CommandSaleInvoiceDTOTransformer.service';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { EditSaleInvoiceDto } from '../dtos/SaleInvoice.dto';
|
||||
|
||||
@Injectable()
|
||||
export class EditSaleInvoice {
|
||||
/**
|
||||
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
|
||||
* @param {CommandSaleInvoiceDTOTransformer} transformerDTO - Command sale invoice DTO transformer.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
|
||||
* @param {TenantModelProxy<typeof Customer>} customerModel - Customer model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly validators: CommandSaleInvoiceValidators,
|
||||
private readonly transformerDTO: CommandSaleInvoiceDTOTransformer,
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
|
||||
@Inject(Customer.name)
|
||||
private readonly customerModel: TenantModelProxy<typeof Customer>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Edit the given sale invoice.
|
||||
* @async
|
||||
* @param {Number} saleInvoiceId - Sale invoice id.
|
||||
* @param {ISaleInvoice} saleInvoice - Sale invoice DTO object.
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public async editSaleInvoice(
|
||||
saleInvoiceId: number,
|
||||
saleInvoiceDTO: EditSaleInvoiceDto,
|
||||
): Promise<SaleInvoice> {
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const oldSaleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphJoined('entries');
|
||||
|
||||
// Validates the given invoice existance.
|
||||
this.validators.validateInvoiceExistance(oldSaleInvoice);
|
||||
|
||||
// Validate customer existance.
|
||||
const customer = await this.customerModel()
|
||||
.query()
|
||||
.findById(saleInvoiceDTO.customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate items ids existance.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
saleInvoiceDTO.entries,
|
||||
);
|
||||
// Validate non-sellable entries items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
saleInvoiceDTO.entries,
|
||||
);
|
||||
// Validate the items entries existance.
|
||||
await this.itemsEntriesService.validateEntriesIdsExistance(
|
||||
saleInvoiceId,
|
||||
'SaleInvoice',
|
||||
saleInvoiceDTO.entries,
|
||||
);
|
||||
// Transform DTO object to model object.
|
||||
const saleInvoiceObj = await this.tranformEditDTOToModel(
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice,
|
||||
);
|
||||
// Validate sale invoice number uniquiness.
|
||||
if (saleInvoiceObj.invoiceNo) {
|
||||
await this.validators.validateInvoiceNumberUnique(
|
||||
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(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.saleInvoice.onEditing, {
|
||||
trx,
|
||||
oldSaleInvoice,
|
||||
saleInvoiceDTO,
|
||||
} as ISaleInvoiceEditingPayload);
|
||||
|
||||
// Upsert the the invoice graph to the storage.
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.upsertGraphAndFetch({
|
||||
id: saleInvoiceId,
|
||||
...saleInvoiceObj,
|
||||
});
|
||||
// Edit event payload.
|
||||
const editEventPayload: ISaleInvoiceEditedPayload = {
|
||||
saleInvoiceId,
|
||||
saleInvoice,
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
};
|
||||
// Triggers `onSaleInvoiceEdited` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onEdited,
|
||||
editEventPayload,
|
||||
);
|
||||
return saleInvoice;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes edit DTO to model.
|
||||
* @param {ICustomer} customer -
|
||||
* @param {ISaleInvoiceEditDTO} saleInvoiceDTO -
|
||||
* @param {ISaleInvoice} oldSaleInvoice
|
||||
*/
|
||||
private tranformEditDTOToModel = async (
|
||||
customer: Customer,
|
||||
saleInvoiceDTO: EditSaleInvoiceDto,
|
||||
oldSaleInvoice: SaleInvoice,
|
||||
) => {
|
||||
return this.transformerDTO.transformDTOToModel(
|
||||
customer,
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { GeneratePaymentLinkTransformer } from './GeneratePaymentLink.transformer';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GenerateShareLink {
|
||||
constructor(
|
||||
private uow: UnitOfWork,
|
||||
private eventPublisher: EventEmitter2,
|
||||
private transformer: TransformerInjectable,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
|
||||
@Inject(PaymentLink.name)
|
||||
private paymentLinkModel: TenantModelProxy<typeof PaymentLink>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates private or public payment link for the given sale invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {string} publicity - Public or private.
|
||||
* @param {string} expiryTime - Expiry time.
|
||||
*/
|
||||
async generatePaymentLink(
|
||||
saleInvoiceId: number,
|
||||
publicity: string = 'private',
|
||||
expiryTime: string = '',
|
||||
) {
|
||||
const foundInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Generate unique uuid for sharable link.
|
||||
const linkId = uuidv4() as string;
|
||||
|
||||
const commonEventPayload = {
|
||||
saleInvoiceId,
|
||||
publicity,
|
||||
expiryTime,
|
||||
};
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onPublicSharableLinkGenerating` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onPublicLinkGenerating,
|
||||
{ ...commonEventPayload, trx },
|
||||
);
|
||||
const paymentLink = await this.paymentLinkModel().query().insert({
|
||||
linkId,
|
||||
publicity,
|
||||
resourceId: foundInvoice.id,
|
||||
resourceType: 'SaleInvoice',
|
||||
});
|
||||
// Triggers `onPublicSharableLinkGenerated` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onPublicLinkGenerated,
|
||||
{
|
||||
...commonEventPayload,
|
||||
paymentLink,
|
||||
trx,
|
||||
},
|
||||
);
|
||||
return this.transformer.transform(
|
||||
paymentLink,
|
||||
new GeneratePaymentLinkTransformer(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { PUBLIC_PAYMENT_LINK } from '../constants';
|
||||
|
||||
export class GeneratePaymentLinkTransformer extends Transformer {
|
||||
/**
|
||||
* Exclude these attributes from payment link object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['linkId'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['link'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the public/private payment linl
|
||||
* @returns {string}
|
||||
*/
|
||||
public link(link) {
|
||||
return PUBLIC_PAYMENT_LINK?.replace('{PAYMENT_LINK_ID}', link.linkId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AutoIncrementOrdersService } from '../../AutoIncrementOrders/AutoIncrementOrders.service';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceIncrement {
|
||||
constructor(
|
||||
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the next unique invoice number.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
public getNextInvoiceNumber(): Promise<string> {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
'sales_invoices',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the invoice next number.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
public incrementNextInvoiceNumber() {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
'sales_invoices',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { ISalesInvoicesFilter } from '@/interfaces';
|
||||
// import { SaleInvoiceApplication } from './SaleInvoices.application';
|
||||
// import { Exportable } from '@/services/Export/Exportable';
|
||||
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
// @Service()
|
||||
// export class SaleInvoicesExportable extends Exportable {
|
||||
// @Inject()
|
||||
// private saleInvoicesApplication: SaleInvoiceApplication;
|
||||
|
||||
// /**
|
||||
// * Retrieves the accounts data to exportable sheet.
|
||||
// * @param {number} tenantId
|
||||
// * @returns
|
||||
// */
|
||||
// public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
||||
// const filterQuery = (query) => {
|
||||
// query.withGraphFetched('branch');
|
||||
// query.withGraphFetched('warehouse');
|
||||
// };
|
||||
// const parsedQuery = {
|
||||
// sortOrder: 'desc',
|
||||
// columnSortBy: 'created_at',
|
||||
// ...query,
|
||||
// page: 1,
|
||||
// pageSize: EXPORT_SIZE_LIMIT,
|
||||
// filterQuery,
|
||||
// } as ISalesInvoicesFilter;
|
||||
|
||||
// return this.saleInvoicesApplication
|
||||
// .getSaleInvoices(tenantId, parsedQuery)
|
||||
// .then((output) => output.salesInvoices);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Knex } from 'knex';
|
||||
// import { ISaleInvoiceCreateDTO } from '@/interfaces';
|
||||
// import { CreateSaleInvoice } from './commands/CreateSaleInvoice.service';
|
||||
// import { Importable } from '@/services/Import/Importable';
|
||||
// import { SaleInvoicesSampleData } from './constants';
|
||||
|
||||
// @Service()
|
||||
// export class SaleInvoicesImportable extends Importable {
|
||||
// @Inject()
|
||||
// private createInvoiceService: CreateSaleInvoice;
|
||||
|
||||
// /**
|
||||
// * Importing to account service.
|
||||
// * @param {number} tenantId
|
||||
// * @param {IAccountCreateDTO} createAccountDTO
|
||||
// * @returns
|
||||
// */
|
||||
// public importable(
|
||||
// tenantId: number,
|
||||
// createAccountDTO: ISaleInvoiceCreateDTO,
|
||||
// trx?: Knex.Transaction
|
||||
// ) {
|
||||
// return this.createInvoiceService.createSaleInvoice(
|
||||
// tenantId,
|
||||
// createAccountDTO,
|
||||
// {},
|
||||
// trx
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Concurrrency controlling of the importing process.
|
||||
// * @returns {number}
|
||||
// */
|
||||
// public get concurrency() {
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieves the sample data that used to download accounts sample sheet.
|
||||
// */
|
||||
// public sampleData(): any[] {
|
||||
// return SaleInvoicesSampleData;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,118 @@
|
||||
// @ts-nocheck
|
||||
import { GetSaleInvoice } from '../queries/GetSaleInvoice.service';
|
||||
import {
|
||||
DEFAULT_INVOICE_MAIL_CONTENT,
|
||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
} from '../constants';
|
||||
import { GetInvoicePaymentMail } from '../queries/GetInvoicePaymentMail.service';
|
||||
import { GenerateShareLink } from './GenerateInvoicePaymentLink.service';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification';
|
||||
import { SaleInvoiceMailOptions } from '../SaleInvoice.types';
|
||||
|
||||
@Injectable()
|
||||
export class SendSaleInvoiceMailCommon {
|
||||
constructor(
|
||||
private getSaleInvoiceService: GetSaleInvoice,
|
||||
private contactMailNotification: ContactMailNotification,
|
||||
private getInvoicePaymentMail: GetInvoicePaymentMail,
|
||||
private generatePaymentLinkService: GenerateShareLink,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: () => typeof SaleInvoice,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the mail options.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
* @param {string} defaultSubject - Subject text.
|
||||
* @param {string} defaultBody - Subject body.
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async getInvoiceMailOptions(
|
||||
invoiceId: number,
|
||||
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT,
|
||||
): Promise<SaleInvoiceMailOptions> {
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(invoiceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const contactMailDefaultOptions =
|
||||
await this.contactMailNotification.getDefaultMailOptions(
|
||||
saleInvoice.customerId,
|
||||
);
|
||||
const formatArgs = await this.getInvoiceFormatterArgs(invoiceId);
|
||||
|
||||
return {
|
||||
...contactMailDefaultOptions,
|
||||
subject: defaultSubject,
|
||||
message: defaultMessage,
|
||||
attachInvoice: true,
|
||||
formatArgs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given invoice mail options.
|
||||
* @param {number} invoiceId
|
||||
* @param {SaleInvoiceMailOptions} mailOptions
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async formatInvoiceMailOptions(
|
||||
invoiceId: number,
|
||||
mailOptions: SaleInvoiceMailOptions,
|
||||
): Promise<SaleInvoiceMailOptions> {
|
||||
const formatterArgs = await this.getInvoiceFormatterArgs(invoiceId);
|
||||
const formattedOptions =
|
||||
await this.contactMailNotification.formatMailOptions(
|
||||
mailOptions,
|
||||
formatterArgs,
|
||||
);
|
||||
// Generates the a new payment link for the given invoice.
|
||||
const paymentLink =
|
||||
await this.generatePaymentLinkService.generatePaymentLink(
|
||||
invoiceId,
|
||||
'public',
|
||||
);
|
||||
const message = await this.getInvoicePaymentMail.getMailTemplate(
|
||||
invoiceId,
|
||||
{
|
||||
// # Invoice message
|
||||
invoiceMessage: formattedOptions.message,
|
||||
preview: formattedOptions.message,
|
||||
|
||||
// # Payment link
|
||||
viewInvoiceButtonUrl: paymentLink.link,
|
||||
},
|
||||
);
|
||||
return { ...formattedOptions, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of the given sale invoice.
|
||||
* @param {number} invoiceId - Sale invoice id.
|
||||
* @param {string} text - The given text.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
// @ts-nocheck
|
||||
public getInvoiceFormatterArgs = async (
|
||||
invoiceId: number,
|
||||
): Promise<Record<string, string | number>> => {
|
||||
const invoice = await this.getSaleInvoiceService.getSaleInvoice(invoiceId);
|
||||
const commonArgs = await this.contactMailNotification.getCommonFormatArgs();
|
||||
|
||||
return {
|
||||
...commonArgs,
|
||||
'Customer Name': invoice.customer.displayName,
|
||||
'Invoice Number': invoice.invoiceNo,
|
||||
'Invoice Due Amount': invoice.dueAmountFormatted,
|
||||
'Invoice Due Date': invoice.dueDateFormatted,
|
||||
'Invoice Date': invoice.invoiceDateFormatted,
|
||||
'Invoice Amount': invoice.totalFormatted,
|
||||
'Overdue Days': invoice.overdueDays,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Queue } from 'bullmq';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { SaleInvoicePdf } from '../queries/SaleInvoicePdf.service';
|
||||
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { mergeAndValidateMailOptions } from '@/modules/MailNotification/utils';
|
||||
import {
|
||||
SaleInvoiceMailOptions,
|
||||
SendInvoiceMailDTO,
|
||||
SendSaleInvoiceMailJobPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { ISaleInvoiceMailSend } from '../SaleInvoice.types';
|
||||
import { Mail } from '@/modules/Mail/Mail';
|
||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||
import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class SendSaleInvoiceMail {
|
||||
/**
|
||||
* @param {SaleInvoicePdf} invoicePdf - Sale invoice pdf service.
|
||||
* @param {SendSaleInvoiceMailCommon} invoiceMail - Sale invoice mail service.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {MailTransporter} mailTransporter - Mail transporter service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly invoicePdf: SaleInvoicePdf,
|
||||
private readonly invoiceMail: SendSaleInvoiceMailCommon,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly mailTransporter: MailTransporter,
|
||||
private readonly tenancyContect: TenancyContext,
|
||||
|
||||
@InjectQueue(SendSaleInvoiceQueue) private readonly sendInvoiceQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sends the invoice mail of the given sale invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {SendInvoiceMailDTO} messageDTO - Message DTO.
|
||||
*/
|
||||
public async triggerMail(
|
||||
saleInvoiceId: number,
|
||||
messageOptions: SendInvoiceMailDTO,
|
||||
) {
|
||||
const tenant = await this.tenancyContect.getTenant();
|
||||
const user = await this.tenancyContect.getSystemUser();
|
||||
|
||||
const organizationId = tenant.organizationId;
|
||||
const userId = user.id;
|
||||
|
||||
const payload = {
|
||||
saleInvoiceId,
|
||||
messageOptions,
|
||||
userId,
|
||||
organizationId,
|
||||
} as SendSaleInvoiceMailJobPayload;
|
||||
|
||||
await this.sendInvoiceQueue.add(SendSaleInvoiceMailJob, payload);
|
||||
|
||||
// Triggers the event `onSaleInvoicePreMailSend`.
|
||||
await this.eventEmitter.emitAsync(events.saleInvoice.onPreMailSend, {
|
||||
saleInvoiceId,
|
||||
messageOptions,
|
||||
} as ISaleInvoiceMailSend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted mail options.
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {SendInvoiceMailDTO} messageOptions
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
async getFormattedMailOptions(
|
||||
saleInvoiceId: number,
|
||||
messageOptions: SendInvoiceMailDTO,
|
||||
): Promise<SaleInvoiceMailOptions> {
|
||||
const defaultMessageOptions =
|
||||
await this.invoiceMail.getInvoiceMailOptions(saleInvoiceId);
|
||||
|
||||
// Merges message options with default options and parses the options values.
|
||||
const parsedMessageOptions = mergeAndValidateMailOptions(
|
||||
defaultMessageOptions,
|
||||
messageOptions,
|
||||
);
|
||||
return this.invoiceMail.formatInvoiceMailOptions(
|
||||
saleInvoiceId,
|
||||
parsedMessageOptions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the mail invoice.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {SendInvoiceMailDTO} messageDTO - Message options.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
saleInvoiceId: number,
|
||||
messageOptions: SendInvoiceMailDTO,
|
||||
) {
|
||||
const formattedMessageOptions = await this.getFormattedMailOptions(
|
||||
saleInvoiceId,
|
||||
messageOptions,
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(formattedMessageOptions.subject)
|
||||
.setTo(formattedMessageOptions.to)
|
||||
.setCC(formattedMessageOptions.cc)
|
||||
.setBCC(formattedMessageOptions.bcc)
|
||||
.setContent(formattedMessageOptions.message);
|
||||
|
||||
// Attach invoice document.
|
||||
if (formattedMessageOptions.attachInvoice) {
|
||||
// Retrieves document buffer of the invoice pdf document.
|
||||
const [invoicePdfBuffer, invoiceFilename] =
|
||||
await this.invoicePdf.getSaleInvoicePdf(saleInvoiceId);
|
||||
|
||||
mail.setAttachments([
|
||||
{ filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer },
|
||||
]);
|
||||
}
|
||||
const eventPayload = {
|
||||
saleInvoiceId,
|
||||
messageOptions,
|
||||
formattedMessageOptions,
|
||||
} as ISaleInvoiceMailSend;
|
||||
|
||||
// Triggers the event `onSaleInvoiceSend`.
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.saleInvoice.onMailSend,
|
||||
eventPayload,
|
||||
);
|
||||
await this.mailTransporter.send(mail);
|
||||
|
||||
// Triggers the event `onSaleInvoiceSend`.
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.saleInvoice.onMailSent,
|
||||
eventPayload,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import events from '@/subscribers/events';
|
||||
// import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||
|
||||
// @Service()
|
||||
// export class SendSaleInvoiceMailJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'sale-invoice-mail-send',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
|
||||
// const sendInvoiceMail = Container.get(SendSaleInvoiceMail);
|
||||
|
||||
// try {
|
||||
// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,32 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
||||
|
||||
// @Service()
|
||||
// export class SendSaleInvoiceReminderMailJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'sale-invoice-reminder-mail-send',
|
||||
// { priority: 'high', concurrency: 1 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
|
||||
// const sendInvoiceMail = Container.get(SendInvoiceMailReminder);
|
||||
|
||||
// try {
|
||||
// await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ISaleInvoiceWriteoffCreatePayload,
|
||||
ISaleInvoiceWriteoffDTO,
|
||||
ISaleInvoiceWrittenOffCanceledPayload,
|
||||
ISaleInvoiceWrittenOffCancelPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { ERRORS } from '../constants';
|
||||
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators.service';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '../../Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class WriteoffSaleInvoice {
|
||||
/**
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
|
||||
* @param {typeof SaleInvoice} saleInvoiceModel - Sale invoice model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validators: CommandSaleInvoiceValidators,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes-off the sale invoice on bad debt expense account.
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {ISaleInvoiceWriteoffDTO} writeoffDTO
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public writeOff = async (
|
||||
saleInvoiceId: number,
|
||||
writeoffDTO: ISaleInvoiceWriteoffDTO,
|
||||
): Promise<SaleInvoice> => {
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// 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(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 this.saleInvoiceModel()
|
||||
.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 (
|
||||
saleInvoiceId: number,
|
||||
): Promise<SaleInvoice> => {
|
||||
// Validate the sale invoice existance.
|
||||
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.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(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onSaleInvoiceWrittenoffCancel` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onWrittenoffCancel,
|
||||
{
|
||||
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,
|
||||
{
|
||||
saleInvoice,
|
||||
trx,
|
||||
} as ISaleInvoiceWrittenOffCanceledPayload,
|
||||
);
|
||||
return newSaleInvoice;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Should sale invoice not be written-off.
|
||||
* @param {SaleInvoice} saleInvoice
|
||||
*/
|
||||
private validateSaleInvoiceNotWrittenoff(saleInvoice: SaleInvoice) {
|
||||
if (!saleInvoice.isWrittenoff) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_WRITTEN_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should sale invoice already written-off.
|
||||
* @param {SaleInvoice} saleInvoice
|
||||
*/
|
||||
private validateSaleInvoiceAlreadyWrittenoff(saleInvoice: SaleInvoice) {
|
||||
if (saleInvoice.isWrittenoff) {
|
||||
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_WRITTEN_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// @ts-nocheck
|
||||
import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { SaleInvoice } from '../../models/SaleInvoice';
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceInventoryTransactions {
|
||||
constructor(
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
private readonly inventoryService: InventoryTransactionsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Records the inventory transactions of the given sale invoice in case
|
||||
* the invoice has inventory entries only.
|
||||
* @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(
|
||||
saleInvoice: SaleInvoice,
|
||||
override?: boolean,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Loads the inventory items entries of the given sale invoice.
|
||||
const inventoryEntries =
|
||||
await this.itemsEntriesService.filterInventoryEntries(
|
||||
saleInvoice.entries,
|
||||
trx,
|
||||
);
|
||||
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(
|
||||
transaction,
|
||||
override,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverting the inventory transactions once the invoice deleted.
|
||||
* @param {number} billId - Bill id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async revertInventoryTransactions(
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Delete the inventory transaction of the given sale invoice.
|
||||
const { oldInventoryTransactions } =
|
||||
await this.inventoryService.deleteInventoryTransactions(
|
||||
saleInvoiceId,
|
||||
'SaleInvoice',
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { SaleInvoice } from '../../models/SaleInvoice';
|
||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { AccountNormal } from '@/interfaces/Account';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
|
||||
export class SaleInvoiceWriteoffGL {
|
||||
private saleInvoiceModel: SaleInvoice;
|
||||
private ARAccountId: number;
|
||||
|
||||
/**
|
||||
* Sets the sale invoice model.
|
||||
* @param {SaleInvoice} saleInvoiceModel -
|
||||
*/
|
||||
constructor(saleInvoiceModel: SaleInvoice) {
|
||||
this.saleInvoiceModel = saleInvoiceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the A/R account ID.
|
||||
* @param {number} ARAccountId -
|
||||
*/
|
||||
setARAccountId(ARAccountId: number) {
|
||||
this.ARAccountId = ARAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off common GL entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
*/
|
||||
private get invoiceWriteoffGLCommonEntry() {
|
||||
return {
|
||||
date: this.saleInvoiceModel.invoiceDate,
|
||||
|
||||
currencyCode: this.saleInvoiceModel.currencyCode,
|
||||
exchangeRate: this.saleInvoiceModel.exchangeRate,
|
||||
|
||||
transactionId: this.saleInvoiceModel.id,
|
||||
transactionType: 'InvoiceWriteOff',
|
||||
transactionNumber: this.saleInvoiceModel.invoiceNo,
|
||||
|
||||
referenceNo: this.saleInvoiceModel.referenceNo,
|
||||
branchId: this.saleInvoiceModel.branchId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off receiveable GL entry.
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get invoiceWriteoffGLReceivableEntry(): ILedgerEntry {
|
||||
const commontEntry = this.invoiceWriteoffGLCommonEntry;
|
||||
|
||||
return {
|
||||
...commontEntry,
|
||||
credit: this.saleInvoiceModel.writtenoffAmountLocal,
|
||||
accountId: this.ARAccountId,
|
||||
contactId: this.saleInvoiceModel.customerId,
|
||||
debit: 0,
|
||||
index: 1,
|
||||
indexGroup: 300,
|
||||
accountNormal:
|
||||
this.saleInvoiceModel.writtenoffExpenseAccount.accountNormal,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off expense GL entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get invoiceWriteoffGLExpenseEntry(): ILedgerEntry {
|
||||
const commontEntry = this.invoiceWriteoffGLCommonEntry;
|
||||
|
||||
return {
|
||||
...commontEntry,
|
||||
debit: this.saleInvoiceModel.writtenoffAmount,
|
||||
accountId: this.saleInvoiceModel.writtenoffExpenseAccountId,
|
||||
credit: 0,
|
||||
index: 2,
|
||||
indexGroup: 300,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off GL entries.
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getInvoiceWriteoffGLEntries(): ILedgerEntry[] {
|
||||
const creditEntry = this.invoiceWriteoffGLExpenseEntry;
|
||||
const debitEntry = this.invoiceWriteoffGLReceivableEntry;
|
||||
|
||||
return [debitEntry, creditEntry];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice write-off ledger.
|
||||
* @param {number} ARAccountId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
public getInvoiceWriteoffLedger(): ILedger {
|
||||
const entries = this.getInvoiceWriteoffGLEntries();
|
||||
|
||||
return new Ledger(entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
|
||||
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
|
||||
import { SaleInvoice } from '../../models/SaleInvoice';
|
||||
import { SaleInvoiceWriteoffGL } from './SaleInvoiceWriteoffGL';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceWriteoffGLStorage {
|
||||
/**
|
||||
* @param {LedgerStorageService} ledgerStorage - Ledger storage service.
|
||||
* @param {AccountRepository} accountRepository - Account repository.
|
||||
* @param {Account} accountModel - Account model.
|
||||
* @param {SaleInvoice} saleInvoiceModel - Sale invoice model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly ledgerStorage: LedgerStorageService,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes the invoice write-off GL entries.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async writeInvoiceWriteoffEntries(
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
// Retrieves the sale invoice.
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query(trx)
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('writtenoffExpenseAccount');
|
||||
|
||||
// Find or create the A/R account.
|
||||
const ARAccount =
|
||||
await this.accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode,
|
||||
{},
|
||||
trx,
|
||||
);
|
||||
const ledger = new SaleInvoiceWriteoffGL(saleInvoice)
|
||||
.setARAccountId(ARAccount.id)
|
||||
.getInvoiceWriteoffLedger();
|
||||
|
||||
return this.ledgerStorage.commit(ledger, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the invoice write-off GL entries.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async rewriteInvoiceWriteoffEntries(
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
await this.revertInvoiceWriteoffEntries(saleInvoiceId, trx);
|
||||
await this.writeInvoiceWriteoffEntries(saleInvoiceId, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the invoice write-off GL entries.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public revertInvoiceWriteoffEntries = async (
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
saleInvoiceId,
|
||||
'InvoiceWriteOff',
|
||||
trx,
|
||||
);
|
||||
};
|
||||
}
|
||||
243
packages/server/src/modules/SaleInvoices/constants.ts
Normal file
243
packages/server/src/modules/SaleInvoices/constants.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
// import config from '@/config';
|
||||
|
||||
export const SendSaleInvoiceQueue = 'SendSaleInvoiceQueue';
|
||||
export const SendSaleInvoiceMailJob = 'SendSaleInvoiceMailJob';
|
||||
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
export const DEFAULT_INVOICE_MAIL_SUBJECT =
|
||||
'Invoice {Invoice Number} from {Company Name} for {Customer Name}';
|
||||
export const DEFAULT_INVOICE_MAIL_CONTENT = `Hi {Customer Name},
|
||||
|
||||
Here's invoice # {Invoice Number} for {Invoice Amount}
|
||||
|
||||
The amount outstanding of {Invoice Due Amount} is due on {Invoice Due Date}.
|
||||
|
||||
From your online payment page you can print a PDF or view your outstanding bills.
|
||||
|
||||
If you have any questions, please let us know.
|
||||
|
||||
Thanks,
|
||||
{Company Name}
|
||||
`;
|
||||
|
||||
export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =
|
||||
'Invoice {InvoiceNumber} reminder from {CompanyName}';
|
||||
export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
|
||||
<p>Dear {CustomerName}</p>
|
||||
<p>You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.</p>
|
||||
<p>Invoice <strong>#{InvoiceNumber}</strong><br />
|
||||
Due Date : <strong>{InvoiceDueDate}</strong><br />
|
||||
Amount : <strong>{InvoiceAmount}</strong></p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const PUBLIC_PAYMENT_LINK = `${BASE_URL}/payment/{PAYMENT_LINK_ID}`;
|
||||
|
||||
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',
|
||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
export const SaleInvoicesSampleData = [
|
||||
{
|
||||
'Invoice No.': 'B-101',
|
||||
'Reference No.': 'REF0',
|
||||
'Invoice Date': '2024-01-01',
|
||||
'Due Date': '2024-03-01',
|
||||
Customer: 'Harley Veum',
|
||||
'Exchange Rate': 1,
|
||||
'Invoice Message': 'Aspernatur doloremque amet quia aut.',
|
||||
'Terms & Conditions': 'Quia illum aut dolores.',
|
||||
Delivered: 'T',
|
||||
Item: 'VonRueden, Ruecker and Hettinger',
|
||||
Quantity: 100,
|
||||
Rate: 100,
|
||||
Description: 'Description',
|
||||
},
|
||||
{
|
||||
'Invoice No.': 'B-102',
|
||||
'Reference No.': 'REF0',
|
||||
'Invoice Date': '2024-01-01',
|
||||
'Due Date': '2024-03-01',
|
||||
Customer: 'Harley Veum',
|
||||
'Exchange Rate': 1,
|
||||
'Invoice Message': 'Est omnis enim vel.',
|
||||
'Terms & Conditions': 'Iusto et sint nobis sit.',
|
||||
Delivered: 'T',
|
||||
Item: 'Thompson - Reichert',
|
||||
Quantity: 200,
|
||||
Rate: 50,
|
||||
Description: 'Description',
|
||||
},
|
||||
{
|
||||
'Invoice No.': 'B-103',
|
||||
'Reference No.': 'REF0',
|
||||
'Invoice Date': '2024-01-01',
|
||||
'Due Date': '2024-03-01',
|
||||
Customer: 'Harley Veum',
|
||||
'Exchange Rate': 1,
|
||||
'Invoice Message':
|
||||
'Repudiandae voluptatibus repellat minima voluptatem rerum veniam.',
|
||||
'Terms & Conditions': 'Id quod inventore ex rerum velit sed.',
|
||||
Delivered: 'T',
|
||||
Item: 'VonRueden, Ruecker and Hettinger',
|
||||
Quantity: 100,
|
||||
Rate: 100,
|
||||
Description: 'Description',
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultInvoicePdfTemplateAttributes = {
|
||||
primaryColor: 'red',
|
||||
secondaryColor: 'red',
|
||||
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
showCompanyLogo: true,
|
||||
companyLogoKey: '',
|
||||
companyLogoUri: '',
|
||||
|
||||
dueDateLabel: 'Date due',
|
||||
showDueDate: true,
|
||||
|
||||
dateIssueLabel: 'Date of issue',
|
||||
showDateIssue: true,
|
||||
|
||||
// # Invoice number,
|
||||
invoiceNumberLabel: 'Invoice number',
|
||||
showInvoiceNumber: true,
|
||||
|
||||
// # Customer address
|
||||
showCustomerAddress: true,
|
||||
customerAddress: '',
|
||||
|
||||
// # Company address
|
||||
showCompanyAddress: true,
|
||||
companyAddress: '',
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Entries
|
||||
lineItemLabel: 'Item',
|
||||
lineQuantityLabel: 'Qty',
|
||||
lineRateLabel: 'Rate',
|
||||
lineTotalLabel: 'Total',
|
||||
|
||||
totalLabel: 'Total',
|
||||
subtotalLabel: 'Subtotal',
|
||||
discountLabel: 'Discount',
|
||||
paymentMadeLabel: 'Payment Made',
|
||||
balanceDueLabel: 'Balance Due',
|
||||
|
||||
// Totals
|
||||
showTotal: true,
|
||||
showSubtotal: true,
|
||||
showDiscount: true,
|
||||
showTaxes: true,
|
||||
showPaymentMade: true,
|
||||
showDueAmount: true,
|
||||
showBalanceDue: true,
|
||||
|
||||
discount: '0.00',
|
||||
|
||||
// Footer paragraphs.
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
showTermsConditions: true,
|
||||
|
||||
lines: [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
taxes: [
|
||||
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
|
||||
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
|
||||
],
|
||||
|
||||
// # Statement
|
||||
statementLabel: 'Statement',
|
||||
showStatement: true,
|
||||
};
|
||||
208
packages/server/src/modules/SaleInvoices/dtos/SaleInvoice.dto.ts
Normal file
208
packages/server/src/modules/SaleInvoices/dtos/SaleInvoice.dto.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
enum DiscountType {
|
||||
Percentage = 'percentage',
|
||||
Amount = 'amount',
|
||||
}
|
||||
|
||||
class PaymentMethodDto {
|
||||
@IsInt()
|
||||
paymentIntegrationId: number;
|
||||
|
||||
@IsBoolean()
|
||||
enable: boolean;
|
||||
}
|
||||
|
||||
|
||||
class AttachmentDto {
|
||||
@IsString()
|
||||
key: string;
|
||||
}
|
||||
|
||||
class CommandSaleInvoiceDto {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ description: 'Customer ID', example: 1 })
|
||||
customerId: number;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ description: 'Invoice date', example: '2023-01-01T00:00:00Z' })
|
||||
invoiceDate: Date;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ description: 'Due date', example: '2023-01-15T00:00:00Z' })
|
||||
dueDate: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Invoice number',
|
||||
required: false,
|
||||
example: 'INV-001',
|
||||
})
|
||||
invoiceNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Reference number',
|
||||
required: false,
|
||||
example: 'REF-001',
|
||||
})
|
||||
referenceNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
description: 'Whether the invoice is delivered',
|
||||
default: false,
|
||||
required: false,
|
||||
})
|
||||
delivered: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Invoice message',
|
||||
required: false,
|
||||
example: 'Thank you for your business',
|
||||
})
|
||||
invoiceMessage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Terms and conditions',
|
||||
required: false,
|
||||
example: 'Payment due within 14 days',
|
||||
})
|
||||
termsConditions?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
description: 'Exchange rate',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 1.0,
|
||||
})
|
||||
exchangeRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ description: 'Warehouse ID', required: false, example: 1 })
|
||||
warehouseId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ description: 'Branch ID', required: false, example: 1 })
|
||||
branchId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ description: 'Project ID', required: false, example: 1 })
|
||||
projectId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
description: 'Whether tax is inclusive',
|
||||
required: false,
|
||||
example: false,
|
||||
})
|
||||
isInclusiveTax?: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ItemEntryDto)
|
||||
@ArrayMinSize(1)
|
||||
@ApiProperty({
|
||||
description: 'Invoice line items',
|
||||
type: [ItemEntryDto],
|
||||
minItems: 1,
|
||||
})
|
||||
entries: ItemEntryDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ description: 'PDF template ID', required: false, example: 1 })
|
||||
pdfTemplateId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PaymentMethodDto)
|
||||
@ApiProperty({
|
||||
description: 'Payment methods',
|
||||
type: [PaymentMethodDto],
|
||||
required: false,
|
||||
})
|
||||
paymentMethods?: PaymentMethodDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({ description: 'Discount value', required: false, example: 10 })
|
||||
discount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(DiscountType)
|
||||
@ApiProperty({
|
||||
description: 'Discount type',
|
||||
enum: DiscountType,
|
||||
required: false,
|
||||
example: DiscountType.Percentage,
|
||||
})
|
||||
discountType?: DiscountType;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({
|
||||
description: 'Adjustment amount',
|
||||
required: false,
|
||||
example: 5,
|
||||
})
|
||||
adjustment?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'ID of the estimate this invoice is created from',
|
||||
required: false,
|
||||
example: 1,
|
||||
})
|
||||
fromEstimateId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AttachmentDto)
|
||||
@ApiProperty({
|
||||
description: 'The attachments of the sale receipt',
|
||||
example: [{ key: '123456' }],
|
||||
})
|
||||
attachments?: AttachmentDto[];
|
||||
}
|
||||
|
||||
export class CreateSaleInvoiceDto extends CommandSaleInvoiceDto {}
|
||||
export class EditSaleInvoiceDto extends CommandSaleInvoiceDto {}
|
||||
211
packages/server/src/modules/SaleInvoices/ledger/InvoiceGL.ts
Normal file
211
packages/server/src/modules/SaleInvoices/ledger/InvoiceGL.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import * as R from 'ramda';
|
||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { AccountNormal } from '@/modules/Accounts/Accounts.types';
|
||||
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
|
||||
export class InvoiceGL {
|
||||
private saleInvoice: SaleInvoice;
|
||||
private ARAccountId: number;
|
||||
private taxPayableAccountId: number;
|
||||
private discountAccountId: number;
|
||||
private otherChargesAccountId: number;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {SaleInvoice} saleInvoice - Sale invoice.
|
||||
*/
|
||||
constructor(saleInvoice: SaleInvoice) {
|
||||
this.saleInvoice = saleInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the receivable account id.
|
||||
* @param {number} ARAccountId - Receivable account id.
|
||||
*/
|
||||
setARAccountId(ARAccountId: number) {
|
||||
this.ARAccountId = ARAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tax payable account id.
|
||||
* @param {number} taxPayableAccountId - Tax payable account id.
|
||||
*/
|
||||
setTaxPayableAccountId(taxPayableAccountId: number) {
|
||||
this.taxPayableAccountId = taxPayableAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the discount account id.
|
||||
* @param {number} discountAccountId - Discount account id.
|
||||
*/
|
||||
setDiscountAccountId(discountAccountId: number) {
|
||||
this.discountAccountId = discountAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the other charges account id.
|
||||
* @param {number} otherChargesAccountId - Other charges account id.
|
||||
*/
|
||||
setOtherChargesAccountId(otherChargesAccountId: number) {
|
||||
this.otherChargesAccountId = otherChargesAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL common entry.
|
||||
*/
|
||||
private get invoiceGLCommonEntry() {
|
||||
return {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
|
||||
currencyCode: this.saleInvoice.currencyCode,
|
||||
exchangeRate: this.saleInvoice.exchangeRate,
|
||||
|
||||
transactionType: 'SaleInvoice',
|
||||
transactionId: this.saleInvoice.id,
|
||||
|
||||
date: this.saleInvoice.invoiceDate,
|
||||
userId: this.saleInvoice.userId,
|
||||
|
||||
transactionNumber: this.saleInvoice.invoiceNo,
|
||||
referenceNumber: this.saleInvoice.referenceNo,
|
||||
|
||||
createdAt: this.saleInvoice.createdAt,
|
||||
indexGroup: 10,
|
||||
|
||||
branchId: this.saleInvoice.branchId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve receivable entry of the invoice.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
public get invoiceReceivableEntry(): ILedgerEntry {
|
||||
const commonEntry = this.invoiceGLCommonEntry;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: this.saleInvoice.totalLocal,
|
||||
accountId: this.ARAccountId,
|
||||
contactId: this.saleInvoice.customerId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve item income entry of the invoice.
|
||||
* @param {ItemEntry} entry - Item entry.
|
||||
* @param {number} index - Index.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceItemEntry = R.curry(
|
||||
(entry: ItemEntry, index: number): ILedgerEntry => {
|
||||
const commonEntry = this.invoiceGLCommonEntry;
|
||||
const localAmount =
|
||||
entry.totalExcludingTax * this.saleInvoice.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: localAmount,
|
||||
accountId: entry.sellAccountId,
|
||||
note: entry.description,
|
||||
index: index + 2,
|
||||
itemId: entry.itemId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retreives the GL entry of tax payable.
|
||||
* @param {ItemEntry} entry - Item entry.
|
||||
* @param {number} index - Index.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceTaxEntry(entry: ItemEntry, index: number): ILedgerEntry {
|
||||
const commonEntry = this.invoiceGLCommonEntry;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: entry.taxAmount,
|
||||
accountId: this.taxPayableAccountId,
|
||||
index: index + 1,
|
||||
indexGroup: 30,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice discount GL entry.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get invoiceDiscountEntry(): ILedgerEntry {
|
||||
const commonEntry = this.invoiceGLCommonEntry;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: this.saleInvoice.discountAmountLocal,
|
||||
accountId: this.discountAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
} as ILedgerEntry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice adjustment GL entry.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get adjustmentEntry(): ILedgerEntry {
|
||||
const commonEntry = this.invoiceGLCommonEntry;
|
||||
const adjustmentAmount = Math.abs(this.saleInvoice.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: this.saleInvoice.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
credit: this.saleInvoice.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
accountId: this.otherChargesAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL entries.
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getInvoiceGLEntries = (): ILedgerEntry[] => {
|
||||
const creditEntries = this.saleInvoice.entries.map(
|
||||
(entry, index) => this.getInvoiceItemEntry(entry, index),
|
||||
);
|
||||
const taxEntries = this.saleInvoice.entries
|
||||
.filter((entry) => entry.taxAmount > 0)
|
||||
.map((entry, index) => this.getInvoiceTaxEntry(entry, index));
|
||||
|
||||
return [
|
||||
this.invoiceReceivableEntry,
|
||||
...creditEntries,
|
||||
...taxEntries,
|
||||
this.invoiceDiscountEntry,
|
||||
this.adjustmentEntry,
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice ledger.
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getInvoiceLedger = (): ILedger => {
|
||||
const entries = this.getInvoiceGLEntries();
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { LedgerStorageService } from '../../Ledger/LedgerStorage.service';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { AccountRepository } from '../../Accounts/repositories/Account.repository';
|
||||
import { InvoiceGL } from './InvoiceGL';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceGLEntries {
|
||||
constructor(
|
||||
private readonly ledegrRepository: LedgerStorageService,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes a sale invoice GL entries.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public writeInvoiceGLEntries = async (
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query(trx)
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
// Find or create the A/R account.
|
||||
const ARAccount =
|
||||
await this.accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode,
|
||||
{},
|
||||
trx,
|
||||
);
|
||||
// Find or create tax payable account.
|
||||
const taxPayableAccount =
|
||||
await this.accountRepository.findOrCreateTaxPayable({}, trx);
|
||||
// Find or create the discount expense account.
|
||||
const discountAccount =
|
||||
await this.accountRepository.findOrCreateDiscountAccount({}, trx);
|
||||
// Find or create the other charges account.
|
||||
const otherChargesAccount =
|
||||
await this.accountRepository.findOrCreateOtherChargesAccount({}, trx);
|
||||
|
||||
// Retrieves the ledger of the invoice.
|
||||
const invoiceGL = new InvoiceGL(saleInvoice);
|
||||
|
||||
invoiceGL.setARAccountId(ARAccount.id);
|
||||
invoiceGL.setTaxPayableAccountId(taxPayableAccount.id);
|
||||
invoiceGL.setDiscountAccountId(discountAccount.id);
|
||||
invoiceGL.setOtherChargesAccountId(otherChargesAccount.id);
|
||||
|
||||
const ledger = invoiceGL.getInvoiceLedger();
|
||||
|
||||
// Commits the ledger entries to the storage as UOW.
|
||||
await this.ledegrRepository.commit(ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the given invoice GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewritesInvoiceGLEntries = async (
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
// Reverts the invoice GL entries.
|
||||
await this.revertInvoiceGLEntries(saleInvoiceId, trx);
|
||||
|
||||
// Writes the invoice GL entries.
|
||||
await this.writeInvoiceGLEntries(saleInvoiceId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given invoice GL entries.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertInvoiceGLEntries = async (
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
await this.ledegrRepository.deleteByReference(
|
||||
saleInvoiceId,
|
||||
'SaleInvoice',
|
||||
trx,
|
||||
);
|
||||
};
|
||||
}
|
||||
765
packages/server/src/modules/SaleInvoices/models/SaleInvoice.ts
Normal file
765
packages/server/src/modules/SaleInvoices/models/SaleInvoice.ts
Normal file
@@ -0,0 +1,765 @@
|
||||
import { Model, raw } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { MomentInput, unitOfTime } from 'moment';
|
||||
import { defaultTo } from 'ramda';
|
||||
import { TaxRateTransaction } from '@/modules/TaxRates/models/TaxRateTransaction.model';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
|
||||
import { DiscountType } from '@/common/types/Discount';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { ISearchRole } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types';
|
||||
import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model';
|
||||
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
|
||||
|
||||
@InjectAttachable()
|
||||
export class SaleInvoice extends TenantBaseModel{
|
||||
public taxAmountWithheld: number;
|
||||
public balance: number;
|
||||
public paymentAmount: number;
|
||||
public exchangeRate: number;
|
||||
|
||||
public creditedAmount: number;
|
||||
public isInclusiveTax: boolean;
|
||||
|
||||
public dueDate: Date;
|
||||
public deliveredAt: Date | string;
|
||||
public currencyCode: string;
|
||||
public invoiceDate: Date;
|
||||
|
||||
public createdAt?: Date;
|
||||
public updatedAt?: Date | null;
|
||||
|
||||
public writtenoffExpenseAccountId: number;
|
||||
public writtenoffAmount: number;
|
||||
public writtenoffAt: Date;
|
||||
|
||||
public discountType: DiscountType;
|
||||
public discount: number;
|
||||
public adjustment: number;
|
||||
|
||||
public customerId: number;
|
||||
public invoiceNo: string;
|
||||
public referenceNo: string;
|
||||
|
||||
public pdfTemplateId: number;
|
||||
public userId: number;
|
||||
|
||||
public branchId: number;
|
||||
public warehouseId: number;
|
||||
|
||||
public taxes!: TaxRateTransaction[];
|
||||
public entries!: ItemEntry[];
|
||||
public attachments!: Document[];
|
||||
public writtenoffExpenseAccount!: Account;
|
||||
public paymentMethods!: TransactionPaymentServiceEntry[];
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'sales_invoices';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get pluralName() {
|
||||
return 'asdfsdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'isDelivered',
|
||||
'isOverdue',
|
||||
'isPartiallyPaid',
|
||||
'isFullyPaid',
|
||||
'isWrittenoff',
|
||||
'isPaid',
|
||||
|
||||
'dueAmount',
|
||||
'balanceAmount',
|
||||
'remainingDays',
|
||||
'overdueDays',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
'subtotalExludingTax',
|
||||
|
||||
'taxAmountWithheldLocal',
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'writtenoffAmountLocal',
|
||||
'adjustmentLocal',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice amount.
|
||||
* @todo Sugger attribute to balance, we need to rename the balance to amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amount() {
|
||||
return this.balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice amount in base currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountLocal() {
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.amountLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sale invoice amount excluding tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalExludingTax() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal - this.taxAmountWithheld
|
||||
: this.subtotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax amount withheld in base currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get taxAmountWithheldLocal() {
|
||||
return this.taxAmountWithheld * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Local discount amount.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get adjustmentLocal(): number | null {
|
||||
return this.adjustment ? this.adjustment * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
const adjustmentAmount = defaultTo(this.adjustment, 0);
|
||||
|
||||
return R.compose(
|
||||
R.add(adjustmentAmount),
|
||||
R.subtract(R.__, this.discountAmount),
|
||||
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld)),
|
||||
)(this.subtotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total in local currency. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the invoice is delivered.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isDelivered() {
|
||||
return !!this.deliveredAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the due date is over.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isOverdue() {
|
||||
return this.overdueDays > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sale invoice balance.
|
||||
* @return {number}
|
||||
*/
|
||||
get balanceAmount() {
|
||||
return this.paymentAmount + this.writtenoffAmount + this.creditedAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice due amount.
|
||||
* Equation (Invoice amount - payment amount = Due amount)
|
||||
* @return {boolean}
|
||||
*/
|
||||
get dueAmount() {
|
||||
return Math.max(this.total - this.balanceAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmine whether the invoice paid partially.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isPartiallyPaid() {
|
||||
return this.dueAmount !== this.total && this.dueAmount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deetarmine whether the invoice paid fully.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isFullyPaid() {
|
||||
return this.dueAmount === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the invoice paid fully or partially.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isPaid() {
|
||||
return this.isPartiallyPaid || this.isFullyPaid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the sale invoice is written-off.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isWrittenoff() {
|
||||
return Boolean(this.writtenoffAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the remaining days in number
|
||||
* @return {number|null}
|
||||
*/
|
||||
get remainingDays() {
|
||||
const dateMoment = moment();
|
||||
const dueDateMoment = moment(this.dueDate);
|
||||
|
||||
return Math.max(dueDateMoment.diff(dateMoment, 'days'), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Written-off amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get writtenoffAmountLocal() {
|
||||
return this.writtenoffAmount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the overdue days in number.
|
||||
* @return {number|null}
|
||||
*/
|
||||
get overdueDays() {
|
||||
const dateMoment = moment();
|
||||
const dueDateMoment = moment(this.dueDate);
|
||||
|
||||
return Math.max(dateMoment.diff(dueDateMoment, 'days'), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filters the due invoices.
|
||||
*/
|
||||
dueInvoices(query) {
|
||||
query.where(
|
||||
raw(`
|
||||
COALESCE(BALANCE, 0) -
|
||||
COALESCE(PAYMENT_AMOUNT, 0) -
|
||||
COALESCE(WRITTENOFF_AMOUNT, 0) -
|
||||
COALESCE(CREDITED_AMOUNT, 0) > 0
|
||||
`),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Filters the invoices between the given date range.
|
||||
*/
|
||||
filterDateRange(
|
||||
query,
|
||||
startDate: MomentInput,
|
||||
endDate?: MomentInput,
|
||||
type: unitOfTime.StartOf = 'day',
|
||||
) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
if (startDate) {
|
||||
query.where('invoice_date', '>=', fromDate);
|
||||
}
|
||||
if (endDate) {
|
||||
query.where('invoice_date', '<=', toDate);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Filters the invoices in draft status.
|
||||
*/
|
||||
draft(query) {
|
||||
query.where('delivered_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the published invoices.
|
||||
*/
|
||||
published(query) {
|
||||
query.whereNot('delivered_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the delivered invoices.
|
||||
*/
|
||||
delivered(query) {
|
||||
query.whereNot('delivered_at', null);
|
||||
},
|
||||
/**
|
||||
* Filters the unpaid invoices.
|
||||
*/
|
||||
unpaid(query) {
|
||||
query.where(raw('PAYMENT_AMOUNT = 0'));
|
||||
},
|
||||
/**
|
||||
* Filters the overdue invoices.
|
||||
*/
|
||||
overdue(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||
query.where('due_date', '<', asDate);
|
||||
},
|
||||
/**
|
||||
* Filters the not overdue invoices.
|
||||
*/
|
||||
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||
query.where('due_date', '>=', asDate);
|
||||
},
|
||||
/**
|
||||
* Filters the partially invoices.
|
||||
*/
|
||||
partiallyPaid(query) {
|
||||
query.whereNot('payment_amount', 0);
|
||||
query.whereNot(raw('`PAYMENT_AMOUNT` = `BALANCE`'));
|
||||
},
|
||||
/**
|
||||
* Filters the paid invoices.
|
||||
*/
|
||||
paid(query) {
|
||||
query.where(raw('PAYMENT_AMOUNT = BALANCE'));
|
||||
},
|
||||
/**
|
||||
* Filters the sale invoices from the given date.
|
||||
*/
|
||||
fromDate(query, fromDate) {
|
||||
query.where('invoice_date', '<=', fromDate);
|
||||
},
|
||||
/**
|
||||
* Sort the sale invoices by full-payment invoices.
|
||||
*/
|
||||
sortByStatus(query, order) {
|
||||
query.orderByRaw(`PAYMENT_AMOUNT = BALANCE ${order}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sort the sale invoices by the due amount.
|
||||
*/
|
||||
sortByDueAmount(query, order) {
|
||||
query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the max invoice
|
||||
*/
|
||||
maxInvoiceNo(query, prefix, number) {
|
||||
query
|
||||
.select(raw(`REPLACE(INVOICE_NO, "${prefix}", "") AS INV_NUMBER`))
|
||||
.havingRaw('CHAR_LENGTH(INV_NUMBER) = ??', [number.length])
|
||||
.orderBy('invNumber', 'DESC')
|
||||
.limit(1)
|
||||
.first();
|
||||
},
|
||||
|
||||
byPrefixAndNumber(query, prefix, number) {
|
||||
query.where('invoice_no', `${prefix}${number}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Status filter.
|
||||
*/
|
||||
statusFilter(query, filterType) {
|
||||
switch (filterType) {
|
||||
case 'draft':
|
||||
query.modify('draft');
|
||||
break;
|
||||
case 'delivered':
|
||||
query.modify('delivered');
|
||||
break;
|
||||
case 'unpaid':
|
||||
query.modify('unpaid');
|
||||
break;
|
||||
case 'overdue':
|
||||
default:
|
||||
query.modify('overdue');
|
||||
break;
|
||||
case 'partially-paid':
|
||||
query.modify('partiallyPaid');
|
||||
break;
|
||||
case 'paid':
|
||||
query.modify('paid');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters by branches.
|
||||
*/
|
||||
filterByBranches(query, branchesIds) {
|
||||
const formattedBranchesIds = castArray(branchesIds);
|
||||
|
||||
query.whereIn('branchId', formattedBranchesIds);
|
||||
},
|
||||
|
||||
dueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||
query.modify('dueInvoices');
|
||||
query.modify('notOverdue', asDate);
|
||||
query.modify('fromDate', asDate);
|
||||
},
|
||||
|
||||
overdueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||
query.modify('dueInvoices');
|
||||
query.modify('overdue', asDate);
|
||||
query.modify('fromDate', asDate);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const {
|
||||
AccountTransaction,
|
||||
} = require('../../Accounts/models/AccountTransaction.model');
|
||||
const {
|
||||
ItemEntry,
|
||||
} = require('../../TransactionItemEntry/models/ItemEntry');
|
||||
const { Customer } = require('../../Customers/models/Customer');
|
||||
// const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
|
||||
const {
|
||||
PaymentReceivedEntry,
|
||||
} = require('../../PaymentReceived/models/PaymentReceivedEntry');
|
||||
const { Branch } = require('../../Branches/models/Branch.model');
|
||||
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
|
||||
const { Account } = require('../../Accounts/models/Account.model');
|
||||
const {
|
||||
TaxRateTransaction,
|
||||
} = require('../../TaxRates/models/TaxRateTransaction.model');
|
||||
const { Document } = require('../../ChromiumlyTenancy/models/Document');
|
||||
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
|
||||
const {
|
||||
TransactionPaymentServiceEntry,
|
||||
} = require('../../PaymentServices/models/TransactionPaymentServiceEntry.model');
|
||||
const {
|
||||
PdfTemplateModel,
|
||||
} = require('../../PdfTemplate/models/PdfTemplate');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Sale invoice associated entries.
|
||||
*/
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ItemEntry,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'SaleInvoice');
|
||||
builder.orderBy('index', 'ASC');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Belongs to customer model.
|
||||
*/
|
||||
customer: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Customer,
|
||||
join: {
|
||||
from: 'sales_invoices.customerId',
|
||||
to: 'contacts.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('contact_service', 'Customer');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice has associated account transactions.
|
||||
*/
|
||||
transactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: AccountTransaction,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'accounts_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'SaleInvoice');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated cost transactions.
|
||||
*/
|
||||
// costTransactions: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: InventoryCostLotTracker.default,
|
||||
// join: {
|
||||
// from: 'sales_invoices.id',
|
||||
// to: 'inventory_cost_lot_tracker.transactionId',
|
||||
// },
|
||||
// filter(builder) {
|
||||
// builder.where('transaction_type', 'SaleInvoice');
|
||||
// },
|
||||
// },
|
||||
|
||||
/**
|
||||
* Invoice may has associated payment entries.
|
||||
*/
|
||||
paymentEntries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PaymentReceivedEntry,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'payment_receives_entries.invoiceId',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated branch.
|
||||
*/
|
||||
branch: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Branch,
|
||||
join: {
|
||||
from: 'sales_invoices.branchId',
|
||||
to: 'branches.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated warehouse.
|
||||
*/
|
||||
warehouse: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Warehouse,
|
||||
join: {
|
||||
from: 'sales_invoices.warehouseId',
|
||||
to: 'warehouses.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated written-off expense account.
|
||||
*/
|
||||
writtenoffExpenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account,
|
||||
join: {
|
||||
from: 'sales_invoices.writtenoffExpenseAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated tax rate transactions.
|
||||
*/
|
||||
taxes: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: TaxRateTransaction,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'tax_rate_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'SaleInvoice');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale invoice transaction may has many attached attachments.
|
||||
*/
|
||||
attachments: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Document,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
through: {
|
||||
from: 'document_links.modelId',
|
||||
to: 'document_links.documentId',
|
||||
},
|
||||
to: 'documents.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('model_ref', 'SaleInvoice');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale invocie may belongs to matched bank transaction.
|
||||
*/
|
||||
// matchedBankTransaction: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: MatchedBankTransaction,
|
||||
// join: {
|
||||
// from: 'sales_invoices.id',
|
||||
// to: 'matched_bank_transactions.referenceId',
|
||||
// },
|
||||
// filter(query) {
|
||||
// query.where('reference_type', 'SaleInvoice');
|
||||
// },
|
||||
// },
|
||||
|
||||
/**
|
||||
* Sale invoice may belongs to payment methods entries.
|
||||
*/
|
||||
paymentMethods: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: TransactionPaymentServiceEntry,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'transactions_payment_methods.referenceId',
|
||||
},
|
||||
beforeInsert: (model) => {
|
||||
model.referenceType = 'SaleInvoice';
|
||||
},
|
||||
filter: (query) => {
|
||||
query.where('reference_type', 'SaleInvoice');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale invoice may belongs to pdf branding template.
|
||||
*/
|
||||
pdfTemplate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PdfTemplateModel,
|
||||
join: {
|
||||
from: 'sales_invoices.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change payment amount.
|
||||
* @param {Integer} invoiceId
|
||||
* @param {Numeric} amount
|
||||
*/
|
||||
static async changePaymentAmount(invoiceId, amount, trx) {
|
||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||
|
||||
await this.query(trx)
|
||||
.where('id', invoiceId)
|
||||
[changeMethod]('payment_amount', Math.abs(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sale invoice meta.
|
||||
*/
|
||||
// static get meta() {
|
||||
// return SaleInvoiceMeta;
|
||||
// }
|
||||
|
||||
static dueAmountFieldSortQuery(query, role) {
|
||||
query.modify('sortByDueAmount', role.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the default custom views, roles and columns.
|
||||
*/
|
||||
// static get defaultViews() {
|
||||
// return DEFAULT_VIEWS;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Model searchable.
|
||||
*/
|
||||
static get searchable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model search attributes.
|
||||
*/
|
||||
static get searchRoles(): ISearchRole[] {
|
||||
return [
|
||||
{ fieldKey: 'invoice_no', comparator: 'contains' },
|
||||
// { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
|
||||
// { condition: 'or', fieldKey: 'amount', comparator: 'equals' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents mutate base currency since the model is not empty.
|
||||
*/
|
||||
static get preventMutateBaseCurrency() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { JOB_REF, Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants';
|
||||
import { SendSaleInvoiceMail } from '../commands/SendSaleInvoiceMail';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types';
|
||||
|
||||
@Processor({
|
||||
name: SendSaleInvoiceQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendSaleInvoiceMailProcessor {
|
||||
constructor(
|
||||
private readonly sendSaleInvoiceMail: SendSaleInvoiceMail,
|
||||
@Inject(REQUEST) private readonly request: Request,
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>,
|
||||
private readonly clsService: ClsService,
|
||||
) {}
|
||||
|
||||
@Process(SendSaleInvoiceMailJob)
|
||||
async handleSendInvoice() {
|
||||
const { messageOptions, saleInvoiceId, organizationId, userId } =
|
||||
this.jobRef.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
try {
|
||||
await this.sendSaleInvoiceMail.sendMail(saleInvoiceId, messageOptions);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer';
|
||||
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
|
||||
import { GetPdfTemplateTransformer } from '@/modules/PdfTemplate/queries/GetPdfTemplate.transformer';
|
||||
|
||||
export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer {
|
||||
/**
|
||||
* Exclude these attributes from payment link object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'customerName',
|
||||
'dueAmount',
|
||||
'dueDateFormatted',
|
||||
'invoiceDateFormatted',
|
||||
'total',
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
'subtotalLocalFormatted',
|
||||
'dueAmount',
|
||||
'dueAmountFormatted',
|
||||
'paymentAmount',
|
||||
'paymentAmountFormatted',
|
||||
'dueDate',
|
||||
'dueDateFormatted',
|
||||
'invoiceNo',
|
||||
'invoiceMessage',
|
||||
'termsConditions',
|
||||
'entries',
|
||||
'taxes',
|
||||
'organization',
|
||||
'isReceivable',
|
||||
'hasStripePaymentMethod',
|
||||
'formattedCustomerAddress',
|
||||
'brandingTemplate',
|
||||
];
|
||||
};
|
||||
|
||||
public customerName(invoice) {
|
||||
return invoice.customer.displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the organization metadata for the payment link.
|
||||
* @returns
|
||||
*/
|
||||
public organization(invoice) {
|
||||
return this.item(
|
||||
this.context.organization,
|
||||
new GetPaymentLinkOrganizationMetaTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the branding template for the payment link.
|
||||
* @param {} invoice
|
||||
* @returns
|
||||
*/
|
||||
public brandingTemplate(invoice) {
|
||||
return this.item(
|
||||
invoice.pdfTemplate,
|
||||
new GetInvoicePaymentLinkBrandingTemplate()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the sale invoice.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {}
|
||||
*/
|
||||
protected entries = (invoice) => {
|
||||
return this.item(
|
||||
invoice.entries,
|
||||
new GetInvoicePaymentLinkEntryMetaTransformer(),
|
||||
{
|
||||
currencyCode: invoice.currencyCode,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sale invoice entries.
|
||||
* @returns {}
|
||||
*/
|
||||
protected taxes = (invoice) => {
|
||||
return this.item(
|
||||
invoice.taxes,
|
||||
new GetInvoicePaymentLinkTaxEntryTransformer(),
|
||||
{
|
||||
subtotal: invoice.subtotal,
|
||||
isInclusiveTax: invoice.isInclusiveTax,
|
||||
currencyCode: invoice.currencyCode,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
protected isReceivable(invoice) {
|
||||
return invoice.dueAmount > 0;
|
||||
}
|
||||
|
||||
protected hasStripePaymentMethod(invoice) {
|
||||
return invoice.paymentMethods.some(
|
||||
(paymentMethod) => paymentMethod.paymentIntegration.service === 'Stripe'
|
||||
);
|
||||
}
|
||||
|
||||
get customerAddressFormat() {
|
||||
return `{ADDRESS_1}
|
||||
{ADDRESS_2}
|
||||
{CITY} {STATE} {POSTAL_CODE}
|
||||
{COUNTRY}
|
||||
{PHONE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted customer address.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCustomerAddress(invoice) {
|
||||
return contactAddressTextFormat(
|
||||
invoice.customer,
|
||||
this.customerAddressFormat
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetPaymentLinkOrganizationMetaTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to item entry object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'primaryColor',
|
||||
'name',
|
||||
'address',
|
||||
'logoUri',
|
||||
'addressTextFormatted',
|
||||
];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of organization address.
|
||||
* @returns {string}
|
||||
*/
|
||||
public addressTextFormatted() {
|
||||
return this.context.organization.addressTextFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
class GetInvoicePaymentLinkEntryMetaTransformer extends ItemEntryTransformer {
|
||||
/**
|
||||
* Include these attributes to item entry object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'quantity',
|
||||
'quantityFormatted',
|
||||
'rate',
|
||||
'rateFormatted',
|
||||
'total',
|
||||
'totalFormatted',
|
||||
'itemName',
|
||||
'description',
|
||||
];
|
||||
};
|
||||
|
||||
public itemName(entry) {
|
||||
return entry.item.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude these attributes from payment link object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
}
|
||||
|
||||
class GetInvoicePaymentLinkTaxEntryTransformer extends SaleInvoiceTaxEntryTransformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['name', 'taxRateCode', 'taxRateAmount', 'taxRateAmountFormatted'];
|
||||
};
|
||||
}
|
||||
|
||||
class GetInvoicePaymentLinkBrandingTemplate extends GetPdfTemplateTransformer {
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['companyLogoUri', 'primaryColor'];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
primaryColor = (template) => {
|
||||
return template.attributes?.primaryColor;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
InvoicePaymentEmailProps,
|
||||
renderInvoicePaymentEmail,
|
||||
} from '@bigcapital/email-components';
|
||||
import { GetSaleInvoice } from './GetSaleInvoice.service';
|
||||
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { GetInvoicePaymentMailAttributesTransformer } from './GetInvoicePaymentMailAttributes.transformer';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class GetInvoicePaymentMail {
|
||||
constructor(
|
||||
private readonly getSaleInvoiceService: GetSaleInvoice,
|
||||
private readonly getBrandingTemplate: GetPdfTemplateService,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the mail template attributes of the given invoice.
|
||||
* Invoice template attributes are composed of the invoice and branding template attributes.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
*/
|
||||
public async getMailTemplateAttributes(invoiceId: number) {
|
||||
const invoice = await this.getSaleInvoiceService.getSaleInvoice(invoiceId);
|
||||
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
|
||||
invoice.pdfTemplateId,
|
||||
);
|
||||
const mailTemplateAttributes = await this.transformer.transform(
|
||||
invoice,
|
||||
new GetInvoicePaymentMailAttributesTransformer(),
|
||||
{
|
||||
invoice,
|
||||
brandingTemplate,
|
||||
},
|
||||
);
|
||||
return mailTemplateAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail template html content.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
*/
|
||||
public async getMailTemplate(
|
||||
invoiceId: number,
|
||||
overrideAttributes?: Partial<InvoicePaymentEmailProps>,
|
||||
): Promise<string> {
|
||||
const attributes = await this.getMailTemplateAttributes(invoiceId);
|
||||
const mergedAttributes = { ...attributes, ...overrideAttributes };
|
||||
|
||||
return renderInvoicePaymentEmail(mergedAttributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class GetInvoicePaymentMailAttributesTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to item entry object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'companyLogoUri',
|
||||
'companyName',
|
||||
|
||||
'invoiceAmount',
|
||||
|
||||
'primaryColor',
|
||||
|
||||
'invoiceAmount',
|
||||
'invoiceMessage',
|
||||
|
||||
'dueDate',
|
||||
'dueDateLabel',
|
||||
|
||||
'invoiceNumber',
|
||||
'invoiceNumberLabel',
|
||||
|
||||
'total',
|
||||
'totalLabel',
|
||||
|
||||
'dueAmount',
|
||||
'dueAmountLabel',
|
||||
|
||||
'viewInvoiceButtonLabel',
|
||||
'viewInvoiceButtonUrl',
|
||||
|
||||
'items',
|
||||
];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public companyLogoUri(): string {
|
||||
return this.options.brandingTemplate?.companyLogoUri;
|
||||
}
|
||||
|
||||
public companyName(): string {
|
||||
return this.context.organization.name;
|
||||
}
|
||||
|
||||
public invoiceAmount(): string {
|
||||
return this.options.invoice.totalFormatted;
|
||||
}
|
||||
|
||||
public primaryColor(): string {
|
||||
return this.options?.brandingTemplate?.attributes?.primaryColor;
|
||||
}
|
||||
|
||||
public invoiceMessage(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public dueDate(): string {
|
||||
return this.options?.invoice?.dueDateFormatted;
|
||||
}
|
||||
|
||||
public dueDateLabel(): string {
|
||||
return 'Due {dueDate}';
|
||||
}
|
||||
|
||||
public invoiceNumber(): string {
|
||||
return this.options?.invoice?.invoiceNo;
|
||||
}
|
||||
|
||||
public invoiceNumberLabel(): string {
|
||||
return 'Invoice # {invoiceNumber}';
|
||||
}
|
||||
|
||||
public total(): string {
|
||||
return this.options.invoice?.totalFormatted;
|
||||
}
|
||||
|
||||
public totalLabel(): string {
|
||||
return 'Total';
|
||||
}
|
||||
|
||||
public dueAmount(): string {
|
||||
return this.options?.invoice.dueAmountFormatted;
|
||||
}
|
||||
|
||||
public dueAmountLabel(): string {
|
||||
return 'Due Amount';
|
||||
}
|
||||
|
||||
public viewInvoiceButtonLabel(): string {
|
||||
return 'View Invoice';
|
||||
}
|
||||
|
||||
public viewInvoiceButtonUrl(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public items(): Array<any> {
|
||||
return this.item(
|
||||
this.options.invoice?.entries,
|
||||
new GetInvoiceMailTemplateItemAttrsTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetInvoiceMailTemplateItemAttrsTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to item entry object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['quantity', 'label', 'rate'];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public quantity(entry): string {
|
||||
return entry?.quantity;
|
||||
}
|
||||
|
||||
public label(entry): string {
|
||||
return entry?.item?.name;
|
||||
}
|
||||
|
||||
public rate(entry): string {
|
||||
return entry?.rateFormatted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransaction.transformer';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetInvoicePaymentsService {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(PaymentReceivedEntry.name)
|
||||
private readonly paymentReceivedEntryModel: TenantModelProxy<
|
||||
typeof PaymentReceivedEntry
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice associated payments transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
*/
|
||||
public getInvoicePayments = async (invoiceId: number) => {
|
||||
const paymentsEntries = await this.paymentReceivedEntryModel()
|
||||
.query()
|
||||
.where('invoiceId', invoiceId)
|
||||
.withGraphJoined('payment.depositAccount')
|
||||
.withGraphJoined('invoice')
|
||||
.orderBy('payment:paymentDate', 'ASC');
|
||||
|
||||
return this.transformer.transform(
|
||||
paymentsEntries,
|
||||
new InvoicePaymentTransactionTransformer(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
|
||||
import { CommandSaleInvoiceValidators } from '../commands/CommandSaleInvoiceValidators.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleInvoice {
|
||||
constructor(
|
||||
private transformer: TransformerInjectable,
|
||||
private validators: CommandSaleInvoiceValidators,
|
||||
private eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice with associated entries.
|
||||
* @param {Number} saleInvoiceId -
|
||||
* @param {ISystemUser} authorizedUser -
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public async getSaleInvoice(saleInvoiceId: number): Promise<SaleInvoice> {
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('entries.tax')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('taxes.taxRate')
|
||||
.withGraphFetched('attachments')
|
||||
.withGraphFetched('paymentMethods');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
const transformed = await this.transformer.transform(
|
||||
saleInvoice,
|
||||
new SaleInvoiceTransformer(),
|
||||
);
|
||||
const eventPayload = {
|
||||
saleInvoiceId,
|
||||
};
|
||||
// Triggers the `onSaleInvoiceItemViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onViewed,
|
||||
eventPayload,
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class GetSaleInvoiceMailReminder {
|
||||
public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { GetSaleInvoiceMailStateTransformer } from './GetSaleInvoiceMailState.transformer';
|
||||
import { SendSaleInvoiceMailCommon } from '../commands/SendInvoiceInvoiceMailCommon.service';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { SaleInvoiceMailState } from '../SaleInvoice.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleInvoiceMailState {
|
||||
constructor(
|
||||
private transformer: TransformerInjectable,
|
||||
private invoiceMail: SendSaleInvoiceMailCommon,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice mail state of the given sale invoice.
|
||||
* Invoice mail state includes the mail options, branding attributes and the invoice details.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @returns {Promise<SaleInvoiceMailState>}
|
||||
*/
|
||||
public async getInvoiceMailState(
|
||||
saleInvoiceId: number,
|
||||
): Promise<SaleInvoiceMailState> {
|
||||
const saleInvoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('pdfTemplate')
|
||||
.throwIfNotFound();
|
||||
|
||||
const mailOptions =
|
||||
await this.invoiceMail.getInvoiceMailOptions(saleInvoiceId);
|
||||
|
||||
// Transforms the sale invoice mail state.
|
||||
const transformed = await this.transformer.transform(
|
||||
saleInvoice,
|
||||
new GetSaleInvoiceMailStateTransformer(),
|
||||
{
|
||||
mailOptions,
|
||||
},
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
|
||||
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
|
||||
|
||||
export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'invoiceDate',
|
||||
'invoiceDateFormatted',
|
||||
|
||||
'dueDate',
|
||||
'dueDateFormatted',
|
||||
|
||||
'dueAmount',
|
||||
'dueAmountFormatted',
|
||||
|
||||
'total',
|
||||
'totalFormatted',
|
||||
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
|
||||
'invoiceNo',
|
||||
|
||||
'entries',
|
||||
|
||||
'companyName',
|
||||
'companyLogoUri',
|
||||
|
||||
'primaryColor',
|
||||
|
||||
'customerName',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the customer name of the invoice.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected customerName = (invoice) => {
|
||||
return invoice.customer.displayName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company name.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected companyName = () => {
|
||||
return this.context.organization.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company logo uri.
|
||||
* @returns {string | null}
|
||||
*/
|
||||
protected companyLogoUri = (invoice) => {
|
||||
return invoice.pdfTemplate?.companyLogoUri;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the primary color.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected primaryColor = (invoice) => {
|
||||
return invoice.pdfTemplate?.attributes?.primaryColor;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param invoice
|
||||
* @returns
|
||||
*/
|
||||
protected entries = (invoice) => {
|
||||
return this.item(
|
||||
invoice.entries,
|
||||
new GetSaleInvoiceMailStateEntryTransformer(),
|
||||
{
|
||||
currencyCode: invoice.currencyCode,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges the mail options with the invoice object.
|
||||
*/
|
||||
public transform = (object: any) => {
|
||||
return {
|
||||
...this.options.mailOptions,
|
||||
...object,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class GetSaleInvoiceMailStateEntryTransformer extends ItemEntryTransformer {
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
public name = (entry) => {
|
||||
return entry.item.name;
|
||||
};
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'name',
|
||||
'quantity',
|
||||
'quantityFormatted',
|
||||
'rate',
|
||||
'rateFormatted',
|
||||
'total',
|
||||
'totalFormatted',
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { ISaleInvocieState } from '../SaleInvoice.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleInvoiceState {
|
||||
constructor(
|
||||
@Inject(PdfTemplateModel.name)
|
||||
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the create/edit invoice state.
|
||||
* @return {Promise<ISaleInvoice>}
|
||||
*/
|
||||
public async getSaleInvoiceState(): Promise<ISaleInvocieState> {
|
||||
const defaultPdfTemplate = await this.pdfTemplateModel()
|
||||
.query()
|
||||
.findOne({ resource: 'SaleInvoice' })
|
||||
.modify('default');
|
||||
|
||||
return {
|
||||
defaultTemplateId: defaultPdfTemplate?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as R from 'ramda';
|
||||
import { SaleInvoiceTransformer } from './SaleInvoice.transformer';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
|
||||
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { ISalesInvoicesFilter } from '../SaleInvoice.types';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleInvoicesService {
|
||||
constructor(
|
||||
private readonly dynamicListService: DynamicListService,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve sales invoices filterable and paginated list.
|
||||
* @param {ISalesInvoicesFilter} filterDTO -
|
||||
* @returns {Promise<{ salesInvoices: SaleInvoice[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }>}
|
||||
*/
|
||||
public async getSaleInvoices(filterDTO: ISalesInvoicesFilter): Promise<{
|
||||
salesInvoices: SaleInvoice[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
// Parses stringified filter roles.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
SaleInvoice,
|
||||
filter,
|
||||
);
|
||||
const { results, pagination } = await SaleInvoice.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('entries.item');
|
||||
builder.withGraphFetched('customer');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
filterDTO?.filterQuery?.(builder as any);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Retrieves the transformed sale invoices.
|
||||
const salesInvoices = await this.transformer.transform(
|
||||
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 { Injectable, Inject } from '@nestjs/common';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetSaleInvoicesPayable {
|
||||
constructor(
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve due sales invoices.
|
||||
* @param {number} customerId - Customer id.
|
||||
*/
|
||||
public async getPayableInvoices(
|
||||
customerId?: number,
|
||||
): Promise<Array<SaleInvoice>> {
|
||||
const salesInvoices = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.onBuild((query) => {
|
||||
query.modify('dueInvoices');
|
||||
query.modify('delivered');
|
||||
|
||||
if (customerId) {
|
||||
query.where('customer_id', customerId);
|
||||
}
|
||||
});
|
||||
return salesInvoices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Transformer } from "../../Transformer/Transformer";
|
||||
|
||||
export class InvoicePaymentTransactionTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedPaymentAmount', 'formattedPaymentDate'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentAmount = (entry): string => {
|
||||
return this.formatNumber(entry.paymentAmount, {
|
||||
currencyCode: entry.payment.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted payment date.
|
||||
* @param entry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentDate = (entry): string => {
|
||||
return this.formatDate(entry.payment.paymentDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public transform = (entry) => {
|
||||
return {
|
||||
invoiceId: entry.invoiceId,
|
||||
paymentReceiveId: entry.paymentReceiveId,
|
||||
|
||||
paymentDate: entry.payment.paymentDate,
|
||||
formattedPaymentDate: entry.formattedPaymentDate,
|
||||
|
||||
paymentAmount: entry.paymentAmount,
|
||||
formattedPaymentAmount: entry.formattedPaymentAmount,
|
||||
currencyCode: entry.payment.currencyCode,
|
||||
|
||||
paymentNumber: entry.payment.paymentReceiveNo,
|
||||
paymentReferenceNo: entry.payment.referenceNo,
|
||||
|
||||
invoiceNumber: entry.invoice.invoiceNo,
|
||||
invoiceReferenceNo: entry.invoice.referenceNo,
|
||||
|
||||
depositAccountId: entry.payment.depositAccountId,
|
||||
depositAccountName: entry.payment.depositAccount.name,
|
||||
depositAccountSlug: entry.payment.depositAccount.slug,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { mergePdfTemplateWithDefaultAttributes } from '../utils';
|
||||
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
|
||||
import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
|
||||
import { defaultEstimatePdfBrandingAttributes } from '@/modules/SaleEstimates/constants';
|
||||
|
||||
@Injectable()
|
||||
export class SaleEstimatePdfTemplate {
|
||||
constructor(
|
||||
private readonly getPdfTemplateService: GetPdfTemplateService,
|
||||
private readonly getOrgBrandingAttrs: GetOrganizationBrandingAttributesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the estimate pdf template.
|
||||
* @param {number} invoiceTemplateId
|
||||
* @returns
|
||||
*/
|
||||
public async getEstimatePdfTemplate(estimateTemplateId: number) {
|
||||
const template =
|
||||
await this.getPdfTemplateService.getPdfTemplate(estimateTemplateId);
|
||||
// Retreives the organization branding attributes.
|
||||
const commonOrgBrandingAttrs =
|
||||
await this.getOrgBrandingAttrs.getOrganizationBrandingAttributes();
|
||||
|
||||
// Merge the default branding attributes with organization attrs.
|
||||
const orgainizationBrandingAttrs = {
|
||||
...defaultEstimatePdfBrandingAttributes,
|
||||
...commonOrgBrandingAttrs,
|
||||
};
|
||||
const brandingTemplateAttrs = {
|
||||
...template.attributes,
|
||||
companyLogoUri: template.companyLogoUri,
|
||||
};
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
brandingTemplateAttrs,
|
||||
orgainizationBrandingAttrs,
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { ItemEntryTransformer } from '../../TransactionItemEntry/ItemEntry.transformer';
|
||||
import { AttachmentTransformer } from '../../Attachments/Attachment.transformer';
|
||||
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntry.transformer';
|
||||
|
||||
export class SaleInvoiceTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'invoiceDateFormatted',
|
||||
'dueDateFormatted',
|
||||
'createdAtFormatted',
|
||||
'dueAmountFormatted',
|
||||
'paymentAmountFormatted',
|
||||
'balanceAmountFormatted',
|
||||
'exchangeRateFormatted',
|
||||
'subtotalFormatted',
|
||||
'subtotalLocalFormatted',
|
||||
'subtotalExludingTaxFormatted',
|
||||
'taxAmountWithheldFormatted',
|
||||
'taxAmountWithheldLocalFormatted',
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'taxes',
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice date.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected invoiceDateFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatDate(invoice.invoiceDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted due date.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dueDateFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatDate(invoice.dueDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted created at date.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected createdAtFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatDate(invoice.createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice due amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dueAmountFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted payment amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected paymentAmountFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.paymentAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted invoice balance.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected balanceAmountFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.balanceAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted exchange rate.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected exchangeRateFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.exchangeRate, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal in base currency.
|
||||
* (Tax inclusive if the tax inclusive is enabled)
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.subtotal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
money: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal in foreign currency.
|
||||
* (Tax inclusive if the tax inclusive is enabled)
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalLocalFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.subtotalLocal, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal excluding tax in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalExludingTaxFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.subtotalExludingTax, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted tax amount withheld in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxAmountWithheldFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.taxAmountWithheld, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted tax amount withheld in base currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxAmountWithheldLocalFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.taxAmountWithheldLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.total, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in base currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (invoice: SaleInvoice): string => {
|
||||
return this.formatNumber(invoice.totalLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the taxes lines of sale invoice.
|
||||
* @param {ISaleInvoice} invoice
|
||||
*/
|
||||
protected taxes = (invoice: SaleInvoice) => {
|
||||
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
|
||||
subtotal: invoice.subtotal,
|
||||
isInclusiveTax: invoice.isInclusiveTax,
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the sale invoice.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {}
|
||||
*/
|
||||
protected entries = (invoice: SaleInvoice) => {
|
||||
return this.item(invoice.entries, new ItemEntryTransformer(), {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sale invoice attachments.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns
|
||||
*/
|
||||
protected attachments = (invoice: SaleInvoice) => {
|
||||
return this.item(invoice.attachments, new AttachmentTransformer());
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { GetSaleInvoice } from './GetSaleInvoice.service';
|
||||
import { transformInvoiceToPdfTemplate } from '../utils';
|
||||
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate.service';
|
||||
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
import { SaleInvoice } from '../models/SaleInvoice';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { events } from '@/common/events/events';
|
||||
import { InvoicePdfTemplateAttributes } from '../SaleInvoice.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoicePdf {
|
||||
constructor(
|
||||
private chromiumlyTenancy: ChromiumlyTenancy,
|
||||
private getInvoiceService: GetSaleInvoice,
|
||||
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate,
|
||||
private eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
|
||||
@Inject(PdfTemplateModel.name)
|
||||
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice html content.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getSaleInvoiceHtml(invoiceId: number): Promise<string> {
|
||||
const brandingAttributes =
|
||||
await this.getInvoiceBrandingAttributes(invoiceId);
|
||||
|
||||
return renderInvoicePaperTemplateHtml({
|
||||
...brandingAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @returns {Promise<[Buffer, string]>}
|
||||
*/
|
||||
public async getSaleInvoicePdf(invoiceId: number): Promise<[Buffer, string]> {
|
||||
const filename = await this.getInvoicePdfFilename(invoiceId);
|
||||
|
||||
const htmlContent = await this.getSaleInvoiceHtml(invoiceId);
|
||||
|
||||
// Converts the given html content to pdf document.
|
||||
const buffer = await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
|
||||
const eventPayload = { saleInvoiceId: invoiceId };
|
||||
|
||||
// Triggers the `onSaleInvoicePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onPdfViewed,
|
||||
eventPayload,
|
||||
);
|
||||
return [buffer, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename pdf document of the given invoice.
|
||||
* @param {number} invoiceId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
private async getInvoicePdfFilename(invoiceId: number): Promise<string> {
|
||||
const invoice = await this.saleInvoiceModel().query().findById(invoiceId);
|
||||
return `Invoice-${invoice.invoiceNo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the branding attributes of the given sale invoice.
|
||||
* @param {number} invoiceId
|
||||
* @returns {Promise<InvoicePdfTemplateAttributes>}
|
||||
*/
|
||||
private async getInvoiceBrandingAttributes(
|
||||
invoiceId: number,
|
||||
): Promise<InvoicePdfTemplateAttributes> {
|
||||
const invoice = await this.getInvoiceService.getSaleInvoice(invoiceId);
|
||||
|
||||
// Retrieve the invoice template id or get the default template id if not found.
|
||||
const templateId =
|
||||
invoice.pdfTemplateId ??
|
||||
(
|
||||
await this.pdfTemplateModel().query().findOne({
|
||||
resource: 'SaleInvoice',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
|
||||
// Get the branding template attributes.
|
||||
const brandingTemplate =
|
||||
await this.invoiceBrandingTemplateService.getInvoicePdfTemplate(
|
||||
templateId,
|
||||
);
|
||||
|
||||
// Merge the branding template attributes with the invoice.
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformInvoiceToPdfTemplate(invoice),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mergePdfTemplateWithDefaultAttributes } from '../utils';
|
||||
import { defaultInvoicePdfTemplateAttributes } from '../constants';
|
||||
import { GetOrganizationBrandingAttributesService } from '@/modules/PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
|
||||
import { GetPdfTemplateService } from '@/modules/PdfTemplate/queries/GetPdfTemplate.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoicePdfTemplate {
|
||||
constructor(
|
||||
private readonly getPdfTemplateService: GetPdfTemplateService,
|
||||
private readonly getOrgBrandingAttributes: GetOrganizationBrandingAttributesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the invoice pdf template.
|
||||
* @param {number} invoiceTemplateId
|
||||
* @returns
|
||||
*/
|
||||
async getInvoicePdfTemplate(invoiceTemplateId: number) {
|
||||
const template =
|
||||
await this.getPdfTemplateService.getPdfTemplate(invoiceTemplateId);
|
||||
// Retrieves the organization branding attributes.
|
||||
const commonOrgBrandingAttrs =
|
||||
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes();
|
||||
|
||||
const organizationBrandingAttrs = {
|
||||
...defaultInvoicePdfTemplateAttributes,
|
||||
...commonOrgBrandingAttrs,
|
||||
};
|
||||
const brandingTemplateAttrs = {
|
||||
...template.attributes,
|
||||
companyLogoUri: template.companyLogoUri,
|
||||
};
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
brandingTemplateAttrs,
|
||||
organizationBrandingAttrs,
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '../../TaxRates/utils';
|
||||
|
||||
export class SaleInvoiceTaxEntryTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'name',
|
||||
'taxRateCode',
|
||||
'taxRate',
|
||||
'taxRateId',
|
||||
'taxRateAmount',
|
||||
'taxRateAmountFormatted',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate code.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateCode = (taxEntry) => {
|
||||
return taxEntry.taxRate.code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate id.
|
||||
* @param taxEntry
|
||||
* @returns {number}
|
||||
*/
|
||||
protected taxRate = (taxEntry) => {
|
||||
return taxEntry.taxAmount || taxEntry.taxRate.rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate name.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected name = (taxEntry) => {
|
||||
return taxEntry.taxRate.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate amount.
|
||||
* @param taxEntry
|
||||
*/
|
||||
protected taxRateAmount = (taxEntry) => {
|
||||
const taxRate = this.taxRate(taxEntry);
|
||||
|
||||
return this.options.isInclusiveTax
|
||||
? getInclusiveTaxAmount(this.options.subtotal, taxRate)
|
||||
: getExlusiveTaxAmount(this.options.subtotal, taxRate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted tax rate amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateAmountFormatted = (taxEntry) => {
|
||||
return this.formatNumber(this.taxRateAmount(taxEntry), {
|
||||
currencyCode: this.options.currencyCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DeliverSaleInvoice } from '../commands/DeliverSaleInvoice.service';
|
||||
import { ERRORS } from '../constants';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ISaleInvoiceMailSent } from '../SaleInvoice.types';
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceChangeStatusOnMailSentSubscriber {
|
||||
constructor(private readonly markInvoiceDelivedService: DeliverSaleInvoice) {}
|
||||
|
||||
/**
|
||||
* Marks the invoice delivered once the invoice mail sent.
|
||||
* @param {ISaleInvoiceMailSent}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onMailReminderSent)
|
||||
@OnEvent(events.saleInvoice.onMailSent)
|
||||
async markInvoiceDelivered({
|
||||
saleInvoiceId,
|
||||
messageOptions,
|
||||
}: ISaleInvoiceMailSent) {
|
||||
try {
|
||||
await this.markInvoiceDelivedService.deliverSaleInvoice(saleInvoiceId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ServiceError &&
|
||||
error.errorType === ERRORS.SALE_INVOICE_ALREADY_DELIVERED
|
||||
) {
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IInventoryCostLotsGLEntriesWriteEvent } from '@/modules/InventoryCost/types/InventoryCost.types';
|
||||
import { SaleInvoiceCostGLEntries } from '../SaleInvoiceCostGLEntries';
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceCostGLEntriesSubscriber {
|
||||
constructor(private readonly invoiceCostEntries: SaleInvoiceCostGLEntries) {}
|
||||
|
||||
/**
|
||||
* Writes the invoices cost GL entries once the inventory cost lots be written.
|
||||
* @param {IInventoryCostLotsGLEntriesWriteEvent}
|
||||
*/
|
||||
async writeInvoicesCostEntriesOnCostLotsWritten({
|
||||
trx,
|
||||
startingDate,
|
||||
}: IInventoryCostLotsGLEntriesWriteEvent) {
|
||||
await this.invoiceCostEntries.writeInventoryCostJournalEntries(
|
||||
startingDate,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceDeletePayload,
|
||||
ISaleInvoiceEditedPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { SaleInvoiceGLEntries } from '../ledger/InvoiceGLEntries';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceGLEntriesSubscriber {
|
||||
constructor(public readonly saleInvoiceGLEntries: SaleInvoiceGLEntries) {}
|
||||
|
||||
/**
|
||||
* Records journal entries of the non-inventory invoice.
|
||||
* @param {ISaleInvoiceCreatedPayload} payload -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onCreated)
|
||||
@OnEvent(events.saleInvoice.onDelivered)
|
||||
public async handleWriteJournalEntriesOnInvoiceCreated({
|
||||
saleInvoiceId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceCreatedPayload) {
|
||||
// Can't continue if the sale invoice is not delivered yet.
|
||||
if (!saleInvoice.deliveredAt) return null;
|
||||
|
||||
await this.saleInvoiceGLEntries.writeInvoiceGLEntries(saleInvoiceId, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records journal entries of the non-inventory invoice.
|
||||
* @param {ISaleInvoiceEditedPayload} payload -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onEdited)
|
||||
public async handleRewriteJournalEntriesOnceInvoiceEdit({
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditedPayload) {
|
||||
// Can't continue if the sale invoice is not delivered yet.
|
||||
if (!saleInvoice.deliveredAt) return null;
|
||||
|
||||
await this.saleInvoiceGLEntries.rewritesInvoiceGLEntries(
|
||||
saleInvoice.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reverting journal entries once sale invoice delete.
|
||||
* @param {ISaleInvoiceDeletePayload} payload -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onDeleted)
|
||||
public async handleRevertingInvoiceJournalEntriesOnDelete({
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
}: ISaleInvoiceDeletePayload) {
|
||||
await this.saleInvoiceGLEntries.revertInvoiceGLEntries(saleInvoiceId, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ISaleInvoiceEditingPayload } from '../SaleInvoice.types';
|
||||
import { InvoicePaymentsGLEntriesRewrite } from '../InvoicePaymentsGLRewrite';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicePaymentGLRewriteSubscriber {
|
||||
constructor(
|
||||
private readonly invoicePaymentsRewriteGLEntries: InvoicePaymentsGLEntriesRewrite,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes associated invoiceso of payment receive once edit.
|
||||
* @param {ISaleInvoiceEditingPayload} -
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onEdited)
|
||||
async paymentGLEntriesRewriteOnPaymentEdit({
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditingPayload) {
|
||||
await this.invoicePaymentsRewriteGLEntries.invoicePaymentsGLEntriesRewrite(
|
||||
oldSaleInvoice.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||
import { PaymentIntegrationTransactionLinkDeleteEventPayload } from '../SaleInvoice.types';
|
||||
import { PaymentIntegrationTransactionLinkEventPayload } from '../SaleInvoice.types';
|
||||
import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceDeletingPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicePaymentIntegrationSubscriber {
|
||||
constructor(private readonly eventPublisher: EventEmitter2) {}
|
||||
|
||||
/**
|
||||
* Handles the creation of payment integration events when a sale invoice is created.
|
||||
* This method filters enabled payment methods from the invoice and emits a payment
|
||||
* integration link event for each method.
|
||||
* @param {ISaleInvoiceCreatedPayload} payload - The payload containing sale invoice creation details.
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onCreated)
|
||||
public handleCreatePaymentIntegrationEvents({
|
||||
saleInvoiceDTO,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceCreatedPayload) {
|
||||
const paymentMethods =
|
||||
saleInvoice.paymentMethods?.filter((method) => method.enable) || [];
|
||||
|
||||
paymentMethods.map(
|
||||
async (paymentMethod: TransactionPaymentServiceEntry) => {
|
||||
const payload = {
|
||||
...omit(paymentMethod, ['id']),
|
||||
saleInvoiceId: saleInvoice.id,
|
||||
trx,
|
||||
};
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.paymentIntegrationLink.onPaymentIntegrationLink,
|
||||
payload as PaymentIntegrationTransactionLinkEventPayload,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ISaleInvoiceDeletingPayload} payload
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onDeleting)
|
||||
public handleCreatePaymentIntegrationEventsOnDeleteInvoice({
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceDeletingPayload) {
|
||||
const paymentMethods =
|
||||
oldSaleInvoice.paymentMethods?.filter((method) => method.enable) || [];
|
||||
|
||||
paymentMethods.map(
|
||||
async (paymentMethod: TransactionPaymentServiceEntry) => {
|
||||
const payload = {
|
||||
...omit(paymentMethod, ['id']),
|
||||
oldSaleInvoiceId: oldSaleInvoice.id,
|
||||
trx,
|
||||
} as PaymentIntegrationTransactionLinkDeleteEventPayload;
|
||||
|
||||
// Triggers `onPaymentIntegrationDeleteLink` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.paymentIntegrationLink.onPaymentIntegrationDeleteLink,
|
||||
payload,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { InvoiceInventoryTransactions } from '../commands/inventory/InvoiceInventoryTransactions';
|
||||
import { events } from '@/common/events/events';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceEditedPayload,
|
||||
ISaleInvoiceDeletedPayload,
|
||||
ISaleInvoiceEventDeliveredPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceWriteInventoryTransactionsSubscriber {
|
||||
constructor(
|
||||
private readonly saleInvoiceInventory: InvoiceInventoryTransactions,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handles the writing inventory transactions once the invoice created.
|
||||
* @param {ISaleInvoiceCreatedPayload} payload
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onCreated)
|
||||
public async handleWritingInventoryTransactions({
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceCreatedPayload | ISaleInvoiceEventDeliveredPayload) {
|
||||
// Can't continue if the sale invoice is not delivered yet.
|
||||
if (!saleInvoice.deliveredAt) return null;
|
||||
|
||||
await this.saleInvoiceInventory.recordInventoryTranscactions(
|
||||
saleInvoice,
|
||||
false,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewriting the inventory transactions once the sale invoice be edited.
|
||||
* @param {ISaleInvoiceEditPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onEdited)
|
||||
public async handleRewritingInventoryTransactions({
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditedPayload) {
|
||||
await this.saleInvoiceInventory.recordInventoryTranscactions(
|
||||
saleInvoice,
|
||||
true,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deleting the inventory transactions once the invoice deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onDeleted)
|
||||
public async handleDeletingInventoryTransactions({
|
||||
saleInvoiceId,
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceDeletedPayload) {
|
||||
await this.saleInvoiceInventory.revertInventoryTransactions(
|
||||
saleInvoiceId,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
ISaleInvoiceWriteoffCreatePayload,
|
||||
ISaleInvoiceWrittenOffCanceledPayload,
|
||||
} from '../SaleInvoice.types';
|
||||
import { SaleInvoiceWriteoffGLStorage } from '../commands/writeoff/SaleInvoiceWriteoffGLStorage';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class SaleInvoiceWriteoffSubscriber {
|
||||
constructor(private readonly writeGLStorage: SaleInvoiceWriteoffGLStorage) {}
|
||||
|
||||
/**
|
||||
* Write the written-off sale invoice journal entries.
|
||||
* @param {ISaleInvoiceWriteoffCreatePayload}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onWrittenoff)
|
||||
public async writeJournalEntriesOnceWriteoffCreate({
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceWriteoffCreatePayload) {
|
||||
await this.writeGLStorage.writeInvoiceWriteoffEntries(saleInvoice.id, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the written-of sale invoice jounral entries.
|
||||
* @param {ISaleInvoiceWrittenOffCanceledPayload}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onWrittenoffCanceled)
|
||||
public async revertJournalEntriesOnce({
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceWrittenOffCanceledPayload) {
|
||||
await this.writeGLStorage.revertInvoiceWriteoffEntries(saleInvoice.id, trx);
|
||||
}
|
||||
}
|
||||
50
packages/server/src/modules/SaleInvoices/utils.ts
Normal file
50
packages/server/src/modules/SaleInvoices/utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// @ts-nocheck
|
||||
import { pickBy } from 'lodash';
|
||||
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
|
||||
export const mergePdfTemplateWithDefaultAttributes = (
|
||||
brandingTemplate?: Record<string, any>,
|
||||
defaultAttributes: Record<string, any> = {}
|
||||
) => {
|
||||
const brandingAttributes = pickBy(
|
||||
brandingTemplate,
|
||||
(val, key) => val !== null && Object.keys(defaultAttributes).includes(key)
|
||||
);
|
||||
return {
|
||||
...defaultAttributes,
|
||||
...brandingAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformInvoiceToPdfTemplate = (
|
||||
invoice: ISaleInvoice
|
||||
): Partial<InvoicePdfTemplateAttributes> => {
|
||||
return {
|
||||
dueDate: invoice.dueDateFormatted,
|
||||
dateIssue: invoice.invoiceDateFormatted,
|
||||
invoiceNumber: invoice.invoiceNo,
|
||||
|
||||
total: invoice.totalFormatted,
|
||||
subtotal: invoice.subtotalFormatted,
|
||||
paymentMade: invoice.paymentAmountFormatted,
|
||||
dueAmount: invoice.dueAmountFormatted,
|
||||
|
||||
termsConditions: invoice.termsConditions,
|
||||
statement: invoice.invoiceMessage,
|
||||
|
||||
lines: invoice.entries.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
taxes: invoice.taxes.map((tax) => ({
|
||||
label: tax.name,
|
||||
amount: tax.taxRateAmountFormatted,
|
||||
})),
|
||||
|
||||
customerAddress: contactAddressTextFormat(invoice.customer),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user