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

Commit 975679a

Browse files
authored
feat: ensure invoice PDF attached to email contains the latest user-entered data (#129)
1 parent 7c33155 commit 975679a

10 files changed

Lines changed: 311 additions & 35 deletions

File tree

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

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
IReportDayGroupByEmployee,
2323
IGetEmployeeHourlyRateInput,
2424
IEmployeeHourlyRate,
25-
IReportDayGroupByProject
25+
IReportDayGroupByProject,
26+
IInvoiceItem
2627
} from '@gauzy/contracts';
2728
import { filter, tap } from 'rxjs/operators';
2829
import { compareDate, distinctUntilChange, extractNumber, isEmpty, isNotEmpty } from '@gauzy/ui-core/common';
@@ -687,7 +688,24 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
687688
return;
688689
}
689690

690-
const { invoiceNumber, invoiceDate, dueDate } = this.form.value;
691+
if (!this.organization) {
692+
return;
693+
}
694+
const { id: organizationId } = this.organization;
695+
const { tenantId } = this.store.user;
696+
const {
697+
invoiceNumber,
698+
invoiceDate,
699+
dueDate,
700+
discountValue,
701+
discountType,
702+
tax,
703+
tax2,
704+
taxType,
705+
tax2Type,
706+
notes,
707+
tags
708+
} = this.form.value;
691709

