mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
Merge branch 'develop' into migrate-server-nestjs
This commit is contained in:
@@ -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,
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 we’re 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;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>}
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user