@things-factory/invoice 10.1.223 → 10.1.227
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-client/document/document-amount.d.ts +7 -2
- package/dist-client/document/document-amount.js +11 -6
- package/dist-client/document/document-amount.js.map +1 -1
- package/dist-client/document/document-defaults.d.ts +24 -0
- package/dist-client/document/document-defaults.js +43 -0
- package/dist-client/document/document-defaults.js.map +1 -0
- package/dist-client/document/document-form.d.ts +24 -6
- package/dist-client/document/document-form.js +126 -15
- package/dist-client/document/document-form.js.map +1 -1
- package/dist-client/document/document-lines.d.ts +35 -1
- package/dist-client/document/document-lines.js +58 -9
- package/dist-client/document/document-lines.js.map +1 -1
- package/dist-client/document/document-party.d.ts +37 -0
- package/dist-client/document/document-party.js +217 -0
- package/dist-client/document/document-party.js.map +1 -0
- package/dist-client/document/document-preview.d.ts +37 -0
- package/dist-client/document/document-preview.js +181 -0
- package/dist-client/document/document-preview.js.map +1 -0
- package/dist-client/document/document-print.d.ts +3 -13
- package/dist-client/document/document-print.js +3 -45
- package/dist-client/document/document-print.js.map +1 -1
- package/dist-client/index.d.ts +9 -3
- package/dist-client/index.js +9 -3
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/invoice-editor.d.ts +37 -12
- package/dist-client/pages/invoice-editor.js +171 -79
- package/dist-client/pages/invoice-editor.js.map +1 -1
- package/dist-client/pages/invoice-list.d.ts +21 -1
- package/dist-client/pages/invoice-list.js +145 -14
- package/dist-client/pages/invoice-list.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/document-defaults-setting-let.d.ts +19 -0
- package/dist-client/viewparts/document-defaults-setting-let.js +139 -0
- package/dist-client/viewparts/document-defaults-setting-let.js.map +1 -0
- package/dist-server/role-tasks.js +5 -2
- package/dist-server/role-tasks.js.map +1 -1
- package/dist-server/service/document-defaults/document-defaults-resolver.d.ts +28 -0
- package/dist-server/service/document-defaults/document-defaults-resolver.js +136 -0
- package/dist-server/service/document-defaults/document-defaults-resolver.js.map +1 -0
- package/dist-server/service/document-defaults/document-defaults-rules.d.ts +25 -0
- package/dist-server/service/document-defaults/document-defaults-rules.js +50 -0
- package/dist-server/service/document-defaults/document-defaults-rules.js.map +1 -0
- package/dist-server/service/document-defaults/index.d.ts +6 -0
- package/dist-server/service/document-defaults/index.js +11 -0
- package/dist-server/service/document-defaults/index.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -0
- package/dist-server/service/index.js +4 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/invoice/commercial-document.d.ts +2 -0
- package/dist-server/service/invoice/commercial-document.js +11 -1
- package/dist-server/service/invoice/commercial-document.js.map +1 -1
- package/dist-server/service/invoice/invoice-document-file.d.ts +20 -0
- package/dist-server/service/invoice/invoice-document-file.js +38 -0
- package/dist-server/service/invoice/invoice-document-file.js.map +1 -0
- package/dist-server/service/invoice/invoice-query.d.ts +3 -1
- package/dist-server/service/invoice/invoice-query.js +33 -0
- package/dist-server/service/invoice/invoice-query.js.map +1 -1
- package/dist-server/service/invoice/invoice-rules.d.ts +18 -11
- package/dist-server/service/invoice/invoice-rules.js +49 -96
- package/dist-server/service/invoice/invoice-rules.js.map +1 -1
- package/dist-server/service/invoice/invoice-service.d.ts +1 -0
- package/dist-server/service/invoice/invoice-service.js +5 -2
- package/dist-server/service/invoice/invoice-service.js.map +1 -1
- package/dist-server/service/invoice/invoice-type.d.ts +12 -0
- package/dist-server/service/invoice/invoice-type.js +37 -1
- package/dist-server/service/invoice/invoice-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-shared/document-amounts.d.ts +56 -0
- package/dist-shared/document-amounts.js +134 -0
- package/dist-shared/document-amounts.js.map +1 -0
- package/dist-shared/document-money.d.ts +12 -0
- package/dist-shared/document-money.js +38 -0
- package/dist-shared/document-money.js.map +1 -0
- package/dist-shared/document-print.d.ts +37 -0
- package/dist-shared/document-print.js +87 -0
- package/dist-shared/document-print.js.map +1 -0
- package/dist-shared/document-start.d.ts +15 -0
- package/dist-shared/document-start.js +51 -0
- package/dist-shared/document-start.js.map +1 -0
- package/dist-shared/invoice-standing.d.ts +25 -0
- package/dist-shared/invoice-standing.js +51 -0
- package/dist-shared/invoice-standing.js.map +1 -0
- package/package.json +9 -7
- package/shared/document-amounts.ts +209 -0
- package/shared/document-money.ts +33 -0
- package/shared/document-print.ts +131 -0
- package/shared/document-start.ts +46 -0
- package/shared/invoice-standing.ts +49 -0
- package/translations/en.json +32 -6
- package/translations/es.json +22 -1
- package/translations/ja.json +22 -1
- package/translations/ko.json +32 -6
- package/translations/ms.json +22 -1
- package/translations/zh.json +22 -1
- package/tsconfig.shared.tsbuildinfo +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type TaxKindCode = 'TAXABLE' | 'ZERO_RATED' | 'EXEMPT';
|
|
2
|
+
export declare const TAX_KINDS: readonly TaxKindCode[];
|
|
3
|
+
export interface DocumentLine {
|
|
4
|
+
description?: string | null;
|
|
5
|
+
quantity?: number | string | null;
|
|
6
|
+
unitPrice?: number | string | null;
|
|
7
|
+
discount?: number | string | null;
|
|
8
|
+
taxKind?: string | null;
|
|
9
|
+
taxRate?: number | string | null;
|
|
10
|
+
/** The tax the sender printed on the line - only on a received document. */
|
|
11
|
+
receivedTaxAmount?: number | string | null;
|
|
12
|
+
}
|
|
13
|
+
/** Why a line cannot be reckoned - the server's refusal codes, `invoice-<problem>`. */
|
|
14
|
+
export type LineProblem = 'item-invalid' | 'discount-exceeds' | 'amount-too-large' | 'received-tax-required';
|
|
15
|
+
export interface ReckonedLine {
|
|
16
|
+
/** The line's supply - quantity × unit price, less the discount - in the currency. Absent when the line has a problem. */
|
|
17
|
+
supplyAmount?: number;
|
|
18
|
+
/** The tax the sender printed, on a received document. */
|
|
19
|
+
receivedTaxAmount?: number | null;
|
|
20
|
+
problem?: LineProblem;
|
|
21
|
+
}
|
|
22
|
+
export interface TaxGroup {
|
|
23
|
+
taxKind: string;
|
|
24
|
+
taxRate: number;
|
|
25
|
+
supplyAmount: number;
|
|
26
|
+
taxAmount: number;
|
|
27
|
+
}
|
|
28
|
+
/** Why the discount of the whole document cannot be taken - the server's refusal code, `invoice-<problem>`. */
|
|
29
|
+
export type DiscountProblem = 'document-discount-invalid';
|
|
30
|
+
export interface DocumentAmounts {
|
|
31
|
+
lines: ReckonedLine[];
|
|
32
|
+
/** Each group's supply after its part of the document discount, and its tax. */
|
|
33
|
+
taxGroups: TaxGroup[];
|
|
34
|
+
/** The lines' supply before the document discount. */
|
|
35
|
+
lineSupplyAmount: number;
|
|
36
|
+
/** The discount of the whole document taken off the lines' supply - none when it has a problem. */
|
|
37
|
+
discountAmount: number;
|
|
38
|
+
/** A discount below zero, past the lines' supply or finer than the currency's minor unit; it is not taken. */
|
|
39
|
+
discountProblem?: DiscountProblem;
|
|
40
|
+
/** The lines' supply less the document discount - the supply tax is reckoned on. */
|
|
41
|
+
supplyAmount: number;
|
|
42
|
+
taxAmount: number;
|
|
43
|
+
totalAmount: number;
|
|
44
|
+
/** The totals pass the largest amount a document may carry. */
|
|
45
|
+
tooLarge: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** The rate a tax kind takes unless the person writes another: the standard VAT for a taxable line, none otherwise. */
|
|
48
|
+
export declare function defaultTaxRate(taxKind: string | null | undefined, standard?: number): number;
|
|
49
|
+
/**
|
|
50
|
+
* Each line's supply, the tax of each (kind, rate) group and the document totals, in the currency (whose digits must
|
|
51
|
+
* be known - `currencyDigits`). `received` keeps the line tax the sender printed; otherwise the groups reckon it. A
|
|
52
|
+
* blank discount is none; a blank quantity, price, kind or rate is a line not yet reckonable. `discountAmount` is the
|
|
53
|
+
* discount of the whole document; a blank one is none. On a received document it lowers the supply, and the tax stays
|
|
54
|
+
* as printed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function documentAmounts(lines: readonly DocumentLine[], currency: string, received?: boolean, discountAmount?: unknown): DocumentAmounts;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TAX_KINDS = void 0;
|
|
4
|
+
exports.defaultTaxRate = defaultTaxRate;
|
|
5
|
+
exports.documentAmounts = documentAmounts;
|
|
6
|
+
const currency_digits_js_1 = require("./currency-digits.js");
|
|
7
|
+
exports.TAX_KINDS = ['TAXABLE', 'ZERO_RATED', 'EXEMPT'];
|
|
8
|
+
/** The rate a tax kind takes unless the person writes another: the standard VAT for a taxable line, none otherwise. */
|
|
9
|
+
function defaultTaxRate(taxKind, standard = 10) {
|
|
10
|
+
return taxKind === 'ZERO_RATED' || taxKind === 'EXEMPT' ? 0 : standard;
|
|
11
|
+
}
|
|
12
|
+
const MAX_MINOR = 10n ** 14n;
|
|
13
|
+
const SCALE = 10000n;
|
|
14
|
+
const numberOf = (value) => (typeof value === 'number' ? value : value === null || value === undefined || value === '' ? NaN : Number(value));
|
|
15
|
+
/** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */
|
|
16
|
+
function scaled(value) {
|
|
17
|
+
const number = numberOf(value);
|
|
18
|
+
if (!Number.isFinite(number))
|
|
19
|
+
return undefined;
|
|
20
|
+
const whole = Math.round(number * 10_000);
|
|
21
|
+
return Math.abs(number * 10_000 - whole) > 1e-6 ? undefined : BigInt(whole);
|
|
22
|
+
}
|
|
23
|
+
function minorOf(value, digits) {
|
|
24
|
+
const number = numberOf(value);
|
|
25
|
+
if (!Number.isFinite(number))
|
|
26
|
+
return undefined;
|
|
27
|
+
const factor = 10 ** digits;
|
|
28
|
+
const whole = Math.round(number * factor);
|
|
29
|
+
return Math.abs(number * factor - whole) > 1e-6 ? undefined : BigInt(whole);
|
|
30
|
+
}
|
|
31
|
+
/** numerator ÷ denominator, rounded half-up; both non-negative. */
|
|
32
|
+
function divideHalfUp(numerator, denominator) {
|
|
33
|
+
return (numerator * 2n + denominator) / (denominator * 2n);
|
|
34
|
+
}
|
|
35
|
+
const amountOf = (minor, digits) => Number(minor) / 10 ** digits;
|
|
36
|
+
/**
|
|
37
|
+
* Each line's supply, the tax of each (kind, rate) group and the document totals, in the currency (whose digits must
|
|
38
|
+
* be known - `currencyDigits`). `received` keeps the line tax the sender printed; otherwise the groups reckon it. A
|
|
39
|
+
* blank discount is none; a blank quantity, price, kind or rate is a line not yet reckonable. `discountAmount` is the
|
|
40
|
+
* discount of the whole document; a blank one is none. On a received document it lowers the supply, and the tax stays
|
|
41
|
+
* as printed.
|
|
42
|
+
*/
|
|
43
|
+
function documentAmounts(lines, currency, received = false, discountAmount = 0) {
|
|
44
|
+
const digits = (0, currency_digits_js_1.currencyDigits)(currency) ?? 0;
|
|
45
|
+
const factor = 10n ** BigInt(digits);
|
|
46
|
+
const groups = new Map();
|
|
47
|
+
const reckoned = lines.map((line) => {
|
|
48
|
+
const quantity = scaled(line.quantity);
|
|
49
|
+
const unitPrice = scaled(line.unitPrice);
|
|
50
|
+
const rate = scaled(line.taxRate);
|
|
51
|
+
const discount = line.discount === null || line.discount === undefined || line.discount === '' ? 0n : minorOf(line.discount, digits);
|
|
52
|
+
const kind = String(line.taxKind ?? '');
|
|
53
|
+
if (!String(line.description ?? '').trim() ||
|
|
54
|
+
quantity === undefined ||
|
|
55
|
+
unitPrice === undefined ||
|
|
56
|
+
rate === undefined ||
|
|
57
|
+
discount === undefined ||
|
|
58
|
+
quantity <= 0n ||
|
|
59
|
+
unitPrice < 0n ||
|
|
60
|
+
discount < 0n ||
|
|
61
|
+
rate < 0n ||
|
|
62
|
+
rate > 100n * SCALE ||
|
|
63
|
+
!exports.TAX_KINDS.includes(kind) ||
|
|
64
|
+
(kind !== 'TAXABLE' && rate !== 0n)) {
|
|
65
|
+
return { problem: 'item-invalid' };
|
|
66
|
+
}
|
|
67
|
+
const gross = divideHalfUp(quantity * unitPrice * factor, SCALE * SCALE);
|
|
68
|
+
if (discount > gross)
|
|
69
|
+
return { problem: 'discount-exceeds' };
|
|
70
|
+
const supply = gross - discount;
|
|
71
|
+
if (supply > MAX_MINOR)
|
|
72
|
+
return { problem: 'amount-too-large' };
|
|
73
|
+
let printed;
|
|
74
|
+
if (received) {
|
|
75
|
+
printed = line.receivedTaxAmount === null || line.receivedTaxAmount === undefined || line.receivedTaxAmount === '' ? undefined : minorOf(line.receivedTaxAmount, digits);
|
|
76
|
+
if (printed === undefined || printed < 0n)
|
|
77
|
+
return { supplyAmount: amountOf(supply, digits), problem: 'received-tax-required' };
|
|
78
|
+
}
|
|
79
|
+
const key = `${kind}:${rate}`;
|
|
80
|
+
const group = groups.get(key) ?? { taxKind: kind, rate, supply: 0n, tax: 0n };
|
|
81
|
+
group.supply += supply;
|
|
82
|
+
if (printed !== undefined)
|
|
83
|
+
group.tax += printed;
|
|
84
|
+
groups.set(key, group);
|
|
85
|
+
return { supplyAmount: amountOf(supply, digits), receivedTaxAmount: printed === undefined ? null : amountOf(printed, digits) };
|
|
86
|
+
});
|
|
87
|
+
const kept = [...groups.values()];
|
|
88
|
+
const lineSupply = kept.reduce((sum, group) => sum + group.supply, 0n);
|
|
89
|
+
const asked = discountAmount === null || discountAmount === undefined || discountAmount === '' ? 0n : minorOf(discountAmount, digits);
|
|
90
|
+
const discountProblem = asked === undefined || asked < 0n || asked > lineSupply ? 'document-discount-invalid' : undefined;
|
|
91
|
+
const discount = discountProblem ? 0n : asked;
|
|
92
|
+
const parts = shares(discount, kept.map(group => group.supply));
|
|
93
|
+
let supply = 0n;
|
|
94
|
+
let tax = 0n;
|
|
95
|
+
const taxGroups = kept.map((group, index) => {
|
|
96
|
+
group.supply -= parts[index];
|
|
97
|
+
if (!received)
|
|
98
|
+
group.tax = divideHalfUp(group.supply * group.rate, 100n * SCALE);
|
|
99
|
+
supply += group.supply;
|
|
100
|
+
tax += group.tax;
|
|
101
|
+
return { taxKind: group.taxKind, taxRate: Number(group.rate) / 10_000, supplyAmount: amountOf(group.supply, digits), taxAmount: amountOf(group.tax, digits) };
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
lines: reckoned,
|
|
105
|
+
taxGroups,
|
|
106
|
+
lineSupplyAmount: amountOf(lineSupply, digits),
|
|
107
|
+
discountAmount: amountOf(discount, digits),
|
|
108
|
+
...(discountProblem ? { discountProblem } : {}),
|
|
109
|
+
supplyAmount: amountOf(supply, digits),
|
|
110
|
+
taxAmount: amountOf(tax, digits),
|
|
111
|
+
totalAmount: amountOf(supply + tax, digits),
|
|
112
|
+
tooLarge: lineSupply > MAX_MINOR || supply + tax > MAX_MINOR
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `total` split in proportion to `weights`, in whole units: each its share cut down, then the units left over one
|
|
117
|
+
* at a time to the largest cut-off remainders (the first of equal ones first), so the parts add up to `total`.
|
|
118
|
+
*/
|
|
119
|
+
function shares(total, weights) {
|
|
120
|
+
const whole = weights.reduce((sum, weight) => sum + weight, 0n);
|
|
121
|
+
if (!total || !whole)
|
|
122
|
+
return weights.map(() => 0n);
|
|
123
|
+
const parts = weights.map(weight => (total * weight) / whole);
|
|
124
|
+
let left = total - parts.reduce((sum, part) => sum + part, 0n);
|
|
125
|
+
const byRemainder = weights.map((weight, index) => ({ index, remainder: (total * weight) % whole })).sort((a, b) => (a.remainder === b.remainder ? a.index - b.index : a.remainder > b.remainder ? -1 : 1));
|
|
126
|
+
for (const { index } of byRemainder) {
|
|
127
|
+
if (left <= 0n)
|
|
128
|
+
break;
|
|
129
|
+
parts[index] += 1n;
|
|
130
|
+
left -= 1n;
|
|
131
|
+
}
|
|
132
|
+
return parts;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=document-amounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-amounts.js","sourceRoot":"","sources":["../shared/document-amounts.ts"],"names":[],"mappings":"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","sourcesContent":["import { currencyDigits } from './currency-digits.js'\n\n/*\n * A commercial document's arithmetic - the one place it is decided, read by the invoice and quotation services when a\n * document is saved and by their screens while it is typed, so a line's supply and the totals on screen are the\n * ones the document will keep (ADR-0120 decision 4).\n *\n * - Lines carry supply and a tax kind and rate; the tax is reckoned once per (kind, rate) group: the group's supply\n * total × rate ÷ 100, rounded half-up to the currency's minor unit. The document tax is the sum of the groups.\n * - A discount of the whole document comes off the lines' supply before tax (ADR-0170 decision 8): it is spread over\n * the groups in proportion to each group's supply, in minor units, the remainder going one unit at a time to the\n * groups whose share was cut most (largest remainder), so the parts add up to the discount exactly. Each group's\n * tax is reckoned on its supply after its part.\n * - Everything is counted in integers: quantities, prices and rates to four decimals, money in minor units.\n * - A received document keeps the tax its sender printed on each line; nothing recomputes it.\n *\n * No imports but the currency's digits and no throwing: a line that cannot be reckoned says why (`problem`), and is\n * left out of the totals. The server refuses such a document; a screen shows the rest while the line is typed.\n */\n\nexport type TaxKindCode = 'TAXABLE' | 'ZERO_RATED' | 'EXEMPT'\n\nexport const TAX_KINDS: readonly TaxKindCode[] = ['TAXABLE', 'ZERO_RATED', 'EXEMPT']\n\nexport interface DocumentLine {\n description?: string | null\n quantity?: number | string | null\n unitPrice?: number | string | null\n discount?: number | string | null\n taxKind?: string | null\n taxRate?: number | string | null\n /** The tax the sender printed on the line - only on a received document. */\n receivedTaxAmount?: number | string | null\n}\n\n/** Why a line cannot be reckoned - the server's refusal codes, `invoice-<problem>`. */\nexport type LineProblem = 'item-invalid' | 'discount-exceeds' | 'amount-too-large' | 'received-tax-required'\n\nexport interface ReckonedLine {\n /** The line's supply - quantity × unit price, less the discount - in the currency. Absent when the line has a problem. */\n supplyAmount?: number\n /** The tax the sender printed, on a received document. */\n receivedTaxAmount?: number | null\n problem?: LineProblem\n}\n\nexport interface TaxGroup {\n taxKind: string\n taxRate: number\n supplyAmount: number\n taxAmount: number\n}\n\n/** Why the discount of the whole document cannot be taken - the server's refusal code, `invoice-<problem>`. */\nexport type DiscountProblem = 'document-discount-invalid'\n\nexport interface DocumentAmounts {\n lines: ReckonedLine[]\n /** Each group's supply after its part of the document discount, and its tax. */\n taxGroups: TaxGroup[]\n /** The lines' supply before the document discount. */\n lineSupplyAmount: number\n /** The discount of the whole document taken off the lines' supply - none when it has a problem. */\n discountAmount: number\n /** A discount below zero, past the lines' supply or finer than the currency's minor unit; it is not taken. */\n discountProblem?: DiscountProblem\n /** The lines' supply less the document discount - the supply tax is reckoned on. */\n supplyAmount: number\n taxAmount: number\n totalAmount: number\n /** The totals pass the largest amount a document may carry. */\n tooLarge: boolean\n}\n\n/** The rate a tax kind takes unless the person writes another: the standard VAT for a taxable line, none otherwise. */\nexport function defaultTaxRate(taxKind: string | null | undefined, standard = 10): number {\n return taxKind === 'ZERO_RATED' || taxKind === 'EXEMPT' ? 0 : standard\n}\n\nconst MAX_MINOR = 10n ** 14n\nconst SCALE = 10_000n\n\nconst numberOf = (value: unknown): number => (typeof value === 'number' ? value : value === null || value === undefined || value === '' ? NaN : Number(value))\n\n/** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */\nfunction scaled(value: unknown): bigint | undefined {\n const number = numberOf(value)\n if (!Number.isFinite(number)) return undefined\n const whole = Math.round(number * 10_000)\n return Math.abs(number * 10_000 - whole) > 1e-6 ? undefined : BigInt(whole)\n}\n\nfunction minorOf(value: unknown, digits: number): bigint | undefined {\n const number = numberOf(value)\n if (!Number.isFinite(number)) return undefined\n const factor = 10 ** digits\n const whole = Math.round(number * factor)\n return Math.abs(number * factor - whole) > 1e-6 ? undefined : BigInt(whole)\n}\n\n/** numerator ÷ denominator, rounded half-up; both non-negative. */\nfunction divideHalfUp(numerator: bigint, denominator: bigint): bigint {\n return (numerator * 2n + denominator) / (denominator * 2n)\n}\n\nconst amountOf = (minor: bigint, digits: number) => Number(minor) / 10 ** digits\n\n/**\n * Each line's supply, the tax of each (kind, rate) group and the document totals, in the currency (whose digits must\n * be known - `currencyDigits`). `received` keeps the line tax the sender printed; otherwise the groups reckon it. A\n * blank discount is none; a blank quantity, price, kind or rate is a line not yet reckonable. `discountAmount` is the\n * discount of the whole document; a blank one is none. On a received document it lowers the supply, and the tax stays\n * as printed.\n */\nexport function documentAmounts(lines: readonly DocumentLine[], currency: string, received = false, discountAmount: unknown = 0): DocumentAmounts {\n const digits = currencyDigits(currency) ?? 0\n const factor = 10n ** BigInt(digits)\n const groups = new Map<string, { taxKind: string; rate: bigint; supply: bigint; tax: bigint }>()\n\n const reckoned = lines.map((line): ReckonedLine => {\n const quantity = scaled(line.quantity)\n const unitPrice = scaled(line.unitPrice)\n const rate = scaled(line.taxRate)\n const discount = line.discount === null || line.discount === undefined || line.discount === '' ? 0n : minorOf(line.discount, digits)\n const kind = String(line.taxKind ?? '')\n if (\n !String(line.description ?? '').trim() ||\n quantity === undefined ||\n unitPrice === undefined ||\n rate === undefined ||\n discount === undefined ||\n quantity <= 0n ||\n unitPrice < 0n ||\n discount < 0n ||\n rate < 0n ||\n rate > 100n * SCALE ||\n !TAX_KINDS.includes(kind as TaxKindCode) ||\n (kind !== 'TAXABLE' && rate !== 0n)\n ) {\n return { problem: 'item-invalid' }\n }\n\n const gross = divideHalfUp(quantity * unitPrice * factor, SCALE * SCALE)\n if (discount > gross) return { problem: 'discount-exceeds' }\n const supply = gross - discount\n if (supply > MAX_MINOR) return { problem: 'amount-too-large' }\n\n let printed: bigint | undefined\n if (received) {\n printed = line.receivedTaxAmount === null || line.receivedTaxAmount === undefined || line.receivedTaxAmount === '' ? undefined : minorOf(line.receivedTaxAmount, digits)\n if (printed === undefined || printed < 0n) return { supplyAmount: amountOf(supply, digits), problem: 'received-tax-required' }\n }\n\n const key = `${kind}:${rate}`\n const group = groups.get(key) ?? { taxKind: kind, rate, supply: 0n, tax: 0n }\n group.supply += supply\n if (printed !== undefined) group.tax += printed\n groups.set(key, group)\n\n return { supplyAmount: amountOf(supply, digits), receivedTaxAmount: printed === undefined ? null : amountOf(printed, digits) }\n })\n\n const kept = [...groups.values()]\n const lineSupply = kept.reduce((sum, group) => sum + group.supply, 0n)\n const asked = discountAmount === null || discountAmount === undefined || discountAmount === '' ? 0n : minorOf(discountAmount, digits)\n const discountProblem: DiscountProblem | undefined = asked === undefined || asked < 0n || asked > lineSupply ? 'document-discount-invalid' : undefined\n const discount = discountProblem ? 0n : asked!\n const parts = shares(discount, kept.map(group => group.supply))\n\n let supply = 0n\n let tax = 0n\n const taxGroups = kept.map((group, index) => {\n group.supply -= parts[index]\n if (!received) group.tax = divideHalfUp(group.supply * group.rate, 100n * SCALE)\n supply += group.supply\n tax += group.tax\n return { taxKind: group.taxKind, taxRate: Number(group.rate) / 10_000, supplyAmount: amountOf(group.supply, digits), taxAmount: amountOf(group.tax, digits) }\n })\n\n return {\n lines: reckoned,\n taxGroups,\n lineSupplyAmount: amountOf(lineSupply, digits),\n discountAmount: amountOf(discount, digits),\n ...(discountProblem ? { discountProblem } : {}),\n supplyAmount: amountOf(supply, digits),\n taxAmount: amountOf(tax, digits),\n totalAmount: amountOf(supply + tax, digits),\n tooLarge: lineSupply > MAX_MINOR || supply + tax > MAX_MINOR\n }\n}\n\n/**\n * `total` split in proportion to `weights`, in whole units: each its share cut down, then the units left over one\n * at a time to the largest cut-off remainders (the first of equal ones first), so the parts add up to `total`.\n */\nfunction shares(total: bigint, weights: readonly bigint[]): bigint[] {\n const whole = weights.reduce((sum, weight) => sum + weight, 0n)\n if (!total || !whole) return weights.map(() => 0n)\n const parts = weights.map(weight => (total * weight) / whole)\n let left = total - parts.reduce((sum, part) => sum + part, 0n)\n const byRemainder = weights.map((weight, index) => ({ index, remainder: (total * weight) % whole })).sort((a, b) => (a.remainder === b.remainder ? a.index - b.index : a.remainder > b.remainder ? -1 : 1))\n for (const { index } of byRemainder) {\n if (left <= 0n) break\n parts[index] += 1n\n left -= 1n\n }\n return parts\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An amount in its currency as the language writes money (2026-10-28, the user: 「한글인 경우에는 KRW보다는 '원'이 더 좋을
|
|
3
|
+
* 것 같다」): won in Korean reads 「5,000,000원」; every other pair takes the language's own currency form - 「₩5,000,000」
|
|
4
|
+
* and 「$12,000.00」 in English, 「US$12,000.00」 in Korean - with exactly the currency's decimal places. A code the
|
|
5
|
+
* platform does not know is written plainly.
|
|
6
|
+
*/
|
|
7
|
+
export declare function documentAmount(value: number | string | null | undefined, currency: string, lng: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* A number without the currency - a price or a discount in a table of lines: as typed (up to four decimals), or with
|
|
10
|
+
* exactly the currency's decimal places when `exact` (a printed amount).
|
|
11
|
+
*/
|
|
12
|
+
export declare function documentNumber(value: number | string | null | undefined, currency: string, lng: string, exact?: boolean): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.documentAmount = documentAmount;
|
|
4
|
+
exports.documentNumber = documentNumber;
|
|
5
|
+
const currency_digits_js_1 = require("./currency-digits.js");
|
|
6
|
+
/*
|
|
7
|
+
* How a commercial document writes money - the one place it is decided, read by the screens (in the screen's
|
|
8
|
+
* language, client/document/document-amount.ts) and by the print the server makes (in the print's language,
|
|
9
|
+
* document-print.ts). No i18n: the language comes in as a parameter.
|
|
10
|
+
*/
|
|
11
|
+
const unset = (value) => value === null || value === undefined || value === '' || !Number.isFinite(Number(value));
|
|
12
|
+
/**
|
|
13
|
+
* An amount in its currency as the language writes money (2026-10-28, the user: 「한글인 경우에는 KRW보다는 '원'이 더 좋을
|
|
14
|
+
* 것 같다」): won in Korean reads 「5,000,000원」; every other pair takes the language's own currency form - 「₩5,000,000」
|
|
15
|
+
* and 「$12,000.00」 in English, 「US$12,000.00」 in Korean - with exactly the currency's decimal places. A code the
|
|
16
|
+
* platform does not know is written plainly.
|
|
17
|
+
*/
|
|
18
|
+
function documentAmount(value, currency, lng) {
|
|
19
|
+
if (unset(value))
|
|
20
|
+
return '';
|
|
21
|
+
const digits = (0, currency_digits_js_1.currencyDigits)(currency);
|
|
22
|
+
if (digits === undefined)
|
|
23
|
+
return `${currency} ${Number(value).toLocaleString(lng)}`;
|
|
24
|
+
if (lng.split('-')[0] === 'ko' && currency === 'KRW')
|
|
25
|
+
return `${Number(value).toLocaleString('ko', { minimumFractionDigits: digits, maximumFractionDigits: digits })}원`;
|
|
26
|
+
return new Intl.NumberFormat(lng, { style: 'currency', currency, minimumFractionDigits: digits, maximumFractionDigits: digits }).format(Number(value));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A number without the currency - a price or a discount in a table of lines: as typed (up to four decimals), or with
|
|
30
|
+
* exactly the currency's decimal places when `exact` (a printed amount).
|
|
31
|
+
*/
|
|
32
|
+
function documentNumber(value, currency, lng, exact = false) {
|
|
33
|
+
if (unset(value))
|
|
34
|
+
return '';
|
|
35
|
+
const digits = (0, currency_digits_js_1.currencyDigits)(currency) ?? 0;
|
|
36
|
+
return Number(value).toLocaleString(lng, exact ? { minimumFractionDigits: digits, maximumFractionDigits: digits } : { minimumFractionDigits: 0, maximumFractionDigits: Math.max(digits, 4) });
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=document-money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-money.js","sourceRoot":"","sources":["../shared/document-money.ts"],"names":[],"mappings":";;AAgBA,wCAMC;AAMD,wCAIC;AAhCD,6DAAqD;AAErD;;;;GAIG;AAEH,MAAM,KAAK,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;AAE1H;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,KAAyC,EAAE,QAAgB,EAAE,GAAW;IACrG,IAAI,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAC3B,MAAM,MAAM,GAAG,IAAA,mCAAc,EAAC,QAAQ,CAAC,CAAA;IACvC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,GAAG,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAA;IACnF,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,qBAAqB,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,CAAC,GAAG,CAAA;IACvK,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;AACxJ,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,KAAyC,EAAE,QAAgB,EAAE,GAAW,EAAE,KAAK,GAAG,KAAK;IACpH,IAAI,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAC3B,MAAM,MAAM,GAAG,IAAA,mCAAc,EAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5C,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,CAAC,EAAE,qBAAqB,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;AAC/L,CAAC","sourcesContent":["import { currencyDigits } from './currency-digits.js'\n\n/*\n * How a commercial document writes money - the one place it is decided, read by the screens (in the screen's\n * language, client/document/document-amount.ts) and by the print the server makes (in the print's language,\n * document-print.ts). No i18n: the language comes in as a parameter.\n */\n\nconst unset = (value: unknown) => value === null || value === undefined || value === '' || !Number.isFinite(Number(value))\n\n/**\n * An amount in its currency as the language writes money (2026-10-28, the user: 「한글인 경우에는 KRW보다는 '원'이 더 좋을\n * 것 같다」): won in Korean reads 「5,000,000원」; every other pair takes the language's own currency form - 「₩5,000,000」\n * and 「$12,000.00」 in English, 「US$12,000.00」 in Korean - with exactly the currency's decimal places. A code the\n * platform does not know is written plainly.\n */\nexport function documentAmount(value: number | string | null | undefined, currency: string, lng: string): string {\n if (unset(value)) return ''\n const digits = currencyDigits(currency)\n if (digits === undefined) return `${currency} ${Number(value).toLocaleString(lng)}`\n if (lng.split('-')[0] === 'ko' && currency === 'KRW') return `${Number(value).toLocaleString('ko', { minimumFractionDigits: digits, maximumFractionDigits: digits })}원`\n return new Intl.NumberFormat(lng, { style: 'currency', currency, minimumFractionDigits: digits, maximumFractionDigits: digits }).format(Number(value))\n}\n\n/**\n * A number without the currency - a price or a discount in a table of lines: as typed (up to four decimals), or with\n * exactly the currency's decimal places when `exact` (a printed amount).\n */\nexport function documentNumber(value: number | string | null | undefined, currency: string, lng: string, exact = false): string {\n if (unset(value)) return ''\n const digits = currencyDigits(currency) ?? 0\n return Number(value).toLocaleString(lng, exact ? { minimumFractionDigits: digits, maximumFractionDigits: digits } : { minimumFractionDigits: 0, maximumFractionDigits: Math.max(digits, 4) })\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type PrintedDocument = Record<string, any>;
|
|
2
|
+
/** The languages the print has words for - this package's and sales' print words in each (ADR-0170 decision 9). */
|
|
3
|
+
export declare const PRINT_LANGUAGES: readonly ["ko", "en"];
|
|
4
|
+
export type PrintLanguage = (typeof PRINT_LANGUAGES)[number];
|
|
5
|
+
/** The language as one the print has words for, or undefined - the caller refuses it or falls back. */
|
|
6
|
+
export declare function printLanguageOf(language: unknown): PrintLanguage | undefined;
|
|
7
|
+
export type PrintOptions = {
|
|
8
|
+
/** The language of the print - its words and how it writes money. */
|
|
9
|
+
language: PrintLanguage;
|
|
10
|
+
/** The words of the print, in `language`. */
|
|
11
|
+
t: (key: string) => string;
|
|
12
|
+
/** The kind's title - 「청구서」, 「견적서」. */
|
|
13
|
+
title: string;
|
|
14
|
+
/** The kind's own terms beside the number - the due date; the version and the last day of an offer. */
|
|
15
|
+
terms: {
|
|
16
|
+
label: string;
|
|
17
|
+
value?: string | null;
|
|
18
|
+
}[];
|
|
19
|
+
/** How the kind names its number. */
|
|
20
|
+
numberLabel?: string;
|
|
21
|
+
};
|
|
22
|
+
/** The look of the printed page - A4 with its margins. */
|
|
23
|
+
export declare const DOCUMENT_CSS = "\n @page{size:A4;margin:18mm}\n body{font:13px/1.5 system-ui,sans-serif;color:#000}\n h1{font-size:26px;margin:0}\n h2{font-size:13px;color:#555;margin:0 0 4px}\n header,.parties{display:flex;justify-content:space-between;gap:32px}\n .parties section{flex:1}\n table{width:100%;border-collapse:collapse;margin:24px 0}\n th,td{padding:8px;border-bottom:1px solid #999;text-align:right;font-variant-numeric:tabular-nums}\n th:first-child,td:first-child{text-align:left}\n .text{white-space:pre-wrap}\n .totals{width:50%;margin-left:auto}\n .grand td{font-weight:600;font-size:15px}\n tr{break-inside:avoid}\n";
|
|
24
|
+
/**
|
|
25
|
+
* On a screen, the page as a sheet of A4 on a grey desk with the print's margins - the preview reads as the PDF will.
|
|
26
|
+
* Printing and the PDF read the print media, where none of this applies.
|
|
27
|
+
*/
|
|
28
|
+
export declare const DOCUMENT_SCREEN_CSS = "\n@media screen {\n html{background:#e9edf1}\n body{margin:0;padding:24px 0}\n .sheet{box-sizing:border-box;width:210mm;min-height:297mm;margin:0 auto;padding:18mm;background:#fff;\n box-shadow:0 1px 3px rgba(23,36,44,.12),0 8px 24px rgba(23,36,44,.08)}\n}\n@media screen and (max-width:210mm) {\n .sheet{width:auto;min-height:0;margin:0 8px;padding:8mm}\n}\n";
|
|
29
|
+
/** The document's sheet - the head, the parties, the lines, the totals and the terms - without a page around it. */
|
|
30
|
+
export declare function documentBody(document: PrintedDocument, { language, t: word, title, terms, numberLabel }: PrintOptions): string;
|
|
31
|
+
/**
|
|
32
|
+
* The document as one HTML page - what the preview shows and the browser prints. Only escaped business data enters
|
|
33
|
+
* the page.
|
|
34
|
+
*/
|
|
35
|
+
export declare function documentHtml(document: PrintedDocument, options: PrintOptions): string;
|
|
36
|
+
/** 「견적서-Q2026-001.pdf」 - the kind's word and the number, without what a file name may not hold. */
|
|
37
|
+
export declare function documentFileName(word: string, number: string, extension: 'html' | 'pdf'): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOCUMENT_SCREEN_CSS = exports.DOCUMENT_CSS = exports.PRINT_LANGUAGES = void 0;
|
|
4
|
+
exports.printLanguageOf = printLanguageOf;
|
|
5
|
+
exports.documentBody = documentBody;
|
|
6
|
+
exports.documentHtml = documentHtml;
|
|
7
|
+
exports.documentFileName = documentFileName;
|
|
8
|
+
const document_money_js_1 = require("./document-money.js");
|
|
9
|
+
/** The languages the print has words for - this package's and sales' print words in each (ADR-0170 decision 9). */
|
|
10
|
+
exports.PRINT_LANGUAGES = ['ko', 'en'];
|
|
11
|
+
/** The language as one the print has words for, or undefined - the caller refuses it or falls back. */
|
|
12
|
+
function printLanguageOf(language) {
|
|
13
|
+
return exports.PRINT_LANGUAGES.includes(language) ? language : undefined;
|
|
14
|
+
}
|
|
15
|
+
const escape = (text) => String(text ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
16
|
+
/** The look of the printed page - A4 with its margins. */
|
|
17
|
+
exports.DOCUMENT_CSS = `
|
|
18
|
+
@page{size:A4;margin:18mm}
|
|
19
|
+
body{font:13px/1.5 system-ui,sans-serif;color:#000}
|
|
20
|
+
h1{font-size:26px;margin:0}
|
|
21
|
+
h2{font-size:13px;color:#555;margin:0 0 4px}
|
|
22
|
+
header,.parties{display:flex;justify-content:space-between;gap:32px}
|
|
23
|
+
.parties section{flex:1}
|
|
24
|
+
table{width:100%;border-collapse:collapse;margin:24px 0}
|
|
25
|
+
th,td{padding:8px;border-bottom:1px solid #999;text-align:right;font-variant-numeric:tabular-nums}
|
|
26
|
+
th:first-child,td:first-child{text-align:left}
|
|
27
|
+
.text{white-space:pre-wrap}
|
|
28
|
+
.totals{width:50%;margin-left:auto}
|
|
29
|
+
.grand td{font-weight:600;font-size:15px}
|
|
30
|
+
tr{break-inside:avoid}
|
|
31
|
+
`;
|
|
32
|
+
/**
|
|
33
|
+
* On a screen, the page as a sheet of A4 on a grey desk with the print's margins - the preview reads as the PDF will.
|
|
34
|
+
* Printing and the PDF read the print media, where none of this applies.
|
|
35
|
+
*/
|
|
36
|
+
exports.DOCUMENT_SCREEN_CSS = `
|
|
37
|
+
@media screen {
|
|
38
|
+
html{background:#e9edf1}
|
|
39
|
+
body{margin:0;padding:24px 0}
|
|
40
|
+
.sheet{box-sizing:border-box;width:210mm;min-height:297mm;margin:0 auto;padding:18mm;background:#fff;
|
|
41
|
+
box-shadow:0 1px 3px rgba(23,36,44,.12),0 8px 24px rgba(23,36,44,.08)}
|
|
42
|
+
}
|
|
43
|
+
@media screen and (max-width:210mm) {
|
|
44
|
+
.sheet{width:auto;min-height:0;margin:0 8px;padding:8mm}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
/** The document's sheet - the head, the parties, the lines, the totals and the terms - without a page around it. */
|
|
48
|
+
function documentBody(document, { language, t: word, title, terms, numberLabel = 'field.invoice-number' }) {
|
|
49
|
+
const t = (key) => escape(word(key));
|
|
50
|
+
/* A table's cells carry numbers; the total carries its currency, as the print's language writes money. */
|
|
51
|
+
const amount = (value) => escape((0, document_money_js_1.documentNumber)(value ?? 0, document.currency, language, true));
|
|
52
|
+
const money = (value) => escape((0, document_money_js_1.documentAmount)(value ?? 0, document.currency, language));
|
|
53
|
+
const kind = (taxKind) => taxKind === 'ZERO_RATED' ? t('text.invoice-tax-kind-zero-rated') : taxKind === 'EXEMPT' ? t('text.invoice-tax-kind-exempt') : t('text.invoice-tax-kind-taxable');
|
|
54
|
+
const party = (label, name, address, number) => `<section><h2>${label}</h2><strong>${escape(name)}</strong><p class="text">${escape(address)}</p><p>${escape(number)}</p></section>`;
|
|
55
|
+
return `<div class="sheet">
|
|
56
|
+
<header><h1>${escape(title)}</h1><div><p>${t(numberLabel)}: ${escape(document.number)}</p><p>${t('field.invoice-issue-date')}: ${escape(document.issueDate)}</p>${terms
|
|
57
|
+
.map(one => `<p>${escape(one.label)}: ${escape(one.value)}</p>`)
|
|
58
|
+
.join('')}</div></header>
|
|
59
|
+
<div class="parties">${party(t('field.invoice-supplier-name'), document.supplierName, document.supplierAddress, document.supplierRegistrationNumber)}${party(t('field.invoice-buyer-name'), document.buyerName, document.buyerAddress, document.buyerRegistrationNumber)}</div>
|
|
60
|
+
${document.purchaseOrder ? `<p>${t('field.invoice-purchase-order')}: ${escape(document.purchaseOrder)}</p>` : ''}
|
|
61
|
+
<table><thead><tr><th>${t('field.invoice-line-description')}</th><th>${t('field.invoice-quantity')}</th><th>${t('field.invoice-unit')}</th><th>${t('field.invoice-unit-price')}</th><th>${t('field.invoice-discount')}</th><th>${t('field.invoice-tax-kind')}</th><th>${t('field.invoice-supply-amount')}</th></tr></thead><tbody>${(document.items ?? [])
|
|
62
|
+
.map((item) => `<tr><td>${escape(item.description)}</td><td>${escape(item.quantity)}</td><td>${escape(item.unit)}</td><td>${amount(item.unitPrice)}</td><td>${amount(item.discount)}</td><td>${kind(item.taxKind)} ${escape(item.taxRate)}%</td><td>${amount(item.supplyAmount)}</td></tr>`)
|
|
63
|
+
.join('')}</tbody></table>
|
|
64
|
+
<table class="totals"><tbody>${Number(document.discountAmount) > 0
|
|
65
|
+
? `<tr><td>${t('field.invoice-line-sum')}</td><td>${amount(document.lineSupplyAmount)}</td><td></td></tr><tr><td>${t('field.invoice-discount')}</td><td>− ${amount(document.discountAmount)}</td><td></td></tr>`
|
|
66
|
+
: ''}${(document.taxGroups ?? [])
|
|
67
|
+
.map((group) => `<tr><td>${kind(group.taxKind)} ${escape(group.taxRate)}%</td><td>${amount(group.supplyAmount)}</td><td>${amount(group.taxAmount)}</td></tr>`)
|
|
68
|
+
.join('')}<tr><td>${t('field.invoice-sum')}</td><td>${amount(document.supplyAmount)}</td><td>${amount(document.taxAmount)}</td></tr><tr class="grand"><td colspan="2">${t('field.invoice-total-amount')}</td><td>${money(document.totalAmount)}</td></tr></tbody></table>
|
|
69
|
+
${['paymentTerms', 'bankDetails', 'notes']
|
|
70
|
+
.filter(key => document[key])
|
|
71
|
+
.map(key => `<p class="text">${escape(document[key])}</p>`)
|
|
72
|
+
.join('')}
|
|
73
|
+
</div>`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The document as one HTML page - what the preview shows and the browser prints. Only escaped business data enters
|
|
77
|
+
* the page.
|
|
78
|
+
*/
|
|
79
|
+
function documentHtml(document, options) {
|
|
80
|
+
return `<!doctype html><html lang="${options.language}"><head><meta charset="utf-8"><title>${escape(document.number)}</title><style>${exports.DOCUMENT_CSS}${exports.DOCUMENT_SCREEN_CSS}</style></head><body>${documentBody(document, options)}</body></html>`;
|
|
81
|
+
}
|
|
82
|
+
/** 「견적서-Q2026-001.pdf」 - the kind's word and the number, without what a file name may not hold. */
|
|
83
|
+
function documentFileName(word, number, extension) {
|
|
84
|
+
const safe = (text) => String(text ?? '').trim().replace(/[\\/:*?"<>|\s]+/g, '-');
|
|
85
|
+
return `${[safe(word), safe(number)].filter(Boolean).join('-')}.${extension}`;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=document-print.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-print.js","sourceRoot":"","sources":["../shared/document-print.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentAmount, documentNumber } from './document-money.js'\n\n/*\n * A commercial document as the buyer reads it - the one layout every print of an invoice or a quotation is, made by\n * the server from the stored document (ADR-0170 decision 10): the preview's page, the PDF to download and what the\n * browser prints are this page. Pure - no i18next: the words come in as a translator of the print's language.\n */\n\nexport type PrintedDocument = Record<string, any>\n\n/** The languages the print has words for - this package's and sales' print words in each (ADR-0170 decision 9). */\nexport const PRINT_LANGUAGES = ['ko', 'en'] as const\nexport type PrintLanguage = (typeof PRINT_LANGUAGES)[number]\n\n/** The language as one the print has words for, or undefined - the caller refuses it or falls back. */\nexport function printLanguageOf(language: unknown): PrintLanguage | undefined {\n return (PRINT_LANGUAGES as readonly unknown[]).includes(language) ? (language as PrintLanguage) : undefined\n}\n\nexport type PrintOptions = {\n /** The language of the print - its words and how it writes money. */\n language: PrintLanguage\n /** The words of the print, in `language`. */\n t: (key: string) => string\n /** The kind's title - 「청구서」, 「견적서」. */\n title: string\n /** The kind's own terms beside the number - the due date; the version and the last day of an offer. */\n terms: { label: string; value?: string | null }[]\n /** How the kind names its number. */\n numberLabel?: string\n}\n\nconst escape = (text: unknown) => String(text ?? '').replace(/[&<>\"']/g, c => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[c]!)\n\n/** The look of the printed page - A4 with its margins. */\nexport const DOCUMENT_CSS = `\n @page{size:A4;margin:18mm}\n body{font:13px/1.5 system-ui,sans-serif;color:#000}\n h1{font-size:26px;margin:0}\n h2{font-size:13px;color:#555;margin:0 0 4px}\n header,.parties{display:flex;justify-content:space-between;gap:32px}\n .parties section{flex:1}\n table{width:100%;border-collapse:collapse;margin:24px 0}\n th,td{padding:8px;border-bottom:1px solid #999;text-align:right;font-variant-numeric:tabular-nums}\n th:first-child,td:first-child{text-align:left}\n .text{white-space:pre-wrap}\n .totals{width:50%;margin-left:auto}\n .grand td{font-weight:600;font-size:15px}\n tr{break-inside:avoid}\n`\n\n/**\n * On a screen, the page as a sheet of A4 on a grey desk with the print's margins - the preview reads as the PDF will.\n * Printing and the PDF read the print media, where none of this applies.\n */\nexport const DOCUMENT_SCREEN_CSS = `\n@media screen {\n html{background:#e9edf1}\n body{margin:0;padding:24px 0}\n .sheet{box-sizing:border-box;width:210mm;min-height:297mm;margin:0 auto;padding:18mm;background:#fff;\n box-shadow:0 1px 3px rgba(23,36,44,.12),0 8px 24px rgba(23,36,44,.08)}\n}\n@media screen and (max-width:210mm) {\n .sheet{width:auto;min-height:0;margin:0 8px;padding:8mm}\n}\n`\n\n/** The document's sheet - the head, the parties, the lines, the totals and the terms - without a page around it. */\nexport function documentBody(document: PrintedDocument, { language, t: word, title, terms, numberLabel = 'field.invoice-number' }: PrintOptions): string {\n const t = (key: string) => escape(word(key))\n /* A table's cells carry numbers; the total carries its currency, as the print's language writes money. */\n const amount = (value: number) => escape(documentNumber(value ?? 0, document.currency, language, true))\n const money = (value: number) => escape(documentAmount(value ?? 0, document.currency, language))\n const kind = (taxKind: string) =>\n taxKind === 'ZERO_RATED' ? t('text.invoice-tax-kind-zero-rated') : taxKind === 'EXEMPT' ? t('text.invoice-tax-kind-exempt') : t('text.invoice-tax-kind-taxable')\n const party = (label: string, name: string, address: string, number: string) =>\n `<section><h2>${label}</h2><strong>${escape(name)}</strong><p class=\"text\">${escape(address)}</p><p>${escape(number)}</p></section>`\n\n return `<div class=\"sheet\">\n <header><h1>${escape(title)}</h1><div><p>${t(numberLabel)}: ${escape(document.number)}</p><p>${t('field.invoice-issue-date')}: ${escape(document.issueDate)}</p>${terms\n .map(one => `<p>${escape(one.label)}: ${escape(one.value)}</p>`)\n .join('')}</div></header>\n <div class=\"parties\">${party(t('field.invoice-supplier-name'), document.supplierName, document.supplierAddress, document.supplierRegistrationNumber)}${party(\n t('field.invoice-buyer-name'),\n document.buyerName,\n document.buyerAddress,\n document.buyerRegistrationNumber\n )}</div>\n ${document.purchaseOrder ? `<p>${t('field.invoice-purchase-order')}: ${escape(document.purchaseOrder)}</p>` : ''}\n <table><thead><tr><th>${t('field.invoice-line-description')}</th><th>${t('field.invoice-quantity')}</th><th>${t('field.invoice-unit')}</th><th>${t('field.invoice-unit-price')}</th><th>${t(\n 'field.invoice-discount'\n )}</th><th>${t('field.invoice-tax-kind')}</th><th>${t('field.invoice-supply-amount')}</th></tr></thead><tbody>${(document.items ?? [])\n .map(\n (item: any) =>\n `<tr><td>${escape(item.description)}</td><td>${escape(item.quantity)}</td><td>${escape(item.unit)}</td><td>${amount(item.unitPrice)}</td><td>${amount(item.discount)}</td><td>${kind(item.taxKind)} ${escape(\n item.taxRate\n )}%</td><td>${amount(item.supplyAmount)}</td></tr>`\n )\n .join('')}</tbody></table>\n <table class=\"totals\"><tbody>${\n Number(document.discountAmount) > 0\n ? `<tr><td>${t('field.invoice-line-sum')}</td><td>${amount(document.lineSupplyAmount)}</td><td></td></tr><tr><td>${t('field.invoice-discount')}</td><td>− ${amount(document.discountAmount)}</td><td></td></tr>`\n : ''\n }${(document.taxGroups ?? [])\n .map((group: any) => `<tr><td>${kind(group.taxKind)} ${escape(group.taxRate)}%</td><td>${amount(group.supplyAmount)}</td><td>${amount(group.taxAmount)}</td></tr>`)\n .join('')}<tr><td>${t('field.invoice-sum')}</td><td>${amount(document.supplyAmount)}</td><td>${amount(document.taxAmount)}</td></tr><tr class=\"grand\"><td colspan=\"2\">${t(\n 'field.invoice-total-amount'\n )}</td><td>${money(document.totalAmount)}</td></tr></tbody></table>\n ${['paymentTerms', 'bankDetails', 'notes']\n .filter(key => document[key])\n .map(key => `<p class=\"text\">${escape(document[key])}</p>`)\n .join('')}\n </div>`\n}\n\n/**\n * The document as one HTML page - what the preview shows and the browser prints. Only escaped business data enters\n * the page.\n */\nexport function documentHtml(document: PrintedDocument, options: PrintOptions): string {\n return `<!doctype html><html lang=\"${options.language}\"><head><meta charset=\"utf-8\"><title>${escape(document.number)}</title><style>${DOCUMENT_CSS}${DOCUMENT_SCREEN_CSS}</style></head><body>${documentBody(\n document,\n options\n )}</body></html>`\n}\n\n/** 「견적서-Q2026-001.pdf」 - the kind's word and the number, without what a file name may not hold. */\nexport function documentFileName(word: string, number: string, extension: 'html' | 'pdf'): string {\n const safe = (text: string) => String(text ?? '').trim().replace(/[\\\\/:*?\"<>|\\s]+/g, '-')\n return `${[safe(word), safe(number)].filter(Boolean).join('-')}.${extension}`\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The day `days` after `day` (YYYY-MM-DD), counted in calendar days. */
|
|
2
|
+
export declare function daysAfter(day: string, days: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* The number after `last`, the domain's most recent invoice number: its trailing digits one up, keeping their width
|
|
5
|
+
* and what comes before them - 「INV-2026-007」 → 「INV-2026-008」, 「A99」 → 「A100」. A year of four digits standing
|
|
6
|
+
* apart in it that is not `year` becomes `year` and the count starts again at one - 「INV-2025-017」 →
|
|
7
|
+
* 「INV-2026-001」. With nothing to follow - no number, or none ending in digits - 「INV-<year>-001」.
|
|
8
|
+
*/
|
|
9
|
+
export declare function nextInvoiceNumber(last: string | null | undefined, year: number): string;
|
|
10
|
+
/** Two names as the same name - case and spaces aside: 「(주) 하티오랩」 is 「(주)하티오랩」. */
|
|
11
|
+
export declare function sameName(a: unknown, b: unknown): boolean;
|
|
12
|
+
/** The one choice named `name`; none when no choice or more than one is - a guess would link the wrong party. */
|
|
13
|
+
export declare function partyNamed<T extends {
|
|
14
|
+
name?: string | null;
|
|
15
|
+
}>(name: unknown, choices: readonly T[]): T | undefined;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* What a new commercial document starts with, so a person writes only what is new (ADR-0170 decisions 5 · 11) - the
|
|
4
|
+
* next number, the day a term ends, the counterparty its party's name is. Pure: the screens ask the server for what
|
|
5
|
+
* these read and test them without a browser.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.daysAfter = daysAfter;
|
|
9
|
+
exports.nextInvoiceNumber = nextInvoiceNumber;
|
|
10
|
+
exports.sameName = sameName;
|
|
11
|
+
exports.partyNamed = partyNamed;
|
|
12
|
+
/** The day `days` after `day` (YYYY-MM-DD), counted in calendar days. */
|
|
13
|
+
function daysAfter(day, days) {
|
|
14
|
+
const at = new Date(`${day}T00:00:00Z`);
|
|
15
|
+
if (Number.isNaN(at.getTime()))
|
|
16
|
+
return '';
|
|
17
|
+
at.setUTCDate(at.getUTCDate() + days);
|
|
18
|
+
return at.toISOString().slice(0, 10);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The number after `last`, the domain's most recent invoice number: its trailing digits one up, keeping their width
|
|
22
|
+
* and what comes before them - 「INV-2026-007」 → 「INV-2026-008」, 「A99」 → 「A100」. A year of four digits standing
|
|
23
|
+
* apart in it that is not `year` becomes `year` and the count starts again at one - 「INV-2025-017」 →
|
|
24
|
+
* 「INV-2026-001」. With nothing to follow - no number, or none ending in digits - 「INV-<year>-001」.
|
|
25
|
+
*/
|
|
26
|
+
function nextInvoiceNumber(last, year) {
|
|
27
|
+
const match = /^(.*?)(\d+)$/.exec(String(last ?? '').trim());
|
|
28
|
+
if (!match)
|
|
29
|
+
return `INV-${year}-001`;
|
|
30
|
+
const [, prefix, count] = match;
|
|
31
|
+
const pad = (n) => String(n).padStart(count.length, '0');
|
|
32
|
+
/* A year apart from other digits - not the count itself, not part of a longer number. */
|
|
33
|
+
const years = [...prefix.matchAll(/(?<!\d)((?:19|20)\d{2})(?!\d)/g)];
|
|
34
|
+
const stale = years.find(one => Number(one[1]) !== year);
|
|
35
|
+
if (stale && !years.some(one => Number(one[1]) === year)) {
|
|
36
|
+
const at = stale.index;
|
|
37
|
+
return `${prefix.slice(0, at)}${year}${prefix.slice(at + 4)}${pad(1)}`;
|
|
38
|
+
}
|
|
39
|
+
return `${prefix}${pad(Number(count) + 1)}`;
|
|
40
|
+
}
|
|
41
|
+
/** Two names as the same name - case and spaces aside: 「(주) 하티오랩」 is 「(주)하티오랩」. */
|
|
42
|
+
function sameName(a, b) {
|
|
43
|
+
const plain = (text) => String(text ?? '').replace(/\s+/g, '').toLowerCase();
|
|
44
|
+
return !!plain(a) && plain(a) === plain(b);
|
|
45
|
+
}
|
|
46
|
+
/** The one choice named `name`; none when no choice or more than one is - a guess would link the wrong party. */
|
|
47
|
+
function partyNamed(name, choices) {
|
|
48
|
+
const named = choices.filter(one => sameName(one.name, name));
|
|
49
|
+
return named.length === 1 ? named[0] : undefined;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=document-start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-start.js","sourceRoot":"","sources":["../shared/document-start.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAGH,8BAKC;AAQD,8CAaC;AAGD,4BAGC;AAGD,gCAGC;AAvCD,yEAAyE;AACzE,SAAgB,SAAS,CAAC,GAAW,EAAE,IAAY;IACjD,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,GAAG,YAAY,CAAC,CAAA;IACvC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,CAAA;IACzC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAA;IACrC,OAAO,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACtC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,IAA+B,EAAE,IAAY;IAC7E,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IAC5D,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,IAAI,MAAM,CAAA;IACpC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,KAAK,CAAA;IAC/B,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAChE,yFAAyF;IACzF,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,gCAAgC,CAAC,CAAC,CAAA;IACpE,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAA;IACxD,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACzD,MAAM,EAAE,GAAG,KAAK,CAAC,KAAM,CAAA;QACvB,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IACxE,CAAC;IACD,OAAO,GAAG,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAA;AAC7C,CAAC;AAED,mFAAmF;AACnF,SAAgB,QAAQ,CAAC,CAAU,EAAE,CAAU;IAC7C,MAAM,KAAK,GAAG,CAAC,IAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IACrF,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED,iHAAiH;AACjH,SAAgB,UAAU,CAAqC,IAAa,EAAE,OAAqB;IACjG,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;IAC7D,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAClD,CAAC","sourcesContent":["/*\n * What a new commercial document starts with, so a person writes only what is new (ADR-0170 decisions 5 · 11) - the\n * next number, the day a term ends, the counterparty its party's name is. Pure: the screens ask the server for what\n * these read and test them without a browser.\n */\n\n/** The day `days` after `day` (YYYY-MM-DD), counted in calendar days. */\nexport function daysAfter(day: string, days: number): string {\n const at = new Date(`${day}T00:00:00Z`)\n if (Number.isNaN(at.getTime())) return ''\n at.setUTCDate(at.getUTCDate() + days)\n return at.toISOString().slice(0, 10)\n}\n\n/**\n * The number after `last`, the domain's most recent invoice number: its trailing digits one up, keeping their width\n * and what comes before them - 「INV-2026-007」 → 「INV-2026-008」, 「A99」 → 「A100」. A year of four digits standing\n * apart in it that is not `year` becomes `year` and the count starts again at one - 「INV-2025-017」 →\n * 「INV-2026-001」. With nothing to follow - no number, or none ending in digits - 「INV-<year>-001」.\n */\nexport function nextInvoiceNumber(last: string | null | undefined, year: number): string {\n const match = /^(.*?)(\\d+)$/.exec(String(last ?? '').trim())\n if (!match) return `INV-${year}-001`\n const [, prefix, count] = match\n const pad = (n: number) => String(n).padStart(count.length, '0')\n /* A year apart from other digits - not the count itself, not part of a longer number. */\n const years = [...prefix.matchAll(/(?<!\\d)((?:19|20)\\d{2})(?!\\d)/g)]\n const stale = years.find(one => Number(one[1]) !== year)\n if (stale && !years.some(one => Number(one[1]) === year)) {\n const at = stale.index!\n return `${prefix.slice(0, at)}${year}${prefix.slice(at + 4)}${pad(1)}`\n }\n return `${prefix}${pad(Number(count) + 1)}`\n}\n\n/** Two names as the same name - case and spaces aside: 「(주) 하티오랩」 is 「(주)하티오랩」. */\nexport function sameName(a: unknown, b: unknown): boolean {\n const plain = (text: unknown) => String(text ?? '').replace(/\\s+/g, '').toLowerCase()\n return !!plain(a) && plain(a) === plain(b)\n}\n\n/** The one choice named `name`; none when no choice or more than one is - a guess would link the wrong party. */\nexport function partyNamed<T extends { name?: string | null }>(name: unknown, choices: readonly T[]): T | undefined {\n const named = choices.filter(one => sameName(one.name, name))\n return named.length === 1 ? named[0] : undefined\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Row = {
|
|
2
|
+
status?: string | null;
|
|
3
|
+
dueDate?: string | null;
|
|
4
|
+
direction?: string | null;
|
|
5
|
+
currency?: string | null;
|
|
6
|
+
totalAmount?: number | string | null;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Whether an invoice is past its due date: issued and due before `today`. Read only from its status and due date - the
|
|
10
|
+
* invoice module keeps no payments (an application ties them, as Office does), so a paid invoice past its day reads
|
|
11
|
+
* overdue here too.
|
|
12
|
+
*/
|
|
13
|
+
export declare function overdue(row: Row, today: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The total of the rows by direction and currency - the sales and the purchases apart, each currency apart, voided
|
|
16
|
+
* invoices left out: an invoice cancelled bills nothing. Counted in the currency's minor unit.
|
|
17
|
+
*/
|
|
18
|
+
export declare function totalsOf(rows: readonly Row[]): {
|
|
19
|
+
direction: string;
|
|
20
|
+
currency: string;
|
|
21
|
+
amount: number;
|
|
22
|
+
}[];
|
|
23
|
+
/** The first and last day (YYYY-MM-DD) of a period the list is narrowed to, around `day`; undefined for every period. */
|
|
24
|
+
export declare function issuePeriodOf(period: string, day: string): [string, string] | undefined;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.overdue = overdue;
|
|
4
|
+
exports.totalsOf = totalsOf;
|
|
5
|
+
exports.issuePeriodOf = issuePeriodOf;
|
|
6
|
+
const currency_digits_js_1 = require("./currency-digits.js");
|
|
7
|
+
/**
|
|
8
|
+
* Whether an invoice is past its due date: issued and due before `today`. Read only from its status and due date - the
|
|
9
|
+
* invoice module keeps no payments (an application ties them, as Office does), so a paid invoice past its day reads
|
|
10
|
+
* overdue here too.
|
|
11
|
+
*/
|
|
12
|
+
function overdue(row, today) {
|
|
13
|
+
return row.status === 'ISSUED' && !!row.dueDate && row.dueDate < today;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The total of the rows by direction and currency - the sales and the purchases apart, each currency apart, voided
|
|
17
|
+
* invoices left out: an invoice cancelled bills nothing. Counted in the currency's minor unit.
|
|
18
|
+
*/
|
|
19
|
+
function totalsOf(rows) {
|
|
20
|
+
const sums = new Map();
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
if (row.status === 'VOID')
|
|
23
|
+
continue;
|
|
24
|
+
const currency = row.currency || 'KRW';
|
|
25
|
+
const direction = row.direction === 'PURCHASE' ? 'PURCHASE' : 'SALES';
|
|
26
|
+
const digits = (0, currency_digits_js_1.currencyDigits)(currency) ?? 0;
|
|
27
|
+
const key = `${direction}:${currency}`;
|
|
28
|
+
const sum = sums.get(key) ?? { direction, currency, minor: 0, digits };
|
|
29
|
+
sum.minor += Math.round(Number(row.totalAmount ?? 0) * 10 ** digits);
|
|
30
|
+
sums.set(key, sum);
|
|
31
|
+
}
|
|
32
|
+
return [...sums.values()]
|
|
33
|
+
.sort((a, b) => a.direction.localeCompare(b.direction, 'en') * -1 || a.currency.localeCompare(b.currency, 'en'))
|
|
34
|
+
.map(({ direction, currency, minor, digits }) => ({ direction, currency, amount: minor / 10 ** digits }));
|
|
35
|
+
}
|
|
36
|
+
/** The first and last day (YYYY-MM-DD) of a period the list is narrowed to, around `day`; undefined for every period. */
|
|
37
|
+
function issuePeriodOf(period, day) {
|
|
38
|
+
const [year, month] = day.split('-').map(Number);
|
|
39
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
40
|
+
const monthOf = (y, m) => [`${y}-${pad(m)}-01`, `${y}-${pad(m)}-${pad(new Date(Date.UTC(y, m, 0)).getUTCDate())}`];
|
|
41
|
+
if (period === 'this-month')
|
|
42
|
+
return monthOf(year, month);
|
|
43
|
+
if (period === 'last-month')
|
|
44
|
+
return month === 1 ? monthOf(year - 1, 12) : monthOf(year, month - 1);
|
|
45
|
+
if (period === 'this-year')
|
|
46
|
+
return [`${year}-01-01`, `${year}-12-31`];
|
|
47
|
+
if (period === 'last-year')
|
|
48
|
+
return [`${year - 1}-01-01`, `${year - 1}-12-31`];
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=invoice-standing.js.map
|