Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Line-level discount #766

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading