tango-app-api-payment-subscription 3.5.20 → 3.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/controllers/brandsBilling.controller.js +12 -3
- package/src/controllers/estimate.controller.js +16 -10
- package/src/controllers/invoice.controller.js +87 -48
- package/src/controllers/paymentSubscription.controllers.js +40 -8
- package/src/hbs/estimatePdf.hbs +0 -1
- package/src/hbs/invoicePdf.hbs +13 -18
- package/src/hbs/invoicePdf1.hbs +12 -0
- package/src/utils/currency.js +33 -0
- package/src/utils/validations/helper/handlebar.helper.js +6 -0
package/package.json
CHANGED
|
@@ -1944,7 +1944,11 @@ export async function billingSummary( req, res ) {
|
|
|
1944
1944
|
csm: [ ...( csmByClient.get( key ) || [] ) ].join( ', ' ),
|
|
1945
1945
|
revenueMonths: {},
|
|
1946
1946
|
billedStoresMonths: {},
|
|
1947
|
-
|
|
1947
|
+
// Installation fee per month (keyed YYYY-MM). The summary surfaces
|
|
1948
|
+
// ONLY the current month's value — see installationOut below — so the
|
|
1949
|
+
// column stays hidden until the current-month invoice is generated
|
|
1950
|
+
// with an installation fee.
|
|
1951
|
+
installationMonths: {},
|
|
1948
1952
|
invDollar: 0,
|
|
1949
1953
|
invInr: 0,
|
|
1950
1954
|
} );
|
|
@@ -1956,7 +1960,8 @@ export async function billingSummary( req, res ) {
|
|
|
1956
1960
|
const r = rowOf( inv._id.c );
|
|
1957
1961
|
r.revenueMonths[inv._id.ym] = Math.round( ( ( inv.revenueInr || 0 ) + ( inv.revenueUsd || 0 ) * usdRate ) * 100 ) / 100;
|
|
1958
1962
|
r.billedStoresMonths[inv._id.ym] = inv.stores || 0;
|
|
1959
|
-
r.
|
|
1963
|
+
r.installationMonths[inv._id.ym] = ( r.installationMonths[inv._id.ym] || 0 ) +
|
|
1964
|
+
( inv.installationInr || 0 ) + ( inv.installationUsd || 0 ) * usdRate;
|
|
1960
1965
|
r.invDollar += ( inv.dollarInvoices || 0 );
|
|
1961
1966
|
r.invInr += ( inv.inrInvoices || 0 );
|
|
1962
1967
|
if ( inv.companyName ) {
|
|
@@ -2074,7 +2079,11 @@ export async function billingSummary( req, res ) {
|
|
|
2074
2079
|
// in native USD for dollar clients.
|
|
2075
2080
|
revCur = revCur == null ? null : Math.round( revCur );
|
|
2076
2081
|
const revPrevOut = revPrev == null ? null : Math.round( revPrev );
|
|
2077
|
-
|
|
2082
|
+
// Show the installation fee ONLY for the current month, and only once
|
|
2083
|
+
// that month's invoice (which carries the installationFee line) exists.
|
|
2084
|
+
// Until then it's null so the UI hides the whole column.
|
|
2085
|
+
const curInstallation = r.installationMonths[curKey] || 0;
|
|
2086
|
+
const installationOut = curInstallation ? Math.round( curInstallation ) : null;
|
|
2078
2087
|
// Registered Entity from the billings collection (distinct group names).
|
|
2079
2088
|
// First name shown in the column; the full list is sent so the UI can
|
|
2080
2089
|
// reveal the others on hover. Fall back to the invoice company name when
|
|
@@ -7,7 +7,7 @@ import Handlebars from '../utils/validations/helper/handlebar.helper.js';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import htmlpdf from 'html-pdf-node';
|
|
10
|
-
import { symbolFor } from '../utils/currency.js';
|
|
10
|
+
import { symbolFor, roundAmount } from '../utils/currency.js';
|
|
11
11
|
import { getInvoiceCcEmails } from './invoice.controller.js';
|
|
12
12
|
|
|
13
13
|
// Build the estimate PDF buffer + the template data + a safe filename. Shared
|
|
@@ -45,7 +45,9 @@ async function buildEstimatePdf( estimate ) {
|
|
|
45
45
|
companyAddress: e.companyAddress || '',
|
|
46
46
|
GSTNumber: e.GSTNumber || '',
|
|
47
47
|
PlaceOfSupply: e.PlaceOfSupply || '',
|
|
48
|
-
|
|
48
|
+
// "Default Group" is the placeholder used when no real billing group was
|
|
49
|
+
// chosen — don't surface it on the estimate (the template hides empty).
|
|
50
|
+
groupName: ( e.groupName && e.groupName !== 'Default Group' ) ? e.groupName : '',
|
|
49
51
|
period: e.period || '',
|
|
50
52
|
createdDate: e.createdDate ? dayjs( e.createdDate ).format( 'DD/MM/YYYY' ) : '',
|
|
51
53
|
validTill: e.validTill ? dayjs( e.validTill ).format( 'DD/MM/YYYY' ) : '',
|
|
@@ -211,11 +213,13 @@ export async function createEstimate( req, res ) {
|
|
|
211
213
|
{ 'clientName': 1, 'paymentInvoice.currencyType': 1 },
|
|
212
214
|
);
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
// INR rounds to whole; other currencies keep 2 decimals.
|
|
217
|
+
const estimateCurrency = b.currency || ( client?.paymentInvoice?.currencyType === 'dollar' ? 'dollar' : 'inr' );
|
|
218
|
+
const amount = roundAmount( Number( b.amount ) || 0, estimateCurrency );
|
|
219
|
+
let totalAmount = roundAmount( Number( b.totalAmount ) || 0, estimateCurrency );
|
|
216
220
|
if ( !totalAmount && amount ) {
|
|
217
221
|
// Default to 18% GST when caller sends only the pre-tax amount.
|
|
218
|
-
totalAmount =
|
|
222
|
+
totalAmount = roundAmount( amount * 1.18, estimateCurrency );
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
const { estimate, estimateIndex } = await nextEstimateNumber();
|
|
@@ -239,7 +243,7 @@ export async function createEstimate( req, res ) {
|
|
|
239
243
|
tax: Array.isArray( b.tax ) ? b.tax : [],
|
|
240
244
|
amount,
|
|
241
245
|
totalAmount,
|
|
242
|
-
currency:
|
|
246
|
+
currency: estimateCurrency,
|
|
243
247
|
status: b.status === 'sent' ? 'sent' : 'pending',
|
|
244
248
|
createdDate,
|
|
245
249
|
validTill,
|
|
@@ -273,10 +277,12 @@ export async function updateEstimate( req, res ) {
|
|
|
273
277
|
return res.sendError( 'An accepted estimate cannot be edited.', 409 );
|
|
274
278
|
}
|
|
275
279
|
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
// INR rounds to whole; other currencies keep 2 decimals.
|
|
281
|
+
const estimateCurrency = b.currency || existing.currency;
|
|
282
|
+
const amount = roundAmount( Number( b.amount ) || 0, estimateCurrency );
|
|
283
|
+
let totalAmount = roundAmount( Number( b.totalAmount ) || 0, estimateCurrency );
|
|
278
284
|
if ( !totalAmount && amount ) {
|
|
279
|
-
totalAmount =
|
|
285
|
+
totalAmount = roundAmount( amount * 1.18, estimateCurrency );
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
// Only the editable fields are updated. The estimate number, index,
|
|
@@ -294,7 +300,7 @@ export async function updateEstimate( req, res ) {
|
|
|
294
300
|
tax: Array.isArray( b.tax ) ? b.tax : existing.tax,
|
|
295
301
|
amount,
|
|
296
302
|
totalAmount,
|
|
297
|
-
currency:
|
|
303
|
+
currency: estimateCurrency,
|
|
298
304
|
notes: b.notes ?? existing.notes,
|
|
299
305
|
updatedBy: req.user?.email || req.user?.userName || '',
|
|
300
306
|
};
|
|
@@ -13,7 +13,7 @@ import htmlpdf from 'html-pdf-node';
|
|
|
13
13
|
import archiver from 'archiver';
|
|
14
14
|
import * as basepricingService from '../services/basePrice.service.js';
|
|
15
15
|
import * as paymentAccountService from '../services/paymentAccount.service.js';
|
|
16
|
-
import { symbolFor } from '../utils/currency.js';
|
|
16
|
+
import { symbolFor, roundAmount, wordFor } from '../utils/currency.js';
|
|
17
17
|
import { invoiceStatusEnum } from '../dtos/validation.dtos.js';
|
|
18
18
|
import { findOneApplicationDefault } from '../services/applicationDefault.service.js';
|
|
19
19
|
import * as assignedStoreService from '../services/assignedStore.service.js';
|
|
@@ -162,13 +162,14 @@ export async function createInvoice( req, res ) {
|
|
|
162
162
|
}
|
|
163
163
|
// Recompute totals from the (possibly expanded) line items so a
|
|
164
164
|
// multi-month advance invoice bills the full period.
|
|
165
|
+
const customCurrency = req.body.currency || 'inr';
|
|
165
166
|
const customAmount = customAdvanceMonths > 1 ?
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
roundAmount( customProducts.reduce( ( s, p ) => s + ( Number( p.amount ) || 0 ), 0 ), customCurrency ) :
|
|
168
|
+
roundAmount( Number( req.body.amount ) || 0, customCurrency );
|
|
168
169
|
const customTotal = customAdvanceMonths > 1 ?
|
|
169
|
-
|
|
170
|
-
( Array.isArray( req.body.tax ) ? req.body.tax.reduce( ( s, t ) => s + ( Number( t.taxAmount ) || 0 ) * customAdvanceMonths, 0 ) : 0 ) ) :
|
|
171
|
-
|
|
170
|
+
roundAmount( customProducts.reduce( ( s, p ) => s + ( Number( p.amount ) || 0 ), 0 ) +
|
|
171
|
+
( Array.isArray( req.body.tax ) ? req.body.tax.reduce( ( s, t ) => s + ( Number( t.taxAmount ) || 0 ) * customAdvanceMonths, 0 ) : 0 ), customCurrency ) :
|
|
172
|
+
roundAmount( Number( req.body.totalAmount ) || 0, customCurrency );
|
|
172
173
|
|
|
173
174
|
const data = {
|
|
174
175
|
invoice: `${invPrefix}${Finacialyear}-${invoiceNo}`,
|
|
@@ -396,7 +397,7 @@ export async function createInvoice( req, res ) {
|
|
|
396
397
|
productName: 'oneTimeFee',
|
|
397
398
|
period: 'fullmonth',
|
|
398
399
|
storeCount: newStoreCount,
|
|
399
|
-
amount:
|
|
400
|
+
amount: roundAmount( newStoreCount * oneTimeFeePerStore, group.currency ),
|
|
400
401
|
price: oneTimeFeePerStore,
|
|
401
402
|
description: `One-Time Fee - ${newStoreCount} stores`,
|
|
402
403
|
HsnNumber: '998314',
|
|
@@ -417,21 +418,21 @@ export async function createInvoice( req, res ) {
|
|
|
417
418
|
// existing GST/IGST/CGST/SGST behavior for legacy records that have
|
|
418
419
|
// no taxCalculationType set yet.
|
|
419
420
|
if ( group.taxCalculationType === 'international' ) {
|
|
420
|
-
totalAmount =
|
|
421
|
+
totalAmount = roundAmount( amount, group.currency );
|
|
421
422
|
} else if ( group.gst && group.gst.slice( 0, 2 ) == '33' ) {
|
|
422
423
|
let taxAmount = ( amount * 18 ) / 100;
|
|
423
|
-
totalAmount =
|
|
424
|
+
totalAmount = roundAmount( amount + taxAmount, group.currency );
|
|
424
425
|
taxList.push(
|
|
425
426
|
{
|
|
426
427
|
'currency': '₹',
|
|
427
428
|
'type': 'CGST',
|
|
428
429
|
'value': 9,
|
|
429
|
-
'taxAmount': ( ( amount * 9 ) / 100
|
|
430
|
+
'taxAmount': String( roundAmount( ( amount * 9 ) / 100, group.currency ) ),
|
|
430
431
|
}, {
|
|
431
432
|
'currency': '₹',
|
|
432
433
|
'type': 'SGST',
|
|
433
434
|
'value': 9,
|
|
434
|
-
'taxAmount': ( ( amount * 9 ) / 100
|
|
435
|
+
'taxAmount': String( roundAmount( ( amount * 9 ) / 100, group.currency ) ),
|
|
435
436
|
},
|
|
436
437
|
);
|
|
437
438
|
} else {
|
|
@@ -439,13 +440,13 @@ export async function createInvoice( req, res ) {
|
|
|
439
440
|
if ( group.currency === 'inr' ) {
|
|
440
441
|
taxAmount = ( amount * 18 ) / 100;
|
|
441
442
|
}
|
|
442
|
-
totalAmount =
|
|
443
|
+
totalAmount = roundAmount( amount + taxAmount, group.currency );
|
|
443
444
|
taxList.push(
|
|
444
445
|
{
|
|
445
446
|
'currency': '₹',
|
|
446
447
|
'type': 'IGST',
|
|
447
448
|
'value': 18,
|
|
448
|
-
'taxAmount': ( taxAmount
|
|
449
|
+
'taxAmount': String( roundAmount( taxAmount, group.currency ) ),
|
|
449
450
|
},
|
|
450
451
|
);
|
|
451
452
|
}
|
|
@@ -528,14 +529,14 @@ export async function createInvoice( req, res ) {
|
|
|
528
529
|
invoice: req.body.invoiceId ? req.body.invoiceId : `${invPrefix}${Finacialyear}-${invoiceNo}`,
|
|
529
530
|
products: products,
|
|
530
531
|
status: 'pendingCsm',
|
|
531
|
-
amount:
|
|
532
|
+
amount: roundAmount( amount, group.currency ),
|
|
532
533
|
invoiceIndex: req.body.invoiceId ? findInvoice.invoiceIndex : invoiceNo,
|
|
533
534
|
tax: taxList,
|
|
534
535
|
companyName: ( group.registeredCompanyName || '' ).toUpperCase(),
|
|
535
536
|
companyAddress: address,
|
|
536
537
|
PlaceOfSupply: group.placeOfSupply,
|
|
537
538
|
GSTNumber: group.gst,
|
|
538
|
-
totalAmount:
|
|
539
|
+
totalAmount: roundAmount( totalAmount, group.currency ),
|
|
539
540
|
clientId: group.clientId,
|
|
540
541
|
paymentMethod: 'Online',
|
|
541
542
|
billingDate: new Date( invoicedate ),
|
|
@@ -751,8 +752,8 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
|
|
|
751
752
|
// store's position within the product.
|
|
752
753
|
const price = priceForStore( s.productName, productPosition[s.productName] );
|
|
753
754
|
const runningCost = s.workingdays >= monthDays ?
|
|
754
|
-
|
|
755
|
-
|
|
755
|
+
roundAmount( price * units, invoiceInfo.currency ) :
|
|
756
|
+
roundAmount( ( price / monthDays ) * s.workingdays * units, invoiceInfo.currency );
|
|
756
757
|
return {
|
|
757
758
|
productName: s.productName ? s.productName.charAt( 0 ).toUpperCase() + s.productName.slice( 1 ) : '',
|
|
758
759
|
currencyType: invoiceCurrency,
|
|
@@ -770,8 +771,9 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
|
|
|
770
771
|
};
|
|
771
772
|
} );
|
|
772
773
|
|
|
773
|
-
const totalAmount =
|
|
774
|
-
const
|
|
774
|
+
const totalAmount = roundAmount( data.reduce( ( a, x ) => a + ( x.runningCost || 0 ), 0 ), invoiceInfo.currency );
|
|
775
|
+
const fractionDigits = invoiceInfo.currency === 'inr' ? 0 : 2;
|
|
776
|
+
const totalFormatted = totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits } );
|
|
775
777
|
return { data, totalAmount, totalFormatted };
|
|
776
778
|
}
|
|
777
779
|
|
|
@@ -786,21 +788,24 @@ export async function invoiceDownload( req, res ) {
|
|
|
786
788
|
// client.paymentInvoice or virtualAccount.currency causes historical
|
|
787
789
|
// invoices to re-render in the wrong currency if those settings change.
|
|
788
790
|
const invoiceCurrency = symbolFor( invoiceInfo.currency );
|
|
791
|
+
// INR shows whole numbers; every other currency keeps 2 decimals.
|
|
792
|
+
const fractionDigits = invoiceInfo.currency === 'inr' ? 0 : 2;
|
|
793
|
+
const moneyFmt = { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
|
|
789
794
|
invoiceInfo.products.forEach( ( item, index ) => {
|
|
790
795
|
item.index = index + 1;
|
|
791
796
|
let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
792
797
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
793
798
|
item.productName = firstWord + ' ' + secondWord;
|
|
794
|
-
item.price = item.price .toLocaleString( 'en-IN',
|
|
795
|
-
item.amount = item.amount.toLocaleString( 'en-IN',
|
|
799
|
+
item.price = roundAmount( item.price, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
|
|
800
|
+
item.amount = roundAmount( item.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
|
|
796
801
|
item.currency = invoiceCurrency;
|
|
797
802
|
} );
|
|
798
803
|
|
|
799
804
|
|
|
800
805
|
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
801
806
|
|
|
802
|
-
invoiceInfo.totalAmount =
|
|
803
|
-
let AmountinWords =
|
|
807
|
+
invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
808
|
+
let AmountinWords = amountToWords( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
804
809
|
let getgroup;
|
|
805
810
|
let days = getgroup?.paymentTerm ? getgroup?.paymentTerm : '30';
|
|
806
811
|
let dueDate = invoiceInfo?.dueDate ? dayjs( invoiceInfo?.dueDate ).format( 'DD/MM/YYYY' ) : dayjs().add( days, 'days' ).format( 'DD/MM/YYYY' );
|
|
@@ -814,10 +819,10 @@ export async function invoiceDownload( req, res ) {
|
|
|
814
819
|
invoiceData = {
|
|
815
820
|
...invoiceInfo._doc,
|
|
816
821
|
clientName: clientDetails.clientName,
|
|
817
|
-
amount: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
822
|
+
amount: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
818
823
|
extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
|
|
819
824
|
address: clientDetails.billingDetails.billingAddress,
|
|
820
|
-
subtotal: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
825
|
+
subtotal: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
821
826
|
companyName: invoiceInfo.companyName,
|
|
822
827
|
companyAddress: invoiceInfo.companyAddress,
|
|
823
828
|
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
@@ -826,7 +831,8 @@ export async function invoiceDownload( req, res ) {
|
|
|
826
831
|
amountwords: AmountinWords,
|
|
827
832
|
Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
|
|
828
833
|
currencyType: invoiceCurrency,
|
|
829
|
-
|
|
834
|
+
currencyWord: wordFor( invoiceInfo.currency ),
|
|
835
|
+
totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', moneyFmt ),
|
|
830
836
|
invoiceDate,
|
|
831
837
|
dueDate,
|
|
832
838
|
discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
|
|
@@ -985,19 +991,22 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
985
991
|
// field, recorded at creation from the billing group. See invoiceDownload
|
|
986
992
|
// above for the same pattern.
|
|
987
993
|
const invoiceCurrency = symbolFor( invoiceInfo.currency );
|
|
994
|
+
// INR shows whole numbers; every other currency keeps 2 decimals.
|
|
995
|
+
const fractionDigits = invoiceInfo.currency === 'inr' ? 0 : 2;
|
|
996
|
+
const moneyFmt = { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
|
|
988
997
|
invoiceInfo.products.forEach( ( item, index ) => {
|
|
989
998
|
item.index = index + 1;
|
|
990
999
|
let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
991
1000
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
992
1001
|
item.productName = firstWord + ' ' + secondWord;
|
|
993
|
-
item.price =
|
|
994
|
-
item.amount = item.amount.toLocaleString( 'en-IN',
|
|
1002
|
+
item.price = roundAmount( item.price, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
|
|
1003
|
+
item.amount = roundAmount( item.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt );
|
|
995
1004
|
item.currency = invoiceCurrency;
|
|
996
1005
|
} );
|
|
997
1006
|
|
|
998
1007
|
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
999
|
-
invoiceInfo.totalAmount =
|
|
1000
|
-
let AmountinWords =
|
|
1008
|
+
invoiceInfo.totalAmount = roundAmount( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
1009
|
+
let AmountinWords = amountToWords( invoiceInfo.totalAmount, invoiceInfo.currency );
|
|
1001
1010
|
let getgroup;
|
|
1002
1011
|
if ( invoiceInfo.groupId ) {
|
|
1003
1012
|
getgroup = await billingService.findOne( { _id: invoiceInfo.groupId } );
|
|
@@ -1010,10 +1019,10 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
1010
1019
|
let invoiceData = {
|
|
1011
1020
|
...invoiceInfo._doc,
|
|
1012
1021
|
clientName: clientDetails.clientName,
|
|
1013
|
-
amount: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
1022
|
+
amount: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
1014
1023
|
extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
|
|
1015
1024
|
address: clientDetails.billingDetails.billingAddress,
|
|
1016
|
-
subtotal: invoiceInfo.amount.toLocaleString( 'en-IN',
|
|
1025
|
+
subtotal: roundAmount( invoiceInfo.amount, invoiceInfo.currency ).toLocaleString( 'en-IN', moneyFmt ),
|
|
1017
1026
|
companyName: invoiceInfo.companyName,
|
|
1018
1027
|
companyAddress: invoiceInfo.companyAddress,
|
|
1019
1028
|
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
@@ -1022,7 +1031,8 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
1022
1031
|
amountwords: AmountinWords,
|
|
1023
1032
|
Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
|
|
1024
1033
|
currencyType: invoiceCurrency,
|
|
1025
|
-
|
|
1034
|
+
currencyWord: wordFor( invoiceInfo.currency ),
|
|
1035
|
+
totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', moneyFmt ),
|
|
1026
1036
|
invoiceDate,
|
|
1027
1037
|
dueDate,
|
|
1028
1038
|
discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
|
|
@@ -1208,6 +1218,32 @@ function inWords( num ) {
|
|
|
1208
1218
|
return str.toLowerCase().split( ' ' ).map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ).join( ' ' );
|
|
1209
1219
|
}
|
|
1210
1220
|
|
|
1221
|
+
// Sub-unit (fraction) name per currency for the "Total In Words" cents part.
|
|
1222
|
+
// INR is whole-number only, so it has no fraction.
|
|
1223
|
+
const CURRENCY_FRACTION_WORDS = {
|
|
1224
|
+
dollar: 'Cents',
|
|
1225
|
+
singaporedollar: 'Cents',
|
|
1226
|
+
euro: 'Cents',
|
|
1227
|
+
aed: 'Fils',
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// Spell out an amount. INR -> whole number only. Other currencies keep 2
|
|
1231
|
+
// decimals, so the cents are spelled out too when non-zero
|
|
1232
|
+
// (e.g. 3191.10 -> "Three Thousand One Hundred And Ninety One and Ten Cents").
|
|
1233
|
+
function amountToWords( value, currency ) {
|
|
1234
|
+
const n = Number( value ) || 0;
|
|
1235
|
+
if ( currency === 'inr' ) {
|
|
1236
|
+
return inWords( Math.round( n ) );
|
|
1237
|
+
}
|
|
1238
|
+
const whole = Math.trunc( n );
|
|
1239
|
+
const cents = Math.round( ( n - whole ) * 100 );
|
|
1240
|
+
const fraction = CURRENCY_FRACTION_WORDS[currency] || 'Cents';
|
|
1241
|
+
if ( cents > 0 ) {
|
|
1242
|
+
return `${inWords( whole )} and ${inWords( cents )} ${fraction}`;
|
|
1243
|
+
}
|
|
1244
|
+
return inWords( whole );
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1211
1247
|
|
|
1212
1248
|
// Resolve which basepricing doc applies to a billing group. When the client
|
|
1213
1249
|
// has billingGroupWisePricing enabled AND a doc exists for this group, returns
|
|
@@ -1638,7 +1674,7 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
1638
1674
|
if ( store.workingDays >= currentMonthDays ) {
|
|
1639
1675
|
amount = store.price * storeCount;
|
|
1640
1676
|
} else {
|
|
1641
|
-
amount =
|
|
1677
|
+
amount = roundAmount( ( store.price / currentMonthDays ) * store.workingDays * storeCount, group.currency );
|
|
1642
1678
|
}
|
|
1643
1679
|
|
|
1644
1680
|
let description = store.productName === 'tangoZone' ? 'Product category/section analytics' : 'Customer Footfall Analytics';
|
|
@@ -1837,7 +1873,7 @@ async function stepPrice( group, getClient ) {
|
|
|
1837
1873
|
if ( store.workingDays >= currentMonthDays ) {
|
|
1838
1874
|
amount = price * storeCount;
|
|
1839
1875
|
} else {
|
|
1840
|
-
amount =
|
|
1876
|
+
amount = roundAmount( ( price / currentMonthDays ) * store.workingDays * storeCount, group.currency );
|
|
1841
1877
|
}
|
|
1842
1878
|
|
|
1843
1879
|
let description = store.productName === 'tangoZone' ? 'Product category/section analytics' : 'Customer Footfall Analytics';
|
|
@@ -1913,8 +1949,8 @@ async function stepPrice( group, getClient ) {
|
|
|
1913
1949
|
const price = tierPriceForPosition( s.productName, productPosition[s.productName] );
|
|
1914
1950
|
const fullMonth = s.workingDays >= currentMonthDays;
|
|
1915
1951
|
const amount = fullMonth ?
|
|
1916
|
-
|
|
1917
|
-
|
|
1952
|
+
roundAmount( units * price, group.currency ) :
|
|
1953
|
+
roundAmount( units * ( price / currentMonthDays ) * s.workingDays, group.currency );
|
|
1918
1954
|
const period = fullMonth ? 'fullMonth' : 'proRate';
|
|
1919
1955
|
const key = `${s.productName}_${period}_${price}`;
|
|
1920
1956
|
if ( !grouped[key] ) {
|
|
@@ -1931,7 +1967,8 @@ async function stepPrice( group, getClient ) {
|
|
|
1931
1967
|
} else if ( grp.productName === 'tangoZone' ) {
|
|
1932
1968
|
description = 'Product category/section analytics';
|
|
1933
1969
|
}
|
|
1934
|
-
const amount =
|
|
1970
|
+
const amount = roundAmount( grp.totalAmount, group.currency );
|
|
1971
|
+
const zeroPrice = group.currency === 'inr' ? '0' : '0.00';
|
|
1935
1972
|
return {
|
|
1936
1973
|
productName: grp.productName,
|
|
1937
1974
|
period: grp.period,
|
|
@@ -1943,7 +1980,7 @@ async function stepPrice( group, getClient ) {
|
|
|
1943
1980
|
HsnNumber: '998314',
|
|
1944
1981
|
amount: amount,
|
|
1945
1982
|
month: dayjs().format( 'MMM YYYY' ),
|
|
1946
|
-
price: grp.unitCount ? ( amount / grp.unitCount
|
|
1983
|
+
price: grp.unitCount ? String( roundAmount( amount / grp.unitCount, group.currency ) ) : zeroPrice,
|
|
1947
1984
|
};
|
|
1948
1985
|
} );
|
|
1949
1986
|
|
|
@@ -2229,9 +2266,11 @@ export async function clientInvoiceList( req, res ) {
|
|
|
2229
2266
|
cards.pendingPaymentCount += 1;
|
|
2230
2267
|
}
|
|
2231
2268
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
cards.
|
|
2269
|
+
// These card totals are normalised to INR (dollar invoices are multiplied
|
|
2270
|
+
// by usdRate above), so they follow the INR rule: whole numbers.
|
|
2271
|
+
cards.outstandingAmount = roundAmount( cards.outstandingAmount, 'inr' );
|
|
2272
|
+
cards.overdueAmount = roundAmount( cards.overdueAmount, 'inr' );
|
|
2273
|
+
cards.pendingPaymentAmount = roundAmount( cards.pendingPaymentAmount, 'inr' );
|
|
2235
2274
|
|
|
2236
2275
|
res.sendSuccess( { count: count.length, data: invoiceList, cards } );
|
|
2237
2276
|
} catch ( error ) {
|
|
@@ -2363,14 +2402,14 @@ export async function applyDiscount( req, res ) {
|
|
|
2363
2402
|
try {
|
|
2364
2403
|
let invoice = await invoiceService.findOne( { invoice: req.body.invoice } );
|
|
2365
2404
|
if ( invoice ) {
|
|
2366
|
-
invoice.discountAmount = ( ( invoice.amount * req.body.discount ) / 100
|
|
2367
|
-
invoice.amount = invoice.amount - invoice.discountAmount;
|
|
2405
|
+
invoice.discountAmount = roundAmount( ( invoice.amount * req.body.discount ) / 100, invoice.currency );
|
|
2406
|
+
invoice.amount = roundAmount( invoice.amount - invoice.discountAmount, invoice.currency );
|
|
2368
2407
|
invoice.discountPercentage = req.body.discount;
|
|
2369
2408
|
if ( invoice.currency === 'inr' ) {
|
|
2370
2409
|
if ( invoice.tax.length ) {
|
|
2371
2410
|
for ( let i = 0; i < invoice.tax.length; i++ ) {
|
|
2372
|
-
invoice.tax[i].taxAmount = ( ( invoice.amount * invoice.tax[i].value ) / 100
|
|
2373
|
-
invoice.totalAmount = ( Number( invoice.amount ) + Number( invoice.tax[i].taxAmount )
|
|
2411
|
+
invoice.tax[i].taxAmount = String( roundAmount( ( invoice.amount * invoice.tax[i].value ) / 100, invoice.currency ) );
|
|
2412
|
+
invoice.totalAmount = roundAmount( Number( invoice.amount ) + Number( invoice.tax[i].taxAmount ), invoice.currency );
|
|
2374
2413
|
}
|
|
2375
2414
|
}
|
|
2376
2415
|
}
|
|
@@ -2500,7 +2539,7 @@ export async function recordPayment( req, res ) {
|
|
|
2500
2539
|
}
|
|
2501
2540
|
|
|
2502
2541
|
const previousPaid = Number( invoice.paidAmount ) || 0;
|
|
2503
|
-
const newPaid =
|
|
2542
|
+
const newPaid = roundAmount( previousPaid + amountNum, invoice.currency );
|
|
2504
2543
|
const totalAmount = Number( invoice.totalAmount ) || 0;
|
|
2505
2544
|
|
|
2506
2545
|
// Reject overpayment — finance teams want this caught early. They can
|
|
@@ -2508,7 +2547,7 @@ export async function recordPayment( req, res ) {
|
|
|
2508
2547
|
// invoice total; anything beyond that is a data-entry error.
|
|
2509
2548
|
if ( totalAmount > 0 && newPaid > totalAmount + 0.01 ) {
|
|
2510
2549
|
return res.sendError(
|
|
2511
|
-
`Payment exceeds outstanding balance. Outstanding: ${( totalAmount - previousPaid
|
|
2550
|
+
`Payment exceeds outstanding balance. Outstanding: ${roundAmount( totalAmount - previousPaid, invoice.currency )}`,
|
|
2512
2551
|
400,
|
|
2513
2552
|
);
|
|
2514
2553
|
}
|
|
@@ -13,11 +13,13 @@ import * as cameraService from '../services/camera.service.js';
|
|
|
13
13
|
import * as billingService from '../services/billing.service.js';
|
|
14
14
|
import * as paymentAccountService from '../services/paymentAccount.service.js';
|
|
15
15
|
import * as taggingService from '../services/tagging.service.js';
|
|
16
|
-
import { symbolFor } from '../utils/currency.js';
|
|
16
|
+
import { symbolFor, roundAmount, wordFor } from '../utils/currency.js';
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
import dayjs from 'dayjs';
|
|
20
|
-
|
|
20
|
+
// Use the shared Handlebars instance that has our custom helpers
|
|
21
|
+
// (ifExists / neq / gtZero) registered, so invoicePdf1.hbs can use them.
|
|
22
|
+
import Handlebars from '../utils/validations/helper/handlebar.helper.js';
|
|
21
23
|
import fs from 'fs';
|
|
22
24
|
import path from 'path';
|
|
23
25
|
// import { JSDOM } from 'jsdom';
|
|
@@ -3092,6 +3094,10 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3092
3094
|
if ( invoiceInfo ) {
|
|
3093
3095
|
let clientDetails = await paymentService.findOne( { clientId: invoiceInfo.clientId } );
|
|
3094
3096
|
let amount = 0;
|
|
3097
|
+
// INR shows whole numbers; every other currency keeps 2 decimals.
|
|
3098
|
+
const invoicePdfCurrency = clientDetails?.paymentInvoice?.currencyType;
|
|
3099
|
+
const invoicePdfFractionDigits = invoicePdfCurrency === 'inr' ? 0 : 2;
|
|
3100
|
+
const invoicePdfMoneyFmt = { minimumFractionDigits: invoicePdfFractionDigits, maximumFractionDigits: invoicePdfFractionDigits };
|
|
3095
3101
|
invoiceInfo.products.forEach( ( item, index ) => {
|
|
3096
3102
|
let [ firstWord, secondWord ] = item.product.product.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
3097
3103
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
@@ -3107,25 +3113,25 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3107
3113
|
|
|
3108
3114
|
amount = amount + item.price;
|
|
3109
3115
|
|
|
3110
|
-
item.basePrice =
|
|
3111
|
-
item.price =
|
|
3116
|
+
item.basePrice = roundAmount( item.basePrice, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
|
|
3117
|
+
item.price = roundAmount( item.price, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
|
|
3112
3118
|
item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
|
|
3113
3119
|
} );
|
|
3114
3120
|
for ( let tax of invoiceInfo.tax ) {
|
|
3115
|
-
tax.taxAmount = tax.taxAmount.toLocaleString( 'en-IN',
|
|
3121
|
+
tax.taxAmount = roundAmount( tax.taxAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt );
|
|
3116
3122
|
}
|
|
3117
3123
|
let invoiceDate = dayjs( invoiceInfo.createdAt ).format( 'DD/MM/YYYY' );
|
|
3118
3124
|
let days = clientDetails?.paymentInvoice?.extendPaymentPeriodDays || 10;
|
|
3119
3125
|
let dueDate = invoiceInfo?.dueDate ? dayjs( invoiceInfo?.dueDate ).format( 'DD/MM/YYYY' ) : dayjs().add( days, 'days' ).format( 'DD/MM/YYYY' );
|
|
3120
3126
|
|
|
3121
3127
|
invoiceInfo.totalAmount = invoiceInfo.totalAmount;
|
|
3122
|
-
let AmountinWords =
|
|
3128
|
+
let AmountinWords = amountToWords( invoiceInfo.totalAmount, invoicePdfCurrency );
|
|
3123
3129
|
invoiceData = {
|
|
3124
3130
|
...invoiceInfo._doc,
|
|
3125
3131
|
clientName: clientDetails.clientName,
|
|
3126
3132
|
extendDays: clientDetails.paymentInvoice.extendPaymentPeriodDays,
|
|
3127
3133
|
address: clientDetails.billingDetails.billingAddress,
|
|
3128
|
-
subtotal: amount.toLocaleString( 'en-IN',
|
|
3134
|
+
subtotal: roundAmount( amount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
|
|
3129
3135
|
companyName: invoiceInfo.companyName,
|
|
3130
3136
|
companyAddress: invoiceInfo.companyAddress,
|
|
3131
3137
|
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
@@ -3134,7 +3140,8 @@ export const invoiceDownload = async ( req, res ) => {
|
|
|
3134
3140
|
amountwords: AmountinWords,
|
|
3135
3141
|
Terms: `Term ${clientDetails.paymentInvoice.extendPaymentPeriodDays}`,
|
|
3136
3142
|
currencyType: symbolFor( clientDetails?.paymentInvoice?.currencyType ),
|
|
3137
|
-
|
|
3143
|
+
currencyName: wordFor( invoicePdfCurrency ),
|
|
3144
|
+
totalAmount: roundAmount( invoiceInfo.totalAmount, invoicePdfCurrency ).toLocaleString( 'en-IN', invoicePdfMoneyFmt ),
|
|
3138
3145
|
invoiceDate,
|
|
3139
3146
|
dueDate,
|
|
3140
3147
|
};
|
|
@@ -3247,6 +3254,31 @@ function inWords( num ) {
|
|
|
3247
3254
|
return str.toLowerCase().split( ' ' ).map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ).join( ' ' );
|
|
3248
3255
|
}
|
|
3249
3256
|
|
|
3257
|
+
// Sub-unit (fraction) name per currency for the "Total In Words" cents part.
|
|
3258
|
+
const CURRENCY_FRACTION_WORDS = {
|
|
3259
|
+
dollar: 'Cents',
|
|
3260
|
+
singaporedollar: 'Cents',
|
|
3261
|
+
euro: 'Cents',
|
|
3262
|
+
aed: 'Fils',
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
// Spell out an amount. INR -> whole number only. Other currencies keep 2
|
|
3266
|
+
// decimals, so the cents are spelled out too when non-zero
|
|
3267
|
+
// (e.g. 383.04 -> "Three Hundred And Eighty Three and Four Cents").
|
|
3268
|
+
function amountToWords( value, currency ) {
|
|
3269
|
+
const n = Number( value ) || 0;
|
|
3270
|
+
if ( currency === 'inr' ) {
|
|
3271
|
+
return inWords( Math.round( n ) );
|
|
3272
|
+
}
|
|
3273
|
+
const whole = Math.trunc( n );
|
|
3274
|
+
const cents = Math.round( ( n - whole ) * 100 );
|
|
3275
|
+
const fraction = CURRENCY_FRACTION_WORDS[currency] || 'Cents';
|
|
3276
|
+
if ( cents > 0 ) {
|
|
3277
|
+
return `${inWords( whole )} and ${inWords( cents )} ${fraction}`;
|
|
3278
|
+
}
|
|
3279
|
+
return inWords( whole );
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3250
3282
|
export const updateInvoiceStatus = async ( req, res ) => {
|
|
3251
3283
|
try {
|
|
3252
3284
|
if ( !req.params?.invoiceId ) {
|
package/src/hbs/estimatePdf.hbs
CHANGED
package/src/hbs/invoicePdf.hbs
CHANGED
|
@@ -881,6 +881,10 @@
|
|
|
881
881
|
align-self: stretch;
|
|
882
882
|
flex-shrink: 0;
|
|
883
883
|
position: relative;
|
|
884
|
+
/* Keep the whole totals block (Sub Total ... Total In Words) on a
|
|
885
|
+
single page so the amount-in-words can't be split across pages. */
|
|
886
|
+
break-inside: avoid;
|
|
887
|
+
page-break-inside: avoid;
|
|
884
888
|
}
|
|
885
889
|
|
|
886
890
|
.frame-2394 {
|
|
@@ -1011,6 +1015,8 @@
|
|
|
1011
1015
|
justify-content: flex-start;
|
|
1012
1016
|
flex-shrink: 0;
|
|
1013
1017
|
position: relative;
|
|
1018
|
+
break-inside: avoid;
|
|
1019
|
+
page-break-inside: avoid;
|
|
1014
1020
|
}
|
|
1015
1021
|
|
|
1016
1022
|
.text13 {
|
|
@@ -1401,12 +1407,14 @@
|
|
|
1401
1407
|
<div class="chennai-tamil-nadu-600006-india">
|
|
1402
1408
|
|
|
1403
1409
|
</div>
|
|
1404
|
-
<div class="gstin-33-aagct-3124-r-1-z-2">GSTIN {{GSTNumber}}</div>
|
|
1410
|
+
{{#ifExists GSTNumber}}<div class="gstin-33-aagct-3124-r-1-z-2">GSTIN {{GSTNumber}}</div>{{/ifExists}}
|
|
1405
1411
|
</div>
|
|
1406
1412
|
</div>
|
|
1413
|
+
{{#ifExists PlaceOfSupply}}
|
|
1407
1414
|
<div class="place_of_supply">
|
|
1408
1415
|
Place Of Supply: {{PlaceOfSupply}}
|
|
1409
1416
|
</div>
|
|
1417
|
+
{{/ifExists}}
|
|
1410
1418
|
</div>
|
|
1411
1419
|
<div class="frame-2698">
|
|
1412
1420
|
<div class="frame-9155">
|
|
@@ -1560,7 +1568,7 @@
|
|
|
1560
1568
|
<div class="frame-23972">
|
|
1561
1569
|
{{#neq discountAmount 0}}
|
|
1562
1570
|
<div class="frame-2394">
|
|
1563
|
-
<div class="text9">Discount
|
|
1571
|
+
<div class="text9">Discount</div>
|
|
1564
1572
|
<div class="frame-9157">
|
|
1565
1573
|
<div class="text10">{{currencyType}} {{discountAmount}}</div>
|
|
1566
1574
|
</div>
|
|
@@ -1575,12 +1583,14 @@
|
|
|
1575
1583
|
|
|
1576
1584
|
{{#if gstApplicable}}
|
|
1577
1585
|
{{#each tax }}
|
|
1586
|
+
{{#gtZero taxAmount}}
|
|
1578
1587
|
<div class="frame-2392">
|
|
1579
1588
|
<div class="text9">{{type}} ({{value}}%)</div>
|
|
1580
1589
|
<div class="frame-9157">
|
|
1581
1590
|
<div class="text10">{{../currencyType}} {{taxAmount}}</div>
|
|
1582
1591
|
</div>
|
|
1583
1592
|
</div>
|
|
1593
|
+
{{/gtZero}}
|
|
1584
1594
|
{{/each }}
|
|
1585
1595
|
{{/if}}
|
|
1586
1596
|
|
|
@@ -1605,22 +1615,7 @@
|
|
|
1605
1615
|
<div class="text11">Total In Words</div>
|
|
1606
1616
|
<div class="frame-9157">
|
|
1607
1617
|
<div class="text13">
|
|
1608
|
-
{{
|
|
1609
|
-
Indian Rupee
|
|
1610
|
-
{{/eq}}
|
|
1611
|
-
|
|
1612
|
-
{{#eq billingCurrency 'dollar'}}
|
|
1613
|
-
US Dollar
|
|
1614
|
-
{{/eq}}
|
|
1615
|
-
|
|
1616
|
-
{{#eq billingCurrency 'singaporedollar'}}
|
|
1617
|
-
Singapore Dollar
|
|
1618
|
-
{{/eq}}
|
|
1619
|
-
|
|
1620
|
-
{{#eq billingCurrency 'euro'}}
|
|
1621
|
-
Euro
|
|
1622
|
-
{{/eq}}
|
|
1623
|
-
{{amountwords}} Only
|
|
1618
|
+
{{currencyWord}} {{amountwords}} Only
|
|
1624
1619
|
</div>
|
|
1625
1620
|
</div>
|
|
1626
1621
|
</div>
|
package/src/hbs/invoicePdf1.hbs
CHANGED
|
@@ -844,6 +844,10 @@
|
|
|
844
844
|
align-self: stretch;
|
|
845
845
|
flex-shrink: 0;
|
|
846
846
|
position: relative;
|
|
847
|
+
/* Keep the whole totals block (Sub Total ... Total In Words) on a
|
|
848
|
+
single page so the amount-in-words can't be split across pages. */
|
|
849
|
+
break-inside: avoid;
|
|
850
|
+
page-break-inside: avoid;
|
|
847
851
|
}
|
|
848
852
|
|
|
849
853
|
.frame-2394 {
|
|
@@ -974,6 +978,8 @@
|
|
|
974
978
|
justify-content: flex-start;
|
|
975
979
|
flex-shrink: 0;
|
|
976
980
|
position: relative;
|
|
981
|
+
break-inside: avoid;
|
|
982
|
+
page-break-inside: avoid;
|
|
977
983
|
}
|
|
978
984
|
|
|
979
985
|
.text13 {
|
|
@@ -1368,16 +1374,20 @@
|
|
|
1368
1374
|
class="billing-address-given-by-the-client-flat-no-012-ground-floor-the-banyan-apartment-jsr-layout-j-p-nagar-9th-phase-alahalli-anjanapura-post-bengaluru-560062-karnataka-india">
|
|
1369
1375
|
{{companyAddress}}
|
|
1370
1376
|
</div>
|
|
1377
|
+
{{#ifExists GSTNumber}}
|
|
1371
1378
|
<div class="frame-562">
|
|
1372
1379
|
<div class="gstin-33-aafci-2595-g-1-z-9"> GSTIN {{GSTNumber}}</div>
|
|
1373
1380
|
</div>
|
|
1381
|
+
{{/ifExists}}
|
|
1374
1382
|
</div>
|
|
1375
1383
|
</div>
|
|
1376
1384
|
</div>
|
|
1377
1385
|
</div>
|
|
1386
|
+
{{#ifExists PlaceOfSupply}}
|
|
1378
1387
|
<div class="place-of-supply-tamil-nadu-33">
|
|
1379
1388
|
Place Of Supply: {{PlaceOfSupply}}
|
|
1380
1389
|
</div>
|
|
1390
|
+
{{/ifExists}}
|
|
1381
1391
|
</div>
|
|
1382
1392
|
<div class="invoice-info">
|
|
1383
1393
|
<div class="card-header">
|
|
@@ -1496,12 +1506,14 @@
|
|
|
1496
1506
|
</div>
|
|
1497
1507
|
</div>
|
|
1498
1508
|
{{#each tax }}
|
|
1509
|
+
{{#gtZero taxAmount}}
|
|
1499
1510
|
<div class="frame-2392">
|
|
1500
1511
|
<div class="text9">{{type}} ({{value}}%)</div>
|
|
1501
1512
|
<div class="frame-9157">
|
|
1502
1513
|
<div class="text10">{{currency}} {{taxAmount}}</div>
|
|
1503
1514
|
</div>
|
|
1504
1515
|
</div>
|
|
1516
|
+
{{/gtZero}}
|
|
1505
1517
|
{{/each }}
|
|
1506
1518
|
<!-- <div class="frame-2398">
|
|
1507
1519
|
<div class="text9">CGST (9%)</div>
|
package/src/utils/currency.js
CHANGED
|
@@ -13,3 +13,36 @@ export const CURRENCY_SYMBOLS = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const symbolFor = ( currency ) => CURRENCY_SYMBOLS[currency] ?? '$';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Words form of each currency, used in the invoice "Total In Words" line.
|
|
19
|
+
* Keys match billing.currency enums. Unknown keys fall back to 'US Dollar'
|
|
20
|
+
* to match the pre-existing default.
|
|
21
|
+
*/
|
|
22
|
+
export const CURRENCY_WORDS = {
|
|
23
|
+
inr: 'Indian Rupee',
|
|
24
|
+
dollar: 'US Dollar',
|
|
25
|
+
singaporedollar: 'Singapore Dollar',
|
|
26
|
+
euro: 'Euro',
|
|
27
|
+
aed: 'UAE Dirham',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const wordFor = ( currency ) => CURRENCY_WORDS[currency] ?? 'US Dollar';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Currency-aware monetary rounding.
|
|
34
|
+
* - INR -> rounded to a whole number (no decimals).
|
|
35
|
+
* - every other currency -> kept to exactly 2 decimal places.
|
|
36
|
+
* Returns a Number (not a string) so downstream arithmetic still works.
|
|
37
|
+
* Non-finite input is normalised to 0.
|
|
38
|
+
*
|
|
39
|
+
* @param {number|string} value - the monetary value to round.
|
|
40
|
+
* @param {string} currency - the currency code (e.g. 'inr', 'dollar').
|
|
41
|
+
* @return {number} the rounded amount.
|
|
42
|
+
*/
|
|
43
|
+
export const roundAmount = ( value, currency ) => {
|
|
44
|
+
const n = Number( value );
|
|
45
|
+
if ( !isFinite( n ) ) return 0;
|
|
46
|
+
if ( currency === 'inr' ) return Math.round( n );
|
|
47
|
+
return Math.round( n * 100 ) / 100;
|
|
48
|
+
};
|
|
@@ -13,5 +13,11 @@ Handlebars.registerHelper( 'gte', function( a, b, options ) {
|
|
|
13
13
|
Handlebars.registerHelper( 'ifExists', function( value, options ) {
|
|
14
14
|
return value ? options.fn( this ) : options.inverse( this );
|
|
15
15
|
} );
|
|
16
|
+
// Renders the block only when `value` parses to a number greater than zero.
|
|
17
|
+
// Used to hide tax lines (IGST/CGST/SGST) whose amount is 0 / "0" / "0.00".
|
|
18
|
+
Handlebars.registerHelper( 'gtZero', function( value, options ) {
|
|
19
|
+
const n = Number( String( value ).replace( /,/g, '' ) );
|
|
20
|
+
return isFinite( n ) && n > 0 ? options.fn( this ) : options.inverse( this );
|
|
21
|
+
} );
|
|
16
22
|
|
|
17
23
|
export default Handlebars;
|