Skip to content

Commit

Permalink
Merge pull request #766 from bigcapitalhq/discount-line-level
Browse files Browse the repository at this point in the history
fix: Line-level discount
  • Loading branch information
abouolia authored Dec 12, 2024
2 parents 7af2e7c + d640dc1 commit 1906d9f
Show file tree
Hide file tree
Showing 37 changed files with 318 additions and 40 deletions.
5 changes: 5 additions & 0 deletions packages/server/src/api/controllers/Purchases/Bills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),

check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.landed_cost')
.optional({ nullable: true })
Expand Down
8 changes: 8 additions & 0 deletions packages/server/src/api/controllers/Purchases/VendorCredit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
Expand Down Expand Up @@ -225,6 +229,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/api/controllers/Sales/CreditNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ export default class PaymentReceivesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/api/controllers/Sales/SalesEstimates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ export default class SalesEstimatesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),

check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/api/controllers/Sales/SalesInvoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.tax_code')
.optional({ nullable: true })
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/api/controllers/Sales/SalesReceipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ export default class SalesReceiptsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),

check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.alterTable('items_entries', (table) => {
table.string('discount_type').defaultTo('percentage').after('discount');
});
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('items_entries', (table) => {
table.dropColumn('discount_type');
});
};
7 changes: 5 additions & 2 deletions packages/server/src/interfaces/ItemEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ export interface IItemEntry {

itemId: number;
description: string;
discountType?: string;
discount: number;
quantity: number;
rate: number;
amount: number;

total: number;
amountInclusingTax: number;
amountExludingTax: number;
totalExcludingTax?: number;

subtotalInclusingTax: number;
subtotalExcludingTax: number;
discountAmount: number;

landedCost: number;
Expand Down
54 changes: 46 additions & 8 deletions packages/server/src/models/ItemEntry.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
import { DiscountType } from '@/interfaces';

// Subtotal (qty * rate) (tax inclusive)
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
// Discount (Is percentage ? amount * discount : discount)
// Total (Subtotal - Discount)

export default class ItemEntry extends TenantModel {
public taxRate: number;
public discount: number;
public quantity: number;
public rate: number;
public isInclusiveTax: number;

public discountType: DiscountType;
/**
* Table name.
* @returns {string}
Expand All @@ -31,10 +37,24 @@ export default class ItemEntry extends TenantModel {
*/
static get virtualAttributes() {
return [
// Amount (qty * rate)
'amount',

'taxAmount',
'amountExludingTax',
'amountInclusingTax',

// Subtotal (qty * rate) + (tax inclusive)
'subtotalInclusingTax',

// Subtotal Tax Exclusive (Subtotal - Tax Amount)
'subtotalExcludingTax',

// Subtotal (qty * rate) + (tax inclusive)
'subtotal',

// Discount (Is percentage ? amount * discount : discount)
'discountAmount',

// Total (Subtotal - Discount)
'total',
];
}
Expand All @@ -45,7 +65,15 @@ export default class ItemEntry extends TenantModel {
* @returns {number}
*/
get total() {
return this.amountInclusingTax;
return this.subtotal - this.discountAmount;
}

/**
* Total (excluding tax).
* @returns {number}
*/
get totalExcludingTax() {
return this.subtotalExcludingTax - this.discountAmount;
}

/**
Expand All @@ -57,19 +85,27 @@ export default class ItemEntry extends TenantModel {
return this.quantity * this.rate;
}

/**
* Subtotal amount (tax inclusive).
* @returns {number}
*/
get subtotal() {
return this.subtotalInclusingTax;
}

/**
* Item entry amount including tax.
* @returns {number}
*/
get amountInclusingTax() {
get subtotalInclusingTax() {
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
}

/**
* Item entry amount excluding tax.
* Subtotal amount (tax exclusive).
* @returns {number}
*/
get amountExludingTax() {
get subtotalExcludingTax() {
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
}

Expand All @@ -78,7 +114,9 @@ export default class ItemEntry extends TenantModel {
* @returns {number}
*/
get discountAmount() {
return this.amount * (this.discount / 100);
return this.discountType === DiscountType.Percentage
? this.amount * (this.discount / 100)
: this.discount;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,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,
Expand Down
5 changes: 2 additions & 3 deletions packages/server/src/services/Purchases/Bills/BillGLEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/services/Sales/Estimates/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const transformEstimateToPdfTemplate = (
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
total: estimate.totalFormatted,
Expand All @@ -21,6 +22,7 @@ export const transformEstimateToPdfTemplate = (
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}]`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IItemEntry } from '@/interfaces';
import { DiscountType, IItemEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils';

Expand All @@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
return [
'quantityFormatted',
'rateFormatted',
'totalFormatted',
'discountFormatted',
'discountAmountFormatted',
];
};

/**
Expand Down Expand Up @@ -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,
});
};
}
3 changes: 2 additions & 1 deletion packages/server/src/services/Sales/Invoices/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,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),
};
};
Loading

0 comments on commit 1906d9f

Please sign in to comment.