@things-factory/invoice 0.0.0-stage → 10.1.130

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 (91) hide show
  1. package/README.md +21 -2
  2. package/dist-client/bootstrap.d.ts +1 -0
  3. package/dist-client/bootstrap.js +5 -0
  4. package/dist-client/bootstrap.js.map +1 -0
  5. package/dist-client/document/document-amount.d.ts +6 -0
  6. package/dist-client/document/document-amount.js +19 -0
  7. package/dist-client/document/document-amount.js.map +1 -0
  8. package/dist-client/document/document-form.d.ts +14 -0
  9. package/dist-client/document/document-form.js +145 -0
  10. package/dist-client/document/document-form.js.map +1 -0
  11. package/dist-client/document/document-lines.d.ts +104 -0
  12. package/dist-client/document/document-lines.js +61 -0
  13. package/dist-client/document/document-lines.js.map +1 -0
  14. package/dist-client/document/document-print.d.ts +15 -0
  15. package/dist-client/document/document-print.js +60 -0
  16. package/dist-client/document/document-print.js.map +1 -0
  17. package/dist-client/entity360/invoice-360.d.ts +19 -0
  18. package/dist-client/entity360/invoice-360.js +116 -0
  19. package/dist-client/entity360/invoice-360.js.map +1 -0
  20. package/dist-client/index.d.ts +6 -0
  21. package/dist-client/index.js +7 -0
  22. package/dist-client/index.js.map +1 -0
  23. package/dist-client/invoice-attach.d.ts +19 -0
  24. package/dist-client/invoice-attach.js +19 -0
  25. package/dist-client/invoice-attach.js.map +1 -0
  26. package/dist-client/pages/invoice-editor.d.ts +48 -0
  27. package/dist-client/pages/invoice-editor.js +341 -0
  28. package/dist-client/pages/invoice-editor.js.map +1 -0
  29. package/dist-client/pages/invoice-list.d.ts +39 -0
  30. package/dist-client/pages/invoice-list.js +125 -0
  31. package/dist-client/pages/invoice-list.js.map +1 -0
  32. package/dist-client/route.d.ts +1 -0
  33. package/dist-client/route.js +11 -0
  34. package/dist-client/route.js.map +1 -0
  35. package/dist-client/tsconfig.tsbuildinfo +1 -0
  36. package/dist-server/index.d.ts +2 -0
  37. package/dist-server/index.js +6 -0
  38. package/dist-server/index.js.map +1 -0
  39. package/dist-server/routes.d.ts +1 -0
  40. package/dist-server/routes.js +3 -0
  41. package/dist-server/routes.js.map +1 -0
  42. package/dist-server/service/index.d.ts +12 -0
  43. package/dist-server/service/index.js +19 -0
  44. package/dist-server/service/index.js.map +1 -0
  45. package/dist-server/service/invoice/commercial-document.d.ts +44 -0
  46. package/dist-server/service/invoice/commercial-document.js +200 -0
  47. package/dist-server/service/invoice/commercial-document.js.map +1 -0
  48. package/dist-server/service/invoice/index.d.ts +6 -0
  49. package/dist-server/service/invoice/index.js +10 -0
  50. package/dist-server/service/invoice/index.js.map +1 -0
  51. package/dist-server/service/invoice/invoice-mutation.d.ts +8 -0
  52. package/dist-server/service/invoice/invoice-mutation.js +75 -0
  53. package/dist-server/service/invoice/invoice-mutation.js.map +1 -0
  54. package/dist-server/service/invoice/invoice-query.d.ts +16 -0
  55. package/dist-server/service/invoice/invoice-query.js +117 -0
  56. package/dist-server/service/invoice/invoice-query.js.map +1 -0
  57. package/dist-server/service/invoice/invoice-rules.d.ts +83 -0
  58. package/dist-server/service/invoice/invoice-rules.js +202 -0
  59. package/dist-server/service/invoice/invoice-rules.js.map +1 -0
  60. package/dist-server/service/invoice/invoice-service.d.ts +26 -0
  61. package/dist-server/service/invoice/invoice-service.js +133 -0
  62. package/dist-server/service/invoice/invoice-service.js.map +1 -0
  63. package/dist-server/service/invoice/invoice-type.d.ts +40 -0
  64. package/dist-server/service/invoice/invoice-type.js +146 -0
  65. package/dist-server/service/invoice/invoice-type.js.map +1 -0
  66. package/dist-server/service/invoice/invoice.d.ts +30 -0
  67. package/dist-server/service/invoice/invoice.js +108 -0
  68. package/dist-server/service/invoice/invoice.js.map +1 -0
  69. package/dist-server/service/invoice-event/index.d.ts +5 -0
  70. package/dist-server/service/invoice-event/index.js +9 -0
  71. package/dist-server/service/invoice-event/index.js.map +1 -0
  72. package/dist-server/service/invoice-event/invoice-event-query.d.ts +9 -0
  73. package/dist-server/service/invoice-event/invoice-event-query.js +57 -0
  74. package/dist-server/service/invoice-event/invoice-event-query.js.map +1 -0
  75. package/dist-server/service/invoice-event/invoice-event.d.ts +22 -0
  76. package/dist-server/service/invoice-event/invoice-event.js +98 -0
  77. package/dist-server/service/invoice-event/invoice-event.js.map +1 -0
  78. package/dist-server/service/invoice-item/index.d.ts +5 -0
  79. package/dist-server/service/invoice-item/index.js +9 -0
  80. package/dist-server/service/invoice-item/index.js.map +1 -0
  81. package/dist-server/service/invoice-item/invoice-item-query.d.ts +8 -0
  82. package/dist-server/service/invoice-item/invoice-item-query.js +47 -0
  83. package/dist-server/service/invoice-item/invoice-item-query.js.map +1 -0
  84. package/dist-server/service/invoice-item/invoice-item.d.ts +19 -0
  85. package/dist-server/service/invoice-item/invoice-item.js +80 -0
  86. package/dist-server/service/invoice-item/invoice-item.js.map +1 -0
  87. package/dist-server/tsconfig.tsbuildinfo +1 -0
  88. package/package.json +44 -4
  89. package/things-factory.config.js +11 -0
  90. package/translations/en.json +81 -0
  91. package/translations/ko.json +81 -0
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @things-factory/invoice
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 국내·해외 거래의 상업용 청구 문서를 관리하는 공통 Things Factory 모듈이다. 국내 세금계산서는 기존 `@things-factory/tax-invoice`가 관리한다.
4
+
5
+ ## 모듈 경계
6
+
7
+ `invoice → auth-base, shell`. invoice와 tax-invoice는 서로 의존하지 않는다. 계약·입출금·세금계산서 연결은 두 패키지를 사용하는 operato-office의 `OfficeInvoiceLink`가 담당한다. 공통 모듈은 특정 애플리케이션의 메뉴나 금융 원장을 생성하지 않는다.
8
+
9
+ ## 구조와 사용
10
+
11
+ `things-factory.config.js`, 서버/클라이언트 진입점, service의 entities/subscribers/resolvers 집계는 프레임워크 모듈 구조를 따른다. `invoice`, `invoice-item`, `invoice-event`는 정규화된 엔티티이며 원본 발행 내용은 이벤트 스냅샷으로 보존한다. GraphQL 목록은 ListParam과 items/total을 사용한다. 변경 API는 privilege/transaction 지시자를 사용한다.
12
+
13
+ 초안 작성·수정 → 발행 → 필요 시 사유를 기록한 취소 흐름을 제공한다. 발행된 문서는 수정할 수 없다. 공급자·구매자·주소·통화·결제 조건·은행 정보와 품목을 보존한다. 통화별 소수 자릿수로 금액을 계산하며 데이터베이스에는 shell의 moneyColumn을 사용한다. 종속 애플리케이션은 전이 가드를 등록해 연결된 문서의 취소를 제한할 수 있다.
14
+
15
+ ```sh
16
+ npm run build
17
+ npm test
18
+ ```
19
+
20
+ 패키지는 기존 모노레포 배포 절차로 발행한다. 소비 애플리케이션은 해당 패키지를 의존성에 추가하고 정상적인 엔티티/권한 마이그레이션을 수행한다. 별도 인쇄 UI는 operato-office에서 제공한다.
21
+
22
+ 검증: 실제 메모리 SQLite 엔티티를 이용한 도메인 격리·발행 불변성·트랜잭션·전이 가드 검사와 금액 규칙 검사, 총 8건 통과.
@@ -0,0 +1 @@
1
+ export default function bootstrap(): void;
@@ -0,0 +1,5 @@
1
+ import { registerInvoiceKind } from './entity360/invoice-360.js';
2
+ export default function bootstrap() {
3
+ registerInvoiceKind();
4
+ }
5
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAEhE,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,mBAAmB,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { registerInvoiceKind } from './entity360/invoice-360.js'\n\nexport default function bootstrap() {\n registerInvoiceKind()\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /** Decimal places of a currency — 0 for KRW and JPY, 2 for USD, 3 for KWD. */
2
+ export declare function currencyDigits(currency: string): number;
3
+ /** An amount in its currency, with exactly the currency's decimal places. */
4
+ export declare function documentAmount(value: number | null | undefined, currency: string): string;
5
+ /** The currencies a document may be written in, most used first. */
6
+ export declare function currencyChoices(): string[];
@@ -0,0 +1,19 @@
1
+ import { i18next } from '@operato/i18n';
2
+ /** Decimal places of a currency — 0 for KRW and JPY, 2 for USD, 3 for KWD. */
3
+ export function currencyDigits(currency) {
4
+ return new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2;
5
+ }
6
+ /** An amount in its currency, with exactly the currency's decimal places. */
7
+ export function documentAmount(value, currency) {
8
+ if (value === null || value === undefined || !Number.isFinite(Number(value)))
9
+ return '';
10
+ const digits = currencyDigits(currency);
11
+ return `${currency} ${Number(value).toLocaleString(i18next.language || 'ko', { minimumFractionDigits: digits, maximumFractionDigits: digits })}`;
12
+ }
13
+ /** The currencies a document may be written in, most used first. */
14
+ export function currencyChoices() {
15
+ const all = Intl.supportedValuesOf ? Intl.supportedValuesOf('currency') : [];
16
+ const first = ['KRW', 'USD', 'EUR', 'JPY', 'CNY'];
17
+ return [...first, ...all.filter(code => !first.includes(code))];
18
+ }
19
+ //# sourceMappingURL=document-amount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-amount.js","sourceRoot":"","sources":["../../client/document/document-amount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC,qBAAqB,IAAI,CAAC,CAAA;AAClH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,cAAc,CAAC,KAAgC,EAAE,QAAgB;IAC/E,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,CAAA;IACvF,MAAM,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAA;IACvC,OAAO,GAAG,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,EAAE,qBAAqB,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;AAClJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAI,IAAY,CAAC,iBAAiB,CAAC,CAAC,CAAG,IAAY,CAAC,iBAAiB,CAAC,UAAU,CAAc,CAAC,CAAC,CAAC,EAAE,CAAA;IAC5G,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IACjD,OAAO,CAAC,GAAG,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC","sourcesContent":["import { i18next } from '@operato/i18n'\n\n/** Decimal places of a currency — 0 for KRW and JPY, 2 for USD, 3 for KWD. */\nexport function currencyDigits(currency: string): number {\n return new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2\n}\n\n/** An amount in its currency, with exactly the currency's decimal places. */\nexport function documentAmount(value: number | null | undefined, currency: string): string {\n if (value === null || value === undefined || !Number.isFinite(Number(value))) return ''\n const digits = currencyDigits(currency)\n return `${currency} ${Number(value).toLocaleString(i18next.language || 'ko', { minimumFractionDigits: digits, maximumFractionDigits: digits })}`\n}\n\n/** The currencies a document may be written in, most used first. */\nexport function currencyChoices(): string[] {\n const all = (Intl as any).supportedValuesOf ? ((Intl as any).supportedValuesOf('currency') as string[]) : []\n const first = ['KRW', 'USD', 'EUR', 'JPY', 'CNY']\n return [...first, ...all.filter(code => !first.includes(code))]\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { nothing, type TemplateResult } from 'lit';
2
+ export type DocumentValue = Record<string, any>;
3
+ /** The head fields in the order a person fills them. */
4
+ export declare const HEAD_FIELDS: {
5
+ name: string;
6
+ label: string;
7
+ wide?: boolean;
8
+ prose?: boolean;
9
+ }[];
10
+ export declare const DocumentFormStyles: import("lit").CSSResult;
11
+ /** The head of a document. `dates` holds the fields a kind adds — the due date, the last day of an offer. */
12
+ export declare function renderDocumentHead(value: DocumentValue, editable: boolean, change: (patch: DocumentValue) => void, dates?: TemplateResult | typeof nothing): TemplateResult<1>;
13
+ /** Supply and tax of each tax kind and rate, then the document totals. */
14
+ export declare function renderDocumentTotals(value: DocumentValue): TemplateResult<1>;
@@ -0,0 +1,145 @@
1
+ import { i18next } from '@operato/i18n';
2
+ import { css, html, nothing } from 'lit';
3
+ import { currencyChoices, documentAmount } from './document-amount';
4
+ /** The head fields in the order a person fills them. */
5
+ export const HEAD_FIELDS = [
6
+ { name: 'number', label: 'field.invoice-number' },
7
+ { name: 'issueDate', label: 'field.invoice-issue-date' },
8
+ { name: 'supplierName', label: 'field.invoice-supplier-name' },
9
+ { name: 'buyerName', label: 'field.invoice-buyer-name' },
10
+ { name: 'supplierRegistrationNumber', label: 'field.invoice-supplier-registration-number' },
11
+ { name: 'buyerRegistrationNumber', label: 'field.invoice-buyer-registration-number' },
12
+ { name: 'supplierAddress', label: 'field.invoice-supplier-address', prose: true },
13
+ { name: 'buyerAddress', label: 'field.invoice-buyer-address', prose: true },
14
+ { name: 'purchaseOrder', label: 'field.invoice-purchase-order' },
15
+ { name: 'sourceReference', label: 'field.invoice-source-reference' },
16
+ { name: 'paymentTerms', label: 'field.invoice-payment-terms', wide: true, prose: true },
17
+ { name: 'bankDetails', label: 'field.invoice-bank-details', wide: true, prose: true },
18
+ { name: 'notes', label: 'field.invoice-notes', wide: true, prose: true }
19
+ ];
20
+ export const DocumentFormStyles = css `
21
+ .form {
22
+ display: grid;
23
+ grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
24
+ gap: var(--spacing-large) var(--spacing-huge);
25
+ }
26
+ .form label {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--spacing-small);
30
+ font-size: var(--ui-t-micro);
31
+ color: var(--ui-muted);
32
+ }
33
+ .form .wide {
34
+ grid-column: 1 / -1;
35
+ }
36
+ .form input,
37
+ .form select,
38
+ .form textarea {
39
+ box-sizing: border-box;
40
+ width: 100%;
41
+ height: var(--form-element-height-medium);
42
+ font-size: var(--ui-t-body);
43
+ color: var(--ui-ink);
44
+ }
45
+ .form textarea {
46
+ height: auto;
47
+ min-height: calc(var(--form-element-height-medium) * 2);
48
+ }
49
+ table.totals {
50
+ width: 100%;
51
+ border-collapse: collapse;
52
+ font-variant-numeric: tabular-nums;
53
+ }
54
+ table.totals th,
55
+ table.totals td {
56
+ padding: var(--spacing-small) var(--spacing-medium);
57
+ border-bottom: 1px solid var(--ui-line-soft);
58
+ text-align: right;
59
+ font-size: var(--ui-t-body);
60
+ }
61
+ table.totals th:first-child,
62
+ table.totals td:first-child {
63
+ text-align: left;
64
+ }
65
+ table.totals th {
66
+ color: var(--ui-muted);
67
+ font-weight: 400;
68
+ }
69
+ table.totals tfoot td {
70
+ font-weight: 600;
71
+ }
72
+ `;
73
+ /** The head of a document. `dates` holds the fields a kind adds — the due date, the last day of an offer. */
74
+ export function renderDocumentHead(value, editable, change, dates = nothing) {
75
+ const input = (field) => html `<label class=${field.wide ? 'wide' : ''}
76
+ >${i18next.t(field.label)}
77
+ ${field.prose
78
+ ? html `<textarea .value=${value[field.name] ?? ''} ?disabled=${!editable} @input=${(e) => change({ [field.name]: e.target.value })}></textarea>`
79
+ : html `<input
80
+ type=${field.name === 'issueDate' ? 'date' : 'text'}
81
+ .value=${value[field.name] ?? ''}
82
+ ?disabled=${!editable}
83
+ @input=${(e) => change({ [field.name]: e.target.value })}
84
+ />`}</label
85
+ >`;
86
+ return html `<div class="form">
87
+ ${HEAD_FIELDS.slice(0, 2).map(input)}
88
+ <label
89
+ >${i18next.t('field.invoice-direction')}
90
+ <select .value=${value.direction} ?disabled=${!editable} @change=${(e) => change({ direction: e.target.value })}>
91
+ <option value="SALES" ?selected=${value.direction === 'SALES'}>${i18next.t('text.invoice-direction-sales')}</option>
92
+ <option value="PURCHASE" ?selected=${value.direction === 'PURCHASE'}>${i18next.t('text.invoice-direction-purchase')}</option>
93
+ </select></label
94
+ >
95
+ <label
96
+ >${i18next.t('field.invoice-currency')}
97
+ <select ?disabled=${!editable} @change=${(e) => change({ currency: e.target.value })}>
98
+ ${currencyChoices().map(code => html `<option value=${code} ?selected=${value.currency === code}>${code}</option>`)}
99
+ </select></label
100
+ >
101
+ <label
102
+ >${i18next.t('field.invoice-language')}
103
+ <select ?disabled=${!editable} @change=${(e) => change({ language: e.target.value })}>
104
+ <option value="ko" ?selected=${value.language === 'ko'}>${i18next.t('text.invoice-language-ko')}</option>
105
+ <option value="en" ?selected=${value.language === 'en'}>${i18next.t('text.invoice-language-en')}</option>
106
+ </select></label
107
+ >
108
+ ${dates} ${HEAD_FIELDS.slice(2).map(input)}
109
+ </div>`;
110
+ }
111
+ /** Supply and tax of each tax kind and rate, then the document totals. */
112
+ export function renderDocumentTotals(value) {
113
+ const currency = value.currency || 'KRW';
114
+ const kind = (taxKind) => taxKind === 'ZERO_RATED' ? i18next.t('text.invoice-tax-kind-zero-rated') : taxKind === 'EXEMPT' ? i18next.t('text.invoice-tax-kind-exempt') : i18next.t('text.invoice-tax-kind-taxable');
115
+ return html `<table class="totals">
116
+ <thead>
117
+ <tr>
118
+ <th>${i18next.t('field.invoice-tax-kind')}</th>
119
+ <th>${i18next.t('field.invoice-tax-rate')}</th>
120
+ <th>${i18next.t('field.invoice-supply-amount')}</th>
121
+ <th>${i18next.t('field.invoice-tax-amount')}</th>
122
+ </tr>
123
+ </thead>
124
+ <tbody>
125
+ ${(value.taxGroups ?? []).map((group) => html `<tr>
126
+ <td>${kind(group.taxKind)}</td>
127
+ <td>${group.taxRate}%</td>
128
+ <td>${documentAmount(group.supplyAmount, currency)}</td>
129
+ <td>${documentAmount(group.taxAmount, currency)}</td>
130
+ </tr>`)}
131
+ </tbody>
132
+ <tfoot>
133
+ <tr>
134
+ <td colspan="2">${i18next.t('field.invoice-sum')}</td>
135
+ <td>${documentAmount(value.supplyAmount, currency)}</td>
136
+ <td>${documentAmount(value.taxAmount, currency)}</td>
137
+ </tr>
138
+ <tr>
139
+ <td colspan="3">${i18next.t('field.invoice-total-amount')}</td>
140
+ <td>${documentAmount(value.totalAmount, currency)}</td>
141
+ </tr>
142
+ </tfoot>
143
+ </table>`;
144
+ }
145
+ //# sourceMappingURL=document-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-form.js","sourceRoot":"","sources":["../../client/document/document-form.ts"],"names":[],"mappings":"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","sourcesContent":["import { i18next } from '@operato/i18n'\nimport { css, html, nothing, type TemplateResult } from 'lit'\n\nimport { currencyChoices, documentAmount } from './document-amount'\n\n/*\n * The parts every commercial document editor draws — the head, the totals by tax kind and rate. Invoice and quotation\n * editors put them in their own sections; the quotation module imports them from here (ADR-0120 decision 1).\n */\n\nexport type DocumentValue = Record<string, any>\n\n/** The head fields in the order a person fills them. */\nexport const HEAD_FIELDS: { name: string; label: string; wide?: boolean; prose?: boolean }[] = [\n { name: 'number', label: 'field.invoice-number' },\n { name: 'issueDate', label: 'field.invoice-issue-date' },\n { name: 'supplierName', label: 'field.invoice-supplier-name' },\n { name: 'buyerName', label: 'field.invoice-buyer-name' },\n { name: 'supplierRegistrationNumber', label: 'field.invoice-supplier-registration-number' },\n { name: 'buyerRegistrationNumber', label: 'field.invoice-buyer-registration-number' },\n { name: 'supplierAddress', label: 'field.invoice-supplier-address', prose: true },\n { name: 'buyerAddress', label: 'field.invoice-buyer-address', prose: true },\n { name: 'purchaseOrder', label: 'field.invoice-purchase-order' },\n { name: 'sourceReference', label: 'field.invoice-source-reference' },\n { name: 'paymentTerms', label: 'field.invoice-payment-terms', wide: true, prose: true },\n { name: 'bankDetails', label: 'field.invoice-bank-details', wide: true, prose: true },\n { name: 'notes', label: 'field.invoice-notes', wide: true, prose: true }\n]\n\nexport const DocumentFormStyles = css`\n .form {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));\n gap: var(--spacing-large) var(--spacing-huge);\n }\n .form label {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-small);\n font-size: var(--ui-t-micro);\n color: var(--ui-muted);\n }\n .form .wide {\n grid-column: 1 / -1;\n }\n .form input,\n .form select,\n .form textarea {\n box-sizing: border-box;\n width: 100%;\n height: var(--form-element-height-medium);\n font-size: var(--ui-t-body);\n color: var(--ui-ink);\n }\n .form textarea {\n height: auto;\n min-height: calc(var(--form-element-height-medium) * 2);\n }\n table.totals {\n width: 100%;\n border-collapse: collapse;\n font-variant-numeric: tabular-nums;\n }\n table.totals th,\n table.totals td {\n padding: var(--spacing-small) var(--spacing-medium);\n border-bottom: 1px solid var(--ui-line-soft);\n text-align: right;\n font-size: var(--ui-t-body);\n }\n table.totals th:first-child,\n table.totals td:first-child {\n text-align: left;\n }\n table.totals th {\n color: var(--ui-muted);\n font-weight: 400;\n }\n table.totals tfoot td {\n font-weight: 600;\n }\n`\n\n/** The head of a document. `dates` holds the fields a kind adds — the due date, the last day of an offer. */\nexport function renderDocumentHead(value: DocumentValue, editable: boolean, change: (patch: DocumentValue) => void, dates: TemplateResult | typeof nothing = nothing) {\n const input = (field: { name: string; label: string; wide?: boolean; prose?: boolean }) =>\n html`<label class=${field.wide ? 'wide' : ''}\n >${i18next.t(field.label)}\n ${field.prose\n ? html`<textarea .value=${value[field.name] ?? ''} ?disabled=${!editable} @input=${(e: any) => change({ [field.name]: e.target.value })}></textarea>`\n : html`<input\n type=${field.name === 'issueDate' ? 'date' : 'text'}\n .value=${value[field.name] ?? ''}\n ?disabled=${!editable}\n @input=${(e: any) => change({ [field.name]: e.target.value })}\n />`}</label\n >`\n\n return html`<div class=\"form\">\n ${HEAD_FIELDS.slice(0, 2).map(input)}\n <label\n >${i18next.t('field.invoice-direction')}\n <select .value=${value.direction} ?disabled=${!editable} @change=${(e: any) => change({ direction: e.target.value })}>\n <option value=\"SALES\" ?selected=${value.direction === 'SALES'}>${i18next.t('text.invoice-direction-sales')}</option>\n <option value=\"PURCHASE\" ?selected=${value.direction === 'PURCHASE'}>${i18next.t('text.invoice-direction-purchase')}</option>\n </select></label\n >\n <label\n >${i18next.t('field.invoice-currency')}\n <select ?disabled=${!editable} @change=${(e: any) => change({ currency: e.target.value })}>\n ${currencyChoices().map(code => html`<option value=${code} ?selected=${value.currency === code}>${code}</option>`)}\n </select></label\n >\n <label\n >${i18next.t('field.invoice-language')}\n <select ?disabled=${!editable} @change=${(e: any) => change({ language: e.target.value })}>\n <option value=\"ko\" ?selected=${value.language === 'ko'}>${i18next.t('text.invoice-language-ko')}</option>\n <option value=\"en\" ?selected=${value.language === 'en'}>${i18next.t('text.invoice-language-en')}</option>\n </select></label\n >\n ${dates} ${HEAD_FIELDS.slice(2).map(input)}\n </div>`\n}\n\n/** Supply and tax of each tax kind and rate, then the document totals. */\nexport function renderDocumentTotals(value: DocumentValue) {\n const currency = value.currency || 'KRW'\n const kind = (taxKind: string) =>\n taxKind === 'ZERO_RATED' ? i18next.t('text.invoice-tax-kind-zero-rated') : taxKind === 'EXEMPT' ? i18next.t('text.invoice-tax-kind-exempt') : i18next.t('text.invoice-tax-kind-taxable')\n return html`<table class=\"totals\">\n <thead>\n <tr>\n <th>${i18next.t('field.invoice-tax-kind')}</th>\n <th>${i18next.t('field.invoice-tax-rate')}</th>\n <th>${i18next.t('field.invoice-supply-amount')}</th>\n <th>${i18next.t('field.invoice-tax-amount')}</th>\n </tr>\n </thead>\n <tbody>\n ${(value.taxGroups ?? []).map(\n (group: any) =>\n html`<tr>\n <td>${kind(group.taxKind)}</td>\n <td>${group.taxRate}%</td>\n <td>${documentAmount(group.supplyAmount, currency)}</td>\n <td>${documentAmount(group.taxAmount, currency)}</td>\n </tr>`\n )}\n </tbody>\n <tfoot>\n <tr>\n <td colspan=\"2\">${i18next.t('field.invoice-sum')}</td>\n <td>${documentAmount(value.supplyAmount, currency)}</td>\n <td>${documentAmount(value.taxAmount, currency)}</td>\n </tr>\n <tr>\n <td colspan=\"3\">${i18next.t('field.invoice-total-amount')}</td>\n <td>${documentAmount(value.totalAmount, currency)}</td>\n </tr>\n </tfoot>\n </table>`\n}\n"]}
@@ -0,0 +1,104 @@
1
+ import { DataGrist } from '@operato/data-grist';
2
+ /** The grid of a document's lines — typed or pasted like a spreadsheet while the document is a draft. */
3
+ export declare function documentLinesConfig({ editable, received, currency }: {
4
+ editable: boolean;
5
+ received?: boolean;
6
+ currency: string;
7
+ }): {
8
+ rows: {
9
+ appendable: boolean;
10
+ selectable: {
11
+ multiple: boolean;
12
+ };
13
+ accumulator: boolean;
14
+ };
15
+ pagination: {
16
+ infinite: boolean;
17
+ };
18
+ emptyNote: {
19
+ title: string;
20
+ };
21
+ columns: ({
22
+ type: string;
23
+ name: string;
24
+ header: string;
25
+ width: number;
26
+ record: {
27
+ editable: boolean;
28
+ align: string;
29
+ };
30
+ } | {
31
+ type: string;
32
+ gutterName: string;
33
+ multiple?: undefined;
34
+ name?: undefined;
35
+ header?: undefined;
36
+ width?: undefined;
37
+ record?: undefined;
38
+ accumulator?: undefined;
39
+ } | {
40
+ type: string;
41
+ gutterName: string;
42
+ multiple: boolean;
43
+ name?: undefined;
44
+ header?: undefined;
45
+ width?: undefined;
46
+ record?: undefined;
47
+ accumulator?: undefined;
48
+ } | {
49
+ type: string;
50
+ name: string;
51
+ header: string;
52
+ width: number;
53
+ record: {
54
+ editable: boolean;
55
+ options?: undefined;
56
+ align?: undefined;
57
+ renderer?: undefined;
58
+ };
59
+ gutterName?: undefined;
60
+ multiple?: undefined;
61
+ accumulator?: undefined;
62
+ } | {
63
+ type: string;
64
+ name: string;
65
+ header: string;
66
+ width: number;
67
+ record: {
68
+ editable: boolean;
69
+ options: {
70
+ value: string;
71
+ display: string;
72
+ }[];
73
+ align?: undefined;
74
+ renderer?: undefined;
75
+ };
76
+ gutterName?: undefined;
77
+ multiple?: undefined;
78
+ accumulator?: undefined;
79
+ } | {
80
+ type: string;
81
+ name: string;
82
+ header: string;
83
+ width: number;
84
+ record: {
85
+ editable: boolean;
86
+ align: string;
87
+ renderer: (value: number) => string;
88
+ options?: undefined;
89
+ };
90
+ accumulator: string;
91
+ gutterName?: undefined;
92
+ multiple?: undefined;
93
+ })[];
94
+ };
95
+ /** The lines on the grid, as the save mutation takes them; blank rows are left out. */
96
+ export declare function linesOf(grist: DataGrist | null | undefined): {
97
+ receivedTaxAmount?: number;
98
+ description: string;
99
+ quantity: number;
100
+ unitPrice: number;
101
+ discount: number;
102
+ taxKind: any;
103
+ taxRate: number;
104
+ }[];
@@ -0,0 +1,61 @@
1
+ import { i18next } from '@operato/i18n';
2
+ import { documentAmount } from './document-amount';
3
+ /** The grid of a document's lines — typed or pasted like a spreadsheet while the document is a draft. */
4
+ export function documentLinesConfig({ editable, received, currency }) {
5
+ const number = (name, header, width = 110) => ({ type: 'float', name, header, width, record: { editable, align: 'right' } });
6
+ return {
7
+ rows: { appendable: editable, selectable: { multiple: true }, accumulator: true },
8
+ pagination: { infinite: true },
9
+ emptyNote: { title: i18next.t('text.invoice-lines-empty') },
10
+ columns: [
11
+ { type: 'gutter', gutterName: 'sequence' },
12
+ ...(editable ? [{ type: 'gutter', gutterName: 'row-selector', multiple: true }] : []),
13
+ { type: 'string', name: 'description', header: i18next.t('field.invoice-line-description'), width: 280, record: { editable } },
14
+ number('quantity', i18next.t('field.invoice-quantity'), 90),
15
+ number('unitPrice', i18next.t('field.invoice-unit-price'), 120),
16
+ number('discount', i18next.t('field.invoice-discount')),
17
+ {
18
+ type: 'select',
19
+ name: 'taxKind',
20
+ header: i18next.t('field.invoice-tax-kind'),
21
+ width: 100,
22
+ record: {
23
+ editable,
24
+ options: [
25
+ { value: 'TAXABLE', display: i18next.t('text.invoice-tax-kind-taxable') },
26
+ { value: 'ZERO_RATED', display: i18next.t('text.invoice-tax-kind-zero-rated') },
27
+ { value: 'EXEMPT', display: i18next.t('text.invoice-tax-kind-exempt') }
28
+ ]
29
+ }
30
+ },
31
+ number('taxRate', i18next.t('field.invoice-tax-rate-percent'), 90),
32
+ ...(received ? [number('receivedTaxAmount', i18next.t('field.invoice-received-tax-amount'), 120)] : []),
33
+ {
34
+ type: 'float',
35
+ name: 'supplyAmount',
36
+ header: i18next.t('field.invoice-supply-amount'),
37
+ width: 140,
38
+ record: { editable: false, align: 'right', renderer: (value) => documentAmount(value, currency) },
39
+ accumulator: 'sum'
40
+ }
41
+ ]
42
+ };
43
+ }
44
+ /** The lines on the grid, as the save mutation takes them; blank rows are left out. */
45
+ export function linesOf(grist) {
46
+ const records = (grist?.dirtyData?.records ?? []);
47
+ return records
48
+ .filter(record => record.__dirty__ !== '-' && String(record.description ?? '').trim())
49
+ .map(record => ({
50
+ description: String(record.description).trim(),
51
+ quantity: Number(record.quantity ?? 1),
52
+ unitPrice: Number(record.unitPrice ?? 0),
53
+ discount: Number(record.discount ?? 0),
54
+ taxKind: record.taxKind || 'TAXABLE',
55
+ taxRate: Number(record.taxRate ?? 0),
56
+ ...(record.receivedTaxAmount === undefined || record.receivedTaxAmount === null || record.receivedTaxAmount === ''
57
+ ? {}
58
+ : { receivedTaxAmount: Number(record.receivedTaxAmount) })
59
+ }));
60
+ }
61
+ //# sourceMappingURL=document-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-lines.js","sourceRoot":"","sources":["../../client/document/document-lines.ts"],"names":[],"mappings":"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","sourcesContent":["import { DataGrist } from '@operato/data-grist'\nimport { i18next } from '@operato/i18n'\n\nimport { documentAmount } from './document-amount'\n\n/** The grid of a document's lines — typed or pasted like a spreadsheet while the document is a draft. */\nexport function documentLinesConfig({ editable, received, currency }: { editable: boolean; received?: boolean; currency: string }) {\n const number = (name: string, header: string, width = 110) => ({ type: 'float', name, header, width, record: { editable, align: 'right' } })\n return {\n rows: { appendable: editable, selectable: { multiple: true }, accumulator: true },\n pagination: { infinite: true },\n emptyNote: { title: i18next.t('text.invoice-lines-empty') },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n ...(editable ? [{ type: 'gutter', gutterName: 'row-selector', multiple: true }] : []),\n { type: 'string', name: 'description', header: i18next.t('field.invoice-line-description'), width: 280, record: { editable } },\n number('quantity', i18next.t('field.invoice-quantity'), 90),\n number('unitPrice', i18next.t('field.invoice-unit-price'), 120),\n number('discount', i18next.t('field.invoice-discount')),\n {\n type: 'select',\n name: 'taxKind',\n header: i18next.t('field.invoice-tax-kind'),\n width: 100,\n record: {\n editable,\n options: [\n { value: 'TAXABLE', display: i18next.t('text.invoice-tax-kind-taxable') },\n { value: 'ZERO_RATED', display: i18next.t('text.invoice-tax-kind-zero-rated') },\n { value: 'EXEMPT', display: i18next.t('text.invoice-tax-kind-exempt') }\n ]\n }\n },\n number('taxRate', i18next.t('field.invoice-tax-rate-percent'), 90),\n ...(received ? [number('receivedTaxAmount', i18next.t('field.invoice-received-tax-amount'), 120)] : []),\n {\n type: 'float',\n name: 'supplyAmount',\n header: i18next.t('field.invoice-supply-amount'),\n width: 140,\n record: { editable: false, align: 'right', renderer: (value: number) => documentAmount(value, currency) },\n accumulator: 'sum'\n }\n ]\n }\n}\n\n/** The lines on the grid, as the save mutation takes them; blank rows are left out. */\nexport function linesOf(grist: DataGrist | null | undefined) {\n const records = (grist?.dirtyData?.records ?? []) as any[]\n return records\n .filter(record => record.__dirty__ !== '-' && String(record.description ?? '').trim())\n .map(record => ({\n description: String(record.description).trim(),\n quantity: Number(record.quantity ?? 1),\n unitPrice: Number(record.unitPrice ?? 0),\n discount: Number(record.discount ?? 0),\n taxKind: record.taxKind || 'TAXABLE',\n taxRate: Number(record.taxRate ?? 0),\n ...(record.receivedTaxAmount === undefined || record.receivedTaxAmount === null || record.receivedTaxAmount === ''\n ? {}\n : { receivedTaxAmount: Number(record.receivedTaxAmount) })\n }))\n}\n"]}
@@ -0,0 +1,15 @@
1
+ export type PrintedDocument = Record<string, any>;
2
+ /**
3
+ * A commercial document as the buyer reads it, in the document's own language. `title` and `terms` are the kind's
4
+ * words (「청구서」 and its due date, 「견적서」 and its last day); everything else is the shared head and lines.
5
+ * Only escaped business data enters the page; the browser prints it or saves it as PDF.
6
+ */
7
+ export declare function documentHtml(document: PrintedDocument, { title, terms }: {
8
+ title: string;
9
+ terms: {
10
+ label: string;
11
+ value?: string | null;
12
+ }[];
13
+ }): string;
14
+ /** Print through a hidden frame; the browser's dialog saves it as PDF too. */
15
+ export declare function printDocument(html: string): void;
@@ -0,0 +1,60 @@
1
+ import { i18next } from '@operato/i18n';
2
+ import { currencyDigits } from './document-amount';
3
+ const escape = (text) => String(text ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
4
+ /**
5
+ * A commercial document as the buyer reads it, in the document's own language. `title` and `terms` are the kind's
6
+ * words (「청구서」 and its due date, 「견적서」 and its last day); everything else is the shared head and lines.
7
+ * Only escaped business data enters the page; the browser prints it or saves it as PDF.
8
+ */
9
+ export function documentHtml(document, { title, terms }) {
10
+ const lng = document.language === 'en' ? 'en' : 'ko';
11
+ const t = (key) => escape(i18next.t(key, { lng }));
12
+ const digits = currencyDigits(document.currency);
13
+ const amount = (value) => escape(`${Number(value ?? 0).toLocaleString(lng, { minimumFractionDigits: digits, maximumFractionDigits: digits })}`);
14
+ 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');
15
+ 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>`;
16
+ return `<!doctype html><html lang="${lng}"><head><meta charset="utf-8"><title>${escape(document.number)}</title><style>
17
+ @page{size:A4;margin:18mm}
18
+ body{font:13px/1.5 system-ui,sans-serif;color:#000}
19
+ h1{font-size:26px;margin:0}
20
+ h2{font-size:13px;color:#555;margin:0 0 4px}
21
+ header,.parties{display:flex;justify-content:space-between;gap:32px}
22
+ .parties section{flex:1}
23
+ table{width:100%;border-collapse:collapse;margin:24px 0}
24
+ th,td{padding:8px;border-bottom:1px solid #999;text-align:right;font-variant-numeric:tabular-nums}
25
+ th:first-child,td:first-child{text-align:left}
26
+ .text{white-space:pre-wrap}
27
+ .totals{width:50%;margin-left:auto}
28
+ .grand td{font-weight:600;font-size:15px}
29
+ </style></head><body>
30
+ <header><h1>${escape(title)}</h1><div><p>${t('field.invoice-number')}: ${escape(document.number)}</p><p>${t('field.invoice-issue-date')}: ${escape(document.issueDate)}</p>${terms
31
+ .map(one => `<p>${escape(one.label)}: ${escape(one.value)}</p>`)
32
+ .join('')}</div></header>
33
+ <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>
34
+ ${document.purchaseOrder ? `<p>${t('field.invoice-purchase-order')}: ${escape(document.purchaseOrder)}</p>` : ''}
35
+ <table><thead><tr><th>${t('field.invoice-line-description')}</th><th>${t('field.invoice-quantity')}</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 ?? [])
36
+ .map((item) => `<tr><td>${escape(item.description)}</td><td>${escape(item.quantity)}</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>`)
37
+ .join('')}</tbody></table>
38
+ <table class="totals"><tbody>${(document.taxGroups ?? [])
39
+ .map((group) => `<tr><td>${kind(group.taxKind)} ${escape(group.taxRate)}%</td><td>${amount(group.supplyAmount)}</td><td>${amount(group.taxAmount)}</td></tr>`)
40
+ .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')} (${escape(document.currency)})</td><td>${amount(document.totalAmount)}</td></tr></tbody></table>
41
+ ${['paymentTerms', 'bankDetails', 'notes']
42
+ .filter(key => document[key])
43
+ .map(key => `<p class="text">${escape(document[key])}</p>`)
44
+ .join('')}
45
+ </body></html>`;
46
+ }
47
+ /** Print through a hidden frame; the browser's dialog saves it as PDF too. */
48
+ export function printDocument(html) {
49
+ const frame = window.document.createElement('iframe');
50
+ frame.style.cssText = 'position:fixed;width:0;height:0;border:0;';
51
+ frame.onload = () => {
52
+ const view = frame.contentWindow;
53
+ view.addEventListener('afterprint', () => frame.remove(), { once: true });
54
+ view.focus();
55
+ view.print();
56
+ };
57
+ frame.srcdoc = html;
58
+ window.document.body.append(frame);
59
+ }
60
+ //# sourceMappingURL=document-print.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-print.js","sourceRoot":"","sources":["../../client/document/document-print.ts"],"names":[],"mappings":"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","sourcesContent":["import { i18next } from '@operato/i18n'\n\nimport { currencyDigits } from './document-amount'\n\nconst escape = (text: unknown) => String(text ?? '').replace(/[&<>\"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c]!)\n\nexport type PrintedDocument = Record<string, any>\n\n/**\n * A commercial document as the buyer reads it, in the document's own language. `title` and `terms` are the kind's\n * words (「청구서」 and its due date, 「견적서」 and its last day); everything else is the shared head and lines.\n * Only escaped business data enters the page; the browser prints it or saves it as PDF.\n */\nexport function documentHtml(document: PrintedDocument, { title, terms }: { title: string; terms: { label: string; value?: string | null }[] }) {\n const lng = document.language === 'en' ? 'en' : 'ko'\n const t = (key: string) => escape(i18next.t(key, { lng }))\n const digits = currencyDigits(document.currency)\n const amount = (value: number) => escape(`${Number(value ?? 0).toLocaleString(lng, { minimumFractionDigits: digits, maximumFractionDigits: digits })}`)\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 `<!doctype html><html lang=\"${lng}\"><head><meta charset=\"utf-8\"><title>${escape(document.number)}</title><style>\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 </style></head><body>\n <header><h1>${escape(title)}</h1><div><p>${t('field.invoice-number')}: ${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-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>${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>${(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 )} (${escape(document.currency)})</td><td>${amount(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 </body></html>`\n}\n\n/** Print through a hidden frame; the browser's dialog saves it as PDF too. */\nexport function printDocument(html: string) {\n const frame = window.document.createElement('iframe')\n frame.style.cssText = 'position:fixed;width:0;height:0;border:0;'\n frame.onload = () => {\n const view = frame.contentWindow!\n view.addEventListener('afterprint', () => frame.remove(), { once: true })\n view.focus()\n view.print()\n }\n frame.srcdoc = html\n window.document.body.append(frame)\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { LitElement } from 'lit';
2
+ import { type Entity360DoorAnswer, type Entity360Subject } from '@operato/layout';
3
+ /** The other party: the buyer of a sale, the supplier of a purchase. */
4
+ export declare const counterpartOf: (value: {
5
+ direction?: string;
6
+ supplierName?: string;
7
+ buyerName?: string;
8
+ }) => string;
9
+ export declare const invoiceStatusWord: (status?: string) => string;
10
+ /** An invoice's 360 (ADR-0067). The module draws the invoice; an application hangs its own sections on 'invoice'. */
11
+ export declare function registerInvoiceKind(): void;
12
+ export declare class InvoiceOverviewSection extends LitElement {
13
+ static styles: import("lit").CSSResult[];
14
+ subject?: Entity360Subject;
15
+ answer?: Entity360DoorAnswer<any>;
16
+ updated(changed: Map<string, unknown>): void;
17
+ private read;
18
+ render(): import("lit-html").TemplateResult<1>;
19
+ }