@silexpert/core 2.0.121 → 2.0.122
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/dist/{index-CQ4tY4GS.d.mts → index-BTRchHCY.d.mts} +13 -3
- package/dist/index-BTRchHCY.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/dist/{product-BCFrBxXc.mjs → product-B35JCiip.mjs} +2 -2
- package/dist/product-B35JCiip.mjs.map +1 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.mjs +3 -3
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/ts/api/resources/Exercice.ts +5 -0
- package/ts/types/Enum/EInvoicingStatusCode.ts +15 -1
- package/ts/types/Enum/Unit.ts +7 -0
- package/ts/types/Interface/api/balanceSheet/UpdateBalanceSheet.ts +5 -0
- package/ts/types/Interface/api/commercialManagement/Read.ts +1 -0
- package/ts/types/Interface/api/exercice/QueryExercice.ts +8 -2
- package/ts/types/Interface/models/IBalanceSheet.ts +1 -0
- package/ts/utils/amount.ts +7 -0
- package/ts/utils/commercialManagement/document.ts +1 -1
- package/ts/utils/commercialManagement/product.ts +7 -3
- package/ts/utils/commercialManagement/vat.ts +9 -11
- package/ts/utils/tests/document.spec.ts +95 -0
- package/dist/index-CQ4tY4GS.d.mts.map +0 -1
- package/dist/product-BCFrBxXc.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
GenerateProjectExercice,
|
|
11
11
|
IGedFile,
|
|
12
12
|
SendPreviewProjectExercice,
|
|
13
|
+
QuerySignUrlForProject,
|
|
13
14
|
} from '../../types/index.js';
|
|
14
15
|
import { Resource } from '../Resource.js';
|
|
15
16
|
|
|
@@ -71,6 +72,10 @@ export class Exercice extends Resource {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
getSignUrlForProject(params: QuerySignUrlForProject): Promise<string> {
|
|
76
|
+
return this.axios.$get('exercices/project/sign-url', { params });
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
getProgress(idExercice: number): Promise<number> {
|
|
75
80
|
return this.axios.$get(`exercices/${idExercice}/progress`);
|
|
76
81
|
}
|
|
@@ -17,89 +17,103 @@ export enum EInvoicingStatusCode {
|
|
|
17
17
|
|
|
18
18
|
export const EInvoicingStatusCodeMetadata: Record<
|
|
19
19
|
EInvoicingStatusCode,
|
|
20
|
-
{ shortLabel: string; label: string; color: string; description?: string }
|
|
20
|
+
{ shortLabel: string; label: string; pluralLabel: string; color: string; description?: string }
|
|
21
21
|
> = {
|
|
22
22
|
[EInvoicingStatusCode.DEPOSITED]: {
|
|
23
23
|
shortLabel: 'Déposée',
|
|
24
24
|
label: 'Déposée',
|
|
25
|
+
pluralLabel: 'Déposées',
|
|
25
26
|
color: 'cerulean',
|
|
26
27
|
description: 'Facture déposée et contrôles techniques validés.',
|
|
27
28
|
},
|
|
28
29
|
[EInvoicingStatusCode.ISSUED]: {
|
|
29
30
|
shortLabel: 'Émise',
|
|
30
31
|
label: 'Émise par la plateforme',
|
|
32
|
+
pluralLabel: 'Émises',
|
|
31
33
|
color: 'success',
|
|
32
34
|
description: 'Facture transmise au réseau de facturation.',
|
|
33
35
|
},
|
|
34
36
|
[EInvoicingStatusCode.RECEIVED]: {
|
|
35
37
|
shortLabel: 'Reçue',
|
|
36
38
|
label: 'Reçue',
|
|
39
|
+
pluralLabel: 'Reçues',
|
|
37
40
|
color: 'success',
|
|
38
41
|
description: 'Facture reçue par la plateforme du destinataire.',
|
|
39
42
|
},
|
|
40
43
|
[EInvoicingStatusCode.MADE_AVAILABLE]: {
|
|
41
44
|
shortLabel: 'Mise à disposition',
|
|
42
45
|
label: 'Mise à disposition',
|
|
46
|
+
pluralLabel: 'Mises à disposition',
|
|
43
47
|
color: 'fire',
|
|
44
48
|
description: 'Facture disponible pour le destinataire.',
|
|
45
49
|
},
|
|
46
50
|
[EInvoicingStatusCode.HANDLED]: {
|
|
47
51
|
shortLabel: 'Prise en charge',
|
|
48
52
|
label: 'Prise en charge',
|
|
53
|
+
pluralLabel: 'Prises en charge',
|
|
49
54
|
color: 'fire',
|
|
50
55
|
description: 'Facture en cours de traitement par le destinataire.',
|
|
51
56
|
},
|
|
52
57
|
[EInvoicingStatusCode.APPROVED]: {
|
|
53
58
|
shortLabel: 'Approuvée',
|
|
54
59
|
label: 'Approuvée',
|
|
60
|
+
pluralLabel: 'Approuvées',
|
|
55
61
|
color: 'success',
|
|
56
62
|
description: 'Facture entièrement acceptée.',
|
|
57
63
|
},
|
|
58
64
|
[EInvoicingStatusCode.PARTIALLY_APPROVED]: {
|
|
59
65
|
shortLabel: 'Approbation partielle',
|
|
60
66
|
label: 'Approbation partielle',
|
|
67
|
+
pluralLabel: 'Approbations partielles',
|
|
61
68
|
color: 'error',
|
|
62
69
|
description: 'Facture partiellement acceptée.',
|
|
63
70
|
},
|
|
64
71
|
[EInvoicingStatusCode.IN_DISPUTE]: {
|
|
65
72
|
shortLabel: 'En litige',
|
|
66
73
|
label: 'En litige',
|
|
74
|
+
pluralLabel: 'En litige',
|
|
67
75
|
color: 'error',
|
|
68
76
|
description: 'Facture contestée par le destinataire.',
|
|
69
77
|
},
|
|
70
78
|
[EInvoicingStatusCode.SUSPENDED]: {
|
|
71
79
|
shortLabel: 'Suspendue',
|
|
72
80
|
label: 'Suspendue',
|
|
81
|
+
pluralLabel: 'Suspendues',
|
|
73
82
|
color: 'error',
|
|
74
83
|
description: 'Traitement suspendu, informations attendues.',
|
|
75
84
|
},
|
|
76
85
|
[EInvoicingStatusCode.COMPLETED]: {
|
|
77
86
|
shortLabel: 'Complétée',
|
|
78
87
|
label: 'Complétée',
|
|
88
|
+
pluralLabel: 'Complétées',
|
|
79
89
|
color: 'fire',
|
|
80
90
|
description: 'Informations complémentaires transmises.',
|
|
81
91
|
},
|
|
82
92
|
[EInvoicingStatusCode.REFUSED]: {
|
|
83
93
|
shortLabel: 'Refusée',
|
|
84
94
|
label: 'Refusée',
|
|
95
|
+
pluralLabel: 'Refusées',
|
|
85
96
|
color: 'error',
|
|
86
97
|
description: 'Facture refusée par le destinataire.',
|
|
87
98
|
},
|
|
88
99
|
[EInvoicingStatusCode.PAYMENT_TRANSMITTED]: {
|
|
89
100
|
shortLabel: 'Paiement transmis',
|
|
90
101
|
label: 'Paiement transmis',
|
|
102
|
+
pluralLabel: 'Paiements transmis',
|
|
91
103
|
color: 'success',
|
|
92
104
|
description: 'Paiement déclaré par le destinataire.',
|
|
93
105
|
},
|
|
94
106
|
[EInvoicingStatusCode.PAID]: {
|
|
95
107
|
shortLabel: 'Encaissée',
|
|
96
108
|
label: 'Encaissée',
|
|
109
|
+
pluralLabel: 'Encaissées',
|
|
97
110
|
color: 'success',
|
|
98
111
|
description: 'Paiement reçu et confirmé.',
|
|
99
112
|
},
|
|
100
113
|
[EInvoicingStatusCode.REJECTED]: {
|
|
101
114
|
shortLabel: 'Rejetée',
|
|
102
115
|
label: 'Rejetée',
|
|
116
|
+
pluralLabel: 'Rejetées',
|
|
103
117
|
color: 'error',
|
|
104
118
|
description: 'Facture rejetée, correction nécessaire.',
|
|
105
119
|
},
|
package/ts/types/Enum/Unit.ts
CHANGED
|
@@ -30,6 +30,7 @@ export enum UnitId {
|
|
|
30
30
|
HECTARE = 20,
|
|
31
31
|
MINUTE = 21,
|
|
32
32
|
GRAM = 22,
|
|
33
|
+
UNIT = 23,
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export type IUnitList = Array<{ name: string; id: number; shortName: string; eInvoicingCode: string | null }>;
|
|
@@ -41,6 +42,12 @@ export const Unit: IUnit = {
|
|
|
41
42
|
shortName: 'art',
|
|
42
43
|
codeEN16931: 'NAR',
|
|
43
44
|
},
|
|
45
|
+
UNIT: {
|
|
46
|
+
id: UnitId.UNIT,
|
|
47
|
+
name: 'Unité',
|
|
48
|
+
shortName: 'u',
|
|
49
|
+
codeEN16931: 'NAR',
|
|
50
|
+
},
|
|
44
51
|
HOUR: {
|
|
45
52
|
id: UnitId.HOUR,
|
|
46
53
|
name: 'Heure',
|
|
@@ -21,6 +21,7 @@ export type ReadEstimateInvoice = IEstimateInvoice & { id: number };
|
|
|
21
21
|
|
|
22
22
|
export type ReadProductEditing = IProductInvoice & {
|
|
23
23
|
totalTaxExcluded: number;
|
|
24
|
+
totalTaxExcludedRounded: number;
|
|
24
25
|
unitFormatted: string;
|
|
25
26
|
vatFormatted: string;
|
|
26
27
|
discountFormatted: string | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IsBoolean, IsDate, IsIn, IsOptional } from 'class-validator';
|
|
2
|
-
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
1
|
+
import { IsBoolean, IsDate, IsIn, IsOptional, IsString } from 'class-validator';
|
|
2
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
3
3
|
import { ToBoolean } from '../../../../utils/transforms/boolean.transform.js';
|
|
4
4
|
|
|
5
5
|
export class QueryExercice {
|
|
@@ -44,3 +44,9 @@ export class QueryExerciceProject {
|
|
|
44
44
|
@IsOptional()
|
|
45
45
|
type?: 'juridical' | 'default';
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export class QuerySignUrlForProject {
|
|
49
|
+
@ApiProperty()
|
|
50
|
+
@IsString()
|
|
51
|
+
token: string;
|
|
52
|
+
}
|
package/ts/utils/amount.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { MathematicalSign } from '../types/Enum/Util.js';
|
|
2
2
|
import { FormattedAmount } from '../types/Interface/api/amount/FormattedAmount.js';
|
|
3
3
|
|
|
4
|
+
// Round the magnitude first so negative half-cent values match the formatter's symmetric rounding.
|
|
5
|
+
export function roundAmountToInteger(value: number): number {
|
|
6
|
+
const roundedAbsoluteValue = Math.round(Math.abs(value));
|
|
7
|
+
if (roundedAbsoluteValue === 0) return 0;
|
|
8
|
+
return value < 0 ? -roundedAbsoluteValue : roundedAbsoluteValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export function integerToFormattedAmountString(value: number): string {
|
|
5
12
|
const abs = Math.abs(value / 100);
|
|
6
13
|
let integer = Math.trunc(abs);
|
|
@@ -73,7 +73,7 @@ export function generateCommercialDocument(
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const totalVats = sumBy(vats, (v) => round(v.amount));
|
|
76
|
-
const totalTaxExcluded = sumBy(products,
|
|
76
|
+
const totalTaxExcluded = sumBy(products, 'totalTaxExcludedRounded');
|
|
77
77
|
|
|
78
78
|
const totalTaxIncluded = totalTaxExcluded + totalVats;
|
|
79
79
|
|
|
@@ -3,7 +3,7 @@ import { IProductInvoice, ReadProductEditing } from '../../types/index.js';
|
|
|
3
3
|
import { Unit } from '../../types/Enum/Unit.js';
|
|
4
4
|
import { DiscountType, VatMarginCategyList } from '../../types/Enum/CommercialManagement.js';
|
|
5
5
|
import { getById } from './vat.js';
|
|
6
|
-
import { integerToFormattedAmountString } from '../amount.js';
|
|
6
|
+
import { integerToFormattedAmountString, roundAmountToInteger } from '../amount.js';
|
|
7
7
|
|
|
8
8
|
export function getDiscount(product: IProductInvoice): number {
|
|
9
9
|
if (product.discount2 === null || product.discount2 === undefined || product.discount2 <= 0) return 0;
|
|
@@ -24,16 +24,20 @@ export function getTotalTaxExcluded(product: IProductInvoice): number {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function productToReadProductEditing(payload: IProductInvoice): ReadProductEditing {
|
|
27
|
+
const totalTaxExcluded = getTotalTaxExcluded(payload);
|
|
28
|
+
const totalTaxExcludedRounded = roundAmountToInteger(totalTaxExcluded);
|
|
29
|
+
|
|
27
30
|
return {
|
|
28
31
|
...payload,
|
|
29
|
-
totalTaxExcluded
|
|
32
|
+
totalTaxExcluded,
|
|
33
|
+
totalTaxExcludedRounded,
|
|
30
34
|
unitFormatted: Object.values(Unit).find((unit) => unit.id === payload.unit)?.shortName ?? '',
|
|
31
35
|
vatFormatted: getById(payload.idVat)?.saleName ?? '',
|
|
32
36
|
priceTaxExcludedFormatted: isDefined(payload.priceTaxExcluded)
|
|
33
37
|
? integerToFormattedAmountString(payload.priceTaxExcluded)
|
|
34
38
|
: null,
|
|
35
39
|
totalTaxExcludedFormatted: isDefined(payload.priceTaxExcluded)
|
|
36
|
-
? integerToFormattedAmountString(
|
|
40
|
+
? integerToFormattedAmountString(totalTaxExcludedRounded)
|
|
37
41
|
: null,
|
|
38
42
|
discountFormatted:
|
|
39
43
|
payload.discount2 && payload.discount2 > 0 ? integerToFormattedAmountString(getDiscount(payload)) : null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sumBy } from 'lodash-es';
|
|
2
2
|
import { Vat, VatType } from '../../types/Enum/Vat.js';
|
|
3
|
-
import { integerToFormattedAmountString } from '../amount.js';
|
|
3
|
+
import { integerToFormattedAmountString, roundAmountToInteger } from '../amount.js';
|
|
4
4
|
import { ReadProductEditing, ReadVatEditing } from '../../types/index.js';
|
|
5
5
|
|
|
6
6
|
export function getAmount(amount: number | null, vatValue: number): number {
|
|
@@ -15,19 +15,17 @@ export function getById(id: number): VatType | undefined {
|
|
|
15
15
|
|
|
16
16
|
export function vatToReadVatEditing(products: ReadProductEditing[]): ReadVatEditing {
|
|
17
17
|
const { idVat } = products[0];
|
|
18
|
+
const base = sumBy(products, 'totalTaxExcludedRounded');
|
|
19
|
+
const amount = roundAmountToInteger(getAmount(base, getById(idVat)?.value ?? 0));
|
|
20
|
+
|
|
18
21
|
return {
|
|
19
22
|
id: idVat,
|
|
20
23
|
name: getById(idVat)?.saleName ?? '',
|
|
21
|
-
amount
|
|
24
|
+
amount,
|
|
22
25
|
value: getById(idVat)?.value ?? 0,
|
|
23
|
-
baseFormatted: integerToFormattedAmountString(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(sumBy(products, (p) => (p.totalTaxExcluded ? +p.totalTaxExcluded : 0)) * (getById(idVat)?.value ?? 0)) / 100,
|
|
28
|
-
),
|
|
29
|
-
toPay:
|
|
30
|
-
(sumBy(products, (p) => (p.totalTaxExcluded ? +p.totalTaxExcluded : 0)) * (getById(idVat)?.value ?? 0)) / 100,
|
|
31
|
-
base: sumBy(products, (p) => (p.totalTaxExcluded ? +p.totalTaxExcluded : 0)),
|
|
26
|
+
baseFormatted: integerToFormattedAmountString(base),
|
|
27
|
+
amountFormatted: integerToFormattedAmountString(amount),
|
|
28
|
+
toPay: amount,
|
|
29
|
+
base,
|
|
32
30
|
};
|
|
33
31
|
}
|
|
@@ -2,6 +2,9 @@ import { describe, expect, test } from 'vitest';
|
|
|
2
2
|
import { generateCommercialDocument } from '../commercialManagement/document.js';
|
|
3
3
|
import { EstimateInvoiceType } from '../../types/Enum/EstimateInvoice.js';
|
|
4
4
|
import { CreateInvoiceDocument } from '../../types/Interface/api/commercialManagement/Create.js';
|
|
5
|
+
import { DiscountType } from '../../types/Enum/CommercialManagement.js';
|
|
6
|
+
import { Unit } from '../../types/Enum/Unit.js';
|
|
7
|
+
import { Vat } from '../../types/Enum/Vat.js';
|
|
5
8
|
|
|
6
9
|
// Payload reproducing a real-world case where the seller address fields (`name`, `street`,
|
|
7
10
|
// `number`, `zipCode`, `city`) are entirely absent from the object, not just `null`.
|
|
@@ -113,4 +116,96 @@ describe('generateCommercialDocument', () => {
|
|
|
113
116
|
|
|
114
117
|
expect(result.body).not.toContain('<p>undefined</p>');
|
|
115
118
|
});
|
|
119
|
+
|
|
120
|
+
test('should calculate VAT from rounded line totals after discounts and fractional quantities', () => {
|
|
121
|
+
const payload = {
|
|
122
|
+
...payloadWithoutSellerAddress,
|
|
123
|
+
products: Array.from({ length: 6 }, (_, index) => ({
|
|
124
|
+
idVat: Vat.TX_20.id,
|
|
125
|
+
unit: Unit.ITEM.id,
|
|
126
|
+
itemSaleName: `Rounded line ${index + 1}`,
|
|
127
|
+
priceTaxExcluded: 101,
|
|
128
|
+
quantity: 1.0049,
|
|
129
|
+
discount2: 100,
|
|
130
|
+
discountType: DiscountType.PERCENT,
|
|
131
|
+
reorder: index,
|
|
132
|
+
})),
|
|
133
|
+
} as unknown as CreateInvoiceDocument;
|
|
134
|
+
|
|
135
|
+
const result = generateCommercialDocument(EstimateInvoiceType.FACTURE, payload);
|
|
136
|
+
|
|
137
|
+
expect(result.body).toContain('1,00€');
|
|
138
|
+
expect(result.data.vats).toEqual(
|
|
139
|
+
expect.arrayContaining([
|
|
140
|
+
expect.objectContaining({ base: 600, amount: 120, baseFormatted: '6,00', amountFormatted: '1,20' }),
|
|
141
|
+
]),
|
|
142
|
+
);
|
|
143
|
+
expect(result.data.totalTaxExcluded).toBe(600);
|
|
144
|
+
expect(result.data.totalTaxIncluded).toBe(720);
|
|
145
|
+
expect(result.data.totalTaxIncludedFormatted).toBe('7,20');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should use the rounded half-cent line amount in the PDF and VAT recap', () => {
|
|
149
|
+
const payload = {
|
|
150
|
+
...payloadWithoutSellerAddress,
|
|
151
|
+
products: [
|
|
152
|
+
{
|
|
153
|
+
idVat: Vat.TX_20.id,
|
|
154
|
+
unit: Unit.ITEM.id,
|
|
155
|
+
itemSaleName: 'Half-cent line',
|
|
156
|
+
priceTaxExcluded: 201,
|
|
157
|
+
quantity: 0.5,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
} as unknown as CreateInvoiceDocument;
|
|
161
|
+
|
|
162
|
+
const result = generateCommercialDocument(EstimateInvoiceType.FACTURE, payload);
|
|
163
|
+
|
|
164
|
+
expect(result.body).toContain('1,01€');
|
|
165
|
+
expect(result.data.products[0]).toMatchObject({
|
|
166
|
+
totalTaxExcluded: 100.5,
|
|
167
|
+
totalTaxExcludedRounded: 101,
|
|
168
|
+
totalTaxExcludedFormatted: '1,01',
|
|
169
|
+
});
|
|
170
|
+
expect(result.data.vats).toEqual(
|
|
171
|
+
expect.arrayContaining([expect.objectContaining({ base: 101, baseFormatted: '1,01' })]),
|
|
172
|
+
);
|
|
173
|
+
expect(result.data.totalTaxExcluded).toBe(101);
|
|
174
|
+
expect(result.data.totalTaxExcludedFormatted).toBe('1,01');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should round negative credit note lines symmetrically across VAT rates', () => {
|
|
178
|
+
const payload = {
|
|
179
|
+
...payloadWithoutSellerAddress,
|
|
180
|
+
products: [
|
|
181
|
+
{
|
|
182
|
+
idVat: Vat.TX_5_5.id,
|
|
183
|
+
unit: Unit.ITEM.id,
|
|
184
|
+
itemSaleName: 'Half-cent VAT line',
|
|
185
|
+
priceTaxExcluded: -200,
|
|
186
|
+
quantity: 0.5,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
idVat: Vat.TX_20.id,
|
|
190
|
+
unit: Unit.ITEM.id,
|
|
191
|
+
itemSaleName: 'Half-cent line',
|
|
192
|
+
priceTaxExcluded: -201,
|
|
193
|
+
quantity: 0.5,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
} as unknown as CreateInvoiceDocument;
|
|
197
|
+
|
|
198
|
+
const result = generateCommercialDocument(EstimateInvoiceType.AVOIR, payload);
|
|
199
|
+
|
|
200
|
+
expect(result.body).toContain('- 1,00€');
|
|
201
|
+
expect(result.body).toContain('- 1,01€');
|
|
202
|
+
expect(result.data.vats).toEqual(
|
|
203
|
+
expect.arrayContaining([
|
|
204
|
+
expect.objectContaining({ value: 5.5, base: -100, amount: -6, baseFormatted: '- 1,00' }),
|
|
205
|
+
expect.objectContaining({ value: 20, base: -101, amount: -20, baseFormatted: '- 1,01' }),
|
|
206
|
+
]),
|
|
207
|
+
);
|
|
208
|
+
expect(result.data.totalTaxExcluded).toBe(-201);
|
|
209
|
+
expect(result.data.totalTaxIncluded).toBe(-227);
|
|
210
|
+
});
|
|
116
211
|
});
|