692710
if (!invoiceDate || !dueDate || compareDate(invoiceDate, dueDate)) {
693711
this.toastrService.danger(
@@ -709,18 +727,88 @@ export class InvoiceAddByRoleComponent extends PaginationFilterBaseComponent imp
709727
return;
710728
}
711729

712-
const invoice = await this.createInvoiceEstimate(InvoiceStatusTypesEnum.SENT);
713-
const invoiceItems = await this.createInvoiceEstimateItems();
730+
const invoice: IInvoice = {
731+
invoiceNumber,
732+
invoiceDate: moment(invoiceDate).startOf('day').toDate(),
733+
dueDate: moment(dueDate).endOf('day').toDate(),
734+
currency: this.currency,
735+
discountValue,
736+
discountType,
737+
tax,
738+
tax2,
739+
taxType,
740+
tax2Type,
741+
terms: notes,
742+
paid: false,
743+
totalValue: +this.total.toFixed(2),
744+
fromUserId: this.selectedEmployee?.id,
745+
fromUser: this.selectedEmployee,
746+
fromOrganization: this.organization,
747+
organizationId,
748+
tenantId,
749+
invoiceType: this.selectedInvoiceType,
750+
tags,
751+
isEstimate: this.isEstimate,
752+
status: InvoiceStatusTypesEnum.SENT,
753+
sentTo: this.organizationId,
754+
isArchived: false
755+
};
714756

715-
await firstValueFrom(
716-
this.dialogService.open(InvoiceEmailMutationComponent, {
717-
context: {
718-
invoice: invoice,
719-
invoiceItems: invoiceItems,
720-
isEstimate: this.isEstimate
721-
}
722-
}).onClose
723-
);
757+
const invoiceItems: IInvoiceItem[] = [];
758+
759+
for (const invoiceItem of tableSources) {
760+
const id = invoiceItem.selectedItem ? invoiceItem.selectedItem.id : null;
761+
const itemToAdd = {
762+
description: invoiceItem.description,
763+
currency: invoiceItem.currency,
764+
price: Number(invoiceItem.price),
765+
quantity: Number(invoiceItem.quantity),
766+
totalValue: Number(invoiceItem.totalValue),
767+
invoiceId: invoiceNumber,
768+
applyTax: invoiceItem.applyTax,
769+
applyDiscount: invoiceItem.applyDiscount,
770+
organizationId,
771+
tenantId
772+
};
773+
774+
switch (this.selectedInvoiceType) {
775+
case InvoiceTypeEnum.BY_EMPLOYEE_HOURS:
776+
itemToAdd['employeeId'] = this.selectedEmployee?.employee?.id;
777+
break;
778+
case InvoiceTypeEnum.BY_PROJECT_HOURS:
779+
itemToAdd['projectId'] = id;
780+
itemToAdd['project'] = {
781+
id: invoiceItem.selectedItem?.id,
782+
name: invoiceItem.selectedItem?.name
783+
};
784+
break;
785+
case InvoiceTypeEnum.BY_TASK_HOURS:
786+
itemToAdd['taskId'] = id;
787+
break;
788+
case InvoiceTypeEnum.BY_PRODUCTS:
789+
itemToAdd['productId'] = id;
790+
break;
791+
case InvoiceTypeEnum.BY_EXPENSES:
792+
itemToAdd['expenseId'] = id;
793+
break;
794+
default:
795+
break;
796+
}
797+
invoiceItems.push(itemToAdd);
798+
}
799+
800+
invoice.invoiceItems = invoiceItems;
801+
const dialogRef = this.dialogService.open(InvoiceEmailMutationComponent, {
802+
context: {
803+
invoice,
804+
invoiceItems,
805+
isEstimate: this.isEstimate
806+
}
807+
});
808+
809+
const result = await firstValueFrom(dialogRef.onClose);
810+
811+
if (result !== 'ok') return;
724812

725813
if (this.isEstimate) {
726814
this.toastrService.success(

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ export class InvoiceEditByRoleComponent extends PaginationFilterBaseComponent im
638638
id: invoiceData.id,
639639
invoiceNumber: invoiceData.invoiceNumber,
640640
invoiceDate: invoiceData.invoiceDate,
641-
currency: invoiceData.currency,
641+
currency: this.invoice?.currency,
642642
dueDate: invoiceData.dueDate,
643643
discountValue: invoiceData.discountValue,
644644
discountType: invoiceData.discountType,
@@ -650,6 +650,7 @@ export class InvoiceEditByRoleComponent extends PaginationFilterBaseComponent im
650650
paid: false,
651651
totalValue: +this.total.toFixed(2),
652652
fromUserId: this.invoice.fromUser?.id,
653+
fromUser: this.invoice?.fromUser,
653654
toOrganization: this.invoice.toOrganization,
654655
organizationId,
655656
tenantId,
@@ -675,10 +676,13 @@ export class InvoiceEditByRoleComponent extends PaginationFilterBaseComponent im
675676
};
676677
switch (this.invoice.invoiceType) {
677678
case InvoiceTypeEnum.BY_EMPLOYEE_HOURS:
678-
itemToAdd['employeeId'] = invoiceItem.selectedItem;
679+
itemToAdd['employeeId'] = this.invoice?.fromUser?.employee?.id;
679680
break;
680681
case InvoiceTypeEnum.BY_PROJECT_HOURS:
681-
itemToAdd['projectId'] = invoiceItem.selectedItem;
682+
itemToAdd['project'] = {
683+
id: invoiceItem.selectedItem?.id,
684+
name: invoiceItem.selectedItem?.name
685+
};
682686
break;
683687
case InvoiceTypeEnum.BY_TASK_HOURS:
684688
itemToAdd['taskId'] = invoiceItem.selectedItem;

apps/gauzy/src/app/pages/invoices/invoice-email/invoice-email-mutation.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ <h5 class="title">
77
</h5>
88
</nb-card-header>
99
<nb-card-body class="invoice-email-body">
10-
<ga-invoice-pdf [invoice]="invoice"></ga-invoice-pdf>
10+
<ga-invoice-pdf [invoice]="invoice" [alreadyGeneratedInvoice]="false"></ga-invoice-pdf>
1111
<form [formGroup]="form" *ngIf="form">
1212
<div class="row">
1313
<div class="form-group col-12">

apps/gauzy/src/app/pages/invoices/invoice-email/invoice-email-mutation.component.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { Component, OnInit } from '@angular/core';
22
import { TranslationBaseComponent } from '@gauzy/ui-core/i18n';
3-
import { IInvoice, InvoiceStatusTypesEnum, IInvoiceItem } from '@gauzy/contracts';
3+
import { IInvoice, InvoiceStatusTypesEnum, IInvoiceItem, IInvoiceItemCreateInput } from '@gauzy/contracts';
44
import { TranslateService } from '@ngx-translate/core';
55
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
66
import { NbDialogRef } from '@nebular/theme';
7-
import { InvoiceEstimateHistoryService, InvoicesService, Store, ToastrService } from '@gauzy/ui-core/core';
7+
import {
8+
InvoiceEstimateHistoryService,
9+
InvoiceItemService,
10+
InvoicesService,
11+
Store,
12+
ToastrService
13+
} from '@gauzy/ui-core/core';
14+
import moment from 'moment';
815

916
@Component({
10-
selector: 'ga-invoice-email',
11-
templateUrl: './invoice-email-mutation.component.html',
12-
styleUrls: ['./invoice-email-mutation.component.scss'],
13-
standalone: false
17+
selector: 'ga-invoice-email',
18+
templateUrl: './invoice-email-mutation.component.html',
19+
styleUrls: ['./invoice-email-mutation.component.scss'],
20+
standalone: false
1421
})
1522
export class InvoiceEmailMutationComponent extends TranslationBaseComponent implements OnInit {
1623
invoice: IInvoice;
@@ -26,7 +33,9 @@ export class InvoiceEmailMutationComponent extends TranslationBaseComponent impl
2633
private readonly toastrService: ToastrService,
2734
private readonly invoiceService: InvoicesService,
2835
private readonly store: Store,
29-
private readonly invoiceEstimateHistoryService: InvoiceEstimateHistoryService
36+
private readonly invoiceEstimateHistoryService: InvoiceEstimateHistoryService,
37+
private readonly invoicesService: InvoicesService,
38+
private readonly invoiceItemService: InvoiceItemService
3039
) {
3140
super(translateService);
3241
}
@@ -47,6 +56,11 @@ export class InvoiceEmailMutationComponent extends TranslationBaseComponent impl
4756

4857
const { email } = this.form.value;
4958

59+
if (!this.invoice.id) {
60+
const createdInvoice = await this.createInvoiceEstimate(InvoiceStatusTypesEnum.SENT);
61+
if (createdInvoice) await this.createInvoiceEstimateItems();
62+
}
63+
5064
await this.invoiceService.sendEmail(
5165
email,
5266
this.invoice.invoiceNumber,
@@ -90,6 +104,52 @@ export class InvoiceEmailMutationComponent extends TranslationBaseComponent impl
90104
});
91105
}
92106

107+
async createInvoiceEstimate(status: string, sendTo?: string) {
108+
try {
109+
const createdInvoice = await this.invoicesService.addOwn({
110+
invoiceNumber: this.invoice.invoiceNumber,
111+
invoiceDate: moment(this.invoice.invoiceDate).startOf('day').toDate(),
112+
dueDate: moment(this.invoice.dueDate).endOf('day').toDate(),
113+
currency: this.invoice.currency,
114+
discountValue: this.invoice.discountValue,
115+
discountType: this.invoice.discountType,
116+
tax: this.invoice.tax,
117+
tax2: this.invoice.tax2,
118+
taxType: this.invoice.taxType,
119+
tax2Type: this.invoice.tax2Type,
120+
terms: this.invoice.terms,
121+
paid: false,
122+
totalValue: this.invoice.totalValue,
123+
fromUserId: this.invoice.fromUserId,
124+
fromOrganizationId: this.invoice.fromOrganization?.id,
125+
organizationId: this.invoice.fromOrganization?.id,
126+
tenantId: this.invoice.tenantId,
127+
invoiceType: this.invoice.invoiceType,
128+
tags: this.invoice.tags,
129+
isEstimate: this.invoice.isEstimate,
130+
status: status,
131+
sentTo: sendTo,
132+
isArchived: this.invoice.isArchived
133+
});
134+
this.createdInvoice = createdInvoice;
135+
return createdInvoice;
136+
} catch (error) {
137+
this.toastrService.danger(error);
138+
}
139+
}
140+
141+
async createInvoiceEstimateItems() {
142+
const invoiceItems: IInvoiceItemCreateInput[] = this.invoice.invoiceItems.map((item) => ({
143+
...item,
144+
invoiceId: this.createdInvoice.id
145+
}));
146+
try {
147+
return await this.invoiceItemService.createBulk(this.createdInvoice?.id, invoiceItems);
148+
} catch (error) {
149+
this.toastrService.danger(error);
150+
}
151+
}
152+
93153
cancel() {
94154
this.dialogRef.close();
95155
}

apps/gauzy/src/app/pages/invoices/invoice-pdf/invoice-pdf.component.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
55
import { tap } from 'rxjs/operators';
66
import { InvoicesService } from '@gauzy/ui-core/core';
77
import { TranslationBaseComponent } from '@gauzy/ui-core/i18n';
8+
import { Observable } from 'rxjs';
89

910
@UntilDestroy({ checkProperties: true })
1011
@Component({
@@ -47,6 +48,7 @@ import { TranslationBaseComponent } from '@gauzy/ui-core/i18n';
4748
})
4849
export class InvoicePdfComponent extends TranslationBaseComponent implements OnInit {
4950
@Input() invoice: IInvoice;
51+
@Input() alreadyGeneratedInvoice?: boolean = true;
5052
fileURL: string;
5153
isLoading: boolean;
5254
error: boolean;
@@ -62,15 +64,17 @@ export class InvoicePdfComponent extends TranslationBaseComponent implements OnI
6264
}
6365

6466
loadInvoicePdf() {
65-
if (!this.invoice?.id) {
66-
this.isLoading = false;
67-
return;
68-
}
69-
7067
const { id: invoiceId } = this.invoice;
7168

72-
this.invoicesService
73-
.downloadInvoicePdf(invoiceId)
69+
let pdfObservable: Observable<Blob>;
70+
71+
if (this.alreadyGeneratedInvoice) {
72+
pdfObservable = this.invoicesService.downloadInvoicePdf(invoiceId);
73+
} else {
74+
pdfObservable = this.invoicesService.downloadInvoicePdfByInvoice(this.invoice);
75+
}
76+
77+
pdfObservable
7478
.pipe(
7579
tap((data) => {
7680
if (data && data instanceof Blob) {

packages/contracts/src/lib/invoice-item.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface IInvoiceItemFindInput {
3434
export interface IInvoiceItemCreateInput extends IBasePerTenantAndOrganizationEntityModel {
3535
price: number;
3636
quantity: number;
37-
totalValue: number;
37+
totalValue?: number;
3838
description?: string;
3939
invoiceId?: string;
4040
taskId?: string;

packages/core/src/lib/invoice/generate-invoice-pdf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export async function generateInvoicePdfDefinition(
2121
switch (invoice.invoiceType) {
2222
case InvoiceTypeEnum.BY_EMPLOYEE_HOURS: {
2323
const employee = item.employee;
24-
currentItem[0] = `${employee.user.name}`;
24+
currentItem[0] = `${employee?.user?.name ?? invoice.fromUser?.name}`;
2525
break;
2626
}
2727
case InvoiceTypeEnum.BY_PROJECT_HOURS: {

0 commit comments

Comments
 (0)