@ekwo-ai/core 0.14.0 → 0.15.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/dist/books/analytics.d.ts +32 -0
- package/dist/books/analytics.d.ts.map +1 -0
- package/dist/books/analytics.js +94 -0
- package/dist/books/analytics.js.map +1 -0
- package/dist/books/columns.d.ts.map +1 -1
- package/dist/books/columns.js +5 -0
- package/dist/books/columns.js.map +1 -1
- package/dist/books/contacts.d.ts +43 -0
- package/dist/books/contacts.d.ts.map +1 -1
- package/dist/books/contacts.js +61 -0
- package/dist/books/contacts.js.map +1 -1
- package/dist/books/index.d.ts +3 -0
- package/dist/books/index.d.ts.map +1 -1
- package/dist/books/index.js +3 -0
- package/dist/books/index.js.map +1 -1
- package/dist/books/lines.d.ts +38 -0
- package/dist/books/lines.d.ts.map +1 -0
- package/dist/books/lines.js +73 -0
- package/dist/books/lines.js.map +1 -0
- package/dist/books/matching.d.ts +65 -0
- package/dist/books/matching.d.ts.map +1 -0
- package/dist/books/matching.js +95 -0
- package/dist/books/matching.js.map +1 -0
- package/dist/einvoicing/fiscalisation.d.ts +88 -5
- package/dist/einvoicing/fiscalisation.d.ts.map +1 -1
- package/dist/einvoicing/fiscalisation.js +212 -12
- package/dist/einvoicing/fiscalisation.js.map +1 -1
- package/dist/einvoicing/formats.d.ts +16 -0
- package/dist/einvoicing/formats.d.ts.map +1 -1
- package/dist/einvoicing/formats.js +110 -0
- package/dist/einvoicing/formats.js.map +1 -1
- package/dist/einvoicing/issue.d.ts +14 -1
- package/dist/einvoicing/issue.d.ts.map +1 -1
- package/dist/einvoicing/issue.js +44 -7
- package/dist/einvoicing/issue.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/package.json +16 -15
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matching open items of one counterparty: what settles what, several lines in
|
|
3
|
+
* one gesture, a small difference booked, and a line left unnamed on purpose.
|
|
4
|
+
*
|
|
5
|
+
* `suggest_item_combinations()` proposes, `reconcile_many()` and
|
|
6
|
+
* `reconcile_with_difference()` match, `leave_line_unnamed()` and its undo
|
|
7
|
+
* record a decision about a line (decisions 0071 and 0072). The search, the
|
|
8
|
+
* accounts a difference is booked on, the tolerance and every refusal are the
|
|
9
|
+
* schema's; this file asks for them and shapes the answer.
|
|
10
|
+
*/
|
|
11
|
+
import { money, moneyFields } from './format.js';
|
|
12
|
+
import { only } from './shared.js';
|
|
13
|
+
/**
|
|
14
|
+
* What lines of one counterparty and one account could settle each other, line
|
|
15
|
+
* by line: an exact pair, several lines to the cent, or — when nothing is
|
|
16
|
+
* exact — a pair or several lines within the company's difference tolerance.
|
|
17
|
+
*/
|
|
18
|
+
export async function suggestItemCombinations(backend, args) {
|
|
19
|
+
const found = await backend.rpc('suggest_item_combinations', {
|
|
20
|
+
p_company_id: args.company_id,
|
|
21
|
+
...(args.contact_id === undefined ? {} : { p_contact_id: args.contact_id }),
|
|
22
|
+
});
|
|
23
|
+
const proposals = found.map((row) => ({
|
|
24
|
+
proposal_id: String(row['proposal_id']),
|
|
25
|
+
ordinal: Number(row['ordinal']),
|
|
26
|
+
contact_id: String(row['contact_id']),
|
|
27
|
+
account_id: String(row['account_id']),
|
|
28
|
+
line_id: String(row['line_id']),
|
|
29
|
+
side: String(row['side']),
|
|
30
|
+
amount: money(row['amount']),
|
|
31
|
+
kind: String(row['kind']),
|
|
32
|
+
line_ids: (row['line_ids'] ?? []).map(String),
|
|
33
|
+
total: money(row['total']),
|
|
34
|
+
difference: money(row['difference']),
|
|
35
|
+
because: String(row['because']),
|
|
36
|
+
alternatives: Number(row['alternatives']),
|
|
37
|
+
}));
|
|
38
|
+
return {
|
|
39
|
+
proposals,
|
|
40
|
+
count: proposals.length,
|
|
41
|
+
note: 'Nothing is matched by a proposal. A difference other than 0.00 is closed only by reconcile_with_difference, with a reason, once the user has seen the difference and said so; reconcile_many matches a proposal whose difference is 0.00.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** The reasons a difference may be booked for, with their accounts, and the largest difference the company books. */
|
|
45
|
+
export async function matchingDifferenceOptions(backend, args) {
|
|
46
|
+
const [tolerance] = await backend.rpc('matching_difference_tolerance', { p_company_id: args.company_id });
|
|
47
|
+
const reasons = await backend.rpc('matching_difference_reasons', { p_company_id: args.company_id });
|
|
48
|
+
return {
|
|
49
|
+
tolerance: tolerance === undefined
|
|
50
|
+
? null
|
|
51
|
+
: {
|
|
52
|
+
units: Number(tolerance['tolerance_units']),
|
|
53
|
+
amount: money(tolerance['tolerance']),
|
|
54
|
+
currency_code: tolerance['currency_code'] ?? null,
|
|
55
|
+
},
|
|
56
|
+
reasons,
|
|
57
|
+
note: 'side says which difference a reason answers: charge when the lines carry more debit than credit, income the other way, either for both. A reason that is not available has no account to be booked on, and is refused.',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function matched(rows, what) {
|
|
61
|
+
return only(moneyFields(rows, ['amount', 'difference']), what);
|
|
62
|
+
}
|
|
63
|
+
/** Matches several lines together, their open debits and credits equal to the cent. */
|
|
64
|
+
export async function reconcileMany(backend, args) {
|
|
65
|
+
const done = matched(await backend.rpc('reconcile_many', { p_line_ids: args.line_ids }), 'the lines were not matched');
|
|
66
|
+
return {
|
|
67
|
+
matching: done,
|
|
68
|
+
note: 'The lines are matched under one letter, pair by pair through reconcile(). unreconcile undoes one matching at a time.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Books the difference on the account of the reason and matches everything. */
|
|
72
|
+
export async function reconcileWithDifference(backend, args) {
|
|
73
|
+
const done = matched(await backend.rpc('reconcile_with_difference', { p_line_ids: args.line_ids, p_reason: args.reason }), 'the lines were not matched');
|
|
74
|
+
return {
|
|
75
|
+
matching: done,
|
|
76
|
+
note: 'The difference is booked in a posted entry (adjustment_entry_id) on the same account and counterparty, against the account the pack gives for the reason, and every line is matched under one letter. unreconcile of the matching of the difference line takes the entry back with its mirror.',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Records that a line names nobody on purpose: it is no longer proposed a contact. */
|
|
80
|
+
export async function leaveLineUnnamed(backend, args) {
|
|
81
|
+
const left = only(await backend.rpc('leave_line_unnamed', { p_line_id: args.line_id, p_reason: args.reason }), `line ${args.line_id} was not left unnamed`);
|
|
82
|
+
return {
|
|
83
|
+
left_unnamed: left,
|
|
84
|
+
note: 'The line is no longer proposed a contact. It is still an open item, it can still be matched or named, and undo_leave_line_unnamed takes the decision back.',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Takes back `leave_line_unnamed()`: the line is proposed a contact again. */
|
|
88
|
+
export async function undoLeaveLineUnnamed(backend, args) {
|
|
89
|
+
const removed = only(await backend.rpc('undo_leave_line_unnamed', { p_line_id: args.line_id }), `line ${args.line_id} was not left unnamed`);
|
|
90
|
+
return {
|
|
91
|
+
removed,
|
|
92
|
+
note: 'The line is proposed a contact again.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=matching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matching.js","sourceRoot":"","sources":["../../src/books/matching.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,9 +26,16 @@
|
|
|
26
26
|
* can be checked without trusting this code is checked again by
|
|
27
27
|
* `einvoicing.append_fiscal_record()`: the regime, the digest, the fields, the
|
|
28
28
|
* tail, a record made once.
|
|
29
|
+
*
|
|
30
|
+
* **A clearance regime chains nothing.** There the authority validates every
|
|
31
|
+
* invoice before it is issued, and what the books keep is the submission and
|
|
32
|
+
* the answer: `recordClearanceSubmission()` and `recordClearanceAnswer()`
|
|
33
|
+
* write them through `einvoicing.record_clearance_submission()` and
|
|
34
|
+
* `einvoicing.record_clearance_answer()`, which check every transition. The
|
|
35
|
+
* call to the authority is the host's; nothing here dials anybody.
|
|
29
36
|
*/
|
|
30
37
|
import { type Backend, type Row } from '../books/backend.js';
|
|
31
|
-
import type
|
|
38
|
+
import { type EinvoiceViolation } from './formats.js';
|
|
32
39
|
/** How the records of an invoicing system reach the authority. */
|
|
33
40
|
export type FiscalSubmissionMode = 'realtime' | 'batch' | 'none';
|
|
34
41
|
export declare const FISCAL_SUBMISSION_MODES: readonly ["realtime", "batch", "none"];
|
|
@@ -101,20 +108,42 @@ export declare const FISCALISATION_FORMATS: Readonly<Record<string, Fiscalisatio
|
|
|
101
108
|
* format is a record the authority refuses.
|
|
102
109
|
*/
|
|
103
110
|
export declare function fiscalisationFormat(format: string, formats?: Readonly<Record<string, FiscalisationFormat>>): FiscalisationFormat;
|
|
111
|
+
/** An annual turnover or revenue, as the law states it. */
|
|
112
|
+
export interface FiscalTurnover {
|
|
113
|
+
amount: number;
|
|
114
|
+
currency: string;
|
|
115
|
+
}
|
|
104
116
|
/**
|
|
105
117
|
* The regime of a company as the socle compiled it from its pack, or null
|
|
106
118
|
* where it has none. `chained`: a record for every invoice, chained by a
|
|
107
|
-
* hash. `
|
|
108
|
-
* nothing chained
|
|
119
|
+
* hash. `clearance`: every invoice validated by the authority before it is
|
|
120
|
+
* issued, nothing chained. `reporting`: the documents' data reported to the
|
|
121
|
+
* authority, no hash, nothing chained — the core keeps no chain for it.
|
|
109
122
|
*/
|
|
110
123
|
export interface FiscalRegime {
|
|
111
|
-
kind: 'chained' | 'reporting';
|
|
124
|
+
kind: 'chained' | 'clearance' | 'reporting';
|
|
112
125
|
format: string;
|
|
113
|
-
/** Empty while the obligation is only announced. */
|
|
126
|
+
/** Empty while the obligation is only announced. `turnover_above` where the law phases it by size. */
|
|
114
127
|
mandatory_from: {
|
|
115
128
|
who: string | null;
|
|
116
129
|
from: string;
|
|
130
|
+
turnover_above?: FiscalTurnover;
|
|
131
|
+
}[];
|
|
132
|
+
/** The thresholds of exemption, oldest first; absent where the pack declares none. */
|
|
133
|
+
exemption?: {
|
|
134
|
+
turnover_below: FiscalTurnover;
|
|
135
|
+
valid_from: string;
|
|
136
|
+
legal_reference: string;
|
|
137
|
+
source: string | null;
|
|
117
138
|
}[];
|
|
139
|
+
/** What a clearance regime says of the authority's answer; absent for the other two. */
|
|
140
|
+
clearance?: {
|
|
141
|
+
verification: 'returned' | 'computed' | 'none';
|
|
142
|
+
cancellation_window: string | null;
|
|
143
|
+
rejection_window: string | null;
|
|
144
|
+
legal_reference: string | null;
|
|
145
|
+
source: string | null;
|
|
146
|
+
};
|
|
118
147
|
/** A day only announced, no text in force: it binds nobody. */
|
|
119
148
|
announced_from: string | null;
|
|
120
149
|
announced_by: string | null;
|
|
@@ -152,6 +181,8 @@ export interface FiscalRegime {
|
|
|
152
181
|
by: ('supplier' | 'customer')[];
|
|
153
182
|
}[];
|
|
154
183
|
tax_attributes: Record<string, Record<string, string>>;
|
|
184
|
+
/** What the brick needs on every line that the books do not record; absent where the pack declares none. */
|
|
185
|
+
line_attributes?: Record<string, string>;
|
|
155
186
|
legal_reference: string;
|
|
156
187
|
source: string;
|
|
157
188
|
}
|
|
@@ -192,6 +223,8 @@ export interface FiscaliseOptions {
|
|
|
192
223
|
* is in the chain already is answered with that record, not chained twice.
|
|
193
224
|
*/
|
|
194
225
|
export declare function fiscaliseDocument(backend: Backend, documentId: string, options: FiscaliseOptions, formats?: Readonly<Record<string, FiscalisationFormat>>): Promise<Row>;
|
|
226
|
+
/** The threshold of exemption that holds on a day, or null where the pack declares none that does. */
|
|
227
|
+
export declare function exemptionOn(regime: FiscalRegime, day: string): NonNullable<FiscalRegime['exemption']>[number] | null;
|
|
195
228
|
/**
|
|
196
229
|
* Where the fiscal records of a company stand: its regime, each chain
|
|
197
230
|
* verified, the records still to be sent, the posted sales without a record,
|
|
@@ -201,6 +234,56 @@ export declare function fiscaliseDocument(backend: Backend, documentId: string,
|
|
|
201
234
|
export declare function fiscalisationStatus(backend: Backend, companyId: string, options?: {
|
|
202
235
|
limit?: number;
|
|
203
236
|
}): Promise<Row>;
|
|
237
|
+
/** Where one submission of an invoice to the authority stands. */
|
|
238
|
+
export type ClearanceState = 'submitted' | 'validated' | 'rejected' | 'cancelled';
|
|
239
|
+
export declare const CLEARANCE_STATES: readonly ["submitted", "validated", "rejected", "cancelled"];
|
|
240
|
+
/** What the authority, the buyer or the issuer can bring a submission to. */
|
|
241
|
+
export declare const CLEARANCE_ANSWERS: readonly ["validated", "rejected", "cancelled"];
|
|
242
|
+
/** A submission somebody made to the authority, as the host that made it reports it. */
|
|
243
|
+
export interface ClearanceSubmission {
|
|
244
|
+
/** What the authority named the submission on receipt, where it named it. */
|
|
245
|
+
reference?: string;
|
|
246
|
+
/** The electronic invoice submitted, where `einvoicing.issues` keeps it. */
|
|
247
|
+
issueId?: string;
|
|
248
|
+
/** When it was submitted, ISO 8601. Now when left out. */
|
|
249
|
+
submittedAt?: string;
|
|
250
|
+
/** What the authority said on receipt, as it said it. */
|
|
251
|
+
answer?: Row;
|
|
252
|
+
}
|
|
253
|
+
/** What came back about the latest submission of a document. */
|
|
254
|
+
export interface ClearanceAnswer {
|
|
255
|
+
state: (typeof CLEARANCE_ANSWERS)[number];
|
|
256
|
+
/** The identifier the authority gave the invoice: required on validation. */
|
|
257
|
+
identifier?: string;
|
|
258
|
+
/** When the authority validated it, ISO 8601: required on validation. */
|
|
259
|
+
validatedAt?: string;
|
|
260
|
+
/** The link to its verification, which the invoice carries, where the regime has one. */
|
|
261
|
+
verificationUrl?: string;
|
|
262
|
+
/** Why it was rejected or cancelled, as it was said: required for both. */
|
|
263
|
+
reason?: string;
|
|
264
|
+
/** Who rejected it: the authority (the default) or the buyer. */
|
|
265
|
+
by?: 'authority' | 'buyer';
|
|
266
|
+
/** When the answer came, ISO 8601. Now when left out; the windows of the regime are read against it. */
|
|
267
|
+
at?: string;
|
|
268
|
+
/** The answer itself, as the authority gave it. */
|
|
269
|
+
answer?: Row;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Records that a posted sale was submitted to the authority under its
|
|
273
|
+
* company's clearance regime. Calls nobody: the submission is the host's,
|
|
274
|
+
* and this keeps what it reports. The database refuses a second submission
|
|
275
|
+
* while one waits, after a validation or after a cancellation.
|
|
276
|
+
*/
|
|
277
|
+
export declare function recordClearanceSubmission(backend: Backend, documentId: string, submission?: ClearanceSubmission): Promise<Row>;
|
|
278
|
+
/**
|
|
279
|
+
* Records what came back about the latest submission of a document: its
|
|
280
|
+
* validation, with the identifier and the moment the authority gave it; a
|
|
281
|
+
* rejection by the authority or, within the regime's window, by the buyer;
|
|
282
|
+
* its cancellation by the issuer, within the regime's window. Every
|
|
283
|
+
* transition is checked by `einvoicing.record_clearance_answer()`, which
|
|
284
|
+
* raises on one that cannot have happened.
|
|
285
|
+
*/
|
|
286
|
+
export declare function recordClearanceAnswer(backend: Backend, documentId: string, answer: ClearanceAnswer): Promise<Row>;
|
|
204
287
|
/** The variable the command line and the MCP server read the invoicing system from, as JSON. */
|
|
205
288
|
export declare const FISCAL_SYSTEM_ENV = "EKWO_FISCAL_SYSTEM";
|
|
206
289
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fiscalisation.d.ts","sourceRoot":"","sources":["../../src/einvoicing/fiscalisation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fiscalisation.d.ts","sourceRoot":"","sources":["../../src/einvoicing/fiscalisation.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,9 +26,17 @@
|
|
|
26
26
|
* can be checked without trusting this code is checked again by
|
|
27
27
|
* `einvoicing.append_fiscal_record()`: the regime, the digest, the fields, the
|
|
28
28
|
* tail, a record made once.
|
|
29
|
+
*
|
|
30
|
+
* **A clearance regime chains nothing.** There the authority validates every
|
|
31
|
+
* invoice before it is issued, and what the books keep is the submission and
|
|
32
|
+
* the answer: `recordClearanceSubmission()` and `recordClearanceAnswer()`
|
|
33
|
+
* write them through `einvoicing.record_clearance_submission()` and
|
|
34
|
+
* `einvoicing.record_clearance_answer()`, which check every transition. The
|
|
35
|
+
* call to the authority is the host's; nothing here dials anybody.
|
|
29
36
|
*/
|
|
30
37
|
import { VERIFACTU } from '@ekwo-ai/verifactu';
|
|
31
38
|
import { BooksError } from '../books/backend.js';
|
|
39
|
+
import { EINVOICE_FORMATS } from './formats.js';
|
|
32
40
|
import { assertPostedSale, einvoiceChecksum, readEinvoiceSource } from './issue.js';
|
|
33
41
|
const SCHEMA = 'einvoicing';
|
|
34
42
|
export const FISCAL_SUBMISSION_MODES = ['realtime', 'batch', 'none'];
|
|
@@ -116,6 +124,9 @@ export async function fiscaliseDocument(backend, documentId, options, formats =
|
|
|
116
124
|
if (regime === null) {
|
|
117
125
|
throw new BooksError(`no_fiscalisation_regime: the country pack of this company declares no fiscalisation regime, so ${what} has no fiscal record to keep.`, { hint: 'A pack declares one in einvoicing.fiscalisation; describe_pack (ekwo pack describe) shows it.' });
|
|
118
126
|
}
|
|
127
|
+
if (regime.kind === 'clearance') {
|
|
128
|
+
throw new BooksError(`fiscal_regime_without_chain: the country pack of this company declares a clearance regime, ${regime.format}: every invoice is validated by the authority before it is issued and nothing is chained, so ${what} has no record to chain. Its submission and the authority's answer are recorded with einvoicing_clearance (ekwo einvoice clearance).`);
|
|
129
|
+
}
|
|
119
130
|
const format = fiscalisationFormat(regime.format, formats);
|
|
120
131
|
if (regime.hash === null) {
|
|
121
132
|
throw new BooksError(`fiscal_regime_without_chain: the country pack of this company declares a reporting regime, ${regime.format}: the data of its documents are reported to the authority and nothing is chained, so ${what} has no record to chain. The report is written by ${format.brick}, not by fiscaliseDocument().`);
|
|
@@ -272,12 +283,26 @@ function earliestObligation(regime) {
|
|
|
272
283
|
return days[0] ?? null;
|
|
273
284
|
}
|
|
274
285
|
/**
|
|
275
|
-
* Whether the status counts posted sales as missing a record
|
|
276
|
-
*
|
|
277
|
-
*
|
|
286
|
+
* Whether the status counts posted sales as missing a record, or a
|
|
287
|
+
* validation. Only under a chained or a clearance regime in force: an
|
|
288
|
+
* announced day binds nobody, and a reporting regime keeps no chain — which
|
|
289
|
+
* documents it reports is its brick's to say.
|
|
278
290
|
*/
|
|
279
291
|
function countsMissing(regime) {
|
|
280
|
-
return regime !== null && regime.hash !== null && (regime.announced_from ?? null) === null;
|
|
292
|
+
return regime !== null && (regime.kind === 'clearance' || regime.hash !== null) && (regime.announced_from ?? null) === null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The brick that writes what a regime needs written, or null where none of
|
|
296
|
+
* this release does. Under a clearance regime what is submitted is the
|
|
297
|
+
* electronic invoice itself, so its brick is looked up with the e-invoicing
|
|
298
|
+
* profiles; under the other two, with the fiscalisation formats.
|
|
299
|
+
*/
|
|
300
|
+
function brickOf(regime) {
|
|
301
|
+
return (regime.kind === 'clearance' ? EINVOICE_FORMATS[regime.format] : FISCALISATION_FORMATS[regime.format])?.brick ?? null;
|
|
302
|
+
}
|
|
303
|
+
/** The threshold of exemption that holds on a day, or null where the pack declares none that does. */
|
|
304
|
+
export function exemptionOn(regime, day) {
|
|
305
|
+
return [...(regime.exemption ?? [])].reverse().find((entry) => entry.valid_from <= day) ?? null;
|
|
281
306
|
}
|
|
282
307
|
/**
|
|
283
308
|
* Where the fiscal records of a company stand: its regime, each chain
|
|
@@ -327,7 +352,7 @@ export async function fiscalisationStatus(backend, companyId, options = {}) {
|
|
|
327
352
|
const fiscalised = new Set(records.filter((r) => r['kind'] === 'alta').map((r) => String(r['document_id'])));
|
|
328
353
|
const posted = await backend.select({
|
|
329
354
|
table: 'documents',
|
|
330
|
-
columns: ['id', 'doc_type', 'number', 'document_date::text'],
|
|
355
|
+
columns: ['id', 'doc_type', 'number', 'document_date::text', 'reversed_document_id'],
|
|
331
356
|
where: [
|
|
332
357
|
{ column: 'company_id', op: 'eq', value: companyId },
|
|
333
358
|
{ column: 'state', op: 'eq', value: 'posted' },
|
|
@@ -337,7 +362,12 @@ export async function fiscalisationStatus(backend, companyId, options = {}) {
|
|
|
337
362
|
});
|
|
338
363
|
const from = regime === null ? null : earliestObligation(regime);
|
|
339
364
|
const counted = countsMissing(regime);
|
|
340
|
-
const
|
|
365
|
+
const cleared = regime?.kind === 'clearance' ? await clearanceOf(backend, companyId, posted, limit) : null;
|
|
366
|
+
const without = !counted
|
|
367
|
+
? []
|
|
368
|
+
: cleared !== null
|
|
369
|
+
? posted.filter((d) => cleared.unvalidated.has(String(d['id'])))
|
|
370
|
+
: posted.filter((d) => !fiscalised.has(String(d['id'])));
|
|
341
371
|
const missing = without.filter((d) => from === null || String(d['document_date']) >= from);
|
|
342
372
|
const events = await backend.select({
|
|
343
373
|
schema: SCHEMA,
|
|
@@ -355,8 +385,10 @@ export async function fiscalisationStatus(backend, companyId, options = {}) {
|
|
|
355
385
|
: {
|
|
356
386
|
kind: regime.kind,
|
|
357
387
|
format: regime.format,
|
|
358
|
-
brick:
|
|
388
|
+
brick: brickOf(regime),
|
|
359
389
|
mandatory_from: regime.mandatory_from,
|
|
390
|
+
...(regime.clearance === undefined ? {} : { clearance: regime.clearance }),
|
|
391
|
+
...(regime.exemption === undefined ? {} : { exemption: exemptionOn(regime, today()) }),
|
|
360
392
|
announced_from: regime.announced_from,
|
|
361
393
|
announced_by: regime.announced_by,
|
|
362
394
|
announced_on: regime.announced_on,
|
|
@@ -375,18 +407,186 @@ export async function fiscalisationStatus(backend, companyId, options = {}) {
|
|
|
375
407
|
missing: missing.slice(0, limit),
|
|
376
408
|
missing_count: missing.length,
|
|
377
409
|
before_obligation_count: without.length - missing.length,
|
|
410
|
+
...(cleared === null ? {} : { clearance: cleared.summary }),
|
|
378
411
|
events,
|
|
379
412
|
note: regime === null
|
|
380
413
|
? 'The country pack of this company declares no fiscalisation regime.'
|
|
381
414
|
: !counted
|
|
382
415
|
? uncountedNote(regime)
|
|
383
|
-
:
|
|
384
|
-
?
|
|
385
|
-
:
|
|
386
|
-
?
|
|
387
|
-
:
|
|
416
|
+
: cleared !== null
|
|
417
|
+
? clearanceNote(regime, missing.length, from, cleared.summary)
|
|
418
|
+
: broken.length > 0
|
|
419
|
+
? `The chain of ${broken.map((s) => String(s['system_id'])).join(', ')} breaks where break says. Nothing more is chained to it; this is for the company's accountant before anything else.`
|
|
420
|
+
: missing.length > 0
|
|
421
|
+
? `${missing.length} posted sale(s) dated from ${from ?? 'the start of the obligation'} have no fiscal record. Nothing is fiscalised on its own: einvoicing_fiscalise (ekwo einvoice fiscalise) chains one.`
|
|
422
|
+
: 'Every posted sale within the obligation has its record, and every chain is intact.',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export const CLEARANCE_STATES = ['submitted', 'validated', 'rejected', 'cancelled'];
|
|
426
|
+
/** What the authority, the buyer or the issuer can bring a submission to. */
|
|
427
|
+
export const CLEARANCE_ANSWERS = ['validated', 'rejected', 'cancelled'];
|
|
428
|
+
/** The arguments of a call, without the ones nobody gave: the function's own default then holds. */
|
|
429
|
+
const given = (args) => Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
|
|
430
|
+
/**
|
|
431
|
+
* Records that a posted sale was submitted to the authority under its
|
|
432
|
+
* company's clearance regime. Calls nobody: the submission is the host's,
|
|
433
|
+
* and this keeps what it reports. The database refuses a second submission
|
|
434
|
+
* while one waits, after a validation or after a cancellation.
|
|
435
|
+
*/
|
|
436
|
+
export async function recordClearanceSubmission(backend, documentId, submission = {}) {
|
|
437
|
+
const [row] = await backend.rpc('record_clearance_submission', given({
|
|
438
|
+
p_document_id: documentId,
|
|
439
|
+
p_reference: submission.reference,
|
|
440
|
+
p_issue_id: submission.issueId,
|
|
441
|
+
p_submitted_at: submission.submittedAt,
|
|
442
|
+
p_answer: submission.answer,
|
|
443
|
+
}), SCHEMA);
|
|
444
|
+
if (row === undefined)
|
|
445
|
+
throw new BooksError('einvoicing_no_answer: einvoicing.record_clearance_submission answered nothing');
|
|
446
|
+
return {
|
|
447
|
+
document_id: documentId,
|
|
448
|
+
clearance: row,
|
|
449
|
+
note: `Recorded as attempt ${String(row['attempt'])} of this document, submitted as ${String(row['format'])} and waiting for the authority's answer, which einvoicing_clearance (ekwo einvoice clearance) records. The invoice is valid once the authority has validated it.`,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Records what came back about the latest submission of a document: its
|
|
454
|
+
* validation, with the identifier and the moment the authority gave it; a
|
|
455
|
+
* rejection by the authority or, within the regime's window, by the buyer;
|
|
456
|
+
* its cancellation by the issuer, within the regime's window. Every
|
|
457
|
+
* transition is checked by `einvoicing.record_clearance_answer()`, which
|
|
458
|
+
* raises on one that cannot have happened.
|
|
459
|
+
*/
|
|
460
|
+
export async function recordClearanceAnswer(backend, documentId, answer) {
|
|
461
|
+
const [row] = await backend.rpc('record_clearance_answer', given({
|
|
462
|
+
p_document_id: documentId,
|
|
463
|
+
p_state: answer.state,
|
|
464
|
+
p_identifier: answer.identifier,
|
|
465
|
+
p_validated_at: answer.validatedAt,
|
|
466
|
+
p_verification_url: answer.verificationUrl,
|
|
467
|
+
p_reason: answer.reason,
|
|
468
|
+
p_by: answer.by,
|
|
469
|
+
p_at: answer.at,
|
|
470
|
+
p_answer: answer.answer,
|
|
471
|
+
}), SCHEMA);
|
|
472
|
+
if (row === undefined)
|
|
473
|
+
throw new BooksError('einvoicing_no_answer: einvoicing.record_clearance_answer answered nothing');
|
|
474
|
+
const state = String(row['state']);
|
|
475
|
+
return {
|
|
476
|
+
document_id: documentId,
|
|
477
|
+
clearance: row,
|
|
478
|
+
note: state === 'validated'
|
|
479
|
+
? `Validated by the authority as ${String(row['identifier'])}: the invoice is valid${row['verification_url'] === null ? '' : ', and carries the link to its verification'}.`
|
|
480
|
+
: state === 'cancelled'
|
|
481
|
+
? 'Cancelled at the authority: the invoice is void there. It is undone in the books with cancel_document.'
|
|
482
|
+
: row['rejected_by'] === 'buyer'
|
|
483
|
+
? 'Rejected by the buyer: the invoice stays valid at the authority until the issuer cancels it, within the regime\'s window, or settles with the buyer.'
|
|
484
|
+
: 'Rejected by the authority, which did not validate it. The books are corrected and the invoice submitted again: that is the next attempt.',
|
|
388
485
|
};
|
|
389
486
|
}
|
|
487
|
+
const CLEARANCE = [
|
|
488
|
+
'id', 'document_id', 'attempt', 'format', 'state', 'reference', 'submitted_at', 'identifier', 'validated_at',
|
|
489
|
+
'verification_url', 'rejected_by', 'rejected_at', 'rejection_reason', 'cancelled_at', 'cancellation_reason',
|
|
490
|
+
];
|
|
491
|
+
/**
|
|
492
|
+
* Where the posted sales of a company stand under a clearance regime, by the
|
|
493
|
+
* latest submission of each: validated, waiting, rejected by the authority,
|
|
494
|
+
* rejected by the buyer, or cancelled at the authority while still posted
|
|
495
|
+
* here. `unvalidated` is the sales never submitted or rejected by the
|
|
496
|
+
* authority — the ones the status counts as missing. A credit note that
|
|
497
|
+
* undoes an invoice cancelled at the authority is the books' half of that
|
|
498
|
+
* cancellation, and is not one of them.
|
|
499
|
+
*/
|
|
500
|
+
async function clearanceOf(backend, companyId, posted, limit) {
|
|
501
|
+
const all = await backend.select({
|
|
502
|
+
schema: SCHEMA,
|
|
503
|
+
table: 'clearances',
|
|
504
|
+
columns: CLEARANCE,
|
|
505
|
+
where: [{ column: 'company_id', op: 'eq', value: companyId }],
|
|
506
|
+
order: [{ column: 'document_id' }, { column: 'attempt' }],
|
|
507
|
+
});
|
|
508
|
+
const latest = new Map();
|
|
509
|
+
for (const clearance of all)
|
|
510
|
+
latest.set(String(clearance['document_id']), clearance);
|
|
511
|
+
const voided = new Set([...latest.values()].filter((c) => c['state'] === 'cancelled').map((c) => String(c['document_id'])));
|
|
512
|
+
const unvalidated = new Set();
|
|
513
|
+
const awaiting = [];
|
|
514
|
+
const rejected = [];
|
|
515
|
+
const buyerRejected = [];
|
|
516
|
+
const cancelled = [];
|
|
517
|
+
let validated = 0;
|
|
518
|
+
for (const document of posted) {
|
|
519
|
+
const id = String(document['id']);
|
|
520
|
+
const last = latest.get(id);
|
|
521
|
+
if (last === undefined) {
|
|
522
|
+
const undoes = document['reversed_document_id'];
|
|
523
|
+
if (!(undoes !== null && undoes !== undefined && voided.has(String(undoes))))
|
|
524
|
+
unvalidated.add(id);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const { id: clearanceId, ...clearance } = last;
|
|
528
|
+
const entry = { id, doc_type: document['doc_type'], number: document['number'], document_date: document['document_date'], clearance_id: clearanceId, ...clearance };
|
|
529
|
+
if (last['state'] === 'validated')
|
|
530
|
+
validated += 1;
|
|
531
|
+
else if (last['state'] === 'submitted')
|
|
532
|
+
awaiting.push(entry);
|
|
533
|
+
else if (last['state'] === 'cancelled')
|
|
534
|
+
cancelled.push(entry);
|
|
535
|
+
else if (last['rejected_by'] === 'buyer')
|
|
536
|
+
buyerRejected.push(entry);
|
|
537
|
+
else {
|
|
538
|
+
unvalidated.add(id);
|
|
539
|
+
rejected.push(entry);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
unvalidated,
|
|
544
|
+
summary: {
|
|
545
|
+
validated_count: validated,
|
|
546
|
+
awaiting: awaiting.slice(0, limit),
|
|
547
|
+
awaiting_count: awaiting.length,
|
|
548
|
+
rejected: rejected.slice(0, limit),
|
|
549
|
+
rejected_count: rejected.length,
|
|
550
|
+
buyer_rejected: buyerRejected.slice(0, limit),
|
|
551
|
+
buyer_rejected_count: buyerRejected.length,
|
|
552
|
+
cancelled_still_posted: cancelled.slice(0, limit),
|
|
553
|
+
cancelled_still_posted_count: cancelled.length,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** Today, as the calendar day of this machine. */
|
|
558
|
+
const today = () => generatedNow().slice(0, 10);
|
|
559
|
+
/** What the status says of a company under a clearance regime in force. */
|
|
560
|
+
function clearanceNote(regime, missing, from, summary) {
|
|
561
|
+
const brick = brickOf(regime);
|
|
562
|
+
const sentences = [
|
|
563
|
+
`The country pack of this company declares a clearance regime, ${regime.format}: an invoice is valid once the authority has validated it, and nothing is chained.`,
|
|
564
|
+
brick === null
|
|
565
|
+
? `No brick of packages/formats/ writes ${regime.format} yet (format_without_brick): Ekwo writes no file to submit, and records the submissions a host makes and the answers it receives.`
|
|
566
|
+
: `${brick} writes the file submitted.`,
|
|
567
|
+
];
|
|
568
|
+
if (missing > 0) {
|
|
569
|
+
sentences.push(`${missing} posted sale(s) dated from ${from ?? 'the start of the obligation'} have no validation and none on its way${summary.rejected_count > 0 ? `, ${summary.rejected_count} of them rejected by the authority` : ''}. Nothing is submitted on its own: a host submits them, and einvoicing_clearance (ekwo einvoice clearance) records the submission and the answer.`);
|
|
570
|
+
}
|
|
571
|
+
if (summary.awaiting_count > 0)
|
|
572
|
+
sentences.push(`${summary.awaiting_count} wait for the authority's answer.`);
|
|
573
|
+
if (summary.buyer_rejected_count > 0) {
|
|
574
|
+
sentences.push(`${summary.buyer_rejected_count} were rejected by the buyer after their validation: the issuer cancels them within the regime's window, or settles with the buyer.`);
|
|
575
|
+
}
|
|
576
|
+
if (summary.cancelled_still_posted_count > 0) {
|
|
577
|
+
sentences.push(`${summary.cancelled_still_posted_count} were cancelled at the authority and are still posted here: cancel_document undoes them in the books.`);
|
|
578
|
+
}
|
|
579
|
+
if (missing === 0 && summary.awaiting_count + summary.buyer_rejected_count + summary.cancelled_still_posted_count === 0) {
|
|
580
|
+
sentences.push('Every posted sale within the obligation is validated by the authority.');
|
|
581
|
+
}
|
|
582
|
+
const exemption = exemptionOn(regime, today());
|
|
583
|
+
if (exemption !== null || regime.mandatory_from.some((entry) => entry.turnover_above !== undefined)) {
|
|
584
|
+
sentences.push(`The obligation depends on the size of the taxpayer${exemption === null
|
|
585
|
+
? ''
|
|
586
|
+
: `, and a taxpayer below ${exemption.turnover_below.amount} ${exemption.turnover_below.currency} of annual turnover is exempted since ${exemption.valid_from}`}; the books do not decide which this company is, so the count starts on the earliest day, ${from ?? 'none declared'}.`);
|
|
587
|
+
}
|
|
588
|
+
return sentences.join(' ');
|
|
589
|
+
}
|
|
390
590
|
/** Why the status counts no sale as missing a record under a regime that is announced only, or that reports. */
|
|
391
591
|
function uncountedNote(regime) {
|
|
392
592
|
if (regime.announced_from !== null) {
|