@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.
Files changed (95) hide show
  1. package/dist-client/document/document-amount.d.ts +7 -2
  2. package/dist-client/document/document-amount.js +11 -6
  3. package/dist-client/document/document-amount.js.map +1 -1
  4. package/dist-client/document/document-defaults.d.ts +24 -0
  5. package/dist-client/document/document-defaults.js +43 -0
  6. package/dist-client/document/document-defaults.js.map +1 -0
  7. package/dist-client/document/document-form.d.ts +24 -6
  8. package/dist-client/document/document-form.js +126 -15
  9. package/dist-client/document/document-form.js.map +1 -1
  10. package/dist-client/document/document-lines.d.ts +35 -1
  11. package/dist-client/document/document-lines.js +58 -9
  12. package/dist-client/document/document-lines.js.map +1 -1
  13. package/dist-client/document/document-party.d.ts +37 -0
  14. package/dist-client/document/document-party.js +217 -0
  15. package/dist-client/document/document-party.js.map +1 -0
  16. package/dist-client/document/document-preview.d.ts +37 -0
  17. package/dist-client/document/document-preview.js +181 -0
  18. package/dist-client/document/document-preview.js.map +1 -0
  19. package/dist-client/document/document-print.d.ts +3 -13
  20. package/dist-client/document/document-print.js +3 -45
  21. package/dist-client/document/document-print.js.map +1 -1
  22. package/dist-client/index.d.ts +9 -3
  23. package/dist-client/index.js +9 -3
  24. package/dist-client/index.js.map +1 -1
  25. package/dist-client/pages/invoice-editor.d.ts +37 -12
  26. package/dist-client/pages/invoice-editor.js +171 -79
  27. package/dist-client/pages/invoice-editor.js.map +1 -1
  28. package/dist-client/pages/invoice-list.d.ts +21 -1
  29. package/dist-client/pages/invoice-list.js +145 -14
  30. package/dist-client/pages/invoice-list.js.map +1 -1
  31. package/dist-client/tsconfig.tsbuildinfo +1 -1
  32. package/dist-client/viewparts/document-defaults-setting-let.d.ts +19 -0
  33. package/dist-client/viewparts/document-defaults-setting-let.js +139 -0
  34. package/dist-client/viewparts/document-defaults-setting-let.js.map +1 -0
  35. package/dist-server/role-tasks.js +5 -2
  36. package/dist-server/role-tasks.js.map +1 -1
  37. package/dist-server/service/document-defaults/document-defaults-resolver.d.ts +28 -0
  38. package/dist-server/service/document-defaults/document-defaults-resolver.js +136 -0
  39. package/dist-server/service/document-defaults/document-defaults-resolver.js.map +1 -0
  40. package/dist-server/service/document-defaults/document-defaults-rules.d.ts +25 -0
  41. package/dist-server/service/document-defaults/document-defaults-rules.js +50 -0
  42. package/dist-server/service/document-defaults/document-defaults-rules.js.map +1 -0
  43. package/dist-server/service/document-defaults/index.d.ts +6 -0
  44. package/dist-server/service/document-defaults/index.js +11 -0
  45. package/dist-server/service/document-defaults/index.js.map +1 -0
  46. package/dist-server/service/index.d.ts +2 -0
  47. package/dist-server/service/index.js +4 -1
  48. package/dist-server/service/index.js.map +1 -1
  49. package/dist-server/service/invoice/commercial-document.d.ts +2 -0
  50. package/dist-server/service/invoice/commercial-document.js +11 -1
  51. package/dist-server/service/invoice/commercial-document.js.map +1 -1
  52. package/dist-server/service/invoice/invoice-document-file.d.ts +20 -0
  53. package/dist-server/service/invoice/invoice-document-file.js +38 -0
  54. package/dist-server/service/invoice/invoice-document-file.js.map +1 -0
  55. package/dist-server/service/invoice/invoice-query.d.ts +3 -1
  56. package/dist-server/service/invoice/invoice-query.js +33 -0
  57. package/dist-server/service/invoice/invoice-query.js.map +1 -1
  58. package/dist-server/service/invoice/invoice-rules.d.ts +18 -11
  59. package/dist-server/service/invoice/invoice-rules.js +49 -96
  60. package/dist-server/service/invoice/invoice-rules.js.map +1 -1
  61. package/dist-server/service/invoice/invoice-service.d.ts +1 -0
  62. package/dist-server/service/invoice/invoice-service.js +5 -2
  63. package/dist-server/service/invoice/invoice-service.js.map +1 -1
  64. package/dist-server/service/invoice/invoice-type.d.ts +12 -0
  65. package/dist-server/service/invoice/invoice-type.js +37 -1
  66. package/dist-server/service/invoice/invoice-type.js.map +1 -1
  67. package/dist-server/tsconfig.tsbuildinfo +1 -1
  68. package/dist-shared/document-amounts.d.ts +56 -0
  69. package/dist-shared/document-amounts.js +134 -0
  70. package/dist-shared/document-amounts.js.map +1 -0
  71. package/dist-shared/document-money.d.ts +12 -0
  72. package/dist-shared/document-money.js +38 -0
  73. package/dist-shared/document-money.js.map +1 -0
  74. package/dist-shared/document-print.d.ts +37 -0
  75. package/dist-shared/document-print.js +87 -0
  76. package/dist-shared/document-print.js.map +1 -0
  77. package/dist-shared/document-start.d.ts +15 -0
  78. package/dist-shared/document-start.js +51 -0
  79. package/dist-shared/document-start.js.map +1 -0
  80. package/dist-shared/invoice-standing.d.ts +25 -0
  81. package/dist-shared/invoice-standing.js +51 -0
  82. package/dist-shared/invoice-standing.js.map +1 -0
  83. package/package.json +9 -7
  84. package/shared/document-amounts.ts +209 -0
  85. package/shared/document-money.ts +33 -0
  86. package/shared/document-print.ts +131 -0
  87. package/shared/document-start.ts +46 -0
  88. package/shared/invoice-standing.ts +49 -0
  89. package/translations/en.json +32 -6
  90. package/translations/es.json +22 -1
  91. package/translations/ja.json +22 -1
  92. package/translations/ko.json +32 -6
  93. package/translations/ms.json +22 -1
  94. package/translations/zh.json +22 -1
  95. package/tsconfig.shared.tsbuildinfo +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoice-standing.js","sourceRoot":"","sources":["../shared/invoice-standing.ts"],"names":[],"mappings":"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","sourcesContent":["import { currencyDigits } from './currency-digits.js'\n\n/*\n * How the invoice list reads its rows (ADR-0170 decision 11). Pure.\n */\n\ntype Row = { status?: string | null; dueDate?: string | null; direction?: string | null; currency?: string | null; totalAmount?: number | string | null }\n\n/**\n * Whether an invoice is past its due date: issued and due before `today`. Read only from its status and due date - the\n * invoice module keeps no payments (an application ties them, as Office does), so a paid invoice past its day reads\n * overdue here too.\n */\nexport function overdue(row: Row, today: string): boolean {\n return row.status === 'ISSUED' && !!row.dueDate && row.dueDate < today\n}\n\n/**\n * The total of the rows by direction and currency - the sales and the purchases apart, each currency apart, voided\n * invoices left out: an invoice cancelled bills nothing. Counted in the currency's minor unit.\n */\nexport function totalsOf(rows: readonly Row[]): { direction: string; currency: string; amount: number }[] {\n const sums = new Map<string, { direction: string; currency: string; minor: number; digits: number }>()\n for (const row of rows) {\n if (row.status === 'VOID') continue\n const currency = row.currency || 'KRW'\n const direction = row.direction === 'PURCHASE' ? 'PURCHASE' : 'SALES'\n const digits = currencyDigits(currency) ?? 0\n const key = `${direction}:${currency}`\n const sum = sums.get(key) ?? { direction, currency, minor: 0, digits }\n sum.minor += Math.round(Number(row.totalAmount ?? 0) * 10 ** digits)\n sums.set(key, sum)\n }\n return [...sums.values()]\n .sort((a, b) => a.direction.localeCompare(b.direction, 'en') * -1 || a.currency.localeCompare(b.currency, 'en'))\n .map(({ direction, currency, minor, digits }) => ({ direction, currency, amount: minor / 10 ** digits }))\n}\n\n/** The first and last day (YYYY-MM-DD) of a period the list is narrowed to, around `day`; undefined for every period. */\nexport function issuePeriodOf(period: string, day: string): [string, string] | undefined {\n const [year, month] = day.split('-').map(Number)\n const pad = (n: number) => String(n).padStart(2, '0')\n const monthOf = (y: number, m: number): [string, string] => [`${y}-${pad(m)}-01`, `${y}-${pad(m)}-${pad(new Date(Date.UTC(y, m, 0)).getUTCDate())}`]\n if (period === 'this-month') return monthOf(year, month)\n if (period === 'last-month') return month === 1 ? monthOf(year - 1, 12) : monthOf(year, month - 1)\n if (period === 'this-year') return [`${year}-01-01`, `${year}-12-31`]\n if (period === 'last-year') return [`${year - 1}-01-01`, `${year - 1}-12-31`]\n return undefined\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/invoice",
3
- "version": "10.1.223",
3
+ "version": "10.1.227",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -32,11 +32,13 @@
32
32
  "@operato/shell": "^10.0.0",
33
33
  "@operato/styles": "^10.29.1",
34
34
  "@operato/utils": "^10.0.0",
35
- "@things-factory/ai-assistant": "^10.1.223",
36
- "@things-factory/attachment-base": "^10.1.223",
37
- "@things-factory/auth-base": "^10.1.223",
38
- "@things-factory/biz-base": "^10.1.223",
39
- "@things-factory/shell": "^10.1.208",
35
+ "@things-factory/ai-assistant": "^10.1.227",
36
+ "@things-factory/attachment-base": "^10.1.227",
37
+ "@things-factory/auth-base": "^10.1.227",
38
+ "@things-factory/biz-base": "^10.1.227",
39
+ "@things-factory/pdf": "^10.1.227",
40
+ "@things-factory/setting-base": "^10.1.227",
41
+ "@things-factory/shell": "^10.1.227",
40
42
  "graphql-upload": "^15.0.2"
41
43
  },
