Skip to content

Commit

Permalink
Merge pull request #167 from fairnesscoop/feat/list-invoices-calculat…
Browse files Browse the repository at this point in the history
…e-amount

Add invoice amount on invoices list
  • Loading branch information
mmarchois authored Nov 26, 2020
2 parents 5e7011f + 77aa84c commit 646d146
Show file tree
Hide file tree
Showing 17 changed files with 105 additions and 44 deletions.
9 changes: 4 additions & 5 deletions client/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions client/src/routes/accounting/daily_rates/_Table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
<thead>
<tr
class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
<th class="px-4 py-3">{$_('accounting.daily_rates.users')}</th>
<th class="px-4 py-3">{$_('accounting.daily_rates.user')}</th>
<th class="px-4 py-3">{$_('accounting.daily_rates.customer')}</th>
<th class="px-4 py-3">{$_('accounting.daily_rates.task')}</th>
<th class="px-4 py-3">{$_('accounting.daily_rates.amount')}</th>
<th class="px-4 py-3">{$_('accounting.daily_rates.tasks')}</th>
<th class="px-4 py-3">{$_('accounting.daily_rates.customers')}</th>
<th class="px-4 py-3">{$_('common.actions')}</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
{#each items as { id, user, task, customer, amount } (id)}
<tr class="text-gray-700 dark:text-gray-400">
<td class="px-4 py-3 text-sm">{user.firstName} {user.lastName}</td>
<td class="px-4 py-3 text-sm">{format(amount)}</td>
<td class="px-4 py-3 text-sm">{task.name}</td>
<td class="px-4 py-3 text-sm">{customer.name}</td>
<td class="px-4 py-3 text-sm">{task.name}</td>
<td class="px-4 py-3 text-sm">{format(amount)}</td>
<td class="px-4 py-3">
<div class="flex items-center space-x-4 text-sm">
<EditLink href="{`/accounting/daily_rates/${id}/edit`}" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</td>
</tr>
<tr class="text-gray-700 dark:text-gray-400">
<td class="px-4 py-3 text-sm">{$_('human_resources.leaves.requests.leave_types')}</td>
<td class="px-4 py-3 text-sm">{$_('human_resources.leaves.requests.leave_type.title')}</td>
<td class="px-4 py-3 text-sm">
{$_(`human_resources.leaves.requests.leave_type.${leaveRequest.type}`)}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<thead>
<tr
class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
<th class="px-4 py-3">{$_('human_resources.leaves.requests.users')}</th>
<th class="px-4 py-3">{$_('human_resources.leaves.requests.user')}</th>
<th class="px-4 py-3">{$_('human_resources.leaves.requests.periods')}</th>
<th class="px-4 py-3">{$_('human_resources.leaves.requests.leave_types')}</th>
<th class="px-4 py-3">{$_('human_resources.leaves.requests.leave_type.title')}</th>
<th class="px-4 py-3">{$_('human_resources.leaves.requests.status')}</th>
<th class="px-4 py-3">{$_('common.actions')}</th>
</tr>
Expand Down
14 changes: 14 additions & 0 deletions server/migrations/1606403084585-InvoiceUnitPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class InvoiceUnitPrice1606403084585 implements MigrationInterface {
name = 'InvoiceUnitPrice1606403084585'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "invoice_item" RENAME COLUMN "timeSpent" TO "quantity"`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "invoice_item" RENAME COLUMN "quantity" TO "timeSpent"`);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,23 @@ describe('GenerateInvoiceCommandHandler', () => {
task_name: 'Développement',
first_name: 'Mathieu',
last_name: 'MARCHOIS',
amount: 60000
daily_rate: 60000
},
{
time_spent: '420',
billable: true,
task_name: 'Architecture',
first_name: 'Mathieu',
last_name: 'MARCHOIS',
amount: null
daily_rate: null
},
{
time_spent: '4200',
billable: true,
task_name: 'Développement',
first_name: 'Mathieu',
last_name: 'MARCHOIS',
amount: 60000
daily_rate: 60000
}
];

Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,13 +28,34 @@ 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');
when(invoice1.getStatus()).thenReturn(InvoiceStatus.DRAFT);
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');
Expand All @@ -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)],
Expand All @@ -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',
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,28 @@ 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(),
invoice.getInvoiceId(),
invoice.getStatus(),
invoice.getCreatedAt(),
invoice.getExpiryDate(),
0,
amountExcludingVat * 1.2,
new ProjectView(
project.getId(),
project.getName(),
Expand Down
2 changes: 1 addition & 1 deletion server/src/Domain/Accounting/DailyRate.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'})
Expand Down
1 change: 1 addition & 0 deletions server/src/Domain/Accounting/Invoice.entity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
6 changes: 5 additions & 1 deletion server/src/Domain/Accounting/Invoice.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -94,4 +94,8 @@ export class Invoice {
public getQuote(): Quote | undefined {
return this.quote;
}

public getItems(): InvoiceItem[] {
return this.items;
}
}
5 changes: 3 additions & 2 deletions server/src/Domain/Accounting/InvoiceItem.entity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ describe('InvoiceItem.entity', () => {
const invoiceitem = new InvoiceItem(
instance(invoice),
'Développement web',
420,
1,
72000,
0
);

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));
});
Expand Down
16 changes: 8 additions & 8 deletions server/src/Domain/Accounting/InvoiceItem.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
}
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions server/src/Domain/Accounting/QuoteItem.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading

0 comments on commit 646d146

Please sign in to comment.