@ekwo-ai/des 0.4.0
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/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ekwo AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# `@ekwo-ai/des`
|
|
2
|
+
|
|
3
|
+
The French **DES** — *déclaration européenne de services* — in TypeScript, with
|
|
4
|
+
no dependencies.
|
|
5
|
+
|
|
6
|
+
Give it the rows of a recapitulative statement and it gives you the XML the
|
|
7
|
+
DGDDI accepts, a name for it, and the list of what could not be put in it.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { generateDes } from '@ekwo-ai/des';
|
|
11
|
+
|
|
12
|
+
const { file, filename, violations } = generateDes(rows, {
|
|
13
|
+
vatNumber: 'FRKK999999999',
|
|
14
|
+
year: 2026,
|
|
15
|
+
month: 8,
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`StatementRow` is the row shape of the `ec_sales_list(company, from, to)`
|
|
20
|
+
function of [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os), declared here so
|
|
21
|
+
that nothing is imported from it. Any book-keeping system that can produce
|
|
22
|
+
those columns can use this package; it reads no database and knows no
|
|
23
|
+
accounting.
|
|
24
|
+
|
|
25
|
+
## It is the services half, and only the services half
|
|
26
|
+
|
|
27
|
+
France splits the recapitulative statement in two. Services taxed where the
|
|
28
|
+
customer is are declared here. Supplies of goods go on the **état
|
|
29
|
+
récapitulatif TVA**, filed through DEBWEB2 in the INSTAT XML envelope, which is
|
|
30
|
+
a different format and is not in this package — see the end of this file for
|
|
31
|
+
why. A row of goods handed to `generateDes` comes back as a violation rather
|
|
32
|
+
than being written into a declaration that has no field for it.
|
|
33
|
+
|
|
34
|
+
## The specification this was written from
|
|
35
|
+
|
|
36
|
+
| What | Where |
|
|
37
|
+
|---|---|
|
|
38
|
+
| DES — description des échanges DTI+ en XML (DGDDI/DNSCE) | <https://www.douane.gouv.fr/sites/default/files/uploads/files/2020-10/ManuelDesXML.pdf> |
|
|
39
|
+
| Manuel utilisateur DES | <https://www.douane.gouv.fr/sites/default/files/uploads/files/2020-01/Manuel%20Utilisateur%20DES.pdf> |
|
|
40
|
+
| The administration's own page | <https://www.douane.gouv.fr/fiche/la-declaration-europeenne-de-services-des> |
|
|
41
|
+
| Rounding and the obligation itself | <https://bofip.impots.gouv.fr/bofip/979-PGP.html/identifiant=BOI-TVA-DECLA-20-20-40-20220216> |
|
|
42
|
+
| Filing calendar | <https://www.douane.gouv.fr/fiche/calendrier-des-declarations-relatives-letat-recapitulatif-tva-et-aux-des> |
|
|
43
|
+
|
|
44
|
+
Two things a reader should know about those documents:
|
|
45
|
+
|
|
46
|
+
- **No XSD is published for the DES.** The only schema on the portal is the one
|
|
47
|
+
of DEBWEB2. The DES is described by a dictionary of tags and a table of
|
|
48
|
+
checks, and the checks in that table are what this package applies.
|
|
49
|
+
- **The cahier des charges contradicts itself about the encoding.** Its
|
|
50
|
+
conventions section prescribes ISO-8859-1 and its own complete example
|
|
51
|
+
declares UTF-8. The file holds nothing but digits and the letters of a VAT
|
|
52
|
+
number, so the two are the same bytes; this package writes UTF-8, which is
|
|
53
|
+
the half of the document that was executed.
|
|
54
|
+
|
|
55
|
+
The specification has not been reissued since 2014. That is not an oversight
|
|
56
|
+
here: a targeted search of the portal found no later version.
|
|
57
|
+
|
|
58
|
+
## The checks, from the table in the cahier des charges
|
|
59
|
+
|
|
60
|
+
| Field | What it holds |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `num_des` | 1 to 6 digits |
|
|
63
|
+
| `num_tvaFr` | Thirteen characters: `FR`, a two-character key, nine digits of SIREN |
|
|
64
|
+
| `mois_des` | `01` to `12` |
|
|
65
|
+
| `an_des` | Four digits, 2010 or later |
|
|
66
|
+
| `numlin_des` | Six digits, from 1 |
|
|
67
|
+
| `valeur` | A whole number, never zero, between −9 999 999 999 and 99 999 999 999 |
|
|
68
|
+
| `partner_des` | 4 to 14 characters, **country prefix included** |
|
|
69
|
+
|
|
70
|
+
**The tags must appear in that order**, which the document says in as many
|
|
71
|
+
words, and this package writes them in it.
|
|
72
|
+
|
|
73
|
+
**Amounts are whole euros**, rounded away from zero at the half: below 0,50 €
|
|
74
|
+
an amount is dropped, at 0,50 € or more it counts for one. **Negative values
|
|
75
|
+
are allowed** on the DES, unlike on the état récapitulatif of goods, and a
|
|
76
|
+
minoration is declared as one.
|
|
77
|
+
|
|
78
|
+
## What it refuses and what it reports
|
|
79
|
+
|
|
80
|
+
An exception, because it would make the whole file meaningless: a month outside
|
|
81
|
+
1 to 12, a year before 2010, a declaration number outside 1 to 999 999, or a
|
|
82
|
+
declarant number that is not the thirteen characters the specification fixes.
|
|
83
|
+
|
|
84
|
+
A violation, because one line is wrong and the rest of the file is not:
|
|
85
|
+
|
|
86
|
+
| `code` | Why |
|
|
87
|
+
|---|---|
|
|
88
|
+
| whatever the producer flagged | The row arrived with an `issue` on it |
|
|
89
|
+
| `not_a_service` | Goods or a triangular operation: the état récapitulatif TVA, not this file |
|
|
90
|
+
| `wrong_currency` | The DES is filed in euro and the line is not |
|
|
91
|
+
| `no_vat_number` | A line is identified by the customer's number and this one has none |
|
|
92
|
+
| `vat_country_is_the_declarant_country` | The customer is not in another Member State |
|
|
93
|
+
| `partner_length` | Fewer than four or more than fourteen characters, prefix included |
|
|
94
|
+
| `nil_value` | The value rounds to zero, which the format refuses |
|
|
95
|
+
|
|
96
|
+
## The file name
|
|
97
|
+
|
|
98
|
+
**The administration imposes none.** The manual describes a file picker and
|
|
99
|
+
nothing else: no convention, no extension beyond `.xml`, no size limit.
|
|
100
|
+
`desFileName` is a name of ours, and a caller is free to pick another.
|
|
101
|
+
|
|
102
|
+
## Why the état récapitulatif TVA on goods is not here
|
|
103
|
+
|
|
104
|
+
It is a real format with a published schema, and it was left out on purpose
|
|
105
|
+
rather than forgotten. Two things in it do not come from a set of books:
|
|
106
|
+
|
|
107
|
+
- **`envelopeId` is a habilitation number** issued by the collection centre to
|
|
108
|
+
a declarant who asked for one. A file writer cannot invent it, and a package
|
|
109
|
+
that took it as a parameter would be pretending the hard part is the XML.
|
|
110
|
+
- **An `Item` carries statistical fields** — the CN8 commodity code, the net
|
|
111
|
+
mass, the mode of transport, the region — which an accounting core does not
|
|
112
|
+
hold. The fiscal-only declaration, `declarationTypeCode` 4, needs fewer of
|
|
113
|
+
them than the statistical one, but the published schema is shared with the
|
|
114
|
+
statistical flow and is more permissive than the regulation, so what is truly
|
|
115
|
+
required could not be settled from the documents alone.
|
|
116
|
+
|
|
117
|
+
Whoever picks it up will want the DEBWEB2 cahier des charges,
|
|
118
|
+
<https://www.douane.gouv.fr/sites/default/files/2022-02/15/DEB%20_Manuel_mode_DTI%2B_Xml_v1.3.8.pdf>,
|
|
119
|
+
and its schema,
|
|
120
|
+
<https://www.douane.gouv.fr/sites/default/files/2022-02/24/Sch%C3%A9maXSD.xsd>.
|
|
121
|
+
The six régime codes of the fiscal declaration — 10, 20, 21, 25, 26 and 31 —
|
|
122
|
+
are on <https://www.douane.gouv.fr/fiche/letat-recapitulatif-tva>.
|
|
123
|
+
|
|
124
|
+
## What else is out of scope
|
|
125
|
+
|
|
126
|
+
Filing. This package writes bytes; enregistrement on the portal is what makes a
|
|
127
|
+
DES a declaration, and it is the portal's business. Note that the deposit is a
|
|
128
|
+
**window** and not a deadline: a declaration may not be enregistrée before the
|
|
129
|
+
first day of the month following the period, nor after the tenth working day.
|
|
130
|
+
|
|
131
|
+
MIT.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The French DES — *déclaration européenne de services*.
|
|
3
|
+
*
|
|
4
|
+
* One XML document, `fichier_des`, described by the cahier des charges the
|
|
5
|
+
* DGDDI publishes as *DES — description des échanges DTI+ en XML*. Give it the
|
|
6
|
+
* rows of a recapitulative statement and it writes the file, a name for it,
|
|
7
|
+
* and the list of what could not be put in it.
|
|
8
|
+
*
|
|
9
|
+
* **It is the services half and only the services half.** France splits the
|
|
10
|
+
* recapitulative statement in two: services are declared here, supplies of
|
|
11
|
+
* goods on the *état récapitulatif TVA* of DEBWEB2, which is a different file
|
|
12
|
+
* in a different format. A row of goods handed to this package comes back as a
|
|
13
|
+
* violation rather than being written into a declaration that has no place for
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
* Sources are in the README. This package depends on nothing, reads no
|
|
17
|
+
* database and knows no accounting.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* One line of a recapitulative statement, as `ec_sales_list(company, from,
|
|
21
|
+
* to)` of [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os) returns it. Declared
|
|
22
|
+
* here so that nothing is imported from it.
|
|
23
|
+
*/
|
|
24
|
+
export interface StatementRow {
|
|
25
|
+
/** Country of the customer's VAT identification number, ISO 3166-1 alpha-2. */
|
|
26
|
+
vat_country: string | null;
|
|
27
|
+
/** The number itself, without its country prefix. */
|
|
28
|
+
vat_number: string | null;
|
|
29
|
+
/** `goods`, `services`, `triangular`. Only services reach this file. */
|
|
30
|
+
nature: string;
|
|
31
|
+
/** Taxable amount, credit notes already deducted. */
|
|
32
|
+
amount: string | number;
|
|
33
|
+
/** ISO 4217 code the amount is in. */
|
|
34
|
+
currency_code: string;
|
|
35
|
+
/** Why this line cannot be declared as it stands, or null when it can. */
|
|
36
|
+
issue?: string | null;
|
|
37
|
+
/** Names of the customers behind the line, for a message a human reads. */
|
|
38
|
+
contact_names?: string[] | null;
|
|
39
|
+
}
|
|
40
|
+
export interface DesOptions {
|
|
41
|
+
/**
|
|
42
|
+
* The declarant's own VAT number, thirteen characters: `FR`, a two-character
|
|
43
|
+
* key and the nine digits of the SIREN. The cahier des charges fixes the
|
|
44
|
+
* length and refuses anything else.
|
|
45
|
+
*/
|
|
46
|
+
vatNumber: string;
|
|
47
|
+
/** The month the tax became chargeable in the customer's Member State. */
|
|
48
|
+
year: number;
|
|
49
|
+
month: number;
|
|
50
|
+
/** Number of the declaration inside the file, at most six digits. Default 1. */
|
|
51
|
+
declarationNumber?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Something that does not add up, said in the terms of this format. */
|
|
54
|
+
export interface Violation {
|
|
55
|
+
code: string;
|
|
56
|
+
message: string;
|
|
57
|
+
vatNumber?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface Des {
|
|
60
|
+
/** The XML, as text. */
|
|
61
|
+
file: string;
|
|
62
|
+
/** A name for it. See the README: the administration imposes none. */
|
|
63
|
+
filename: string;
|
|
64
|
+
/** Everything that could not be put in the file, and why. */
|
|
65
|
+
violations: Violation[];
|
|
66
|
+
}
|
|
67
|
+
/** The nature this declaration carries. The others are another file's. */
|
|
68
|
+
export declare const NATURE = "services";
|
|
69
|
+
/** The DES has no currency field: the cahier des charges declares euro. */
|
|
70
|
+
export declare const CURRENCY = "EUR";
|
|
71
|
+
export declare class DesError extends Error {
|
|
72
|
+
name: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Euros, whole, rounded away from zero at the half.
|
|
76
|
+
*
|
|
77
|
+
* The BOFiP says it in words (BOI-TVA-DECLA-20-20-40 § 170): an amount below
|
|
78
|
+
* 0,50 € is dropped and one of 0,50 € or more counts for one. `Math.round`
|
|
79
|
+
* would send −0,5 to −0, which is the wrong direction for a minoration, so the
|
|
80
|
+
* sign is taken out and put back.
|
|
81
|
+
*/
|
|
82
|
+
export declare function roundToEuro(value: string | number): number;
|
|
83
|
+
/**
|
|
84
|
+
* A name for the file.
|
|
85
|
+
*
|
|
86
|
+
* **The administration imposes none.** The manual describes a file picker and
|
|
87
|
+
* nothing else; no convention, no extension beyond `.xml`, no size limit. So
|
|
88
|
+
* this is a name of ours, and a caller is free to pick another.
|
|
89
|
+
*/
|
|
90
|
+
export declare function desFileName(vatNumber: string, year: number, month: number): string;
|
|
91
|
+
/**
|
|
92
|
+
* Writes the declaration.
|
|
93
|
+
*
|
|
94
|
+
* Refuses, by exception, what would make the whole file meaningless: a month
|
|
95
|
+
* outside 1 to 12, a year before the first the format accepts, or a declarant
|
|
96
|
+
* number that is not the thirteen characters the cahier des charges fixes.
|
|
97
|
+
* Everything wrong with one line comes back in `violations`.
|
|
98
|
+
*
|
|
99
|
+
* The encoding is UTF-8. The cahier des charges contradicts itself — its
|
|
100
|
+
* conventions section prescribes ISO-8859-1 and its own complete example
|
|
101
|
+
* declares UTF-8 — and the file it describes holds nothing but digits and the
|
|
102
|
+
* letters of a VAT number, so the two are the same bytes. The example is the
|
|
103
|
+
* half of the document that was executed.
|
|
104
|
+
*/
|
|
105
|
+
export declare function generateDes(rows: StatementRow[], options: DesOptions): Des;
|
|
106
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qDAAqD;IACrD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wEAAwE;AACxE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,GAAG;IAClB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,0EAA0E;AAC1E,eAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,2EAA2E;AAC3E,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAK9B,qBAAa,QAAS,SAAQ,KAAK;IACxB,IAAI,SAAc;CAC5B;AAUD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAI1D;AAwFD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,UAAU,GAAG,GAAG,CA6C1E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The French DES — *déclaration européenne de services*.
|
|
3
|
+
*
|
|
4
|
+
* One XML document, `fichier_des`, described by the cahier des charges the
|
|
5
|
+
* DGDDI publishes as *DES — description des échanges DTI+ en XML*. Give it the
|
|
6
|
+
* rows of a recapitulative statement and it writes the file, a name for it,
|
|
7
|
+
* and the list of what could not be put in it.
|
|
8
|
+
*
|
|
9
|
+
* **It is the services half and only the services half.** France splits the
|
|
10
|
+
* recapitulative statement in two: services are declared here, supplies of
|
|
11
|
+
* goods on the *état récapitulatif TVA* of DEBWEB2, which is a different file
|
|
12
|
+
* in a different format. A row of goods handed to this package comes back as a
|
|
13
|
+
* violation rather than being written into a declaration that has no place for
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
* Sources are in the README. This package depends on nothing, reads no
|
|
17
|
+
* database and knows no accounting.
|
|
18
|
+
*/
|
|
19
|
+
/** The nature this declaration carries. The others are another file's. */
|
|
20
|
+
export const NATURE = 'services';
|
|
21
|
+
/** The DES has no currency field: the cahier des charges declares euro. */
|
|
22
|
+
export const CURRENCY = 'EUR';
|
|
23
|
+
/** `FR`, a two-character key, nine digits of SIREN. Thirteen characters. */
|
|
24
|
+
const DECLARANT_NUMBER = /^FR[0-9A-Z]{2}[0-9]{9}$/;
|
|
25
|
+
export class DesError extends Error {
|
|
26
|
+
name = 'DesError';
|
|
27
|
+
}
|
|
28
|
+
function escapeXml(value) {
|
|
29
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
30
|
+
}
|
|
31
|
+
function normalise(value) {
|
|
32
|
+
return value.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Euros, whole, rounded away from zero at the half.
|
|
36
|
+
*
|
|
37
|
+
* The BOFiP says it in words (BOI-TVA-DECLA-20-20-40 § 170): an amount below
|
|
38
|
+
* 0,50 € is dropped and one of 0,50 € or more counts for one. `Math.round`
|
|
39
|
+
* would send −0,5 to −0, which is the wrong direction for a minoration, so the
|
|
40
|
+
* sign is taken out and put back.
|
|
41
|
+
*/
|
|
42
|
+
export function roundToEuro(value) {
|
|
43
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
44
|
+
if (!Number.isFinite(n))
|
|
45
|
+
throw new DesError(`invalid amount: ${String(value)}`);
|
|
46
|
+
return Math.sign(n) * Math.round(Math.abs(n));
|
|
47
|
+
}
|
|
48
|
+
function linesOf(rows, options) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const violations = [];
|
|
51
|
+
const home = normalise(options.vatNumber).slice(0, 2);
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const country = row.vat_country === null ? '' : normalise(row.vat_country);
|
|
54
|
+
const number = row.vat_number === null ? '' : normalise(row.vat_number);
|
|
55
|
+
const partner = `${country}${number}`;
|
|
56
|
+
const named = row.contact_names?.join(', ');
|
|
57
|
+
const about = named === undefined || named === '' ? partner : named;
|
|
58
|
+
if (row.issue !== null && row.issue !== undefined && row.issue !== '') {
|
|
59
|
+
violations.push({
|
|
60
|
+
code: row.issue,
|
|
61
|
+
message: `${about}: this supply cannot be declared — ${row.issue.replace(/_/g, ' ')}.`,
|
|
62
|
+
...(partner === '' ? {} : { vatNumber: partner }),
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (row.nature !== NATURE) {
|
|
67
|
+
violations.push({
|
|
68
|
+
code: 'not_a_service',
|
|
69
|
+
message: `${about}: the DES declares services. A supply of ${row.nature} belongs to the état récapitulatif TVA, which is another file.`,
|
|
70
|
+
...(partner === '' ? {} : { vatNumber: partner }),
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (row.currency_code.toUpperCase() !== CURRENCY) {
|
|
75
|
+
violations.push({
|
|
76
|
+
code: 'wrong_currency',
|
|
77
|
+
message: `${about}: the DES is filed in ${CURRENCY} and this line is in ${row.currency_code}. Convert it before declaring it.`,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (country === '' || number === '') {
|
|
82
|
+
violations.push({
|
|
83
|
+
code: 'no_vat_number',
|
|
84
|
+
message: `${about}: a line of the DES is identified by the customer's VAT number and this one has none.`,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (country === home) {
|
|
89
|
+
violations.push({
|
|
90
|
+
code: 'vat_country_is_the_declarant_country',
|
|
91
|
+
message: `${about}: the DES carries services supplied to another Member State, and ${country} is the declarant's own.`,
|
|
92
|
+
vatNumber: partner,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (partner.length < 4 || partner.length > 14) {
|
|
97
|
+
violations.push({
|
|
98
|
+
code: 'partner_length',
|
|
99
|
+
message: `${about}: the cahier des charges holds between four and fourteen characters for a customer's number, prefix included, and this one has ${partner.length}.`,
|
|
100
|
+
vatNumber: partner,
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const value = roundToEuro(row.amount);
|
|
105
|
+
if (value === 0) {
|
|
106
|
+
violations.push({
|
|
107
|
+
code: 'nil_value',
|
|
108
|
+
message: `${about}: the value of a line may not be zero, and this one rounds to nothing.`,
|
|
109
|
+
vatNumber: partner,
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
lines.push({ partner, value });
|
|
114
|
+
}
|
|
115
|
+
lines.sort((a, b) => a.partner.localeCompare(b.partner) || a.value - b.value);
|
|
116
|
+
return { lines, violations };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A name for the file.
|
|
120
|
+
*
|
|
121
|
+
* **The administration imposes none.** The manual describes a file picker and
|
|
122
|
+
* nothing else; no convention, no extension beyond `.xml`, no size limit. So
|
|
123
|
+
* this is a name of ours, and a caller is free to pick another.
|
|
124
|
+
*/
|
|
125
|
+
export function desFileName(vatNumber, year, month) {
|
|
126
|
+
return `DES-${normalise(vatNumber)}-${year}${String(month).padStart(2, '0')}.xml`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Writes the declaration.
|
|
130
|
+
*
|
|
131
|
+
* Refuses, by exception, what would make the whole file meaningless: a month
|
|
132
|
+
* outside 1 to 12, a year before the first the format accepts, or a declarant
|
|
133
|
+
* number that is not the thirteen characters the cahier des charges fixes.
|
|
134
|
+
* Everything wrong with one line comes back in `violations`.
|
|
135
|
+
*
|
|
136
|
+
* The encoding is UTF-8. The cahier des charges contradicts itself — its
|
|
137
|
+
* conventions section prescribes ISO-8859-1 and its own complete example
|
|
138
|
+
* declares UTF-8 — and the file it describes holds nothing but digits and the
|
|
139
|
+
* letters of a VAT number, so the two are the same bytes. The example is the
|
|
140
|
+
* half of the document that was executed.
|
|
141
|
+
*/
|
|
142
|
+
export function generateDes(rows, options) {
|
|
143
|
+
if (!Number.isInteger(options.month) || options.month < 1 || options.month > 12) {
|
|
144
|
+
throw new DesError(`month out of range: ${options.month}`);
|
|
145
|
+
}
|
|
146
|
+
// The format's own floor. It was introduced for the declarations of 2010 and
|
|
147
|
+
// refuses a year before that rather than silently accepting one.
|
|
148
|
+
if (!Number.isInteger(options.year) || options.year < 2010) {
|
|
149
|
+
throw new DesError(`year out of range: ${options.year}`);
|
|
150
|
+
}
|
|
151
|
+
const declarant = normalise(options.vatNumber);
|
|
152
|
+
if (!DECLARANT_NUMBER.test(declarant)) {
|
|
153
|
+
throw new DesError(`the declarant's number is thirteen characters, FR then a key then nine digits: ${options.vatNumber}`);
|
|
154
|
+
}
|
|
155
|
+
const declaration = options.declarationNumber ?? 1;
|
|
156
|
+
if (!Number.isInteger(declaration) || declaration < 1 || declaration > 999999) {
|
|
157
|
+
throw new DesError(`declaration number out of range: ${options.declarationNumber}`);
|
|
158
|
+
}
|
|
159
|
+
const { lines, violations } = linesOf(rows, options);
|
|
160
|
+
const parts = [];
|
|
161
|
+
parts.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
162
|
+
parts.push('<fichier_des>');
|
|
163
|
+
parts.push('<declaration_des>');
|
|
164
|
+
parts.push(`<num_des>${String(declaration).padStart(5, '0')}</num_des>`);
|
|
165
|
+
parts.push(`<num_tvaFr>${escapeXml(declarant)}</num_tvaFr>`);
|
|
166
|
+
parts.push(`<mois_des>${String(options.month).padStart(2, '0')}</mois_des>`);
|
|
167
|
+
parts.push(`<an_des>${options.year}</an_des>`);
|
|
168
|
+
for (const [index, line] of lines.entries()) {
|
|
169
|
+
parts.push('<ligne_des>');
|
|
170
|
+
parts.push(`<numlin_des>${String(index + 1).padStart(6, '0')}</numlin_des>`);
|
|
171
|
+
parts.push(`<valeur>${line.value}</valeur>`);
|
|
172
|
+
parts.push(`<partner_des>${escapeXml(line.partner)}</partner_des>`);
|
|
173
|
+
parts.push('</ligne_des>');
|
|
174
|
+
}
|
|
175
|
+
parts.push('</declaration_des>');
|
|
176
|
+
parts.push('</fichier_des>');
|
|
177
|
+
return {
|
|
178
|
+
file: `${parts.join('\n')}\n`,
|
|
179
|
+
filename: desFileName(declarant, options.year, options.month),
|
|
180
|
+
violations,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekwo-ai/des",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "The French DES — declaration europeenne de services — in TypeScript: the XML the DGDDI accepts, from plain statement rows. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"des",
|
|
7
|
+
"declaration-europeenne-de-services",
|
|
8
|
+
"ec-sales-list",
|
|
9
|
+
"intra-community",
|
|
10
|
+
"vat",
|
|
11
|
+
"france",
|
|
12
|
+
"douane",
|
|
13
|
+
"dgddi",
|
|
14
|
+
"xml"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Ekwo AI",
|
|
18
|
+
"homepage": "https://github.com/Ekwo-ai/ekwo-os/tree/main/packages/formats/des#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Ekwo-ai/ekwo-os.git",
|
|
22
|
+
"directory": "packages/formats/des"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Ekwo-ai/ekwo-os/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.0.0",
|
|
52
|
+
"typescript": "^5.9.0",
|
|
53
|
+
"vitest": "^3.2.0"
|
|
54
|
+
}
|
|
55
|
+
}
|