Merge branch 'develop' into migrate-server-nestjs

This commit is contained in:
Ahmed Bouhuolia
2024-12-29 11:14:15 +02:00
282 changed files with 11273 additions and 3304 deletions

View File

@@ -238,6 +238,7 @@ export default class Ledger implements ILedger {
return {
credit: defaultTo(entry.credit, 0),
debit: defaultTo(entry.debit, 0),
exchangeRate: entry.exchangeRate,
currencyCode: entry.currencyCode,

View File

@@ -9,15 +9,18 @@ import {
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformLedgerEntryToTransaction } from './utils';
// Filter the blank entries.
const filterBlankEntry = (entry: ILedgerEntry) => Boolean(entry.credit || entry.debit);
@Service()
export class LedgerEntriesStorage {
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
/**
* Saves entries of the given ledger.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} knex
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} knex
* @returns {Promise<void>}
*/
public saveEntries = async (
@@ -26,7 +29,7 @@ export class LedgerEntriesStorage {
trx?: Knex.Transaction
) => {
const saveEntryQueue = async.queue(this.saveEntryTask, 10);
const entries = ledger.getEntries();
const entries = ledger.filter(filterBlankEntry).getEntries();
entries.forEach((entry) => {
saveEntryQueue.push({ tenantId, entry, trx });
@@ -57,8 +60,8 @@ export class LedgerEntriesStorage {
/**
* Saves the ledger entry to the account transactions repository.
* @param {number} tenantId
* @param {ILedgerEntry} entry
* @param {number} tenantId
* @param {ILedgerEntry} entry
* @returns {Promise<void>}
*/
private saveEntry = async (

View File

@@ -12,6 +12,7 @@ import {
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import { SaleReceipt } from '@/models';
@Service()
export default class CreditNoteGLEntries {
@@ -29,11 +30,15 @@ export default class CreditNoteGLEntries {
*/
private getCreditNoteGLedger = (
creditNote: ICreditNote,
receivableAccount: number
receivableAccount: number,
discountAccount: number,
adjustmentAccount: number
): Ledger => {
const ledgerEntries = this.getCreditNoteGLEntries(
creditNote,
receivableAccount
receivableAccount,
discountAccount,
adjustmentAccount
);
return new Ledger(ledgerEntries);
};
@@ -49,9 +54,16 @@ export default class CreditNoteGLEntries {
tenantId: number,
creditNote: ICreditNote,
payableAccount: number,
discountAccount: number,
adjustmentAccount: number,
trx?: Knex.Transaction
): Promise<void> => {
const ledger = this.getCreditNoteGLedger(creditNote, payableAccount);
const ledger = this.getCreditNoteGLedger(
creditNote,
payableAccount,
discountAccount,
adjustmentAccount
);
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
@@ -98,11 +110,18 @@ export default class CreditNoteGLEntries {
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
creditNoteWithItems.currencyCode
);
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{}
);
const adjustmentAccount =
await accountRepository.findOrCreateOtherChargesAccount({});
// Saves the credit note GL entries.
await this.saveCreditNoteGLEntries(
tenantId,
creditNoteWithItems,
ARAccount.id,
discountAccount.id,
adjustmentAccount.id,
trx
);
};
@@ -169,7 +188,7 @@ export default class CreditNoteGLEntries {
return {
...commonEntry,
credit: creditNote.localAmount,
credit: creditNote.totalLocal,
accountId: ARAccountId,
contactId: creditNote.customerId,
index: 1,
@@ -191,11 +210,11 @@ export default class CreditNoteGLEntries {
index: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
const localAmount = entry.amount * creditNote.exchangeRate;
const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate;
return {
...commonEntry,
debit: localAmount,
debit: totalLocal,
accountId: entry.sellAccountId || entry.item.sellAccountId,
note: entry.description,
index: index + 2,
@@ -206,6 +225,50 @@ export default class CreditNoteGLEntries {
}
);
/**
* Retrieves the credit note discount entry.
* @param {ICreditNote} creditNote
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getDiscountEntry = (
creditNote: ICreditNote,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
return {
...commonEntry,
credit: creditNote.discountAmountLocal,
accountId: discountAccountId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the credit note adjustment entry.
* @param {ICreditNote} creditNote
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
creditNote: ICreditNote,
adjustmentAccountId: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
const adjustmentAmount = Math.abs(creditNote.adjustmentLocal);
return {
...commonEntry,
credit: creditNote.adjustmentLocal < 0 ? adjustmentAmount : 0,
debit: creditNote.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: adjustmentAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieve the credit note GL entries.
* @param {ICreditNote} creditNote - Credit note.
@@ -214,13 +277,21 @@ export default class CreditNoteGLEntries {
*/
public getCreditNoteGLEntries = (
creditNote: ICreditNote,
ARAccountId: number
ARAccountId: number,
discountAccountId: number,
adjustmentAccountId: number
): ILedgerEntry[] => {
const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId);
const getItemEntry = this.getCreditNoteItemEntry(creditNote);
const itemsEntries = creditNote.entries.map(getItemEntry);
return [AREntry, ...itemsEntries];
const discountEntry = this.getDiscountEntry(creditNote, discountAccountId);
const adjustmentEntry = this.getAdjustmentEntry(
creditNote,
adjustmentAccountId
);
return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries];
};
}

View File

@@ -18,6 +18,18 @@ export class CreditNoteTransformer extends Transformer {
'formattedAmount',
'formattedCreditsUsed',
'formattedSubtotal',
'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'entries',
'attachments',
];
@@ -34,7 +46,7 @@ export class CreditNoteTransformer extends Transformer {
/**
* Retrieve formatted created at date.
* @param credit
* @param credit
* @returns {string}
*/
protected formattedCreatedAt = (credit): string => {
@@ -83,6 +95,85 @@ export class CreditNoteTransformer extends Transformer {
return formatNumber(credit.amount, { money: false });
};
/**
* Retrieves formatted discount amount.
* @param credit
* @returns {string}
*/
protected discountAmountFormatted = (credit): string => {
return formatNumber(credit.discountAmount, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {ICreditNote} credit
* @returns {string}
*/
protected discountAmountLocalFormatted = (credit): string => {
return formatNumber(credit.discountAmountLocal, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param credit
* @returns {string}
*/
protected discountPercentageFormatted = (credit): string => {
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
};
/**
* Retrieves formatted adjustment amount.
* @param credit
* @returns {string}
*/
protected adjustmentFormatted = (credit): string => {
return this.formatMoney(credit.adjustment, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {ICreditNote} credit
* @returns {string}
*/
protected adjustmentLocalFormatted = (credit): string => {
return formatNumber(credit.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted total.
* @param credit
* @returns {string}
*/
protected totalFormatted = (credit): string => {
return formatNumber(credit.total, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the formatted total in local currency.
* @param credit
* @returns {string}
*/
protected totalLocalFormatted = (credit): string => {
return formatNumber(credit.totalLocal, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ICreditNote} credit

View File

@@ -24,7 +24,7 @@ export class CreateItem {
/**
* Authorize the creating item.
* @param {number} tenantId
* @param {number} tenantId
* @param {IItemDTO} itemDTO
*/
async authorize(tenantId: number, itemDTO: IItemDTO) {

View File

@@ -52,10 +52,18 @@ export class BillGLEntries {
{},
trx
);
// Find or create other expenses account.
const otherExpensesAccount =
await accountRepository.findOrCreateOtherExpensesAccount({}, trx);
// Find or create purchase discount account.
const purchaseDiscountAccount =
await accountRepository.findOrCreatePurchaseDiscountAccount({}, trx);
const billLedger = this.getBillLedger(
bill,
APAccount.id,
taxPayableAccount.id
taxPayableAccount.id,
purchaseDiscountAccount.id,
otherExpensesAccount.id
);
// Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx);
@@ -102,6 +110,7 @@ export class BillGLEntries {
return {
debit: 0,
credit: 0,
currencyCode: bill.currencyCode,
exchangeRate: bill.exchangeRate || 1,
@@ -130,13 +139,12 @@ export class BillGLEntries {
private getBillItemEntry = R.curry(
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
const localAmount = bill.exchangeRate * entry.amountExludingTax;
const totalLocal = bill.exchangeRate * entry.totalExcludingTax;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return {
...commonJournalMeta,
debit: localAmount + landedCostAmount,
debit: totalLocal + landedCostAmount,
accountId:
['inventory'].indexOf(entry.item.type) !== -1
? entry.item.inventoryAccountId
@@ -240,6 +248,52 @@ export class BillGLEntries {
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the purchase discount GL entry.
* @param {IBill} bill
* @param {number} purchaseDiscountAccountId
* @returns {ILedgerEntry}
*/
private getPurchaseDiscountEntry = (
bill: IBill,
purchaseDiscountAccountId: number
) => {
const commonEntry = this.getBillCommonEntry(bill);
return {
...commonEntry,
credit: bill.discountAmountLocal,
accountId: purchaseDiscountAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the purchase other charges GL entry.
* @param {IBill} bill
* @param {number} otherChargesAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
bill: IBill,
otherExpensesAccountId: number
) => {
const commonEntry = this.getBillCommonEntry(bill);
const adjustmentAmount = Math.abs(bill.adjustmentLocal);
return {
...commonEntry,
debit: bill.adjustmentLocal > 0 ? adjustmentAmount : 0,
credit: bill.adjustmentLocal < 0 ? adjustmentAmount : 0,
accountId: otherExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the given bill GL entries.
* @param {IBill} bill
@@ -249,7 +303,9 @@ export class BillGLEntries {
private getBillGLEntries = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
@@ -262,8 +318,24 @@ export class BillGLEntries {
);
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
const purchaseDiscountEntry = this.getPurchaseDiscountEntry(
bill,
purchaseDiscountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
bill,
otherExpensesAccountId
);
// Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
return [
payableEntry,
...itemsEntries,
...landedCostEntries,
...taxEntries,
purchaseDiscountEntry,
adjustmentEntry,
];
};
/**
@@ -275,14 +347,17 @@ export class BillGLEntries {
private getBillLedger = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
) => {
const entries = this.getBillGLEntries(
bill,
payableAccountId,
taxPayableAccountId
taxPayableAccountId,
purchaseDiscountAccountId,
otherExpensesAccountId
);
return new Ledger(entries);
};
}

View File

@@ -20,10 +20,21 @@ export class PurchaseInvoiceTransformer extends Transformer {
'formattedBalance',
'formattedDueAmount',
'formattedExchangeRate',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted',
'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
@@ -160,6 +171,63 @@ export class PurchaseInvoiceTransformer extends Transformer {
});
};
/**
* Retrieves the formatted discount amount.
* @param {IBill} bill
* @returns {string}
*/
protected discountAmountFormatted = (bill): string => {
return formatNumber(bill.discountAmount, {
currencyCode: bill.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {IBill} bill
* @returns {string}
*/
protected discountAmountLocalFormatted = (bill): string => {
return formatNumber(bill.discountAmountLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount percentage.
* @param {IBill} bill
* @returns {string}
*/
protected discountPercentageFormatted = (bill): string => {
return bill.discountPercentage ? `${bill.discountPercentage}%` : '';
};
/**
* Retrieves the formatted adjustment amount.
* @param {IBill} bill
* @returns {string}
*/
protected adjustmentFormatted = (bill): string => {
return formatNumber(bill.adjustment, {
currencyCode: bill.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {IBill} bill
* @returns {string}
*/
protected adjustmentLocalFormatted = (bill): string => {
return formatNumber(bill.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the total formatted.
* @param {IBill} bill

View File

@@ -56,7 +56,7 @@ export default class VendorCreditGLEntries {
return {
...commonEntity,
debit: vendorCredit.localAmount,
debit: vendorCredit.totalLocal,
accountId: APAccountId,
contactId: vendorCredit.vendorId,
accountNormal: AccountNormal.CREDIT,
@@ -77,11 +77,11 @@ export default class VendorCreditGLEntries {
index: number
): ILedgerEntry => {
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
const localAmount = entry.amount * vendorCredit.exchangeRate;
const totalLocal = entry.totalExcludingTax * vendorCredit.exchangeRate;
return {
...commonEntity,
credit: localAmount,
credit: totalLocal,
index: index + 2,
itemId: entry.itemId,
itemQuantity: entry.quantity,
@@ -94,6 +94,52 @@ export default class VendorCreditGLEntries {
}
);
/**
* Retrieves the vendor credit discount GL entry.
* @param {IVendorCredit} vendorCredit
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
public getDiscountEntry = (
vendorCredit: IVendorCredit,
purchaseDiscountAccountId: number
) => {
const commonEntry = this.getVendorCreditGLCommonEntry(vendorCredit);
return {
...commonEntry,
debit: vendorCredit.discountAmountLocal,
accountId: purchaseDiscountAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the vendor credit adjustment GL entry.
* @param {IVendorCredit} vendorCredit
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
public getAdjustmentEntry = (
vendorCredit: IVendorCredit,
otherExpensesAccountId: number
) => {
const commonEntry = this.getVendorCreditGLCommonEntry(vendorCredit);
const adjustmentAmount = Math.abs(vendorCredit.adjustmentLocal);
return {
...commonEntry,
credit: vendorCredit.adjustmentLocal > 0 ? adjustmentAmount : 0,
debit: vendorCredit.adjustmentLocal < 0 ? adjustmentAmount : 0,
accountId: otherExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieve the vendor credit GL entries.
* @param {IVendorCredit} vendorCredit -
@@ -102,7 +148,9 @@ export default class VendorCreditGLEntries {
*/
public getVendorCreditGLEntries = (
vendorCredit: IVendorCredit,
payableAccountId: number
payableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getVendorCreditPayableGLEntry(
vendorCredit,
@@ -111,7 +159,15 @@ export default class VendorCreditGLEntries {
const getItemEntry = this.getVendorCreditGLItemEntry(vendorCredit);
const itemsEntries = vendorCredit.entries.map(getItemEntry);
return [payableEntry, ...itemsEntries];
const discountEntry = this.getDiscountEntry(
vendorCredit,
purchaseDiscountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
vendorCredit,
otherExpensesAccountId
);
return [payableEntry, discountEntry, adjustmentEntry, ...itemsEntries];
};
/**
@@ -158,10 +214,17 @@ export default class VendorCreditGLEntries {
{},
trx
);
const purchaseDiscountAccount =
await accountRepository.findOrCreatePurchaseDiscountAccount({}, trx);
const otherExpensesAccount =
await accountRepository.findOrCreateOtherExpensesAccount({}, trx);
// Saves the vendor credit GL entries.
const ledgerEntries = this.getVendorCreditGLEntries(
vendorCredit,
APAccount.id
APAccount.id,
purchaseDiscountAccount.id,
otherExpensesAccount.id
);
const ledger = new Ledger(ledgerEntries);

View File

@@ -17,6 +17,15 @@ export class VendorCreditTransformer extends Transformer {
'formattedCreatedAt',
'formattedCreditsRemaining',
'formattedInvoicedAmount',
'discountAmountFormatted',
'discountPercentageFormatted',
'discountAmountLocalFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'entries',
'attachments',
];
@@ -33,7 +42,7 @@ export class VendorCreditTransformer extends Transformer {
/**
* Retireve formatted created at date.
* @param vendorCredit
* @param vendorCredit
* @returns {string}
*/
protected formattedCreatedAt = (vendorCredit): string => {
@@ -71,6 +80,63 @@ export class VendorCreditTransformer extends Transformer {
});
};
/**
* Retrieves the formatted discount amount.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected discountAmountFormatted = (credit): string => {
return formatNumber(credit.discountAmount, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected discountAmountLocalFormatted = (credit): string => {
return formatNumber(credit.discountAmountLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount percentage.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected discountPercentageFormatted = (credit): string => {
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
};
/**
* Retrieves the formatted adjustment amount.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected adjustmentFormatted = (credit): string => {
return formatNumber(credit.adjustment, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected adjustmentLocalFormatted = (credit): string => {
return formatNumber(credit.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted invoiced amount.
* @param credit
@@ -82,6 +148,15 @@ export class VendorCreditTransformer extends Transformer {
});
};
/**
* Retrieves the formatted total.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected totalFormatted = (credit) => {
return formatNumber(credit.total, { currencyCode: credit.currencyCode });
};
/**
* Retrieves the entries of the bill.
* @param {IVendorCredit} vendorCredit

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import {
renderEstimateEmailTemplate,
EstimatePaymentEmailProps,
} from '@bigcapital/email-components';
import { GetSaleEstimate } from './GetSaleEstimate';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetEstimateMailTemplateAttributesTransformer } from './GetEstimateMailTemplateAttributesTransformer';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
@Service()
export class GetEstimateMailTemplate {
@Inject()
private getEstimateService: GetSaleEstimate;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
/**
* Retrieves the mail template attributes of the given estimate.
* Estimate template attributes are composed of the estimate and branding template attributes.
* @param {number} tenantId
* @param {number} estimateId - Estimate id.
* @returns {Promise<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
estimateId: number
): Promise<EstimatePaymentEmailProps> {
const estimate = await this.getEstimateService.getEstimate(
tenantId,
estimateId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
estimate.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
estimate,
new GetEstimateMailTemplateAttributesTransformer(),
{
estimate,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Rertieves the mail template html content.
* @param {number} tenantId
* @param {number} estimateId
* @param overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
estimateId: number,
overrideAttributes?: Partial<any>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
estimateId
);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderEstimateEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,205 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetEstimateMailTemplateAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'estimateAmount',
'primaryColor',
'estimateAmount',
'estimateMessage',
'dueDate',
'dueDateLabel',
'estimateNumber',
'estimateNumberLabel',
'total',
'totalLabel',
'subtotal',
'subtotalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'dueAmount',
'dueAmountLabel',
'viewEstimateButtonLabel',
'viewEstimateButtonUrl',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color.
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Estimate number.
* @returns {string}
*/
public estimateNumber(): string {
return this.options.estimate.estimateNumber;
}
/**
* Estimate number label.
* @returns {string}
*/
public estimateNumberLabel(): string {
return 'Estimate No: {estimateNumber}';
}
/**
* Expiration date.
* @returns {string}
*/
public expirationDate(): string {
return this.options.estimate.formattedExpirationDate;
}
/**
* Expiration date label.
* @returns {string}
*/
public expirationDateLabel(): string {
return 'Expiration Date: {expirationDate}';
}
/**
* Estimate total.
*/
public total(): string {
return this.options.estimate.totalFormatted;
}
/**
* Estimate total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Estimate discount.
* @returns {string}
*/
public discount(): string {
return this.options.estimate?.discountAmountFormatted;
}
/**
* Estimate discount label.
* @returns {string}
*/
public discountLabel(): string {
return 'Discount';
}
/**
* Estimate adjustment.
* @returns {string}
*/
public adjustment(): string {
return this.options.estimate?.adjustmentFormatted;
}
/**
* Estimate adjustment label.
* @returns {string}
*/
public adjustmentLabel(): string {
return 'Adjustment';
}
/**
* Estimate subtotal.
*/
public subtotal(): string {
return this.options.estimate.formattedSubtotal;
}
/**
* Estimate subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Estimate mail items attributes.
*/
public items(): any[] {
return this.item(
this.options.estimate.entries,
new GetEstimateMailTemplateEntryAttributesTransformer()
);
}
}
class GetEstimateMailTemplateEntryAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return ['label', 'quantity', 'rate', 'total'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public label(entry): string {
return entry?.item?.name;
}
public quantity(entry): string {
return entry?.quantity;
}
public rate(entry): string {
return entry?.rateFormatted;
}
public total(entry): string {
return entry?.totalFormatted;
}
}

View File

@@ -0,0 +1,52 @@
import { Inject, Service } from 'typedi';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetSaleEstimateMailStateTransformer } from './GetSaleEstimateMailStateTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetSaleEstimateMailState {
@Inject()
private estimateMail: SendSaleEstimateMail;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the estimate mail state of the given sale estimate.
* Estimate mail state includes the mail options, branding attributes and the estimate details.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailState>}
*/
async getEstimateMailState(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailState> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const saleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.withGraphFetched('customer')
.withGraphFetched('entries.item')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions = await this.estimateMail.getMailOptions(
tenantId,
saleEstimateId
);
const transformed = await this.transformer.transform(
tenantId,
saleEstimate,
new GetSaleEstimateMailStateTransformer(),
{
mailOptions,
}
);
return transformed;
}
}

View File

@@ -0,0 +1,180 @@
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer {
public excludeAttributes = (): string[] => {
return ['*'];
};
public includeAttributes = (): string[] => {
return [
'estimateDate',
'estimateDateFormatted',
'expirationDate',
'expirationDateFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'adjustmentLabel',
'estimateNumber',
'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 || null;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (invoice) => {
return invoice.pdfTemplate?.attributes?.primaryColor || null;
};
/**
* Retrieves the estimate number.
*/
protected estimateDateFormatted = (estimate) => {
return this.formattedEstimateDate(estimate);
};
/**
* Retrieves the expiration date of the estimate.
* @param estimate
* @returns {string}
*/
protected expirationDateFormatted = (estimate) => {
return this.formattedExpirationDate(estimate);
};
/**
* Retrieves the total amount of the estimate.
* @param estimate
* @returns
*/
protected total(estimate) {
return estimate.amount;
}
/**
* Retrieves the subtotal amount of the estimate.
* @param estimate
* @returns {number}
*/
protected subtotal(estimate) {
return estimate.amount;
}
/**
* Retrieves the discount label of the estimate.
* @param estimate
* @returns {string}
*/
protected discountLabel(estimate) {
return estimate.discountType === 'percentage'
? `Discount [${estimate.discountPercentageFormatted}]`
: 'Discount';
}
/**
* Retrieves the formatted subtotal of the estimate.
* @param estimate
* @returns {string}
*/
protected subtotalFormatted = (estimate) => {
return this.formattedSubtotal(estimate);
};
/**
* Retrieves the estimate entries.
* @param invoice
* @returns {Array}
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
new GetSaleEstimateMailStateEntryTransformer(),
{
currencyCode: invoice.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleEstimateMailStateEntryTransformer extends ItemEntryTransformer {
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Item name.
* @param entry
* @returns
*/
public name = (entry) => {
return entry.item.name;
};
public includeAttributes = (): string[] => {
return [
'name',
'quantity',
'unitPrice',
'unitPriceFormatted',
'total',
'totalFormatted',
];
};
}

View File

@@ -18,6 +18,11 @@ export class SaleEstimateTransfromer extends Transformer {
'formattedDeliveredAtDate',
'formattedApprovedAtDate',
'formattedRejectedAtDate',
'discountAmountFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'totalFormatted',
'totalLocalFormatted',
'formattedCreatedAt',
'entries',
'attachments',
@@ -98,6 +103,62 @@ export class SaleEstimateTransfromer extends Transformer {
return formatNumber(estimate.amount, { money: false });
};
/**
* Retrieves formatted discount amount.
* @param estimate
* @returns {string}
*/
protected discountAmountFormatted = (estimate: ISaleEstimate): string => {
return formatNumber(estimate.discountAmount, {
currencyCode: estimate.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param estimate
* @returns {string}
*/
protected discountPercentageFormatted = (estimate: ISaleEstimate): string => {
return estimate.discountPercentage
? `${estimate.discountPercentage}%`
: '';
};
/**
* Retrieves formatted adjustment amount.
* @param estimate
* @returns {string}
*/
protected adjustmentFormatted = (estimate: ISaleEstimate): string => {
return this.formatMoney(estimate.adjustment, {
currencyCode: estimate.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted estimate total.
* @returns {string}
*/
protected totalFormatted = (estimate: ISaleEstimate): string => {
return this.formatMoney(estimate.total, {
currencyCode: estimate.currencyCode,
});
};
/**
* Retrieves the formatted estimate total in local currency.
* @param estimate
* @returns {string}
*/
protected totalLocalFormatted = (estimate: ISaleEstimate): string => {
return this.formatMoney(estimate.totalLocal, {
currencyCode: estimate.currencyCode,
});
};
/**
* Retrieves the entries of the sale estimate.
* @param {ISaleEstimate} estimate

View File

@@ -21,6 +21,7 @@ import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import { GetSaleEstimateState } from './GetSaleEstimateState';
import { GetSaleEstimateMailState } from './GetSaleEstimateMailState';
@Service()
export class SaleEstimatesApplication {
@@ -57,6 +58,9 @@ export class SaleEstimatesApplication {
@Inject()
private sendEstimateMailService: SendSaleEstimateMail;
@Inject()
private getSaleEstimateMailStateService: GetSaleEstimateMailState;
@Inject()
private getSaleEstimateStateService: GetSaleEstimateState;
@@ -220,6 +224,18 @@ export class SaleEstimatesApplication {
);
}
/**
* Retrieve the HTML content of the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public getSaleEstimateHtml(tenantId: number, saleEstimateId: number) {
return this.saleEstimatesPdfService.saleEstimateHtml(
tenantId,
saleEstimateId
);
}
/**
* Send the reminder mail of the given sale estimate.
* @param {number} tenantId
@@ -238,6 +254,22 @@ export class SaleEstimatesApplication {
);
}
/**
* Retrieves the sale estimate mail state.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailOptions>}
*/
public getSaleEstimateMailState(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailOptions> {
return this.getSaleEstimateMailStateService.getEstimateMailState(
tenantId,
saleEstimateId
);
}
/**
* Retrieves the default mail options of the given sale estimate.
* @param {number} tenantId

View File

@@ -1,13 +1,12 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleEstimate } from './GetSaleEstimate';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
import { transformEstimateToPdfTemplate } from './utils';
import { EstimatePdfBrandingAttributes } from './constants';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { renderEstimatePaperTemplateHtml, EstimatePaperTemplateProps } from '@bigcapital/pdf-templates';
@Service()
export class SaleEstimatesPdf {
@@ -17,9 +16,6 @@ export class SaleEstimatesPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getSaleEstimate: GetSaleEstimate;
@@ -29,6 +25,22 @@ export class SaleEstimatesPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve sale estimate html content.
* @param {number} tenantId -
* @param {number} invoiceId -
*/
public async saleEstimateHtml(
tenantId: number,
estimateId: number
): Promise<string> {
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
estimateId
);
return renderEstimatePaperTemplateHtml({ ...brandingAttributes });
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -42,15 +54,10 @@ export class SaleEstimatesPdf {
tenantId,
saleEstimateId
);
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
saleEstimateId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/estimate-regular',
brandingAttributes
);
// Retireves the sale estimate html.
const htmlContent = await this.saleEstimateHtml(tenantId, saleEstimateId);
// Converts the html content to pdf.
const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
@@ -88,7 +95,7 @@ export class SaleEstimatesPdf {
async getEstimateBrandingAttributes(
tenantId: number,
estimateId: number
): Promise<EstimatePdfBrandingAttributes> {
): Promise<EstimatePaperTemplateProps> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const saleEstimate = await this.getSaleEstimate.getEstimate(
tenantId,

View File

@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformEstimateToMailDataArgs } from './utils';
import { GetEstimateMailTemplate } from './GetEstimateMailTemplate';
@Service()
export class SendSaleEstimateMail {
@@ -32,12 +33,15 @@ export class SendSaleEstimateMail {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject('agenda')
private agenda: any;
@Inject()
private getEstimateMailTemplate: GetEstimateMailTemplate;
@Inject()
private eventPublisher: EventPublisher;
@Inject('agenda')
private agenda: any;
/**
* Triggers the reminder mail of the given sale estimate.
* @param {number} tenantId -
@@ -76,7 +80,13 @@ export class SendSaleEstimateMail {
tenantId,
estimateId
);
return transformEstimateToMailDataArgs(estimate);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
return {
...commonArgs,
...transformEstimateToMailDataArgs(estimate),
};
};
/**
@@ -132,9 +142,45 @@ export class SendSaleEstimateMail {
mailOptions,
formatterArgs
);
return { ...formattedOptions };
// Retrieves the estimate mail template.
const message = await this.getEstimateMailTemplate.getMailTemplate(
tenantId,
saleEstimateId,
{
message: formattedOptions.message,
preview: formattedOptions.message,
}
);
return { ...formattedOptions, message };
};
/**
* Retrieves the formatted mail options.
* @param {number} tenantId
* @param {number} saleEstimateId
* @param {SaleEstimateMailOptionsDTO} messageOptions
* @returns
*/
public async getFormattedMailOptions(
tenantId: number,
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO
): Promise<SaleEstimateMailOptions> {
const defaultMessageOptions = await this.getMailOptions(
tenantId,
saleEstimateId
);
const parsedMessageOptions = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOptions
);
return this.formatMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
);
}
/**
* Sends the mail notification of the given sale estimate.
* @param {number} tenantId
@@ -147,20 +193,10 @@ export class SendSaleEstimateMail {
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO
): Promise<void> {
const localMessageOpts = await this.getMailOptions(
tenantId,
saleEstimateId
);
// Overrides and validates the given mail options.
const parsedMessageOptions = mergeAndValidateMailOptions(
localMessageOpts,
messageOptions
) as SaleEstimateMailOptions;
const formattedOptions = await this.formatMailOptions(
const formattedOptions = await this.getFormattedMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
messageOptions
);
const mail = new Mail()
.setSubject(formattedOptions.subject)
@@ -182,7 +218,6 @@ export class SendSaleEstimateMail {
},
]);
}
const eventPayload = {
tenantId,
saleEstimateId,

View File

@@ -1,18 +1,17 @@
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
'Estimate {Estimate Number} is awaiting your approval';
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
<p>
Estimate <strong>#{Estimate Number}</strong><br />
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
Amount : <strong>{Estimate Amount}</strong></br />
</p>
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `Hi {Customer Name},
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Here's estimate # {Estimate Number} for {Estimate Amount}
This estimate is valid until {Estimate Expiration Date}, and were happy to discuss any adjustments you or questions may have.
Please find your estimate attached to this email for your reference.
If you have any questions, please let us know.
Thanks,
{Company Name}`;
export const ERRORS = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
@@ -255,18 +254,27 @@ export interface EstimatePdfBrandingAttributes {
companyAddress: string;
billedToLabel: string;
// # Total
total: string;
totalLabel: string;
showTotal: boolean;
// # Discount
discount: string;
showDiscount: boolean;
discountLabel: string;
// # Subtotal
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
// # Customer Note
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
// # Terms & Conditions
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;

View File

@@ -1,9 +1,9 @@
import { EstimatePaperTemplateProps } from '@bigcapital/pdf-templates';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { EstimatePdfBrandingAttributes } from './constants';
export const transformEstimateToPdfTemplate = (
estimate
): Partial<EstimatePdfBrandingAttributes> => {
): Partial<EstimatePaperTemplateProps> => {
return {
expirationDate: estimate.formattedExpirationDate,
estimateNumebr: estimate.estimateNumber,
@@ -13,13 +13,20 @@ export const transformEstimateToPdfTemplate = (
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
total: estimate.formattedSubtotal,
total: estimate.totalFormatted,
subtotal: estimate.formattedSubtotal,
adjustment: estimate.adjustmentFormatted,
customerNote: estimate.note,
termsConditions: estimate.termsConditions,
customerAddress: contactAddressTextFormat(estimate.customer),
showLineDiscount: estimate.entries.some((entry) => entry.discountFormatted),
discount: estimate.discountAmountFormatted,
discountLabel: estimate.discountPercentageFormatted
? `Discount [${estimate.discountPercentageFormatted}]`
: 'Discount',
};
};

View File

@@ -154,6 +154,6 @@ export class CommandSaleInvoiceDTOTransformer {
* @returns {number}
*/
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
return sumBy(entries, (e) => e.amount);
return sumBy(entries, (e) => e.total);
};
}

View File

@@ -23,6 +23,15 @@ export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
'invoiceNumber',
'invoiceNumberLabel',
'subtotal',
'subtotalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'total',
'totalLabel',
@@ -76,6 +85,30 @@ export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
return 'Invoice # {invoiceNumber}';
}
public subtotal(): string {
return this.options.invoice?.subtotalFormatted;
}
public subtotalLabel(): string {
return 'Subtotal';
}
public discount(): string {
return this.options.invoice?.discountAmountFormatted;
}
public discountLabel(): string {
return 'Discount';
}
public adjustment(): string {
return this.options.invoice?.adjustmentFormatted;
}
public adjustmentLabel(): string {
return 'Adjustment';
}
public total(): string {
return this.options.invoice?.totalFormatted;
}

View File

@@ -28,6 +28,15 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
'total',
'totalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'subtotal',
'subtotalFormatted',
@@ -76,6 +85,17 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
return invoice.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the discount label of the estimate.
* @param estimate
* @returns {string}
*/
protected discountLabel(invoice) {
return invoice.discountType === 'percentage'
? `Discount [${invoice.discountPercentageFormatted}]`
: 'Discount';
}
/**
*
* @param invoice

View File

@@ -44,18 +44,31 @@ export class SaleInvoiceGLEntries {
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode, {}, trx
saleInvoice.currencyCode,
{},
trx
);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
{},
trx
);
// Find or create the discount expense account.
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{},
trx
);
// Find or create the other charges account.
const otherChargesAccount =
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieves the ledger of the invoice.
const ledger = this.getInvoiceGLedger(
saleInvoice,
ARAccount.id,
taxPayableAccount.id
taxPayableAccount.id,
discountAccount.id,
otherChargesAccount.id
);
// Commits the ledger entries to the storage as UOW.
await this.ledegrRepository.commit(tenantId, ledger, trx);
@@ -107,12 +120,16 @@ export class SaleInvoiceGLEntries {
public getInvoiceGLedger = (
saleInvoice: ISaleInvoice,
ARAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
discountAccountId: number,
otherChargesAccountId: number
): ILedger => {
const entries = this.getInvoiceGLEntries(
saleInvoice,
ARAccountId,
taxPayableAccountId
taxPayableAccountId,
discountAccountId,
otherChargesAccountId
);
return new Ledger(entries);
};
@@ -127,6 +144,7 @@ export class SaleInvoiceGLEntries {
): Partial<ILedgerEntry> => ({
credit: 0,
debit: 0,
currencyCode: saleInvoice.currencyCode,
exchangeRate: saleInvoice.exchangeRate,
@@ -181,7 +199,7 @@ export class SaleInvoiceGLEntries {
index: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;
const localAmount = entry.totalExcludingTax * saleInvoice.exchangeRate;
return {
...commonEntry,
@@ -249,6 +267,50 @@ export class SaleInvoiceGLEntries {
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the invoice discount GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getInvoiceDiscountEntry = (
saleInvoice: ISaleInvoice,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
return {
...commonEntry,
debit: saleInvoice.discountAmountLocal,
accountId: discountAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
} as ILedgerEntry;
};
/**
* Retrieves the invoice adjustment GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
saleInvoice: ISaleInvoice,
otherChargesAccountId: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
const adjustmentAmount = Math.abs(saleInvoice.adjustmentLocal);
return {
...commonEntry,
debit: saleInvoice.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: saleInvoice.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: otherChargesAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the invoice GL entries.
* @param {ISaleInvoice} saleInvoice
@@ -258,7 +320,9 @@ export class SaleInvoiceGLEntries {
public getInvoiceGLEntries = (
saleInvoice: ISaleInvoice,
ARAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
discountAccountId: number,
otherChargesAccountId: number
): ILedgerEntry[] => {
const receivableEntry = this.getInvoiceReceivableEntry(
saleInvoice,
@@ -271,6 +335,20 @@ export class SaleInvoiceGLEntries {
saleInvoice,
taxPayableAccountId
);
return [receivableEntry, ...creditEntries, ...taxEntries];
const discountEntry = this.getInvoiceDiscountEntry(
saleInvoice,
discountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
saleInvoice,
otherChargesAccountId
);
return [
receivableEntry,
...creditEntries,
...taxEntries,
discountEntry,
adjustmentEntry,
];
};
}

View File

@@ -1,4 +1,4 @@
import { IItemEntry } from '@/interfaces';
import { DiscountType, IItemEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils';
@@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
return [
'quantityFormatted',
'rateFormatted',
'totalFormatted',
'discountFormatted',
'discountAmountFormatted',
];
};
/**
@@ -43,4 +49,34 @@ export class ItemEntryTransformer extends Transformer {
money: false,
});
};
/**
* Retrieves the formatted discount of item entry.
* @param {IItemEntry} entry
* @returns {string}
*/
protected discountFormatted = (entry: IItemEntry): string => {
if (!entry.discount) {
return '';
}
return entry.discountType === DiscountType.Percentage
? `${entry.discount}%`
: formatNumber(entry.discount, {
currencyCode: this.context.currencyCode,
money: false,
});
};
/**
* Retrieves the formatted discount amount of item entry.
* @param {IItemEntry} entry
* @returns {string}
*/
protected discountAmountFormatted = (entry: IItemEntry): string => {
return formatNumber(entry.discountAmount, {
currencyCode: this.context.currencyCode,
money: false,
excerptZero: true,
});
};
}

View File

@@ -1,15 +1,15 @@
import { Inject, Service } from 'typedi';
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
import {
renderInvoicePaperTemplateHtml,
InvoicePaperTemplateProps,
} from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleInvoice } from './GetSaleInvoice';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformInvoiceToPdfTemplate } from './utils';
import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
@Service()
export class SaleInvoicePdf {
@@ -102,7 +102,7 @@ export class SaleInvoicePdf {
async getInvoiceBrandingAttributes(
tenantId: number,
invoiceId: number
): Promise<InvoicePdfTemplateAttributes> {
): Promise<InvoicePaperTemplateProps> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const invoice = await this.getInvoiceService.getSaleInvoice(

View File

@@ -3,6 +3,7 @@ import { formatNumber } from 'utils';
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
import { ItemEntryTransformer } from './ItemEntryTransformer';
import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
import { DiscountType } from '@/interfaces';
export class SaleInvoiceTransformer extends Transformer {
/**
@@ -23,6 +24,9 @@ export class SaleInvoiceTransformer extends Transformer {
'subtotalExludingTaxFormatted',
'taxAmountWithheldFormatted',
'taxAmountWithheldLocalFormatted',
'discountAmountFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
@@ -158,6 +162,41 @@ export class SaleInvoiceTransformer extends Transformer {
});
};
/**
* Retrieves formatted discount amount.
* @param invoice
* @returns {string}
*/
protected discountAmountFormatted = (invoice): string => {
return formatNumber(invoice.discountAmount, {
currencyCode: invoice.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param invoice
* @returns {string}
*/
protected discountPercentageFormatted = (invoice): string => {
return invoice.discountType === DiscountType.Percentage
? `${invoice.discount}%`
: '';
};
/**
* Retrieves formatted adjustment amount.
* @param invoice
* @returns {string}
*/
protected adjustmentFormatted = (invoice): string => {
return this.formatMoney(invoice.adjustment, {
currencyCode: invoice.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted total in foreign currency.
* @param invoice

View File

@@ -1,6 +1,7 @@
import { pickBy } from 'lodash';
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
import { ISaleInvoice } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { InvoicePaperTemplateProps } from '@bigcapital/pdf-templates';
export const mergePdfTemplateWithDefaultAttributes = (
brandingTemplate?: Record<string, any>,
@@ -18,7 +19,7 @@ export const mergePdfTemplateWithDefaultAttributes = (
export const transformInvoiceToPdfTemplate = (
invoice: ISaleInvoice
): Partial<InvoicePdfTemplateAttributes> => {
): Partial<InvoicePaperTemplateProps> => {
return {
dueDate: invoice.dueDateFormatted,
dateIssue: invoice.invoiceDateFormatted,
@@ -28,6 +29,11 @@ export const transformInvoiceToPdfTemplate = (
subtotal: invoice.subtotalFormatted,
paymentMade: invoice.paymentAmountFormatted,
dueAmount: invoice.dueAmountFormatted,
discount: invoice.discountAmountFormatted,
adjustment: invoice.adjustmentFormatted,
discountLabel: invoice.discountPercentageFormatted
? `Discount [${invoice.discountPercentageFormatted}]`
: 'Discount',
termsConditions: invoice.termsConditions,
statement: invoice.invoiceMessage,
@@ -37,13 +43,14 @@ export const transformInvoiceToPdfTemplate = (
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
taxes: invoice.taxes.map((tax) => ({
label: tax.name,
amount: tax.taxRateAmountFormatted,
})),
showLineDiscount: invoice.entries.some((entry) => entry.discountFormatted),
customerAddress: contactAddressTextFormat(invoice.customer),
};
};

View File

@@ -0,0 +1,52 @@
import { PaymentReceiveMailOpts } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetPaymentReceivedMailStateTransformer } from './GetPaymentReceivedMailStateTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { Inject, Service } from 'typedi';
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
@Service()
export class GetPaymentReceivedMailState {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private paymentReceivedMail: SendPaymentReceiveMailNotification;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the default payment mail options.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public getMailOptions = async (
tenantId: number,
paymentId: number
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findById(paymentId)
.withGraphFetched('customer')
.withGraphFetched('entries.invoice')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions = await this.paymentReceivedMail.getMailOptions(
tenantId,
paymentId
);
const transformed = await this.transformer.transform(
tenantId,
paymentReceive,
new GetPaymentReceivedMailStateTransformer(),
{
mailOptions,
}
);
return transformed;
};
}

View File

@@ -0,0 +1,186 @@
import { PaymentReceiveEntry } from '@/models';
import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer';
import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer';
export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransfromer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'paymentDate',
'paymentDateFormatted',
'paymentAmount',
'paymentAmountFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'paymentNumber',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the payment.
* @returns {string}
*/
protected customerName = (payment) => {
return payment.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 = (payment) => {
return payment.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (payment) => {
return payment.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the formatted payment date.
* @returns {string}
*/
protected paymentDateFormatted = (payment) => {
return this.formatDate(payment.paymentDate);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected total = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected totalFormatted = (payment) => {
return this.formatMoney(payment.amount);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected subtotal = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected subtotalFormatted = (payment) => {
return this.formatMoney(payment.amount);
};
/**
* Retrieves the payment number.
* @param payment
* @returns {string}
*/
protected paymentNumber = (payment) => {
return payment.paymentReceiveNo;
};
/**
* Retrieves the payment entries.
* @param {IPaymentReceived} payment
* @returns {IPaymentReceivedEntry[]}
*/
protected entries = (payment) => {
return this.item(payment.entries, new GetPaymentReceivedEntryMailState());
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
export class GetPaymentReceivedEntryMailState extends PaymentReceivedEntryTransfromer {
/**
* Include these attributes to payment receive entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['paidAmount', 'invoiceNumber'];
};
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the paid amount.
* @param entry
* @returns {string}
*/
public paidAmount = (entry) => {
return this.paymentAmountFormatted(entry);
};
/**
* Retrieves the invoice number.
* @param entry
* @returns {string}
*/
public invoiceNumber = (entry) => {
return entry.invoice.invoiceNo;
};
}

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { GetPaymentReceived } from './GetPaymentReceived';
import { GetPaymentReceivedMailTemplateAttrsTransformer } from './GetPaymentReceivedMailTemplateAttrsTransformer';
import {
PaymentReceivedEmailTemplateProps,
renderPaymentReceivedEmailTemplate,
} from '@bigcapital/email-components';
@Service()
export class GetPaymentReceivedMailTemplate {
@Inject()
private getPaymentReceivedService: GetPaymentReceived;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the mail template attributes of the given payment received.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceivedId - Payment received id.
* @returns {Promise<PaymentReceivedEmailTemplateProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
paymentReceivedId: number
): Promise<PaymentReceivedEmailTemplateProps> {
const paymentReceived =
await this.getPaymentReceivedService.getPaymentReceive(
tenantId,
paymentReceivedId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
paymentReceived.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
paymentReceived,
new GetPaymentReceivedMailTemplateAttrsTransformer(),
{
paymentReceived,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @param {Partial<PaymentReceivedEmailTemplateProps>} overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
paymentReceivedId: number,
overrideAttributes?: Partial<PaymentReceivedEmailTemplateProps>
): Promise<string> {
const mailTemplateAttributes = await this.getMailTemplateAttributes(
tenantId,
paymentReceivedId
);
const mergedAttributes = {
...mailTemplateAttributes,
...overrideAttributes,
};
return renderPaymentReceivedEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,149 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetPaymentReceivedMailTemplateAttrsTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'primaryColor',
'total',
'totalLabel',
'subtotal',
'subtotalLabel',
'paymentNumberLabel',
'paymentNumber',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Total.
* @returns {string}
*/
public total(): string {
return this.options.paymentReceived.formattedAmount;
}
/**
* Total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Subtotal.
* @returns {string}
*/
public subtotal(): string {
return this.options.paymentReceived.formattedAmount;
}
/**
* Subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Payment number label.
* @returns {string}
*/
public paymentNumberLabel(): string {
return 'Payment # {paymentNumber}';
}
/**
* Payment number.
* @returns {string}
*/
public paymentNumber(): string {
return this.options.paymentReceived.paymentReceiveNumber;
}
/**
* Items.
* @returns
*/
public items() {
return this.item(
this.options.paymentReceived.entries,
new GetPaymentReceivedMailTemplateItemAttrsTransformer()
);
}
}
class GetPaymentReceivedMailTemplateItemAttrsTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = () => {
return ['label', 'total'];
};
/**
* Excluded attributes.
* @returns {string[]}
*/
public excludeAttributes = () => {
return ['*'];
};
/**
*
* @param entry
* @returns
*/
public label(entry) {
return entry.invoice.invoiceNo;
}
/**
*
* @param entry
* @returns
*/
public total(entry) {
return entry.paymentAmountFormatted;
}
}

View File

@@ -1,13 +1,13 @@
import { Inject, Service } from 'typedi';
import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetPaymentReceived } from './GetPaymentReceived';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
import { transformPaymentReceivedToPdfTemplate } from './utils';
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class GetPaymentReceivedPdf {
@@ -17,9 +17,6 @@ export default class GetPaymentReceivedPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getPaymentService: GetPaymentReceived;
@@ -29,6 +26,23 @@ export default class GetPaymentReceivedPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves payment received html content.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
public async getPaymentReceivedHtml(
tenantId: number,
paymentReceivedId: number
): Promise<string> {
const brandingAttributes = await this.getPaymentBrandingAttributes(
tenantId,
paymentReceivedId
);
return renderPaymentReceivedPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -39,15 +53,10 @@ export default class GetPaymentReceivedPdf {
tenantId: number,
paymentReceivedId: number
): Promise<[Buffer, string]> {
const brandingAttributes = await this.getPaymentBrandingAttributes(
const htmlContent = await this.getPaymentReceivedHtml(
tenantId,
paymentReceivedId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/payment-receive-standard',
brandingAttributes
);
const filename = await this.getPaymentReceivedFilename(
tenantId,
paymentReceivedId

View File

@@ -20,6 +20,7 @@ import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify';
import GetPaymentReceivedPdf from './GetPaymentReceivedPdf';
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
import { GetPaymentReceivedState } from './GetPaymentReceivedState';
import { GetPaymentReceivedMailState } from './GetPaymentReceivedMailState';
@Service()
export class PaymentReceivesApplication {
@@ -53,6 +54,9 @@ export class PaymentReceivesApplication {
@Inject()
private getPaymentReceivedStateService: GetPaymentReceivedState;
@Inject()
private getPaymentReceivedMailStateService: GetPaymentReceivedMailState;
/**
* Creates a new payment receive.
* @param {number} tenantId
@@ -204,12 +208,15 @@ export class PaymentReceivesApplication {
/**
* Retrieves the default mail options of the given payment transaction.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment received id.
* @returns {Promise<void>}
*/
public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) {
return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId);
return this.getPaymentReceivedMailStateService.getMailOptions(
tenantId,
paymentReceiveId
);
}
/**
@@ -228,6 +235,22 @@ export class PaymentReceivesApplication {
);
};
/**
* Retrieves the given payment receive html document.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @returns {Promise<string>}
*/
public getPaymentReceivedHtml = (
tenantId: number,
paymentReceiveId: number
) => {
return this.getPaymentReceivePdfService.getPaymentReceivedHtml(
tenantId,
paymentReceiveId
);
};
/**
* Retrieves the create/edit initial state of the payment received.
* @param {number} tenantId - The ID of the tenant.

View File

@@ -15,8 +15,9 @@ import { GetPaymentReceived } from './GetPaymentReceived';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformPaymentReceivedToMailDataArgs } from './utils';
import { GetPaymentReceivedMailTemplate } from './GetPaymentReceivedMailTemplate';
import events from '@/subscribers/events';
@Service()
export class SendPaymentReceiveMailNotification {
@@ -29,12 +30,15 @@ export class SendPaymentReceiveMailNotification {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject('agenda')
private agenda: any;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private paymentMailTemplate: GetPaymentReceivedMailTemplate;
@Inject('agenda')
private agenda: any;
/**
* Sends the mail of the given payment receive.
* @param {number} tenantId
@@ -62,37 +66,6 @@ export class SendPaymentReceiveMailNotification {
} as PaymentReceiveMailPresendEvent);
}
/**
* Retrieves the default payment mail options.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public getMailOptions = async (
tenantId: number,
paymentId: number
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findById(paymentId)
.throwIfNotFound();
const formatArgs = await this.textFormatter(tenantId, paymentId);
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceive.customerId
);
return {
...mailOptions,
subject: DEFAULT_PAYMENT_MAIL_SUBJECT,
message: DEFAULT_PAYMENT_MAIL_CONTENT,
...formatArgs,
};
};
/**
* Retrieves the formatted text of the given sale invoice.
* @param {number} tenantId - Tenant id.
@@ -108,7 +81,82 @@ export class SendPaymentReceiveMailNotification {
tenantId,
invoiceId
);
return transformPaymentReceivedToMailDataArgs(payment);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
const paymentArgs = transformPaymentReceivedToMailDataArgs(payment);
return {
...commonArgs,
...paymentArgs,
};
};
/**
* Retrieves the mail options of the given payment received.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceivedId - Payment received id.
* @param {string} defaultSubject - Default subject of the mail.
* @param {string} defaultContent - Default content of the mail.
* @returns
*/
public getMailOptions = async (
tenantId: number,
paymentReceivedId: number,
defaultSubject: string = DEFAULT_PAYMENT_MAIL_SUBJECT,
defaultContent: string = DEFAULT_PAYMENT_MAIL_CONTENT
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceived = await PaymentReceive.query().findById(
paymentReceivedId
);
const formatArgs = await this.textFormatter(tenantId, paymentReceivedId);
// Retrieves the default mail options.
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceived.customerId
);
return {
...mailOptions,
message: defaultContent,
subject: defaultSubject,
attachPdf: true,
formatArgs,
};
};
/**
* Formats the mail options of the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {PaymentReceiveMailOpts} mailOptions
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public formattedMailOptions = async (
tenantId: number,
paymentReceiveId: number,
mailOptions: PaymentReceiveMailOpts
): Promise<PaymentReceiveMailOpts> => {
const formatterArgs = await this.textFormatter(tenantId, paymentReceiveId);
const formattedOptions =
await this.contactMailNotification.formatMailOptions(
tenantId,
mailOptions,
formatterArgs
);
// Retrieves the mail template.
const message = await this.paymentMailTemplate.getMailTemplate(
tenantId,
paymentReceiveId,
{
message: formattedOptions.message,
preview: formattedOptions.message,
}
);
return { ...formattedOptions, message };
};
/**
@@ -136,10 +184,10 @@ export class SendPaymentReceiveMailNotification {
messageDTO
);
// Formats the message options.
return this.contactMailNotification.formatMailOptions(
return this.formattedMailOptions(
tenantId,
parsedMessageOpts,
formatterArgs
paymentReceiveId,
parsedMessageOpts
);
};

View File

@@ -1,18 +1,15 @@
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Payment Received for {Customer Name} from {Company Name}';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
<p>
Payment Date : <strong>{Payment Date}</strong><br />
Amount : <strong>{Payment Amount}</strong></br />
</p>
' Payment Confirmation from {Company Name} Thank You!';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `Dear {Customer Name}
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!
Payment Transaction: {Payment Number}
Payment Date : {Payment Date}
Amount : {Payment Amount}
Regards,
{Company Name}`;
export const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
import { GetSaleReceiptMailStateTransformer } from './GetSaleReceiptMailStateTransformer';
@Service()
export class GetSaleReceiptMailState {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private receiptMail: SaleReceiptMailNotification;
/**
* Retrieves the sale receipt mail state of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
*/
public async getMailState(tenantId: number, saleReceiptId: number) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.throwIfNotFound();
const mailOptions = await this.receiptMail.getMailOptions(
tenantId,
saleReceiptId
);
return this.transformer.transform(
tenantId,
saleReceipt,
new GetSaleReceiptMailStateTransformer(),
{
mailOptions,
}
);
}
}

View File

@@ -0,0 +1,221 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
import { DiscountType } from '@/interfaces';
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
export class GetSaleReceiptMailStateTransformer extends SaleReceiptTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
'total',
'totalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'subtotal',
'subtotalFormatted',
'receiptDate',
'receiptDateFormatted',
'closedAtDate',
'closedAtDateFormatted',
'receiptNumber',
'entries',
];
};
/**
* Retrieves the customer name of the invoice.
* @returns {string}
*/
protected customerName = (receipt) => {
return receipt.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 = (receipt) => {
return receipt.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (receipt) => {
return receipt.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the total amount.
* @param receipt
* @returns
*/
protected total = (receipt) => {
return receipt.total;
};
/**
* Retrieves the formatted total amount.
* @param receipt
* @returns {string}
*/
protected totalFormatted = (receipt) => {
return this.formatMoney(receipt.total, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the discount label of the estimate.
* @param estimate
* @returns {string}
*/
protected discountLabel(receipt) {
return receipt.discountType === DiscountType.Percentage
? `Discount [${receipt.discountPercentageFormatted}]`
: 'Discount';
}
/**
* Retrieves the subtotal of the receipt.
* @param receipt
* @returns
*/
protected subtotal = (receipt) => {
return receipt.subtotal;
};
/**
* Retrieves the formatted subtotal of the receipt.
* @param receipt
* @returns
*/
protected subtotalFormatted = (receipt) => {
return this.formatMoney(receipt.subtotal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the receipt date.
* @param receipt
* @returns
*/
protected receiptDate = (receipt): string => {
return receipt.receiptDate;
};
/**
* Retrieves the formatted receipt date.
* @param {ISaleReceipt} invoice
* @returns {string}
*/
protected receiptDateFormatted = (receipt): string => {
return this.formatDate(receipt.receiptDate);
};
/**
*
* @param receipt
* @returns
*/
protected closedAtDate = (receipt): string => {
return receipt.closedAt;
};
/**
* Retrieve formatted estimate closed at date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected closedAtDateFormatted = (receipt): string => {
return this.formatDate(receipt.closedAt);
};
/**
*
* @param invoice
* @returns
*/
protected entries = (receipt) => {
return this.item(
receipt.entries,
new GetSaleReceiptEntryMailStateTransformer(),
{
currencyCode: receipt.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleReceiptEntryMailStateTransformer 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',
];
};
}

View File

@@ -0,0 +1,75 @@
import {
ReceiptEmailTemplateProps,
renderReceiptEmailTemplate,
} from '@bigcapital/email-components';
import { Inject, Service } from 'typedi';
import { GetSaleReceipt } from './GetSaleReceipt';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetSaleReceiptMailTemplateAttributesTransformer } from './GetSaleReceiptMailTemplateAttributesTransformer';
@Service()
export class GetSaleReceiptMailTemplate {
@Inject()
private getReceiptService: GetSaleReceipt;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
/**
* Retrieves the mail template attributes of the given estimate.
* Estimate template attributes are composed of the estimate and branding template attributes.
* @param {number} tenantId
* @param {number} receiptId - Receipt id.
* @returns {Promise<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
receiptId: number
): Promise<ReceiptEmailTemplateProps> {
const receipt = await this.getReceiptService.getSaleReceipt(
tenantId,
receiptId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
receipt.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
receipt,
new GetSaleReceiptMailTemplateAttributesTransformer(),
{
receipt,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId
* @param {number} estimateId
* @param overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
estimateId: number,
overrideAttributes?: Partial<any>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
estimateId
);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderReceiptEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,201 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetSaleReceiptMailTemplateAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'primaryColor',
'receiptAmount',
'receiptMessage',
'date',
'dateLabel',
'receiptNumber',
'receiptNumberLabel',
'total',
'totalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'subtotal',
'subtotalLabel',
'paidAmount',
'paidAmountLabel',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Receipt number.
* @returns {string}
*/
public receiptNumber(): string {
return this.options.receipt.receiptNumber;
}
/**
* Receipt number label.
* @returns {string}
*/
public receiptNumberLabel(): string {
return 'Receipt # {receiptNumber}';
}
/**
* Date.
* @returns {string}
*/
public date(): string {
return this.options.receipt.date;
}
/**
* Date label.
* @returns {string}
*/
public dateLabel(): string {
return 'Date';
}
/**
* Receipt total.
*/
public total(): string {
return this.options.receipt.totalFormatted;
}
/**
* Receipt total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Receipt discount.
* @returns {string}
*/
public discount(): string {
return this.options.receipt?.discountAmountFormatted;
}
/**
* Receipt discount label.
* @returns {string}
*/
public discountLabel(): string {
return 'Discount';
}
/**
* Receipt adjustment.
* @returns {string}
*/
public adjustment(): string {
return this.options.receipt?.adjustmentFormatted;
}
/**
* Receipt adjustment label.
* @returns {string}
*/
public adjustmentLabel(): string {
return 'Adjustment';
}
/**
* Receipt subtotal.
* @returns {string}
*/
public subtotal(): string {
return this.options.receipt.subtotalFormatted;
}
/**
* Receipt subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Receipt mail items attributes.
*/
public items(): any[] {
return this.item(
this.options.receipt.entries,
new GetSaleReceiptMailTemplateEntryAttributesTransformer()
);
}
}
class GetSaleReceiptMailTemplateEntryAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return ['label', 'quantity', 'rate', 'total'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public label(entry): string {
return entry?.item?.name;
}
public quantity(entry): string {
return entry?.quantity;
}
public rate(entry): string {
return entry?.rateFormatted;
}
public total(entry): string {
return entry?.totalFormatted;
}
}

View File

@@ -8,7 +8,7 @@ export class GetSaleReceiptState {
private tenancy: HasTenancyService;
/**
* Retireves the sale receipt state.
* Retrieves the sale receipt state.
* @param {Number} tenantId -
* @return {Promise<ISaleReceiptState>}
*/

View File

@@ -18,6 +18,7 @@ import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
import { GetSaleReceiptState } from './GetSaleReceiptState';
import { GetSaleReceiptMailState } from './GetSaleReceiptMailState';
@Service()
export class SaleReceiptApplication {
@@ -51,6 +52,9 @@ export class SaleReceiptApplication {
@Inject()
private getSaleReceiptStateService: GetSaleReceiptState;
@Inject()
private getSaleReceiptMailStateService: GetSaleReceiptMailState;
/**
* Creates a new sale receipt with associated entries.
* @param {number} tenantId
@@ -152,6 +156,19 @@ export class SaleReceiptApplication {
);
}
/**
* Retrieves the given sale receipt html.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns {Promise<string>}
*/
public getSaleReceiptHtml(tenantId: number, saleReceiptId: number) {
return this.getSaleReceiptPdfService.saleReceiptHtml(
tenantId,
saleReceiptId
);
}
/**
* Notify receipt customer by SMS of the given sale receipt.
* @param {number} tenantId
@@ -221,4 +238,20 @@ export class SaleReceiptApplication {
public getSaleReceiptState(tenantId: number): Promise<ISaleReceiptState> {
return this.getSaleReceiptStateService.getSaleReceiptState(tenantId);
}
/**
* Retrieves the mail state of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public getSaleReceiptMailState(
tenantId: number,
saleReceiptId: number
): Promise<ISaleReceiptState> {
return this.getSaleReceiptMailStateService.getMailState(
tenantId,
saleReceiptId
);
}
}

View File

@@ -31,13 +31,27 @@ export class SaleReceiptGLEntries {
trx?: Knex.Transaction
): Promise<void> => {
const { SaleReceipt } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const saleReceipt = await SaleReceipt.query(trx)
.findById(saleReceiptId)
.withGraphFetched('entries.item');
// Find or create the discount expense account.
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{},
trx
);
// Find or create the other charges account.
const otherChargesAccount =
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieve the income entries ledger.
const incomeLedger = this.getIncomeEntriesLedger(saleReceipt);
const incomeLedger = this.getIncomeEntriesLedger(
saleReceipt,
discountAccount.id,
otherChargesAccount.id
);
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(tenantId, incomeLedger, trx);
@@ -87,8 +101,16 @@ export class SaleReceiptGLEntries {
* @param {ISaleReceipt} saleReceipt
* @returns {Ledger}
*/
private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => {
const entries = this.getIncomeGLEntries(saleReceipt);
private getIncomeEntriesLedger = (
saleReceipt: ISaleReceipt,
discountAccountId: number,
otherChargesAccountId: number
): Ledger => {
const entries = this.getIncomeGLEntries(
saleReceipt,
discountAccountId,
otherChargesAccountId
);
return new Ledger(entries);
};
@@ -121,10 +143,10 @@ export class SaleReceiptGLEntries {
};
/**
* Retrieve receipt income item GL entry.
* @param {ISaleReceipt} saleReceipt -
* @param {IItemEntry} entry -
* @param {number} index -
* Retrieve receipt income item G/L entry.
* @param {ISaleReceipt} saleReceipt -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getReceiptIncomeItemEntry = R.curry(
@@ -134,11 +156,11 @@ export class SaleReceiptGLEntries {
index: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
const itemIncome = entry.amount * saleReceipt.exchangeRate;
const totalLocal = entry.totalExcludingTax * saleReceipt.exchangeRate;
return {
...commonEntry,
credit: itemIncome,
credit: totalLocal,
accountId: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
@@ -161,24 +183,76 @@ export class SaleReceiptGLEntries {
return {
...commonEntry,
debit: saleReceipt.localAmount,
debit: saleReceipt.totalLocal,
accountId: saleReceipt.depositAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the discount GL entry.
* @param {ISaleReceipt} saleReceipt
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getDiscountEntry = (
saleReceipt: ISaleReceipt,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
return {
...commonEntry,
debit: saleReceipt.discountAmountLocal,
accountId: discountAccountId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the adjustment GL entry.
* @param {ISaleReceipt} saleReceipt
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
saleReceipt: ISaleReceipt,
adjustmentAccountId: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
const adjustmentAmount = Math.abs(saleReceipt.adjustmentLocal);
return {
...commonEntry,
debit: saleReceipt.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: saleReceipt.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: adjustmentAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the income GL entries.
* @param {ISaleReceipt} saleReceipt -
* @returns {ILedgerEntry[]}
*/
private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => {
private getIncomeGLEntries = (
saleReceipt: ISaleReceipt,
discountAccountId: number,
otherChargesAccountId: number
): ILedgerEntry[] => {
const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt);
const creditEntries = saleReceipt.entries.map(getItemEntry);
const depositEntry = this.getReceiptDepositEntry(saleReceipt);
return [depositEntry, ...creditEntries];
const discountEntry = this.getDiscountEntry(saleReceipt, discountAccountId);
const adjustmentEntry = this.getAdjustmentEntry(
saleReceipt,
otherChargesAccountId
);
return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry];
};
}

View File

@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformReceiptToMailDataArgs } from './utils';
import { GetSaleReceiptMailTemplate } from './GetSaleReceiptMailTemplate';
@Service()
export class SaleReceiptMailNotification {
@@ -32,6 +33,9 @@ export class SaleReceiptMailNotification {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject()
private getReceiptMailTemplate: GetSaleReceiptMailTemplate;
@Inject()
private eventPublisher: EventPublisher;
@@ -111,7 +115,13 @@ export class SaleReceiptMailNotification {
tenantId,
receiptId
);
return transformReceiptToMailDataArgs(receipt);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
return {
...commonArgs,
...transformReceiptToMailDataArgs(receipt),
};
};
/**
@@ -133,7 +143,15 @@ export class SaleReceiptMailNotification {
mailOptions,
formatterArgs
)) as SaleReceiptMailOpts;
return formattedOptions;
const message = await this.getReceiptMailTemplate.getMailTemplate(
tenantId,
receiptId,
{
message: formattedOptions.message,
}
);
return { ...formattedOptions, message };
}
/**

View File

@@ -13,11 +13,24 @@ export class SaleReceiptTransformer extends Transformer {
*/
public includeAttributes = (): string[] => {
return [
'formattedSubtotal',
'discountAmountFormatted',
'discountPercentageFormatted',
'discountAmountLocalFormatted',
'subtotalFormatted',
'subtotalLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'formattedAmount',
'formattedReceiptDate',
'formattedClosedAtDate',
'formattedCreatedAt',
'paidFormatted',
'entries',
'attachments',
];
@@ -43,7 +56,7 @@ export class SaleReceiptTransformer extends Transformer {
/**
* Retrieve formatted receipt created at date.
* @param receipt
* @param receipt
* @returns {string}
*/
protected formattedCreatedAt = (receipt: ISaleReceipt): string => {
@@ -55,8 +68,41 @@ export class SaleReceiptTransformer extends Transformer {
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected formattedSubtotal = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, { money: false });
protected subtotalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.subtotal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the estimate formatted subtotal in local currency.
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected subtotalLocalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.subtotalLocal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the receipt formatted total.
* @param receipt
* @returns {string}
*/
protected totalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.total, { currencyCode: receipt.currencyCode });
};
/**
* Retrieves the receipt formatted total in local currency.
* @param receipt
* @returns {string}
*/
protected totalLocalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.totalLocal, {
currencyCode: receipt.currencyCode,
});
};
/**
@@ -64,12 +110,57 @@ export class SaleReceiptTransformer extends Transformer {
* @param {ISaleReceipt} estimate
* @returns {string}
*/
protected formattedAmount = (receipt: ISaleReceipt): string => {
protected amountFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves formatted discount amount.
* @param receipt
* @returns {string}
*/
protected discountAmountFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.discountAmount, {
currencyCode: receipt.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param receipt
* @returns {string}
*/
protected discountPercentageFormatted = (receipt: ISaleReceipt): string => {
return receipt.discountPercentage ? `${receipt.discountPercentage}%` : '';
};
/**
* Retrieves formatted paid amount.
* @param receipt
* @returns {string}
*/
protected paidFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.paid, {
currencyCode: receipt.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted adjustment amount.
* @param receipt
* @returns {string}
*/
protected adjustmentFormatted = (receipt: ISaleReceipt): string => {
return this.formatMoney(receipt.adjustment, {
currencyCode: receipt.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ISaleReceipt} credit

View File

@@ -1,11 +1,13 @@
import { Inject, Service } from 'typedi';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import {
renderReceiptPaperTemplateHtml,
ReceiptPaperTemplateProps,
} from '@bigcapital/pdf-templates';
import { GetSaleReceipt } from './GetSaleReceipt';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
import { transformReceiptToBrandingTemplateAttributes } from './utils';
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@@ -17,9 +19,6 @@ export class SaleReceiptsPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getSaleReceiptService: GetSaleReceipt;
@@ -29,6 +28,19 @@ export class SaleReceiptsPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves sale receipt html content.
* @param {number} tennatId
* @param {number} saleReceiptId
*/
public async saleReceiptHtml(tennatId: number, saleReceiptId: number) {
const brandingAttributes = await this.getReceiptBrandingAttributes(
tennatId,
saleReceiptId
);
return renderReceiptPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieves sale invoice pdf content.
* @param {number} tenantId -
@@ -41,16 +53,9 @@ export class SaleReceiptsPdf {
): Promise<[Buffer, string]> {
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
const brandingAttributes = await this.getReceiptBrandingAttributes(
tenantId,
saleReceiptId
);
// Converts the receipt template to html content.
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/receipt-regular',
brandingAttributes
);
const htmlContent = await this.saleReceiptHtml(tenantId, saleReceiptId);
// Renders the html content to pdf document.
const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
@@ -87,12 +92,12 @@ export class SaleReceiptsPdf {
* Retrieves receipt branding attributes.
* @param {number} tenantId
* @param {number} receiptId
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
* @returns {Promise<ReceiptPaperTemplateProps>}
*/
public async getReceiptBrandingAttributes(
tenantId: number,
receiptId: number
): Promise<ISaleReceiptBrandingTemplateAttributes> {
): Promise<ReceiptPaperTemplateProps> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(

View File

@@ -1,18 +1,17 @@
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
'Receipt {Receipt Number} from {Company Name}';
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
<p>
Receipt <strong>#{Receipt Number}</strong><br />
Amount : <strong>{Receipt Amount}</strong></br />
</p>
export const DEFAULT_RECEIPT_MAIL_CONTENT = `Hi {Customer Name},
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Here's receipt # {Receipt Number} for Receipt {Receipt Amount}
The receipt paid on {Receipt Date}, and the total amount paid is {Receipt Amount}.
Please find your sale receipt attached to this email for your reference
If you have any questions, please let us know.
Thanks,
{Company Name}`;
export const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',

View File

@@ -1,24 +1,30 @@
import {
ISaleReceipt,
ISaleReceiptBrandingTemplateAttributes,
} from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { ReceiptPaperTemplateProps } from '@bigcapital/pdf-templates';
export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt
): Partial<ISaleReceiptBrandingTemplateAttributes> => {
saleReceipt
): Partial<ReceiptPaperTemplateProps> => {
return {
total: saleReceipt.formattedAmount,
subtotal: saleReceipt.formattedSubtotal,
total: saleReceipt.totalFormatted,
subtotal: saleReceipt.subtotalFormatted,
lines: saleReceipt.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
receiptNumber: saleReceipt.receiptNumber,
receiptDate: saleReceipt.formattedReceiptDate,
discount: saleReceipt.discountAmountFormatted,
discountLabel: saleReceipt.discountPercentageFormatted
? `Discount [${saleReceipt.discountPercentageFormatted}]`
: 'Discount',
showLineDiscount: saleReceipt.entries.some(
(entry) => entry.discountFormatted
),
adjustment: saleReceipt.adjustmentFormatted,
customerAddress: contactAddressTextFormat(saleReceipt.customer),
};
};

View File

@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
import { keyBy, sumBy } from 'lodash';
import { ItemEntry } from '@/models';
import HasTenancyService from '../Tenancy/TenancyService';
import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces';
import { IItemEntry } from '@/interfaces';
@Service()
export class ItemEntriesTaxTransactions {