Skip to content

Commit 1906d9f

Browse files
authored
Merge pull request #766 from bigcapitalhq/discount-line-level
fix: Line-level discount
2 parents 7af2e7c + d640dc1 commit 1906d9f

37 files changed

+318
-40
lines changed

packages/server/src/api/controllers/Purchases/Bills.ts

+5
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ export default class BillsController extends BaseController {
127127
.optional({ nullable: true })
128128
.isNumeric()
129129
.toFloat(),
130+
check('entries.*.discount_type')
131+
.default(DiscountType.Percentage)
132+
.isString()
133+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
134+
130135
check('entries.*.description').optional({ nullable: true }).trim(),
131136
check('entries.*.landed_cost')
132137
.optional({ nullable: true })

packages/server/src/api/controllers/Purchases/VendorCredit.ts

+8
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ export default class VendorCreditController extends BaseController {
176176
.optional({ nullable: true })
177177
.isNumeric()
178178
.toFloat(),
179+
check('entries.*.discount_type')
180+
.default(DiscountType.Percentage)
181+
.isString()
182+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
179183
check('entries.*.description').optional({ nullable: true }).trim(),
180184
check('entries.*.warehouse_id')
181185
.optional({ nullable: true })
@@ -225,6 +229,10 @@ export default class VendorCreditController extends BaseController {
225229
.optional({ nullable: true })
226230
.isNumeric()
227231
.toFloat(),
232+
check('entries.*.discount_type')
233+
.default(DiscountType.Percentage)
234+
.isString()
235+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
228236
check('entries.*.description').optional({ nullable: true }).trim(),
229237
check('entries.*.warehouse_id')
230238
.optional({ nullable: true })

packages/server/src/api/controllers/Sales/CreditNotes.ts

+4
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ export default class PaymentReceivesController extends BaseController {
239239
.optional({ nullable: true })
240240
.isNumeric()
241241
.toFloat(),
242+
check('entries.*.discount_type')
243+
.default(DiscountType.Percentage)
244+
.isString()
245+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
242246
check('entries.*.description').optional({ nullable: true }).trim(),
243247
check('entries.*.warehouse_id')
244248
.optional({ nullable: true })

packages/server/src/api/controllers/Sales/SalesEstimates.ts

+5
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ export default class SalesEstimatesController extends BaseController {
187187
.optional({ nullable: true })
188188
.isNumeric()
189189
.toFloat(),
190+
check('entries.*.discount_type')
191+
.default(DiscountType.Percentage)
192+
.isString()
193+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
194+
190195
check('entries.*.warehouse_id')
191196
.optional({ nullable: true })
192197
.isNumeric()

packages/server/src/api/controllers/Sales/SalesInvoices.ts

+4
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ export default class SaleInvoicesController extends BaseController {
243243
.optional({ nullable: true })
244244
.isNumeric()
245245
.toFloat(),
246+
check('entries.*.discount_type')
247+
.default(DiscountType.Percentage)
248+
.isString()
249+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
246250
check('entries.*.description').optional({ nullable: true }).trim(),
247251
check('entries.*.tax_code')
248252
.optional({ nullable: true })

packages/server/src/api/controllers/Sales/SalesReceipts.ts

+5
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ export default class SalesReceiptsController extends BaseController {
164164
.optional({ nullable: true })
165165
.isNumeric()
166166
.toInt(),
167+
check('entries.*.discount_type')
168+
.default(DiscountType.Percentage)
169+
.isString()
170+
.isIn([DiscountType.Percentage, DiscountType.Amount]),
171+
167172
check('entries.*.description').optional({ nullable: true }).trim(),
168173
check('entries.*.warehouse_id')
169174
.optional({ nullable: true })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema.alterTable('items_entries', (table) => {
7+
table.string('discount_type').defaultTo('percentage').after('discount');
8+
});
9+
};
10+
11+
/**
12+
* @param { import("knex").Knex } knex
13+
* @returns { Promise<void> }
14+
*/
15+
exports.down = function (knex) {
16+
return knex.schema.alterTable('items_entries', (table) => {
17+
table.dropColumn('discount_type');
18+
});
19+
};

packages/server/src/interfaces/ItemEntry.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ export interface IItemEntry {
1313

1414
itemId: number;
1515
description: string;
16+
discountType?: string;
1617
discount: number;
1718
quantity: number;
1819
rate: number;
1920
amount: number;
2021

2122
total: number;
22-
amountInclusingTax: number;
23-
amountExludingTax: number;
23+
totalExcludingTax?: number;
24+
25+
subtotalInclusingTax: number;
26+
subtotalExcludingTax: number;
2427
discountAmount: number;
2528

2629
landedCost: number;

packages/server/src/models/ItemEntry.ts

+46-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { Model } from 'objection';
22
import TenantModel from 'models/TenantModel';
33
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
4+
import { DiscountType } from '@/interfaces';
5+
6+
// Subtotal (qty * rate) (tax inclusive)
7+
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
8+
// Discount (Is percentage ? amount * discount : discount)
9+
// Total (Subtotal - Discount)
410

511
export default class ItemEntry extends TenantModel {
612
public taxRate: number;
713
public discount: number;
814
public quantity: number;
915
public rate: number;
1016
public isInclusiveTax: number;
11-
17+
public discountType: DiscountType;
1218
/**
1319
* Table name.
1420
* @returns {string}
@@ -31,10 +37,24 @@ export default class ItemEntry extends TenantModel {
3137
*/
3238
static get virtualAttributes() {
3339
return [
40+
// Amount (qty * rate)
3441
'amount',
42+
3543
'taxAmount',
36-
'amountExludingTax',
37-
'amountInclusingTax',
44+
45+
// Subtotal (qty * rate) + (tax inclusive)
46+
'subtotalInclusingTax',
47+
48+
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
49+
'subtotalExcludingTax',
50+
51+
// Subtotal (qty * rate) + (tax inclusive)
52+
'subtotal',
53+
54+
// Discount (Is percentage ? amount * discount : discount)
55+
'discountAmount',
56+
57+
// Total (Subtotal - Discount)
3858
'total',
3959
];
4060
}
@@ -45,7 +65,15 @@ export default class ItemEntry extends TenantModel {
4565
* @returns {number}
4666
*/
4767
get total() {
48-
return this.amountInclusingTax;
68+
return this.subtotal - this.discountAmount;
69+
}
70+
71+
/**
72+
* Total (excluding tax).
73+
* @returns {number}
74+
*/
75+
get totalExcludingTax() {
76+
return this.subtotalExcludingTax - this.discountAmount;
4977
}
5078

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

88+
/**
89+
* Subtotal amount (tax inclusive).
90+
* @returns {number}
91+
*/
92+
get subtotal() {
93+
return this.subtotalInclusingTax;
94+
}
95+
6096
/**
6197
* Item entry amount including tax.
6298
* @returns {number}
6399
*/
64-
get amountInclusingTax() {
100+
get subtotalInclusingTax() {
65101
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
66102
}
67103

68104
/**
69-
* Item entry amount excluding tax.
105+
* Subtotal amount (tax exclusive).
70106
* @returns {number}
71107
*/
72-
get amountExludingTax() {
108+
get subtotalExcludingTax() {
73109
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
74110
}
75111

@@ -78,7 +114,9 @@ export default class ItemEntry extends TenantModel {
78114
* @returns {number}
79115
*/
80116
get discountAmount() {
81-
return this.amount * (this.discount / 100);
117+
return this.discountType === DiscountType.Percentage
118+
? this.amount * (this.discount / 100)
119+
: this.discount;
82120
}
83121

84122
/**

packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,11 @@ export default class CreditNoteGLEntries {
210210
index: number
211211
): ILedgerEntry => {
212212
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
213-
const localAmount = entry.amount * creditNote.exchangeRate;
213+
const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate;
214214

215215
return {
216216
...commonEntry,
217-
debit: localAmount,
217+
debit: totalLocal,
218218
accountId: entry.sellAccountId || entry.item.sellAccountId,
219219
note: entry.description,
220220
index: index + 2,

packages/server/src/services/Purchases/Bills/BillGLEntries.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,12 @@ export class BillGLEntries {
139139
private getBillItemEntry = R.curry(
140140
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
141141
const commonJournalMeta = this.getBillCommonEntry(bill);
142-
143-
const localAmount = bill.exchangeRate * entry.amountExludingTax;
142+
const totalLocal = bill.exchangeRate * entry.totalExcludingTax;
144143
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
145144

146145
return {
147146
...commonJournalMeta,
148-
debit: localAmount + landedCostAmount,
147+
debit: totalLocal + landedCostAmount,
149148
accountId:
150149
['inventory'].indexOf(entry.item.type) !== -1
151150
? entry.item.inventoryAccountId

packages/server/src/services/Purchases/VendorCredits/VendorCreditGLEntries.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ export default class VendorCreditGLEntries {
7777
index: number
7878
): ILedgerEntry => {
7979
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
80-
const localAmount = entry.amount * vendorCredit.exchangeRate;
80+
const totalLocal = entry.totalExcludingTax * vendorCredit.exchangeRate;
8181

8282
return {
8383
...commonEntity,
84-
credit: localAmount,
84+
credit: totalLocal,
8585
index: index + 2,
8686
itemId: entry.itemId,
8787
itemQuantity: entry.quantity,

packages/server/src/services/Sales/Estimates/utils.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const transformEstimateToPdfTemplate = (
1313
description: entry.description,
1414
rate: entry.rateFormatted,
1515
quantity: entry.quantityFormatted,
16+
discount: entry.discountFormatted,
1617
total: entry.totalFormatted,
1718
})),
1819
total: estimate.totalFormatted,
@@ -21,6 +22,7 @@ export const transformEstimateToPdfTemplate = (
2122
customerNote: estimate.note,
2223
termsConditions: estimate.termsConditions,
2324
customerAddress: contactAddressTextFormat(estimate.customer),
25+
showLineDiscount: estimate.entries.some((entry) => entry.discountFormatted),
2426
discount: estimate.discountAmountFormatted,
2527
discountLabel: estimate.discountPercentageFormatted
2628
? `Discount [${estimate.discountPercentageFormatted}]`

packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,6 @@ export class CommandSaleInvoiceDTOTransformer {
154154
* @returns {number}
155155
*/
156156
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
157-
return sumBy(entries, (e) => e.amount);
157+
return sumBy(entries, (e) => e.total);
158158
};
159159
}

packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export class SaleInvoiceGLEntries {
199199
index: number
200200
): ILedgerEntry => {
201201
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
202-
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;
202+
const localAmount = entry.totalExcludingTax * saleInvoice.exchangeRate;
203203

204204
return {
205205
...commonEntry,

packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IItemEntry } from '@/interfaces';
1+
import { DiscountType, IItemEntry } from '@/interfaces';
22
import { Transformer } from '@/lib/Transformer/Transformer';
33
import { formatNumber } from '@/utils';
44

@@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer {
88
* @returns {Array}
99
*/
1010
public includeAttributes = (): string[] => {
11-
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
11+
return [
12+
'quantityFormatted',
13+
'rateFormatted',
14+
'totalFormatted',
15+
'discountFormatted',
16+
'discountAmountFormatted',
17+
];
1218
};
1319

1420
/**
@@ -43,4 +49,34 @@ export class ItemEntryTransformer extends Transformer {
4349
money: false,
4450
});
4551
};
52+
53+
/**
54+
* Retrieves the formatted discount of item entry.
55+
* @param {IItemEntry} entry
56+
* @returns {string}
57+
*/
58+
protected discountFormatted = (entry: IItemEntry): string => {
59+
if (!entry.discount) {
60+
return '';
61+
}
62+
return entry.discountType === DiscountType.Percentage
63+
? `${entry.discount}%`
64+
: formatNumber(entry.discount, {
65+
currencyCode: this.context.currencyCode,
66+
money: false,
67+
});
68+
};
69+
70+
/**
71+
* Retrieves the formatted discount amount of item entry.
72+
* @param {IItemEntry} entry
73+
* @returns {string}
74+
*/
75+
protected discountAmountFormatted = (entry: IItemEntry): string => {
76+
return formatNumber(entry.discountAmount, {
77+
currencyCode: this.context.currencyCode,
78+
money: false,
79+
excerptZero: true,
80+
});
81+
};
4682
}

packages/server/src/services/Sales/Invoices/utils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ export const transformInvoiceToPdfTemplate = (
4343
description: entry.description,
4444
rate: entry.rateFormatted,
4545
quantity: entry.quantityFormatted,
46+
discount: entry.discountFormatted,
4647
total: entry.totalFormatted,
4748
})),
4849
taxes: invoice.taxes.map((tax) => ({
4950
label: tax.name,
5051
amount: tax.taxRateAmountFormatted,
5152
})),
52-
53+
showLineDiscount: invoice.entries.some((entry) => entry.discountFormatted),
5354
customerAddress: contactAddressTextFormat(invoice.customer),
5455
};
5556
};

0 commit comments

Comments
 (0)