diff --git a/client/i18n/fr.json b/client/i18n/fr.json index 7cfb15d7..9da9f18a 100644 --- a/client/i18n/fr.json +++ b/client/i18n/fr.json @@ -95,10 +95,10 @@ }, "daily_rates": { "title": "TJM", - "users": "Coopérateurs - salariés", + "user": "Coopérateur - salarié", "amount": "Montant HT", - "tasks": "Mission", - "customers": "Nom du client", + "task": "Nom de la mission", + "customer": "Nom du client", "errors": { "already_exist": "Ce TJM a déjà été configuré.", "not_found": "Le TJM n'existe pas." @@ -251,11 +251,10 @@ "requests": { "title": "Demandes de congé", "view": "Demande de {user}", - "users": "Coopérateurs - salariés", + "user": "Coopérateur - salarié", "all_day": "Toute la journée", "periods": "Périodes", "period": "Du {from} au {to}", - "leave_types": "Type de congé", "status": "Etat", "duration": "Durée", "comment": "Commentaire", diff --git a/client/src/routes/accounting/daily_rates/_Table.svelte b/client/src/routes/accounting/daily_rates/_Table.svelte index 422422fd..441d5880 100644 --- a/client/src/routes/accounting/daily_rates/_Table.svelte +++ b/client/src/routes/accounting/daily_rates/_Table.svelte @@ -10,10 +10,10 @@ - {$_('accounting.daily_rates.users')} + {$_('accounting.daily_rates.user')} + {$_('accounting.daily_rates.customer')} + {$_('accounting.daily_rates.task')} {$_('accounting.daily_rates.amount')} - {$_('accounting.daily_rates.tasks')} - {$_('accounting.daily_rates.customers')} {$_('common.actions')} @@ -21,9 +21,9 @@ {#each items as { id, user, task, customer, amount } (id)} {user.firstName} {user.lastName} - {format(amount)} - {task.name} {customer.name} + {task.name} + {format(amount)}
diff --git a/client/src/routes/human_resources/leaves/requests/[id]/_Detail.svelte b/client/src/routes/human_resources/leaves/requests/[id]/_Detail.svelte index 722588c2..0bcff6ec 100644 --- a/client/src/routes/human_resources/leaves/requests/[id]/_Detail.svelte +++ b/client/src/routes/human_resources/leaves/requests/[id]/_Detail.svelte @@ -30,7 +30,7 @@ - {$_('human_resources.leaves.requests.leave_types')} + {$_('human_resources.leaves.requests.leave_type.title')} {$_(`human_resources.leaves.requests.leave_type.${leaveRequest.type}`)} diff --git a/client/src/routes/human_resources/leaves/requests/_Table.svelte b/client/src/routes/human_resources/leaves/requests/_Table.svelte index a735e620..34d9c6a8 100644 --- a/client/src/routes/human_resources/leaves/requests/_Table.svelte +++ b/client/src/routes/human_resources/leaves/requests/_Table.svelte @@ -19,9 +19,9 @@ - {$_('human_resources.leaves.requests.users')} + {$_('human_resources.leaves.requests.user')} {$_('human_resources.leaves.requests.periods')} - {$_('human_resources.leaves.requests.leave_types')} + {$_('human_resources.leaves.requests.leave_type.title')} {$_('human_resources.leaves.requests.status')} {$_('common.actions')} diff --git a/server/migrations/1606403084585-InvoiceUnitPrice.ts b/server/migrations/1606403084585-InvoiceUnitPrice.ts new file mode 100644 index 00000000..8b728cfa --- /dev/null +++ b/server/migrations/1606403084585-InvoiceUnitPrice.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class InvoiceUnitPrice1606403084585 implements MigrationInterface { + name = 'InvoiceUnitPrice1606403084585' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "invoice_item" RENAME COLUMN "timeSpent" TO "quantity"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "invoice_item" RENAME COLUMN "quantity" TO "timeSpent"`); + } + +} diff --git a/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.spec.ts b/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.spec.ts index 7c2b1883..29f55707 100644 --- a/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.spec.ts +++ b/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.spec.ts @@ -111,7 +111,7 @@ describe('GenerateInvoiceCommandHandler', () => { task_name: 'Développement', first_name: 'Mathieu', last_name: 'MARCHOIS', - amount: 60000 + daily_rate: 60000 }, { time_spent: '420', @@ -119,7 +119,7 @@ describe('GenerateInvoiceCommandHandler', () => { task_name: 'Architecture', first_name: 'Mathieu', last_name: 'MARCHOIS', - amount: null + daily_rate: null }, { time_spent: '4200', @@ -127,7 +127,7 @@ describe('GenerateInvoiceCommandHandler', () => { task_name: 'Développement', first_name: 'Mathieu', last_name: 'MARCHOIS', - amount: 60000 + daily_rate: 60000 } ]; @@ -141,11 +141,12 @@ describe('GenerateInvoiceCommandHandler', () => { const savedInvoice = mock(Invoice); when(savedInvoice.getId()).thenReturn('fc8a4cd9-31eb-4fca-814d-b30c05de485d'); + when(project.getDayDuration()).thenReturn(420); const invoiceItems = [ - new InvoiceItem(invoice, 'Développement - Mathieu MARCHOIS', 180, 60000, 100), - new InvoiceItem(invoice, 'Architecture - Mathieu MARCHOIS', 420, 0, 0), - new InvoiceItem(invoice, 'Développement - Mathieu MARCHOIS', 4200, 60000, 0), + new InvoiceItem(invoice, 'Développement - Mathieu MARCHOIS', 43, 60000, 10000), + new InvoiceItem(invoice, 'Architecture - Mathieu MARCHOIS', 100, 0, 0), + new InvoiceItem(invoice, 'Développement - Mathieu MARCHOIS', 1000, 60000, 0), ]; when( diff --git a/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.ts b/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.ts index 0c6c9e1c..0e7642e1 100644 --- a/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.ts +++ b/server/src/Application/Accounting/Command/Invoice/GenerateInvoiceCommandHandler.ts @@ -11,6 +11,7 @@ import { NoBillableEventsFoundException } from 'src/Domain/Accounting/Exception/ import { IDateUtils } from 'src/Application/IDateUtils'; import { IProjectRepository } from 'src/Domain/Project/Repository/IProjectRepository'; import { ProjectNotFoundException } from 'src/Domain/Project/Exception/ProjectNotFoundException'; +import { Project } from 'src/Domain/Project/Project.entity'; @CommandHandler(GenerateInvoiceCommand) export class GenerateInvoiceCommandHandler { @@ -57,7 +58,7 @@ export class GenerateInvoiceCommandHandler { ); for (const event of events) { - invoiceItems.push(this.buildInvoiceItem(invoice, event)); + invoiceItems.push(this.buildInvoiceItem(invoice, project, event)); } const savedInvoice = await this.invoiceRepository.save(invoice); @@ -66,20 +67,22 @@ export class GenerateInvoiceCommandHandler { return savedInvoice.getId(); } - private buildInvoiceItem(invoice: Invoice, { + private buildInvoiceItem(invoice: Invoice, project: Project, { time_spent, billable, task_name, first_name, last_name, - amount + daily_rate }): InvoiceItem { + const quantity = Math.round(time_spent / project.getDayDuration() * 100) / 100; + return new InvoiceItem( invoice, `${task_name} - ${first_name} ${last_name}`, - Number(time_spent), - amount ? Number(amount) : 0, - billable ? 0 : 100 + Math.round(quantity * 100), + daily_rate ? daily_rate : 0, + billable ? 0 : 10000 ); } } diff --git a/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.spec.ts b/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.spec.ts index 655844e9..c3235a67 100644 --- a/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.spec.ts +++ b/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.spec.ts @@ -9,6 +9,7 @@ import { Pagination } from 'src/Application/Common/Pagination'; import { Project } from 'src/Domain/Project/Project.entity'; import { InvoiceView } from '../../View/DailyRate/InvoiceView'; import { ProjectView } from 'src/Application/Project/View/ProjectView'; +import { InvoiceItem } from 'src/Domain/Accounting/InvoiceItem.entity'; describe('GetInvoicesQueryHandler', () => { let invoiceRepository: InvoiceRepository; @@ -27,6 +28,16 @@ describe('GetInvoicesQueryHandler', () => { when(project.getName()).thenReturn('Plateforme web'); when(project.getCustomer()).thenReturn(instance(customer)); + const item11 = mock(InvoiceItem); + when(item11.getAmount()).thenReturn(60000); // 600 + when(item11.getQuantity()).thenReturn(100); // 1 day + when(item11.getDiscount()).thenReturn(0); + + const item12 = mock(InvoiceItem); + when(item12.getAmount()).thenReturn(60000); // 600 + when(item12.getQuantity()).thenReturn(300); // 3 day + when(item12.getDiscount()).thenReturn(5000); // 50 + const invoice1 = mock(Invoice); when(invoice1.getId()).thenReturn('d54f15d6-1a1d-47e8-8672-9f46018f9960'); when(invoice1.getInvoiceId()).thenReturn('FS-2020-0001'); @@ -34,6 +45,17 @@ describe('GetInvoicesQueryHandler', () => { when(invoice1.getCreatedAt()).thenReturn('2020-11-25T17:43:14.299Z'); when(invoice1.getExpiryDate()).thenReturn('2020-12-25T17:43:14.299Z'); when(invoice1.getProject()).thenReturn(instance(project)); + when(invoice1.getItems()).thenReturn([instance(item11), instance(item12)]); + + const item21 = mock(InvoiceItem); + when(item21.getAmount()).thenReturn(60000); // 600 + when(item21.getQuantity()).thenReturn(700); // 7 day + when(item21.getDiscount()).thenReturn(0); + + const item22 = mock(InvoiceItem); + when(item22.getAmount()).thenReturn(70000); // 700 + when(item22.getQuantity()).thenReturn(43); // 0.43 day + when(item22.getDiscount()).thenReturn(0); const invoice2 = mock(Invoice); when(invoice2.getId()).thenReturn('b3332cd1-5631-4b7b-a5d4-ba49910cb877'); @@ -42,6 +64,7 @@ describe('GetInvoicesQueryHandler', () => { when(invoice2.getExpiryDate()).thenReturn('2020-12-25T17:43:14.299Z'); when(invoice2.getStatus()).thenReturn(InvoiceStatus.PAYED); when(invoice2.getProject()).thenReturn(instance(project)); + when(invoice2.getItems()).thenReturn([instance(item21), instance(item22)]); when(invoiceRepository.findInvoices(1)).thenResolve([ [instance(invoice1), instance(invoice2)], @@ -60,7 +83,7 @@ describe('GetInvoicesQueryHandler', () => { InvoiceStatus.DRAFT, '2020-11-25T17:43:14.299Z', '2020-12-25T17:43:14.299Z', - 0, + 1800, new ProjectView( 'deffa668-b9af-4a52-94dd-61a35401b917', 'Plateforme web', @@ -78,7 +101,7 @@ describe('GetInvoicesQueryHandler', () => { InvoiceStatus.PAYED, '2020-11-25T17:43:14.299Z', '2020-12-25T17:43:14.299Z', - 0, + 5401.2, new ProjectView( 'deffa668-b9af-4a52-94dd-61a35401b917', 'Plateforme web', diff --git a/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.ts b/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.ts index 8803250f..be920e8d 100644 --- a/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.ts +++ b/server/src/Application/Accounting/Query/Invoice/GetInvoicesQueryHandler.ts @@ -24,6 +24,20 @@ export class GetInvoicesQueryHandler { const project = invoice.getProject(); const customer = project.getCustomer(); + let amountExcludingVat = 0; + + for (const item of invoice.getItems()) { + const amount = item.getAmount() / 100; + const quantity = item.getQuantity() / 100; + let totalAmount = amount * quantity; + + if (item.getDiscount() > 0) { + totalAmount *= (item.getDiscount() / 10000); + } + + amountExcludingVat += totalAmount; + } + results.push( new InvoiceView( invoice.getId(), @@ -31,7 +45,7 @@ export class GetInvoicesQueryHandler { invoice.getStatus(), invoice.getCreatedAt(), invoice.getExpiryDate(), - 0, + amountExcludingVat * 1.2, new ProjectView( project.getId(), project.getName(), diff --git a/server/src/Domain/Accounting/DailyRate.entity.ts b/server/src/Domain/Accounting/DailyRate.entity.ts index 11e23b78..5ef5be69 100644 --- a/server/src/Domain/Accounting/DailyRate.entity.ts +++ b/server/src/Domain/Accounting/DailyRate.entity.ts @@ -8,7 +8,7 @@ export class DailyRate { @PrimaryGeneratedColumn('uuid') private id: string; - @Column({type: 'integer', nullable: false}) + @Column({type: 'integer', nullable: false, comment: 'Stored in base 100'}) private amount: number; @Column({type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'}) diff --git a/server/src/Domain/Accounting/Invoice.entity.spec.ts b/server/src/Domain/Accounting/Invoice.entity.spec.ts index cfef9e82..e187359f 100644 --- a/server/src/Domain/Accounting/Invoice.entity.spec.ts +++ b/server/src/Domain/Accounting/Invoice.entity.spec.ts @@ -24,5 +24,6 @@ describe('Invoice.entity', () => { expect(invoice.getOwner()).toBe(instance(user)); expect(invoice.getProject()).toBe(instance(project)); expect(invoice.getQuote()).toBeUndefined(); + expect(invoice.getItems()).toBeUndefined(); }); }); diff --git a/server/src/Domain/Accounting/Invoice.entity.ts b/server/src/Domain/Accounting/Invoice.entity.ts index ca2015d8..f1ebdfa5 100644 --- a/server/src/Domain/Accounting/Invoice.entity.ts +++ b/server/src/Domain/Accounting/Invoice.entity.ts @@ -5,7 +5,7 @@ import { ManyToOne, OneToMany } from 'typeorm'; -import { Project } from '../Project/Project.entity'; +import { InvoiceUnits, Project } from '../Project/Project.entity'; import { User } from '../HumanResource/User/User.entity'; import { InvoiceItem } from './InvoiceItem.entity'; import { Quote } from './Quote.entity'; @@ -94,4 +94,8 @@ export class Invoice { public getQuote(): Quote | undefined { return this.quote; } + + public getItems(): InvoiceItem[] { + return this.items; + } } diff --git a/server/src/Domain/Accounting/InvoiceItem.entity.spec.ts b/server/src/Domain/Accounting/InvoiceItem.entity.spec.ts index 7c1af194..c878685b 100644 --- a/server/src/Domain/Accounting/InvoiceItem.entity.spec.ts +++ b/server/src/Domain/Accounting/InvoiceItem.entity.spec.ts @@ -8,7 +8,7 @@ describe('InvoiceItem.entity', () => { const invoiceitem = new InvoiceItem( instance(invoice), 'Développement web', - 420, + 1, 72000, 0 ); @@ -16,7 +16,8 @@ describe('InvoiceItem.entity', () => { expect(invoiceitem.getId()).toBe(undefined); expect(invoiceitem.getAmount()).toBe(72000); expect(invoiceitem.getDiscount()).toBe(0); - expect(invoiceitem.getTimeSpent()).toBe(420); + expect(invoiceitem.getQuantity()).toBe(1); + expect(invoiceitem.getAmount()).toBe(72000); expect(invoiceitem.getTitle()).toBe('Développement web'); expect(invoiceitem.getInvoice()).toBe(instance(invoice)); }); diff --git a/server/src/Domain/Accounting/InvoiceItem.entity.ts b/server/src/Domain/Accounting/InvoiceItem.entity.ts index 7090a26c..a3235aa0 100644 --- a/server/src/Domain/Accounting/InvoiceItem.entity.ts +++ b/server/src/Domain/Accounting/InvoiceItem.entity.ts @@ -9,13 +9,13 @@ export class InvoiceItem { @Column({type: 'varchar', nullable: false}) private title: string; - @Column({type: 'integer', nullable: false, comment: 'Stored in minutes'}) - private timeSpent: number; + @Column({type: 'integer', nullable: false, comment: 'Stored in base 100'}) + private quantity: number; - @Column({type: 'integer', nullable: false}) + @Column({type: 'integer', nullable: false, comment: 'Stored in base 100'}) private amount: number; - @Column({type: 'integer', nullable: true, default: 0}) + @Column({type: 'integer', nullable: true, default: 0, comment: 'Stored in base 100'}) private discount: number; @ManyToOne( @@ -28,13 +28,13 @@ export class InvoiceItem { constructor( invoice: Invoice, title: string, - timeSpent: number, + quantity: number, amount: number, discount?: number ) { this.invoice = invoice; this.title = title; - this.timeSpent = timeSpent; + this.quantity = quantity; this.amount = amount; this.discount = discount; } @@ -55,8 +55,8 @@ export class InvoiceItem { return this.discount; } - public getTimeSpent(): number { - return this.timeSpent; + public getQuantity(): number { + return this.quantity; } public getInvoice(): Invoice { diff --git a/server/src/Domain/Accounting/QuoteItem.entity.ts b/server/src/Domain/Accounting/QuoteItem.entity.ts index e223aff4..148841be 100644 --- a/server/src/Domain/Accounting/QuoteItem.entity.ts +++ b/server/src/Domain/Accounting/QuoteItem.entity.ts @@ -9,10 +9,10 @@ export class QuoteItem { @Column({type: 'varchar', nullable: false}) private title: string; - @Column({type: 'integer', nullable: false}) + @Column({type: 'integer', nullable: false, comment: 'Stored in base 100'}) private quantity: number; - @Column({type: 'integer', nullable: false}) + @Column({type: 'integer', nullable: false, comment: 'Stored in base 100'}) private dailyRate: number; @ManyToOne( diff --git a/server/src/Infrastructure/Accounting/Repository/InvoiceRepository.ts b/server/src/Infrastructure/Accounting/Repository/InvoiceRepository.ts index 7f54d1bd..6dc9fb6b 100644 --- a/server/src/Infrastructure/Accounting/Repository/InvoiceRepository.ts +++ b/server/src/Infrastructure/Accounting/Repository/InvoiceRepository.ts @@ -36,8 +36,9 @@ export class InvoiceRepository implements IInvoiceRepository { 'customer.id', 'customer.name', 'invoiceItem.id', + 'invoiceItem.quantity', 'invoiceItem.amount', - 'invoiceItem.timeSpent' + 'invoiceItem.discount' ]) .where('invoiceItem.discount <> 100') .innerJoin('invoice.project', 'project') diff --git a/server/src/Infrastructure/FairCalendar/Repository/EventRepository.ts b/server/src/Infrastructure/FairCalendar/Repository/EventRepository.ts index a431bc70..93bacccc 100644 --- a/server/src/Infrastructure/FairCalendar/Repository/EventRepository.ts +++ b/server/src/Infrastructure/FairCalendar/Repository/EventRepository.ts @@ -116,7 +116,7 @@ export class EventRepository implements IEventRepository { .innerJoin('dailyRate.user', 'd_user') .innerJoin('dailyRate.task', 'd_task') .innerJoin('dailyRate.customer', 'd_customer') - }, 'amount') + }, 'daily_rate') .innerJoin('event.project', 'project') .innerJoin('event.user', 'user') .innerJoin('event.task', 'task')