42
44
  "publishConfig": {
@@ -49,5 +51,5 @@
49
51
  "url": "git+https://github.com/hatiolab/things-factory.git",
50
52
  "directory": "packages/invoice"
51
53
  },
52
- "gitHead": "868f2026b8373371ad04f42b0a097964d0d0ed0f"
54
+ "gitHead": "f66591e3864aab08a6fb93fb1fcf1c9de6b546ac"
53
55
  }
@@ -0,0 +1,209 @@
1
+ import { currencyDigits } from './currency-digits.js'
2
+
3
+ /*
4
+ * A commercial document's arithmetic - the one place it is decided, read by the invoice and quotation services when a
5
+ * document is saved and by their screens while it is typed, so a line's supply and the totals on screen are the
6
+ * ones the document will keep (ADR-0120 decision 4).
7
+ *
8
+ * - Lines carry supply and a tax kind and rate; the tax is reckoned once per (kind, rate) group: the group's supply
9
+ * total × rate ÷ 100, rounded half-up to the currency's minor unit. The document tax is the sum of the groups.
10
+ * - A discount of the whole document comes off the lines' supply before tax (ADR-0170 decision 8): it is spread over
11
+ * the groups in proportion to each group's supply, in minor units, the remainder going one unit at a time to the
12
+ * groups whose share was cut most (largest remainder), so the parts add up to the discount exactly. Each group's
13
+ * tax is reckoned on its supply after its part.
14
+ * - Everything is counted in integers: quantities, prices and rates to four decimals, money in minor units.
15
+ * - A received document keeps the tax its sender printed on each line; nothing recomputes it.
16
+ *
17
+ * No imports but the currency's digits and no throwing: a line that cannot be reckoned says why (`problem`), and is
18
+ * left out of the totals. The server refuses such a document; a screen shows the rest while the line is typed.
19
+ */
20
+
21
+ export type TaxKindCode = 'TAXABLE' | 'ZERO_RATED' | 'EXEMPT'
22
+
23
+ export const TAX_KINDS: readonly TaxKindCode[] = ['TAXABLE', 'ZERO_RATED', 'EXEMPT']
24
+
25
+ export interface DocumentLine {
26
+ description?: string | null
27
+ quantity?: number | string | null
28
+ unitPrice?: number | string | null
29
+ discount?: number | string | null
30
+ taxKind?: string | null
31
+ taxRate?: number | string | null
32
+ /** The tax the sender printed on the line - only on a received document. */
33
+ receivedTaxAmount?: number | string | null
34
+ }
35
+
36
+ /** Why a line cannot be reckoned - the server's refusal codes, `invoice-<problem>`. */
37
+ export type LineProblem = 'item-invalid' | 'discount-exceeds' | 'amount-too-large' | 'received-tax-required'
38
+
39
+ export interface ReckonedLine {
40
+ /** The line's supply - quantity × unit price, less the discount - in the currency. Absent when the line has a problem. */
41
+ supplyAmount?: number
42
+ /** The tax the sender printed, on a received document. */
43
+ receivedTaxAmount?: number | null
44
+ problem?: LineProblem
45
+ }
46
+
47
+ export interface TaxGroup {
48
+ taxKind: string
49
+ taxRate: number
50
+ supplyAmount: number
51
+ taxAmount: number
52
+ }
53
+
54
+ /** Why the discount of the whole document cannot be taken - the server's refusal code, `invoice-<problem>`. */
55
+ export type DiscountProblem = 'document-discount-invalid'
56
+
57
+ export interface DocumentAmounts {
58
+ lines: ReckonedLine[]
59
+ /** Each group's supply after its part of the document discount, and its tax. */
60
+ taxGroups: TaxGroup[]
61
+ /** The lines' supply before the document discount. */
62
+ lineSupplyAmount: number
63
+ /** The discount of the whole document taken off the lines' supply - none when it has a problem. */
64
+ discountAmount: number
65
+ /** A discount below zero, past the lines' supply or finer than the currency's minor unit; it is not taken. */
66
+ discountProblem?: DiscountProblem
67
+ /** The lines' supply less the document discount - the supply tax is reckoned on. */
68
+ supplyAmount: number
69
+ taxAmount: number
70
+ totalAmount: number
71
+ /** The totals pass the largest amount a document may carry. */
72
+ tooLarge: boolean
73
+ }
74
+
75
+ /** The rate a tax kind takes unless the person writes another: the standard VAT for a taxable line, none otherwise. */
76
+ export function defaultTaxRate(taxKind: string | null | undefined, standard = 10): number {
77
+ return taxKind === 'ZERO_RATED' || taxKind === 'EXEMPT' ? 0 : standard
78
+ }
79
+
80
+ const MAX_MINOR = 10n ** 14n
81
+ const SCALE = 10_000n
82
+
83
+ const numberOf = (value: unknown): number => (typeof value === 'number' ? value : value === null || value === undefined || value === '' ? NaN : Number(value))
84
+
85
+ /** A number with at most four decimals, as an integer of ten-thousandths; undefined when it has more. */
86
+ function scaled(value: unknown): bigint | undefined {
87
+ const number = numberOf(value)
88
+ if (!Number.isFinite(number)) return undefined
89
+ const whole = Math.round(number * 10_000)
90
+ return Math.abs(number * 10_000 - whole) > 1e-6 ? undefined : BigInt(whole)
91
+ }
92
+
93
+ function minorOf(value: unknown, digits: number): bigint | undefined {
94
+ const number = numberOf(value)
95
+ if (!Number.isFinite(number)) return undefined
96
+ const factor = 10 ** digits
97
+ const whole = Math.round(number * factor)
98
+ return Math.abs(number * factor - whole) > 1e-6 ? undefined : BigInt(whole)
99
+ }
100
+
101
+ /** numerator ÷ denominator, rounded half-up; both non-negative. */
102
+ function divideHalfUp(numerator: bigint, denominator: bigint): bigint {
103
+ return (numerator * 2n + denominator) / (denominator * 2n)
104
+ }
105
+
106
+ const amountOf = (minor: bigint, digits: number) => Number(minor) / 10 ** digits
107
+
108
+ /**
109
+ * Each line's supply, the tax of each (kind, rate) group and the document totals, in the currency (whose digits must
110
+ * be known - `currencyDigits`). `received` keeps the line tax the sender printed; otherwise the groups reckon it. A
111
+ * blank discount is none; a blank quantity, price, kind or rate is a line not yet reckonable. `discountAmount` is the
112
+ * discount of the whole document; a blank one is none. On a received document it lowers the supply, and the tax stays
113
+ * as printed.
114
+ */
115
+ export function documentAmounts(lines: readonly DocumentLine[], currency: string, received = false, discountAmount: unknown = 0): DocumentAmounts {
116
+ const digits = currencyDigits(currency) ?? 0
117
+ const factor = 10n ** BigInt(digits)
118
+ const groups = new Map<string, { taxKind: string; rate: bigint; supply: bigint; tax: bigint }>()
119
+
120
+ const reckoned = lines.map((line): ReckonedLine => {
121
+ const quantity = scaled(line.quantity)
122
+ const unitPrice = scaled(line.unitPrice)
123
+ const rate = scaled(line.taxRate)
124
+ const discount = line.discount === null || line.discount === undefined || line.discount === '' ? 0n : minorOf(line.discount, digits)
125
+ const kind = String(line.taxKind ?? '')
126
+ if (
127
+ !String(line.description ?? '').trim() ||
128
+ quantity === undefined ||
129
+ unitPrice === undefined ||
130
+ rate === undefined ||
131
+ discount === undefined ||
132
+ quantity <= 0n ||
133
+ unitPrice < 0n ||
134
+ discount < 0n ||
135
+ rate < 0n ||
136
+ rate > 100n * SCALE ||
137
+ !TAX_KINDS.includes(kind as TaxKindCode) ||
138
+ (kind !== 'TAXABLE' && rate !== 0n)
139
+ ) {
140
+ return { problem: 'item-invalid' }
141
+ }
142
+
143
+ const gross = divideHalfUp(quantity * unitPrice * factor, SCALE * SCALE)
144
+ if (discount > gross) return { problem: 'discount-exceeds' }
145
+ const supply = gross - discount
146
+ if (supply > MAX_MINOR) return { problem: 'amount-too-large' }
147
+
148
+ let printed: bigint | undefined
149
+ if (received) {
150
+ printed = line.receivedTaxAmount === null || line.receivedTaxAmount === undefined || line.receivedTaxAmount === '' ? undefined : minorOf(line.receivedTaxAmount, digits)
151
+ if (printed === undefined || printed < 0n) return { supplyAmount: amountOf(supply, digits), problem: 'received-tax-required' }
152
+ }
153
+
154
+ const key = `${kind}:${rate}`
155
+ const group = groups.get(key) ?? { taxKind: kind, rate, supply: 0n, tax: 0n }
156
+ group.supply += supply
157
+ if (printed !== undefined) group.tax += printed
158
+ groups.set(key, group)
159
+
160
+ return { supplyAmount: amountOf(supply, digits), receivedTaxAmount: printed === undefined ? null : amountOf(printed, digits) }
161
+ })
162
+
163
+ const kept = [...groups.values()]
164
+ const lineSupply = kept.reduce((sum, group) => sum + group.supply, 0n)
165
+ const asked = discountAmount === null || discountAmount === undefined || discountAmount === '' ? 0n : minorOf(discountAmount, digits)
166
+ const discountProblem: DiscountProblem | undefined = asked === undefined || asked < 0n || asked > lineSupply ? 'document-discount-invalid' : undefined
167
+ const discount = discountProblem ? 0n : asked!
168
+ const parts = shares(discount, kept.map(group => group.supply))
169
+
170
+ let supply = 0n
171
+ let tax = 0n
172
+ const taxGroups = kept.map((group, index) => {
173
+ group.supply -= parts[index]
174
+ if (!received) group.tax = divideHalfUp(group.supply * group.rate, 100n * SCALE)
175
+ supply += group.supply
176
+ tax += group.tax
177
+ return { taxKind: group.taxKind, taxRate: Number(group.rate) / 10_000, supplyAmount: amountOf(group.supply, digits), taxAmount: amountOf(group.tax, digits) }
178
+ })
179
+
180
+ return {
181
+ lines: reckoned,
182
+ taxGroups,
183
+ lineSupplyAmount: amountOf(lineSupply, digits),
184
+ discountAmount: amountOf(discount, digits),
185
+ ...(discountProblem ? { discountProblem } : {}),
186
+ supplyAmount: amountOf(supply, digits),
187
+ taxAmount: amountOf(tax, digits),
188
+ totalAmount: amountOf(supply + tax, digits),
189
+ tooLarge: lineSupply > MAX_MINOR || supply + tax > MAX_MINOR
190
+ }
191
+ }
192
+
193
+ /**
194
+ * `total` split in proportion to `weights`, in whole units: each its share cut down, then the units left over one
195
+ * at a time to the largest cut-off remainders (the first of equal ones first), so the parts add up to `total`.
196
+ */
197
+ function shares(total: bigint, weights: readonly bigint[]): bigint[] {
198
+ const whole = weights.reduce((sum, weight) => sum + weight, 0n)
199
+ if (!total || !whole) return weights.map(() => 0n)
200
+ const parts = weights.map(weight => (total * weight) / whole)
201
+ let left = total - parts.reduce((sum, part) => sum + part, 0n)
202
+ 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))
203
+ for (const { index } of byRemainder) {
204
+ if (left <= 0n) break
205
+ parts[index] += 1n
206
+ left -= 1n
207
+ }
208
+ return parts
209
+ }
@@ -0,0 +1,33 @@
1
+ import { currencyDigits } from './currency-digits.js'
2
+
3
+ /*
4
+ * How a commercial document writes money - the one place it is decided, read by the screens (in the screen's
5
+ * language, client/document/document-amount.ts) and by the print the server makes (in the print's language,
6
+ * document-print.ts). No i18n: the language comes in as a parameter.
7
+ */
8
+
9
+ const unset = (value: unknown) => value === null || value === undefined || value === '' || !Number.isFinite(Number(value))
10
+
11
+ /**
12
+ * An amount in its currency as the language writes money (2026-10-28, the user: 「한글인 경우에는 KRW보다는 '원'이 더 좋을
13
+ * 것 같다」): won in Korean reads 「5,000,000원」; every other pair takes the language's own currency form - 「₩5,000,000」
14
+ * and 「$12,000.00」 in English, 「US$12,000.00」 in Korean - with exactly the currency's decimal places. A code the
15
+ * platform does not know is written plainly.
16
+ */
17
+ export function documentAmount(value: number | string | null | undefined, currency: string, lng: string): string {
18
+ if (unset(value)) return ''
19
+ const digits = currencyDigits(currency)
20
+ if (digits === undefined) return `${currency} ${Number(value).toLocaleString(lng)}`
21
+ if (lng.split('-')[0] === 'ko' && currency === 'KRW') return `${Number(value).toLocaleString('ko', { minimumFractionDigits: digits, maximumFractionDigits: digits })}원`
22
+ return new Intl.NumberFormat(lng, { style: 'currency', currency, minimumFractionDigits: digits, maximumFractionDigits: digits }).format(Number(value))
23
+ }
24
+
25
+ /**
26
+ * A number without the currency - a price or a discount in a table of lines: as typed (up to four decimals), or with
27
+ * exactly the currency's decimal places when `exact` (a printed amount).
28
+ */
29
+ export function documentNumber(value: number | string | null | undefined, currency: string, lng: string, exact = false): string {
30
+ if (unset(value)) return ''
31
+ const digits = currencyDigits(currency) ?? 0
32
+ return Number(value).toLocaleString(lng, exact ? { minimumFractionDigits: digits, maximumFractionDigits: digits } : { minimumFractionDigits: 0, maximumFractionDigits: Math.max(digits, 4) })
33
+ }
@@ -0,0 +1,131 @@
1
+ import { documentAmount, documentNumber } from './document-money.js'
2
+
3
+ /*
4
+ * A commercial document as the buyer reads it - the one layout every print of an invoice or a quotation is, made by
5
+ * the server from the stored document (ADR-0170 decision 10): the preview's page, the PDF to download and what the
6
+ * browser prints are this page. Pure - no i18next: the words come in as a translator of the print's language.
7
+ */
8
+
9
+ export type PrintedDocument = Record<string, any>
10
+
11
+ /** The languages the print has words for - this package's and sales' print words in each (ADR-0170 decision 9). */
12
+ export const PRINT_LANGUAGES = ['ko', 'en'] as const
13
+ export type PrintLanguage = (typeof PRINT_LANGUAGES)[number]
14
+
15
+ /** The language as one the print has words for, or undefined - the caller refuses it or falls back. */
16
+ export function printLanguageOf(language: unknown): PrintLanguage | undefined {
17
+ return (PRINT_LANGUAGES as readonly unknown[]).includes(language) ? (language as PrintLanguage) : undefined
18
+ }
19
+
20
+ export type PrintOptions = {
21
+ /** The language of the print - its words and how it writes money. */
22
+ language: PrintLanguage
23
+ /** The words of the print, in `language`. */
24
+ t: (key: string) => string
25
+ /** The kind's title - 「청구서」, 「견적서」. */
26
+ title: string
27
+ /** The kind's own terms beside the number - the due date; the version and the last day of an offer. */
28
+ terms: { label: string; value?: string | null }[]
29
+ /** How the kind names its number. */
30
+ numberLabel?: string
31
+ }
32
+
33
+ const escape = (text: unknown) => String(text ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!)
34
+
35
+ /** The look of the printed page - A4 with its margins. */
36
+ export const DOCUMENT_CSS = `
37
+ @page{size:A4;margin:18mm}
38
+ body{font:13px/1.5 system-ui,sans-serif;color:#000}
39
+ h1{font-size:26px;margin:0}
40
+ h2{font-size:13px;color:#555;margin:0 0 4px}
41
+ header,.parties{display:flex;justify-content:space-between;gap:32px}
42
+ .parties section{flex:1}
43
+ table{width:100%;border-collapse:collapse;margin:24px 0}
44
+ th,td{padding:8px;border-bottom:1px solid #999;text-align:right;font-variant-numeric:tabular-nums}
45
+ th:first-child,td:first-child{text-align:left}
46
+ .text{white-space:pre-wrap}
47
+ .totals{width:50%;margin-left:auto}
48
+ .grand td{font-weight:600;font-size:15px}
49
+ tr{break-inside:avoid}
50
+ `
51
+
52
+ /**
53
+ * 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.
54
+ * Printing and the PDF read the print media, where none of this applies.
55
+ */
56
+ export const DOCUMENT_SCREEN_CSS = `
57
+ @media screen {
58
+ html{background:#e9edf1}
59
+ body{margin:0;padding:24px 0}
60
+ .sheet{box-sizing:border-box;width:210mm;min-height:297mm;margin:0 auto;padding:18mm;background:#fff;
61
+ box-shadow:0 1px 3px rgba(23,36,44,.12),0 8px 24px rgba(23,36,44,.08)}
62
+ }
63
+ @media screen and (max-width:210mm) {
64
+ .sheet{width:auto;min-height:0;margin:0 8px;padding:8mm}
65
+ }
66
+ `
67
+
68
+ /** The document's sheet - the head, the parties, the lines, the totals and the terms - without a page around it. */
69
+ export function documentBody(document: PrintedDocument, { language, t: word, title, terms, numberLabel = 'field.invoice-number' }: PrintOptions): string {
70
+ const t = (key: string) => escape(word(key))
71
+ /* A table's cells carry numbers; the total carries its currency, as the print's language writes money. */
72
+ const amount = (value: number) => escape(documentNumber(value ?? 0, document.currency, language, true))
73
+ const money = (value: number) => escape(documentAmount(value ?? 0, document.currency, language))
74
+ const kind = (taxKind: string) =>
75
+ taxKind === 'ZERO_RATED' ? t('text.invoice-tax-kind-zero-rated') : taxKind === 'EXEMPT' ? t('text.invoice-tax-kind-exempt') : t('text.invoice-tax-kind-taxable')
76
+ const party = (label: string, name: string, address: string, number: string) =>
77
+ `<section><h2>${label}</h2><strong>${escape(name)}</strong><p class="text">${escape(address)}</p><p>${escape(number)}</p></section>`
78
+
79
+ return `<div class="sheet">
80
+ <header><h1>${escape(title)}</h1><div><p>${t(numberLabel)}: ${escape(document.number)}</p><p>${t('field.invoice-issue-date')}: ${escape(document.issueDate)}</p>${terms
81
+ .map(one => `<p>${escape(one.label)}: ${escape(one.value)}</p>`)
82
+ .join('')}</div></header>
83
+ <div class="parties">${party(t('field.invoice-supplier-name'), document.supplierName, document.supplierAddress, document.supplierRegistrationNumber)}${party(
84
+ t('field.invoice-buyer-name'),
85
+ document.buyerName,
86
+ document.buyerAddress,
87
+ document.buyerRegistrationNumber
88
+ )}</div>
89
+ ${document.purchaseOrder ? `<p>${t('field.invoice-purchase-order')}: ${escape(document.purchaseOrder)}</p>` : ''}
90
+ <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(
91
+ 'field.invoice-discount'
92
+ )}</th><th>${t('field.invoice-tax-kind')}</th><th>${t('field.invoice-supply-amount')}</th></tr></thead><tbody>${(document.items ?? [])
93
+ .map(
94
+ (item: any) =>
95
+ `<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(
96
+ item.taxRate
97
+ )}%</td><td>${amount(item.supplyAmount)}</td></tr>`
98
+ )
99
+ .join('')}</tbody></table>
100
+ <table class="totals"><tbody>${
101
+ Number(document.discountAmount) > 0
102
+ ? `<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>`
103
+ : ''
104
+ }${(document.taxGroups ?? [])
105
+ .map((group: any) => `<tr><td>${kind(group.taxKind)} ${escape(group.taxRate)}%</td><td>${amount(group.supplyAmount)}</td><td>${amount(group.taxAmount)}</td></tr>`)
106
+ .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(
107
+ 'field.invoice-total-amount'
108
+ )}</td><td>${money(document.totalAmount)}</td></tr></tbody></table>
109
+ ${['paymentTerms', 'bankDetails', 'notes']
110
+ .filter(key => document[key])
111
+ .map(key => `<p class="text">${escape(document[key])}</p>`)
112
+ .join('')}
113
+ </div>`
114
+ }
115
+
116
+ /**
117
+ * The document as one HTML page - what the preview shows and the browser prints. Only escaped business data enters
118
+ * the page.
119
+ */
120
+ export function documentHtml(document: PrintedDocument, options: PrintOptions): string {
121
+ 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(
122
+ document,
123
+ options
124
+ )}</body></html>`
125
+ }
126
+
127
+ /** 「견적서-Q2026-001.pdf」 - the kind's word and the number, without what a file name may not hold. */
128
+ export function documentFileName(word: string, number: string, extension: 'html' | 'pdf'): string {
129
+ const safe = (text: string) => String(text ?? '').trim().replace(/[\\/:*?"<>|\s]+/g, '-')
130
+ return `${[safe(word), safe(number)].filter(Boolean).join('-')}.${extension}`
131
+ }
@@ -0,0 +1,46 @@
1
+ /*
2
+ * What a new commercial document starts with, so a person writes only what is new (ADR-0170 decisions 5 · 11) - the
3
+ * next number, the day a term ends, the counterparty its party's name is. Pure: the screens ask the server for what
4
+ * these read and test them without a browser.
5
+ */
6
+
7
+ /** The day `days` after `day` (YYYY-MM-DD), counted in calendar days. */
8
+ export function daysAfter(day: string, days: number): string {
9
+ const at = new Date(`${day}T00:00:00Z`)
10
+ if (Number.isNaN(at.getTime())) return ''
11
+ at.setUTCDate(at.getUTCDate() + days)
12
+ return at.toISOString().slice(0, 10)
13
+ }
14
+
15
+ /**
16
+ * The number after `last`, the domain's most recent invoice number: its trailing digits one up, keeping their width
17
+ * and what comes before them - 「INV-2026-007」 → 「INV-2026-008」, 「A99」 → 「A100」. A year of four digits standing
18
+ * apart in it that is not `year` becomes `year` and the count starts again at one - 「INV-2025-017」 →
19
+ * 「INV-2026-001」. With nothing to follow - no number, or none ending in digits - 「INV-<year>-001」.
20
+ */
21
+ export function nextInvoiceNumber(last: string | null | undefined, year: number): string {
22
+ const match = /^(.*?)(\d+)$/.exec(String(last ?? '').trim())
23
+ if (!match) return `INV-${year}-001`
24
+ const [, prefix, count] = match
25
+ const pad = (n: number) => String(n).padStart(count.length, '0')
26
+ /* A year apart from other digits - not the count itself, not part of a longer number. */
27
+ const years = [...prefix.matchAll(/(?<!\d)((?:19|20)\d{2})(?!\d)/g)]
28
+ const stale = years.find(one => Number(one[1]) !== year)
29
+ if (stale && !years.some(one => Number(one[1]) === year)) {
30
+ const at = stale.index!
31
+ return `${prefix.slice(0, at)}${year}${prefix.slice(at + 4)}${pad(1)}`
32
+ }
33
+ return `${prefix}${pad(Number(count) + 1)}`
34
+ }
35
+
36
+ /** Two names as the same name - case and spaces aside: 「(주) 하티오랩」 is 「(주)하티오랩」. */
37
+ export function sameName(a: unknown, b: unknown): boolean {
38
+ const plain = (text: unknown) => String(text ?? '').replace(/\s+/g, '').toLowerCase()
39
+ return !!plain(a) && plain(a) === plain(b)
40
+ }
41
+
42
+ /** The one choice named `name`; none when no choice or more than one is - a guess would link the wrong party. */
43
+ export function partyNamed<T extends { name?: string | null }>(name: unknown, choices: readonly T[]): T | undefined {
44
+ const named = choices.filter(one => sameName(one.name, name))
45
+ return named.length === 1 ? named[0] : undefined
46
+ }
@@ -0,0 +1,49 @@
1
+ import { currencyDigits } from './currency-digits.js'
2
+
3
+ /*
4
+ * How the invoice list reads its rows (ADR-0170 decision 11). Pure.
5
+ */
6
+
7
+ type Row = { status?: string | null; dueDate?: string | null; direction?: string | null; currency?: string | null; totalAmount?: number | string | null }
8
+
9
+ /**
10
+ * Whether an invoice is past its due date: issued and due before `today`. Read only from its status and due date - the
11
+ * invoice module keeps no payments (an application ties them, as Office does), so a paid invoice past its day reads
12
+ * overdue here too.
13
+ */
14
+ export function overdue(row: Row, today: string): boolean {
15
+ return row.status === 'ISSUED' && !!row.dueDate && row.dueDate < today
16
+ }
17
+
18
+ /**
19
+ * The total of the rows by direction and currency - the sales and the purchases apart, each currency apart, voided
20
+ * invoices left out: an invoice cancelled bills nothing. Counted in the currency's minor unit.
21
+ */
22
+ export function totalsOf(rows: readonly Row[]): { direction: string; currency: string; amount: number }[] {
23
+ const sums = new Map<string, { direction: string; currency: string; minor: number; digits: number }>()
24
+ for (const row of rows) {
25
+ if (row.status === 'VOID') continue
26
+ const currency = row.currency || 'KRW'
27
+ const direction = row.direction === 'PURCHASE' ? 'PURCHASE' : 'SALES'
28
+ const digits = currencyDigits(currency) ?? 0
29
+ const key = `${direction}:${currency}`
30
+ const sum = sums.get(key) ?? { direction, currency, minor: 0, digits }
31
+ sum.minor += Math.round(Number(row.totalAmount ?? 0) * 10 ** digits)
32
+ sums.set(key, sum)
33
+ }
34
+ return [...sums.values()]
35
+ .sort((a, b) => a.direction.localeCompare(b.direction, 'en') * -1 || a.currency.localeCompare(b.currency, 'en'))
36
+ .map(({ direction, currency, minor, digits }) => ({ direction, currency, amount: minor / 10 ** digits }))
37
+ }
38
+
39
+ /** The first and last day (YYYY-MM-DD) of a period the list is narrowed to, around `day`; undefined for every period. */
40
+ export function issuePeriodOf(period: string, day: string): [string, string] | undefined {
41
+ const [year, month] = day.split('-').map(Number)
42
+ const pad = (n: number) => String(n).padStart(2, '0')
43
+ const monthOf = (y: number, m: number): [string, string] => [`${y}-${pad(m)}-01`, `${y}-${pad(m)}-${pad(new Date(Date.UTC(y, m, 0)).getUTCDate())}`]
44
+ if (period === 'this-month') return monthOf(year, month)
45
+ if (period === 'last-month') return month === 1 ? monthOf(year - 1, 12) : monthOf(year, month - 1)
46
+ if (period === 'this-year') return [`${year}-01-01`, `${year}-12-31`]
47
+ if (period === 'last-year') return [`${year - 1}-01-01`, `${year - 1}-12-31`]
48
+ return undefined
49
+ }
@@ -22,9 +22,7 @@
22
22
  "error.invoice-counterparty-not-found": "The counterparty was not found. Reload and choose again.",
23
23
  "error.invoice-file-not-on-invoice": "A file is not on this invoice. Reload and try again.",
24
24
  "field.invoice-bank-details": "Bank details",
25
- "field.invoice-buyer-address": "Buyer address",
26
25
  "field.invoice-buyer-name": "Buyer",
27
- "field.invoice-buyer-registration-number": "Buyer registration number",
28
26
  "field.invoice-counterpart": "Counterparty",
29
27
  "field.invoice-currency": "Currency",
30
28
  "field.invoice-direction": "Direction",
@@ -42,20 +40,25 @@
42
40
  "text.invoice-files-none": "No files attached.",
43
41
  "text.invoice-section-files": "Files",
44
42
  "button.invoice-counterparty-clear": "Clear",
45
- "button.invoice-counterparty-find": "Find",
43
+ "button.invoice-download-pdf": "Download PDF",
44
+ "button.invoice-preview": "Preview",
46
45
  "text.invoice-counterparty-search": "Find by name or business number",
47
46
  "field.invoice-counterparty": "Counterparty",
48
47
  "field.invoice-payment-terms": "Payment terms",
49
48
  "field.invoice-purchase-order": "Purchase order",
50
49
  "field.invoice-quantity": "Quantity",
50
+ "field.invoice-unit": "Unit",
51
51
  "field.invoice-received-tax-amount": "Printed tax",
52
52
  "field.invoice-source-reference": "Source reference",
53
53
  "field.invoice-status": "Status",
54
54
  "field.invoice-sum": "Sum",
55
- "field.invoice-supplier-address": "Supplier address",
56
55
  "field.invoice-supplier-name": "Supplier",
57
- "field.invoice-supplier-registration-number": "Supplier registration number",
58
56
  "field.invoice-supply-amount": "Supply amount",
57
+ "field.invoice-document-discount": "Discount on the whole",
58
+ "field.invoice-line-sum": "Lines total",
59
+ "error.invoice-document-discount-invalid": "The discount is zero or more and at most the lines' supply.",
60
+ "error.invoice-print-language-invalid": "Print in Korean or English.",
61
+ "field.invoice-defaults-payment-days": "days an invoice is due",
59
62
  "field.invoice-tax-amount": "Tax",
60
63
  "field.invoice-tax-basis": "Tax basis",
61
64
  "field.invoice-tax-kind": "Taxation",
@@ -90,8 +93,31 @@
90
93
  "title.invoice-list": "Invoices",
91
94
  "title.invoice-new": "New invoice",
92
95
  "title.invoice-print": "INVOICE",
96
+ "title.invoice-print-language": "Print language",
93
97
  "task.invoice.reading.name": "Reading invoices",
94
98
  "task.invoice.reading.description": "Reads invoices.",
95
99
  "task.invoice.keeping.name": "Keeping invoices",
96
- "task.invoice.keeping.description": "Reads and changes invoices."
100
+ "task.invoice.keeping.description": "Reads and changes invoices.",
101
+ "text.invoice-counterparty-none": "no counterparty found",
102
+ "field.invoice-party-name": "name",
103
+ "field.invoice-party-registration-number": "registration no.",
104
+ "field.invoice-party-address": "address",
105
+ "title.invoice-document-defaults": "quotation and invoice defaults",
106
+ "text.invoice-defaults-terms-hint": "One a line. The first is a new document's payment terms; the others are picked on the document.",
107
+ "field.invoice-defaults-valid-days": "days a quotation holds",
108
+ "task.invoice.defaults-reading.name": "reading the document defaults",
109
+ "task.invoice.defaults-reading.description": "Reads the company's words new quotations and invoices start with - our side, the account to pay into, the payment terms.",
110
+ "text.invoice-counterparty-unlinked": "Not linked to a counterparty - pick one above to link it.",
111
+ "text.invoice-filter-all-directions": "Sales and purchases",
112
+ "text.invoice-filter-all-statuses": "Any status",
113
+ "text.invoice-issue-confirm": "To {party} · total {total}. Once issued, its amounts cannot be changed.",
114
+ "text.invoice-issue-confirm-purchase": "From {party} · total {total}. Once issued, its amounts cannot be changed.",
115
+ "text.invoice-overdue": "Overdue",
116
+ "text.invoice-period-all": "Any period",
117
+ "text.invoice-period-last-month": "Last month",
118
+ "text.invoice-period-last-year": "Last year",
119
+ "text.invoice-period-this-month": "This month",
120
+ "text.invoice-period-this-year": "This year",
121
+ "text.invoice-totals-shown": "Totals on this page (voided left out)",
122
+ "title.invoice-preview": "Preview"
97
123
  }
@@ -43,11 +43,14 @@
43
43
  "text.invoice-section-files": "Archivos",
44
44
  "button.invoice-counterparty-clear": "Quitar",
45
45
  "button.invoice-counterparty-find": "Buscar",
46
+ "button.invoice-download-pdf": "Descargar PDF",
47
+ "button.invoice-preview": "Vista previa",
46
48
  "text.invoice-counterparty-search": "Buscar por nombre o número fiscal",
47
49
  "field.invoice-counterparty": "Contraparte",
48
50
  "field.invoice-payment-terms": "Condiciones de pago",
49
51
  "field.invoice-purchase-order": "Pedido de compra",
50
52
  "field.invoice-quantity": "Cantidad",
53
+ "field.invoice-unit": "Unidad",
51
54
  "field.invoice-received-tax-amount": "Impuesto impreso",
52
55
  "field.invoice-source-reference": "Documento de origen",
53
56
  "field.invoice-status": "Estado",
@@ -56,6 +59,10 @@
56
59
  "field.invoice-supplier-name": "Proveedor",
57
60
  "field.invoice-supplier-registration-number": "Número de registro del proveedor",
58
61
  "field.invoice-supply-amount": "Importe del suministro",
62
+ "field.invoice-document-discount": "Descuento total",
63
+ "field.invoice-line-sum": "Total de líneas",
64
+ "error.invoice-document-discount-invalid": "El descuento debe ser cero o más y no superar el total de las líneas.",
65
+ "error.invoice-print-language-invalid": "Imprima en coreano o inglés.",
59
66
  "field.invoice-tax-amount": "Impuesto",
60
67
  "field.invoice-tax-basis": "Base del impuesto",
61
68
  "field.invoice-tax-kind": "Tributación",
@@ -90,8 +97,22 @@
90
97
  "title.invoice-list": "Facturas",
91
98
  "title.invoice-new": "Nueva factura",
92
99
  "title.invoice-print": "FACTURA",
100
+ "title.invoice-print-language": "Idioma de impresión",
93
101
  "task.invoice.reading.name": "Leer facturas",
94
102
  "task.invoice.reading.description": "Lee las facturas.",
95
103
  "task.invoice.keeping.name": "Mantener facturas",
96
- "task.invoice.keeping.description": "Lee y modifica las facturas."
104
+ "task.invoice.keeping.description": "Lee y modifica las facturas.",
105
+ "text.invoice-counterparty-unlinked": "No vinculada a una contraparte: elija una arriba para vincularla.",
106
+ "text.invoice-filter-all-directions": "Ventas y compras",
107
+ "text.invoice-filter-all-statuses": "Cualquier estado",
108
+ "text.invoice-issue-confirm": "Para {party} · total {total}. Una vez emitida, sus importes no se pueden cambiar.",
109
+ "text.invoice-issue-confirm-purchase": "De {party} · total {total}. Una vez emitida, sus importes no se pueden cambiar.",
110
+ "text.invoice-overdue": "Vencida",
111
+ "text.invoice-period-all": "Cualquier periodo",
112
+ "text.invoice-period-last-month": "Mes pasado",
113
+ "text.invoice-period-last-year": "Año pasado",
114
+ "text.invoice-period-this-month": "Este mes",
115
+ "text.invoice-period-this-year": "Este año",
116
+ "text.invoice-totals-shown": "Totales de esta página (sin anuladas)",
117
+ "title.invoice-preview": "Vista previa"
97
118
  }