Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.

Commit 0833309

Browse files
authored
feat: support multiple employee currencies and rates (#110)
1 parent 19c8487 commit 0833309

69 files changed

Lines changed: 888 additions & 1176 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,6 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple
229229
'user',
230230
'organizationDepartments',
231231
'organizationPosition',
232-
'hourlyRates',
233232
'organizationEmploymentTypes',
234233
'tags',
235234
'skills',

apps/gauzy/src/app/pages/invoices/invoice-add/by-role/invoice-add-by-role.component.html

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -492,14 +492,12 @@ <h4>{{ (isEstimate ? 'INVOICES_PAGE.ADD_ESTIMATE' : 'INVOICES_PAGE.ADD_INVOICE')
492492
</ng-container>
493493
</div>
494494

495-
<div class="total-invoice" *ngFor="let currency of getCurrencies()">
495+
<div class="total d-flex">
496496
<div class="total-item">
497-
{{ 'INVOICES_PAGE.SUBTOTAL' | translate }}: {{ currency }}
498-
{{ totalsByCurrency[currency]?.subtotal.toFixed(2) }}
497+
{{ 'INVOICES_PAGE.SUBTOTAL' | translate }}: {{ currency }} {{ subtotal.toFixed(2) }}
499498
</div>
500499
<div class="total-item">
501-
{{ 'INVOICES_PAGE.TOTAL' | translate }}: {{ currency }}
502-
{{ totalsByCurrency[currency]?.total.toFixed(2) }}
500+
{{ 'INVOICES_PAGE.TOTAL' | translate }}: {{ currency }} {{ total.toFixed(2) }}
503501
</div>
504502
</div>
505503
</div>

apps/gauzy/src/app/pages/invoices/invoice-add/by-role/invoice-add-by-role.component.scss

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,3 @@ $shadow: 0 0 0 nb-theme(button-outline-width)
3131
color: nb-theme(button-outline-basic-disabled-text-color);
3232
box-shadow: unset;
3333
}
34-
35-
.total-invoice {
36-
display: flex;
37-
gap: 1rem;
38-
justify-content: flex-end;
39-
max-width: 40%;
40-
margin-left: auto;
41-
}

apps/gauzy/src/app/pages/invoices/invoice-add/by-role/invoice-add-by-role.component.ts

Lines changed: 52 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import {
2121
IReportDayData,
2222
IReportDayGroupByEmployee,
2323
IGetEmployeeHourlyRateInput,
24-
IEmployeeHourlyRate,
25-
ICurrencyTotals
24+
IEmployeeHourlyRate
2625
} from '@gauzy/contracts';
2726
import { filter, tap } from 'rxjs/operators';
2827
import { compareDate, distinctUntilChange, extractNumber, isEmpty, isNotEmpty } from '@gauzy/ui-core/common';
@@ -98,13 +97,12 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
9897
disableSaveButton = true;
9998
organizationId: string;
10099
discountAfterTax: boolean;
100+
subtotal = 0;
101+
total = 0;
101102
currency: string;
102103
selectedLanguage: string;
103104
selectedDateRange: IDateRangePicker;
104-
totalsByCurrency: Record<string, ICurrencyTotals>;
105105
private rate = 0;
106-
private subtotal: Record<string, number>;
107-
private total = 0;
108106

109107
private _isEstimate = false;
110108
@Input() set isEstimate(val: boolean) {
@@ -349,8 +347,8 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
349347
type: 'text',
350348
isFilterable: false,
351349
width: '13%',
352-
valuePrepareFunction: (cell, row) => {
353-
return `${row?.row?.data?.currency ?? ''} ${cell ?? 0}`;
350+
valuePrepareFunction: (cell) => {
351+
return `${this.currency} ${cell ?? this.rate}`;
354352
}
355353
};
356354
quantity = {
@@ -390,8 +388,8 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
390388
type: 'text',
391389
isAddable: false,
392390
isEditable: false,
393-
valuePrepareFunction: (cell, row: any) => {
394-
return `${row?.row?.data?.currency ?? ''} ${parseFloat(cell ?? '0')?.toFixed(2) ?? (0).toFixed(2)}`;
391+
valuePrepareFunction: (cell: string) => {
392+
return `${this.currency} ${parseFloat(cell ?? '0')?.toFixed(2) ?? (0).toFixed(2)}`;
395393
},
396394
isFilterable: false,
397395
width: '13%'
@@ -453,16 +451,11 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
453451
} = this.form.value;
454452

455453
try {
456-
const amounts = Object.entries(this.totalsByCurrency).map(([currency, values]: any) => ({
457-
currency,
458-
totalValue: values.total,
459-
organizationId,
460-
tenantId
461-
}));
462454
const createdInvoice = await this.invoicesService.addOwn({
463455
invoiceNumber,
464456
invoiceDate: moment(invoiceDate).startOf('day').toDate(),
465457
dueDate: moment(dueDate).endOf('day').toDate(),
458+
currency: this.currency,
466459
discountValue,
467460
discountType,
468461
tax,
@@ -471,7 +464,7 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
471464
tax2Type,
472465
terms: notes,
473466
paid: false,
474-
amounts,
467+
totalValue: +this.total.toFixed(2),
475468
fromUserId: this.selectedEmployee?.id,
476469
fromOrganizationId: organizationId,
477470
organizationId,
@@ -746,7 +739,7 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
746739
);
747740
}
748741

749-
private loadInvoiceEmployeeRateData(): Observable<IEmployeeHourlyRate[]> {
742+
private loadInvoiceEmployeeRateData(): Observable<number> {
750743
const request: IGetEmployeeHourlyRateInput = {
751744
organizationId: this.organization.id,
752745
startDate: this.selectedDateRange.startDate.toISOString(),
@@ -755,28 +748,15 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
755748
};
756749

757750
return this.employeeRateService.getEmployeeHourlyRate(request).pipe(
758-
map((data: IEmployeeHourlyRate[] = []) => {
759-
const unique = data.filter(
760-
(item, index, self) =>
761-
index ===
762-
self.findIndex(
763-
(t) =>
764-
t.billRateCurrency === item.billRateCurrency && t.billRateValue === item.billRateValue
765-
)
766-
);
767-
// sort by lastUpdate ascending
768-
const sorted = unique.sort(
769-
(a, b) => new Date(a.lastUpdate).getTime() - new Date(b.lastUpdate).getTime()
770-
);
771-
772-
// take the last (most recent) one
773-
const last = sorted[sorted.length - 1];
774-
if (last) {
775-
this.rate = last.billRateValue;
776-
this.currency = last.billRateCurrency;
751+
map((data: IEmployeeHourlyRate[]) => {
752+
if (data && data.length > 0) {
753+
const rate = data[0];
754+
this.rate = rate.billRateValue;
755+
this.currency = rate.billRateCurrency;
756+
return this.rate;
757+
} else {
758+
return 0;
777759
}
778-
779-
return sorted;
780760
}),
781761
untilDestroyed(this)
782762
);
@@ -900,16 +880,9 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
900880
switch (this.selectedInvoiceType) {
901881
case InvoiceTypeEnum.BY_EMPLOYEE_HOURS:
902882
if (isNotEmpty(this.selectedEmployee)) {
903-
const { time, rates } = await this.getEmployeeData();
904-
rates.forEach((rate) => {
905-
const data = this.createInvoiceData(
906-
this.selectedEmployee?.name,
907-
rate.billRateValue,
908-
time,
909-
rate.billRateCurrency
910-
);
911-
invoiceData.push(data);
912-
});
883+
const { time, rate } = await this.getEmployeeData();
884+
const data = this.createInvoiceData(this.selectedEmployee?.name, rate, time);
885+
invoiceData.push(data);
913886
}
914887
break;
915888
case InvoiceTypeEnum.BY_PROJECT_HOURS:
@@ -960,21 +933,14 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
960933
await generateData();
961934

962935
if (isNotEmpty(invoiceData)) {
963-
const subtotal: Record<string, number> = {};
936+
let subtotal = 0;
964937

965938
invoiceData.forEach((data) => {
966-
const currency = data.currency || this.currency;
967-
const lineTotal = +data.price * +data.quantity;
968-
969-
if (!subtotal[currency]) {
970-
subtotal[currency] = 0;
971-
}
972-
973-
subtotal[currency] += lineTotal;
939+
subtotal += +data.price * +data.quantity;
974940
});
975941
this.subtotal = subtotal;
976942
} else {
977-
this.subtotal = {};
943+
this.subtotal = 0;
978944
}
979945

980946
this.shouldLoadTable = true;
@@ -984,20 +950,16 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
984950
this.calculateTotal();
985951
}
986952

987-
getCurrencies(): string[] {
988-
return this.totalsByCurrency ? Object.keys(this.totalsByCurrency) : [];
989-
}
990-
991953
private getEmployeeData() {
992-
return new Promise<{ time: number; rates: IEmployeeHourlyRate[] }>((resolve, reject) => {
954+
return new Promise<{ time: number; rate: number }>((resolve, reject) => {
993955
forkJoin({
994956
time: this.loadInvoiceTimeLogsData(),
995-
rates: this.loadInvoiceEmployeeRateData()
957+
rate: this.loadInvoiceEmployeeRateData()
996958
})
997959
.pipe(untilDestroyed(this))
998960
.subscribe({
999-
next: ({ time, rates }) => {
1000-
resolve({ time, rates });
961+
next: ({ time, rate }) => {
962+
resolve({ time, rate });
1001963
},
1002964
error: (err) => {
1003965
this.toastrService.error(err?.message || 'An unknown error occurred');
@@ -1041,43 +1003,33 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
10411003
this.form.value.discountValue && this.form.value.discountValue > 0 ? this.form.value.discountValue : 0;
10421004
const tax = this.form.value.tax && this.form.value.tax > 0 ? this.form.value.tax : 0;
10431005
const tax2 = this.form.value.tax2 && this.form.value.tax2 > 0 ? this.form.value.tax2 : 0;
1006+
let totalDiscount = 0;
1007+
let totalTax = 0;
10441008
const tableData = await this.smartTableSource.getAll();
1045-
const totalsByCurrency: Record<string, ICurrencyTotals> = {};
1046-
1047-
tableData.forEach((item) => {
1048-
const currency = item.currency || this.currency;
1049-
const lineTotal = +item.price * +item.quantity;
1050-
1051-
if (!totalsByCurrency[currency]) {
1052-
totalsByCurrency[currency] = { subtotal: 0, totalTax: 0, totalDiscount: 0, total: 0 };
1053-
}
1054-
1055-
// --- SUBTOTAL ---
1056-
totalsByCurrency[currency].subtotal += lineTotal;
1057-
1009+
for (const item of tableData) {
10581010
// --- TAX ---
10591011
if (item.applyTax) {
10601012
switch (this.form.value.taxType) {
10611013
case DiscountTaxTypeEnum.PERCENT:
1062-
totalsByCurrency[currency].totalTax += lineTotal * (+tax / 100);
1014+
totalTax += item.totalValue * (+tax / 100);
10631015
break;
10641016
case DiscountTaxTypeEnum.FLAT_VALUE:
1065-
totalsByCurrency[currency].totalTax += +tax;
1017+
totalTax += +tax;
10661018
break;
10671019
default:
1068-
totalsByCurrency[currency].totalTax = 0;
1020+
totalTax = 0;
10691021
break;
10701022
}
10711023

10721024
switch (this.form.value.tax2Type) {
10731025
case DiscountTaxTypeEnum.PERCENT:
1074-
totalsByCurrency[currency].totalTax += lineTotal * (+tax2 / 100);
1026+
totalTax += item.totalValue * (+tax2 / 100);
10751027
break;
10761028
case DiscountTaxTypeEnum.FLAT_VALUE:
1077-
totalsByCurrency[currency].totalTax += +tax2;
1029+
totalTax += +tax2;
10781030
break;
10791031
default:
1080-
totalsByCurrency[currency].totalTax = 0;
1032+
totalTax += +tax2;
10811033
break;
10821034
}
10831035
}
@@ -1087,40 +1039,25 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
10871039
switch (this.form.value.discountType) {
10881040
case DiscountTaxTypeEnum.PERCENT:
10891041
if (!this.discountAfterTax) {
1090-
totalsByCurrency[currency].totalDiscount += lineTotal * (+discountValue / 100);
1042+
totalDiscount += item.totalValue * (+discountValue / 100);
10911043
}
10921044
break;
10931045
case DiscountTaxTypeEnum.FLAT_VALUE:
1094-
totalsByCurrency[currency].totalDiscount += +discountValue;
1046+
totalDiscount += +discountValue;
10951047
break;
10961048
default:
1097-
totalsByCurrency[currency].totalDiscount = 0;
1049+
totalDiscount = 0;
10981050
break;
10991051
}
11001052
}
1101-
});
1102-
1103-
// --- DISCOUNT AFTER TAX ---
1104-
Object.keys(totalsByCurrency).forEach((currency) => {
1105-
if (this.discountAfterTax && this.form.value.discountType === DiscountTaxTypeEnum.PERCENT) {
1106-
totalsByCurrency[currency].totalDiscount =
1107-
(totalsByCurrency[currency].subtotal + totalsByCurrency[currency].totalTax) *
1108-
(+discountValue / 100);
1109-
}
1110-
1111-
// --- TOTAL ---
1112-
totalsByCurrency[currency].total =
1113-
totalsByCurrency[currency].subtotal -
1114-
totalsByCurrency[currency].totalDiscount +
1115-
totalsByCurrency[currency].totalTax;
1116-
1117-
if (totalsByCurrency[currency].total < 0) totalsByCurrency[currency].total = 0;
1118-
});
1119-
1120-
this.totalsByCurrency = totalsByCurrency;
1121-
1122-
// Global total across all currencies
1123-
this.total = Object.values(totalsByCurrency).reduce((acc, cur) => acc + cur.total, 0);
1053+
}
1054+
if (this.discountAfterTax && this.form.value.discountType === DiscountTaxTypeEnum.PERCENT) {
1055+
totalDiscount = (this.subtotal + totalTax) * (+discountValue / 100);
1056+
}
1057+
this.total = this.subtotal - totalDiscount + totalTax;
1058+
if (this.total < 0) {
1059+
this.total = 0;
1060+
}
11241061

11251062
// Update pagination
11261063
this.setPagination({
@@ -1170,11 +1107,7 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
11701107
) {
11711108
newData = { ...newData, price: extractNumber(newData.price) };
11721109
const itemTotal = +newData.quantity * +extractNumber(newData.price);
1173-
newData.totalValue = itemTotal;
1174-
// Update subtotal for currency
1175-
const currency = newData.currency || this.currency;
1176-
this.subtotal[currency] += itemTotal;
1177-
1110+
this.subtotal += itemTotal;
11781111
await event.confirm.resolve(newData);
11791112
await this.calculateTotal();
11801113
} else {
@@ -1221,11 +1154,10 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
12211154
const newValue = +newData.quantity * +extractNumber(event.newData.price);
12221155
newData.totalValue = newValue;
12231156

1224-
const currency = newData.currency || this.currency;
12251157
if (newValue > oldValue) {
1226-
this.subtotal[currency] += newValue - oldValue;
1158+
this.subtotal += newValue - oldValue;
12271159
} else if (oldValue > newValue) {
1228-
this.subtotal[currency] -= oldValue - newValue;
1160+
this.subtotal -= oldValue - newValue;
12291161
}
12301162
await event.confirm.resolve(newData);
12311163
await this.calculateTotal();
@@ -1239,8 +1171,7 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
12391171
}
12401172

12411173
async onDeleteConfirm(event) {
1242-
const currency = event.data.currency || this.currency;
1243-
this.subtotal[currency] -= +event.data.quantity * +event.data.price;
1174+
this.subtotal -= +event.data.quantity * +event.data.price;
12441175
await event.confirm.resolve(event.data);
12451176
await this.calculateTotal();
12461177
}

0 commit comments

Comments
 (0)