@things-factory/sales 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/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/quotation-editor.d.ts +50 -6
- package/dist-client/pages/quotation-editor.js +303 -33
- package/dist-client/pages/quotation-editor.js.map +1 -1
- package/dist-client/pages/sales-product-list.d.ts +54 -0
- package/dist-client/pages/sales-product-list.js +202 -0
- package/dist-client/pages/sales-product-list.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/sales-product-pick.d.ts +51 -0
- package/dist-client/sales-product-pick.js +210 -0
- package/dist-client/sales-product-pick.js.map +1 -0
- package/dist-client/sales-product-words.d.ts +2 -0
- package/dist-client/sales-product-words.js +8 -0
- package/dist-client/sales-product-words.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +6 -2
- package/dist-server/service/index.js +8 -3
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/quotation/quotation-query.d.ts +3 -0
- package/dist-server/service/quotation/quotation-query.js +36 -0
- package/dist-server/service/quotation/quotation-query.js.map +1 -1
- package/dist-server/service/quotation/quotation-service.d.ts +1 -0
- package/dist-server/service/quotation/quotation-service.js +5 -4
- package/dist-server/service/quotation/quotation-service.js.map +1 -1
- package/dist-server/service/sales-product/index.d.ts +6 -0
- package/dist-server/service/sales-product/index.js +10 -0
- package/dist-server/service/sales-product/index.js.map +1 -0
- package/dist-server/service/sales-product/sales-product-mutation.d.ts +6 -0
- package/dist-server/service/sales-product/sales-product-mutation.js +46 -0
- package/dist-server/service/sales-product/sales-product-mutation.js.map +1 -0
- package/dist-server/service/sales-product/sales-product-query.d.ts +11 -0
- package/dist-server/service/sales-product/sales-product-query.js +78 -0
- package/dist-server/service/sales-product/sales-product-query.js.map +1 -0
- package/dist-server/service/sales-product/sales-product-rules.d.ts +8 -0
- package/dist-server/service/sales-product/sales-product-rules.js +26 -0
- package/dist-server/service/sales-product/sales-product-rules.js.map +1 -0
- package/dist-server/service/sales-product/sales-product-service.d.ts +10 -0
- package/dist-server/service/sales-product/sales-product-service.js +86 -0
- package/dist-server/service/sales-product/sales-product-service.js.map +1 -0
- package/dist-server/service/sales-product/sales-product-type.d.ts +22 -0
- package/dist-server/service/sales-product/sales-product-type.js +81 -0
- package/dist-server/service/sales-product/sales-product-type.js.map +1 -0
- package/dist-server/service/sales-product/sales-product.d.ts +32 -0
- package/dist-server/service/sales-product/sales-product.js +126 -0
- package/dist-server/service/sales-product/sales-product.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/things-factory.config.js +2 -1
- package/translations/en.json +33 -3
- package/translations/es.json +33 -3
- package/translations/ja.json +33 -3
- package/translations/ko.json +34 -4
- package/translations/ms.json +33 -3
- package/translations/zh.json +33 -3
package/dist-client/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { configureQuotationSection, type QuotationSection } from './quotation-at
|
|
|
2
2
|
export { quotationStatusWord } from './quotation-words.js';
|
|
3
3
|
export { configureOpportunitySection, type OpportunitySection } from './opportunity-attach.js';
|
|
4
4
|
export { opportunityStageWord } from './opportunity-words.js';
|
|
5
|
+
export { salesProductKindWord } from './sales-product-words.js';
|
package/dist-client/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { configureQuotationSection } from './quotation-attach.js';
|
|
|
2
2
|
export { quotationStatusWord } from './quotation-words.js';
|
|
3
3
|
export { configureOpportunitySection } from './opportunity-attach.js';
|
|
4
4
|
export { opportunityStageWord } from './opportunity-words.js';
|
|
5
|
+
export { salesProductKindWord } from './sales-product-words.js';
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/dist-client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAyB,MAAM,uBAAuB,CAAA;AACxF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,2BAA2B,EAA2B,MAAM,yBAAyB,CAAA;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA","sourcesContent":["export { configureQuotationSection, type QuotationSection } from './quotation-attach.js'\nexport { quotationStatusWord } from './quotation-words.js'\nexport { configureOpportunitySection, type OpportunitySection } from './opportunity-attach.js'\nexport { opportunityStageWord } from './opportunity-words.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAyB,MAAM,uBAAuB,CAAA;AACxF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,2BAA2B,EAA2B,MAAM,yBAAyB,CAAA;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA","sourcesContent":["export { configureQuotationSection, type QuotationSection } from './quotation-attach.js'\nexport { quotationStatusWord } from './quotation-words.js'\nexport { configureOpportunitySection, type OpportunitySection } from './opportunity-attach.js'\nexport { opportunityStageWord } from './opportunity-words.js'\nexport { salesProductKindWord } from './sales-product-words.js'\n"]}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import '@operato/data-grist/ox-grist.js';
|
|
2
2
|
import '@operato/list-state/ox-page-state.js';
|
|
3
|
+
import '@operato/input/ox-select.js';
|
|
4
|
+
import '@operato/popup/ox-popup-list.js';
|
|
3
5
|
import { PageView } from '@operato/shell';
|
|
4
|
-
import { type DocumentValue } from '@things-factory/invoice/dist-client';
|
|
6
|
+
import { type DocumentDefaults, type DocumentParty, type DocumentValue } from '@things-factory/invoice/dist-client';
|
|
7
|
+
import '@things-factory/invoice/dist-client/document/document-party.js';
|
|
8
|
+
import '../sales-product-pick';
|
|
9
|
+
/** The next quotation number of a year after those given - 「Q2026-001」, 「Q2026-002」 … */
|
|
10
|
+
export declare function nextQuotationNumber(numbers: string[], year: number): string;
|
|
5
11
|
declare const QuotationEditor_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
|
|
6
12
|
/**
|
|
7
13
|
* 견적 편집 — the same head, lines and totals as an invoice, valid until a day.
|
|
@@ -22,10 +28,16 @@ export declare class QuotationEditor extends QuotationEditor_base {
|
|
|
22
28
|
opportunities: {
|
|
23
29
|
id: string;
|
|
24
30
|
name: string;
|
|
25
|
-
counterparty?:
|
|
26
|
-
name: string;
|
|
27
|
-
};
|
|
31
|
+
counterparty?: DocumentParty | null;
|
|
28
32
|
}[];
|
|
33
|
+
/** The company's words a new quotation starts with, the terms it offers and how long an offer holds (ADR-0170). */
|
|
34
|
+
defaults: DocumentDefaults | null;
|
|
35
|
+
/** The counterparty the buyer was taken from on this screen - shown in the picker. */
|
|
36
|
+
party: DocumentParty | null;
|
|
37
|
+
/** The totals of the lines as typed - reckoned as the quotation will keep them, shown before it is saved. */
|
|
38
|
+
amounts: Partial<DocumentValue>;
|
|
39
|
+
/** Whether this person may write invoices - an accepted quotation then makes one. */
|
|
40
|
+
invoiceWriter: boolean;
|
|
29
41
|
private grist?;
|
|
30
42
|
get editable(): boolean;
|
|
31
43
|
get context(): {
|
|
@@ -38,6 +50,27 @@ export declare class QuotationEditor extends QuotationEditor_base {
|
|
|
38
50
|
pageUpdated(changes: any, lifecycle: any): Promise<void>;
|
|
39
51
|
render(): import("lit-html").TemplateResult<1>;
|
|
40
52
|
private renderDecline;
|
|
53
|
+
/**
|
|
54
|
+
* A line made from a product, after the lines there are - a copy the person then changes like any line (ADR-0171) -
|
|
55
|
+
* and the totals reckoned with it.
|
|
56
|
+
*/
|
|
57
|
+
private addProduct;
|
|
58
|
+
/** The discount of the whole quotation as typed, and the totals after it. */
|
|
59
|
+
private discount;
|
|
60
|
+
/** A change of the head; a new currency reckons the lines in its minor unit; the last day follows a moved issue day. */
|
|
61
|
+
private edit;
|
|
62
|
+
/** The opportunity it answers - found by typing; choosing one takes its counterparty as the buyer when none is written. */
|
|
63
|
+
private renderOpportunity;
|
|
64
|
+
/** A counterparty as the other side: the buyer of a sale, the supplier of a purchase. */
|
|
65
|
+
private takeParty;
|
|
66
|
+
/**
|
|
67
|
+
* What a new quotation starts with, so a person writes only what is new: the next number of the year, the last day an
|
|
68
|
+
* offer holds, the company's own words (ADR-0170), and what they leave blank of our side as the last sales quotation
|
|
69
|
+
* wrote it.
|
|
70
|
+
*/
|
|
71
|
+
private prefill;
|
|
72
|
+
/** Days an offer holds - the company's, or thirty. */
|
|
73
|
+
private get validDays();
|
|
41
74
|
private opportunityChoices;
|
|
42
75
|
private readOpportunities;
|
|
43
76
|
private configure;
|
|
@@ -46,9 +79,20 @@ export declare class QuotationEditor extends QuotationEditor_base {
|
|
|
46
79
|
private run;
|
|
47
80
|
private input;
|
|
48
81
|
private save;
|
|
49
|
-
/**
|
|
82
|
+
/** Asked first - to whom and how much, fixed after - then save what is on screen and send it. */
|
|
50
83
|
private send;
|
|
51
84
|
private change;
|
|
52
|
-
|
|
85
|
+
/**
|
|
86
|
+
* An invoice draft of an accepted quotation (ADR-0170 decision 11): its parties, currency, language, terms, account,
|
|
87
|
+
* notes, discount and lines as the quotation has them, issued today and due the company's days after, the quotation
|
|
88
|
+
* named as its source - then opened to be checked and issued. The counterparty is the one of the other party's name,
|
|
89
|
+
* as the invoice editor links it.
|
|
90
|
+
*/
|
|
91
|
+
private makeInvoice;
|
|
92
|
+
/**
|
|
93
|
+
* The saved quotation as it goes out (ADR-0170 decision 10) - the server's page in the language chosen there, the
|
|
94
|
+
* quotation's own first, to download as PDF or print. What is on screen and not saved is not in it.
|
|
95
|
+
*/
|
|
96
|
+
private preview;
|
|
53
97
|
}
|
|
54
98
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import '@operato/data-grist/ox-grist.js';
|
|
3
3
|
import '@operato/list-state/ox-page-state.js';
|
|
4
|
+
import '@operato/input/ox-select.js';
|
|
5
|
+
import '@operato/popup/ox-popup-list.js';
|
|
4
6
|
import gql from 'graphql-tag';
|
|
5
7
|
import { css, html, nothing } from 'lit';
|
|
6
8
|
import { customElement, query, state } from 'lit/decorators.js';
|
|
@@ -8,16 +10,21 @@ import { DataGrist } from '@operato/data-grist';
|
|
|
8
10
|
import { client } from '@operato/graphql';
|
|
9
11
|
import { i18next, localize } from '@operato/i18n';
|
|
10
12
|
import { notify } from '@operato/layout';
|
|
13
|
+
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
11
14
|
import { navigate, PageView } from '@operato/shell';
|
|
12
15
|
import { ButtonStyles, CommonGristStyles, DocumentPageStyles, FormFieldStyles, ScrollbarStyles } from '@operato/styles';
|
|
13
|
-
import {
|
|
16
|
+
import { hasPrivilege } from '@things-factory/auth-base/dist-client';
|
|
17
|
+
import { daysAfter, documentAmount, DocumentFormStyles, documentLinesConfig, linesOf, onLineChange, openDocumentPreview, reckonLines, renderDocumentHead, renderDocumentTotals, documentDefaults, headFromDefaults, nextInvoiceNumber, OUR_SIDE_FIELDS, oursFromLast, partyOfName } from '@things-factory/invoice/dist-client';
|
|
18
|
+
import '@things-factory/invoice/dist-client/document/document-party.js';
|
|
14
19
|
import { quotationSection } from '../quotation-attach';
|
|
20
|
+
import '../sales-product-pick';
|
|
21
|
+
import { lineOfProduct } from '../sales-product-pick';
|
|
15
22
|
import { quotationStatusWord } from '../quotation-words';
|
|
16
23
|
const FIELDS = `
|
|
17
24
|
id number version status validUntil outcomeReason opportunityId opportunity { id name } direction currency language supplierName supplierAddress supplierRegistrationNumber
|
|
18
25
|
buyerName buyerAddress buyerRegistrationNumber issueDate purchaseOrder paymentTerms bankDetails notes sourceReference
|
|
19
|
-
supplyAmount taxAmount totalAmount taxRounding currencyDigits
|
|
20
|
-
items { position description quantity unitPrice discount taxKind taxRate supplyAmount }
|
|
26
|
+
discountAmount lineSupplyAmount supplyAmount taxAmount totalAmount taxRounding currencyDigits
|
|
27
|
+
items { position description quantity unit unitPrice discount taxKind taxRate supplyAmount }
|
|
21
28
|
taxGroups { taxKind taxRate supplyAmount taxAmount }
|
|
22
29
|
`;
|
|
23
30
|
const HEAD = [
|
|
@@ -38,9 +45,35 @@ const HEAD = [
|
|
|
38
45
|
'notes',
|
|
39
46
|
'sourceReference',
|
|
40
47
|
'validUntil',
|
|
41
|
-
'opportunityId'
|
|
48
|
+
'opportunityId',
|
|
49
|
+
'discountAmount'
|
|
42
50
|
];
|
|
43
51
|
const today = () => new Date().toLocaleDateString('en-CA');
|
|
52
|
+
/** What an invoice made from a quotation takes of its head as it is. */
|
|
53
|
+
const INVOICE_HEAD = [
|
|
54
|
+
'direction',
|
|
55
|
+
'currency',
|
|
56
|
+
'language',
|
|
57
|
+
'supplierName',
|
|
58
|
+
'supplierAddress',
|
|
59
|
+
'supplierRegistrationNumber',
|
|
60
|
+
'buyerName',
|
|
61
|
+
'buyerAddress',
|
|
62
|
+
'buyerRegistrationNumber',
|
|
63
|
+
'paymentTerms',
|
|
64
|
+
'bankDetails',
|
|
65
|
+
'notes'
|
|
66
|
+
];
|
|
67
|
+
/** How long an offer holds unless the person writes another last day. */
|
|
68
|
+
const VALID_DAYS = 30;
|
|
69
|
+
/** Days an invoice made from a quotation is due unless the company says otherwise - the invoice editor's. */
|
|
70
|
+
const PAYMENT_DAYS = 30;
|
|
71
|
+
/** The next quotation number of a year after those given - 「Q2026-001」, 「Q2026-002」 … */
|
|
72
|
+
export function nextQuotationNumber(numbers, year) {
|
|
73
|
+
const prefix = `Q${year}-`;
|
|
74
|
+
const last = Math.max(0, ...numbers.filter(one => one?.startsWith(prefix)).map(one => Number(one.slice(prefix.length)) || 0));
|
|
75
|
+
return `${prefix}${String(last + 1).padStart(3, '0')}`;
|
|
76
|
+
}
|
|
44
77
|
/**
|
|
45
78
|
* 견적 편집 — the same head, lines and totals as an invoice, valid until a day.
|
|
46
79
|
*
|
|
@@ -58,6 +91,14 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
58
91
|
this.reason = '';
|
|
59
92
|
/** The open opportunities a draft may answer, and the one it answers when that one is closed. */
|
|
60
93
|
this.opportunities = [];
|
|
94
|
+
/** The company's words a new quotation starts with, the terms it offers and how long an offer holds (ADR-0170). */
|
|
95
|
+
this.defaults = null;
|
|
96
|
+
/** The counterparty the buyer was taken from on this screen - shown in the picker. */
|
|
97
|
+
this.party = null;
|
|
98
|
+
/** The totals of the lines as typed - reckoned as the quotation will keep them, shown before it is saved. */
|
|
99
|
+
this.amounts = {};
|
|
100
|
+
/** Whether this person may write invoices - an accepted quotation then makes one. */
|
|
101
|
+
this.invoiceWriter = false;
|
|
61
102
|
}
|
|
62
103
|
static { this.styles = [
|
|
63
104
|
FormFieldStyles,
|
|
@@ -70,6 +111,16 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
70
111
|
ox-grist {
|
|
71
112
|
height: calc(var(--form-element-height-medium) * 10);
|
|
72
113
|
}
|
|
114
|
+
/* 상품에서 추가 sits over the lines, as wide as a head field. */
|
|
115
|
+
.pick {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
gap: var(--spacing-small);
|
|
119
|
+
max-width: 480px;
|
|
120
|
+
margin-bottom: var(--spacing-medium);
|
|
121
|
+
font-size: var(--ui-t-micro);
|
|
122
|
+
color: var(--ui-muted);
|
|
123
|
+
}
|
|
73
124
|
`
|
|
74
125
|
]; }
|
|
75
126
|
get editable() {
|
|
@@ -88,10 +139,12 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
88
139
|
else
|
|
89
140
|
actions.push({ title: i18next.t('button.quotation-accept'), emphasis: { raised: true }, action: () => this.change('acceptQuotation') });
|
|
90
141
|
}
|
|
142
|
+
if (status === 'ACCEPTED' && this.invoiceWriter)
|
|
143
|
+
actions.push({ title: i18next.t('button.quotation-make-invoice'), icon: 'receipt_long', emphasis: { raised: true }, action: () => this.makeInvoice() });
|
|
91
144
|
if (['SENT', 'REJECTED', 'EXPIRED'].includes(status))
|
|
92
145
|
actions.push({ title: i18next.t('button.quotation-revise'), icon: 'edit_document', action: () => this.change('reviseQuotation') });
|
|
93
146
|
if (this.quotation)
|
|
94
|
-
actions.push({ title: i18next.t('button.
|
|
147
|
+
actions.push({ title: i18next.t('button.invoice-preview'), icon: 'preview', action: () => this.preview() });
|
|
95
148
|
return {
|
|
96
149
|
title: this.quotation ? `${i18next.t('title.quotation')} ${this.quotation.number} · v${this.quotation.version}` : i18next.t('title.quotation-new'),
|
|
97
150
|
...(this.quotation ? { description: quotationStatusWord(status) } : {}),
|
|
@@ -107,11 +160,20 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
107
160
|
const id = lifecycle?.params?.id;
|
|
108
161
|
if (!this.opportunities.length)
|
|
109
162
|
await this.readOpportunities();
|
|
163
|
+
if (!this.defaults)
|
|
164
|
+
this.defaults = await documentDefaults();
|
|
165
|
+
hasPrivilege({ category: 'invoice', privilege: 'mutation', domainOwnerGranted: true }).then(writer => {
|
|
166
|
+
this.invoiceWriter = !!writer;
|
|
167
|
+
this.updateContext();
|
|
168
|
+
});
|
|
110
169
|
if (!id) {
|
|
111
170
|
this.quotation = null;
|
|
112
171
|
/* Written from an opportunity, the draft answers it. */
|
|
113
172
|
this.form = { ...blank(), opportunityId: lifecycle?.params?.opportunity ?? '' };
|
|
173
|
+
this.amounts = {};
|
|
174
|
+
this.party = null;
|
|
114
175
|
this.configure();
|
|
176
|
+
await this.prefill();
|
|
115
177
|
this.updateContext();
|
|
116
178
|
return;
|
|
117
179
|
}
|
|
@@ -125,28 +187,38 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
125
187
|
<div class="page">
|
|
126
188
|
<section class="page-section">
|
|
127
189
|
<header class="page-section-header"><h3>${i18next.t('text.quotation-section-head')}</h3></header>
|
|
128
|
-
${renderDocumentHead(this.form, editable, patch =>
|
|
190
|
+
${renderDocumentHead(this.form, editable, patch => this.edit(patch), html `<label
|
|
129
191
|
>${i18next.t('field.quotation-valid-until')}
|
|
130
192
|
<input type="date" .value=${this.form.validUntil ?? ''} ?disabled=${!editable} @input=${(e) => (this.form = { ...this.form, validUntil: e.target.value })}
|
|
131
193
|
/></label>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</label
|
|
194
|
+
${this.renderOpportunity(editable)}`, 'field.quotation-number', {
|
|
195
|
+
terms: this.defaults?.paymentTerms,
|
|
196
|
+
/* In the group it fills - the buyer's of a sale, the supplier's of a purchase. */
|
|
197
|
+
party: html `<label
|
|
198
|
+
>${i18next.t('field.invoice-counterparty')}
|
|
199
|
+
<document-party-select .party=${this.party} ?disabled=${!editable} @party=${(e) => this.takeParty(e.detail)}></document-party-select>
|
|
200
|
+
</label>`
|
|
201
|
+
})}
|
|
139
202
|
</section>
|
|
140
203
|
<section class="page-section">
|
|
141
204
|
<header class="page-section-header"><h3>${i18next.t('text.quotation-section-lines')}</h3></header>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
${renderDocumentTotals(this.quotation)}
|
|
148
|
-
</section>`
|
|
205
|
+
${editable
|
|
206
|
+
? html `<label class="pick"
|
|
207
|
+
>${i18next.t('field.sales-product-pick')}
|
|
208
|
+
<sales-product-pick .currency=${this.form.currency || 'KRW'} @product=${(e) => this.addProduct(e.detail)}></sales-product-pick>
|
|
209
|
+
</label>`
|
|
149
210
|
: nothing}
|
|
211
|
+
<ox-grist
|
|
212
|
+
mode="GRID"
|
|
213
|
+
.config=${this.gristConfig}
|
|
214
|
+
.fetchHandler=${() => ({ total: this.form.items.length, records: this.form.items })}
|
|
215
|
+
@record-change=${(e) => (this.amounts = onLineChange(this.grist, e, this.form.currency, false, this.form.discountAmount))}
|
|
216
|
+
></ox-grist>
|
|
217
|
+
</section>
|
|
218
|
+
<section class="page-section">
|
|
219
|
+
<header class="page-section-header"><h3>${i18next.t('text.quotation-section-totals')}</h3></header>
|
|
220
|
+
${renderDocumentTotals({ ...this.amounts, currency: this.form.currency }, { value: this.form.discountAmount, editable, change: value => this.discount(value) })}
|
|
221
|
+
</section>
|
|
150
222
|
${this.quotation?.status === 'SENT' ? this.renderDecline() : nothing}
|
|
151
223
|
${this.quotation?.outcomeReason
|
|
152
224
|
? html `<section class="page-section">
|
|
@@ -179,6 +251,109 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
179
251
|
</div>
|
|
180
252
|
</section>`;
|
|
181
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* A line made from a product, after the lines there are - a copy the person then changes like any line (ADR-0171) -
|
|
256
|
+
* and the totals reckoned with it.
|
|
257
|
+
*/
|
|
258
|
+
async addProduct(product) {
|
|
259
|
+
const grist = this.grist;
|
|
260
|
+
if (!grist || !product)
|
|
261
|
+
return;
|
|
262
|
+
grist.addRecord(lineOfProduct(product));
|
|
263
|
+
await grist.updateComplete;
|
|
264
|
+
this.amounts = reckonLines(grist, this.form.currency, false, this.form.discountAmount);
|
|
265
|
+
grist.refresh();
|
|
266
|
+
}
|
|
267
|
+
/** The discount of the whole quotation as typed, and the totals after it. */
|
|
268
|
+
discount(value) {
|
|
269
|
+
this.form = { ...this.form, discountAmount: value };
|
|
270
|
+
this.amounts = reckonLines(this.grist, this.form.currency, false, value);
|
|
271
|
+
}
|
|
272
|
+
/** A change of the head; a new currency reckons the lines in its minor unit; the last day follows a moved issue day. */
|
|
273
|
+
edit(patch) {
|
|
274
|
+
const currency = this.form.currency;
|
|
275
|
+
const following = patch.issueDate && this.form.validUntil === daysAfter(this.form.issueDate, this.validDays);
|
|
276
|
+
this.form = { ...this.form, ...patch, ...(following ? { validUntil: daysAfter(patch.issueDate, this.validDays) } : {}) };
|
|
277
|
+
if (patch.currency && patch.currency !== currency) {
|
|
278
|
+
this.configure();
|
|
279
|
+
this.amounts = reckonLines(this.grist, this.form.currency, false, this.form.discountAmount);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** The opportunity it answers - found by typing; choosing one takes its counterparty as the buyer when none is written. */
|
|
283
|
+
renderOpportunity(editable) {
|
|
284
|
+
const choices = this.opportunityChoices();
|
|
285
|
+
const chosen = choices.find(one => one.id === this.form.opportunityId);
|
|
286
|
+
const label = (one) => `${one.name}${one.counterparty?.name ? ` · ${one.counterparty.name}` : ''}`;
|
|
287
|
+
return html `<label
|
|
288
|
+
>${i18next.t('field.quotation-opportunity')}
|
|
289
|
+
<ox-select
|
|
290
|
+
.value=${this.form.opportunityId ?? ''}
|
|
291
|
+
?disabled=${!editable}
|
|
292
|
+
aria-label=${i18next.t('field.quotation-opportunity')}
|
|
293
|
+
@change=${(e) => {
|
|
294
|
+
const id = String(e.detail ?? '');
|
|
295
|
+
if (id === (this.form.opportunityId ?? ''))
|
|
296
|
+
return;
|
|
297
|
+
const opportunity = choices.find(one => one.id === id);
|
|
298
|
+
this.form = { ...this.form, opportunityId: id };
|
|
299
|
+
if (opportunity?.counterparty && !String(this.form.buyerName ?? '').trim())
|
|
300
|
+
this.takeParty(opportunity.counterparty);
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
<span slot="label">${chosen ? label(chosen) : '—'}</span>
|
|
304
|
+
<ox-popup-list align-left nowrap with-search>
|
|
305
|
+
<div option value="">—</div>
|
|
306
|
+
${choices.map(one => html `<div option value=${one.id}>${label(one)}</div>`)}
|
|
307
|
+
</ox-popup-list>
|
|
308
|
+
</ox-select>
|
|
309
|
+
</label>`;
|
|
310
|
+
}
|
|
311
|
+
/** A counterparty as the other side: the buyer of a sale, the supplier of a purchase. */
|
|
312
|
+
takeParty(party) {
|
|
313
|
+
this.party = party;
|
|
314
|
+
if (!party)
|
|
315
|
+
return;
|
|
316
|
+
const side = this.form.direction === 'PURCHASE' ? 'supplier' : 'buyer';
|
|
317
|
+
this.form = { ...this.form, [`${side}Name`]: party.name, [`${side}RegistrationNumber`]: party.businessNumber ?? this.form[`${side}RegistrationNumber`] ?? '' };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* What a new quotation starts with, so a person writes only what is new: the next number of the year, the last day an
|
|
321
|
+
* offer holds, the company's own words (ADR-0170), and what they leave blank of our side as the last sales quotation
|
|
322
|
+
* wrote it.
|
|
323
|
+
*/
|
|
324
|
+
async prefill() {
|
|
325
|
+
try {
|
|
326
|
+
const year = Number(this.form.issueDate.slice(0, 4));
|
|
327
|
+
const response = await client.query({
|
|
328
|
+
query: gql `
|
|
329
|
+
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
330
|
+
numbered: quotations(filters: $filters, pagination: { page: 1, limit: 1000 }) { items { number } }
|
|
331
|
+
last: quotations(pagination: $pagination, sortings: $sortings) { items { direction ${OUR_SIDE_FIELDS.join(' ')} } }
|
|
332
|
+
}
|
|
333
|
+
`,
|
|
334
|
+
variables: { filters: [{ name: 'number', operator: 'like', value: `Q${year}-%` }], pagination: { page: 1, limit: 1 }, sortings: [{ name: 'createdAt', desc: true }] },
|
|
335
|
+
fetchPolicy: 'no-cache'
|
|
336
|
+
});
|
|
337
|
+
const last = (response.data?.last?.items ?? []).find((one) => one.direction === 'SALES');
|
|
338
|
+
this.form = { ...this.form, ...headFromDefaults(this.form, this.defaults) };
|
|
339
|
+
this.form = {
|
|
340
|
+
...this.form,
|
|
341
|
+
...oursFromLast(this.form, last),
|
|
342
|
+
number: this.form.number || nextQuotationNumber((response.data?.numbered?.items ?? []).map((one) => one.number), year),
|
|
343
|
+
validUntil: this.form.validUntil || daysAfter(this.form.issueDate, this.validDays)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
/* Nothing to start from - the person writes it, as before. */
|
|
348
|
+
}
|
|
349
|
+
const opportunity = this.opportunities.find(one => one.id === this.form.opportunityId);
|
|
350
|
+
if (opportunity?.counterparty && !String(this.form.buyerName ?? '').trim())
|
|
351
|
+
this.takeParty(opportunity.counterparty);
|
|
352
|
+
}
|
|
353
|
+
/** Days an offer holds - the company's, or thirty. */
|
|
354
|
+
get validDays() {
|
|
355
|
+
return this.defaults?.validDays || VALID_DAYS;
|
|
356
|
+
}
|
|
182
357
|
opportunityChoices() {
|
|
183
358
|
const answered = this.quotation?.opportunity;
|
|
184
359
|
return answered && !this.opportunities.some(one => one.id === answered.id) ? [answered, ...this.opportunities] : this.opportunities;
|
|
@@ -189,7 +364,7 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
189
364
|
query: gql `
|
|
190
365
|
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
191
366
|
opportunities(filters: $filters, pagination: $pagination, sortings: $sortings) {
|
|
192
|
-
items { id name counterparty { name } }
|
|
367
|
+
items { id name counterparty { id name businessNumber } }
|
|
193
368
|
}
|
|
194
369
|
}
|
|
195
370
|
`,
|
|
@@ -231,6 +406,7 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
231
406
|
show(quotation) {
|
|
232
407
|
this.quotation = quotation;
|
|
233
408
|
this.form = { ...quotation, items: quotation.items.map((item) => ({ ...item })) };
|
|
409
|
+
this.amounts = quotation;
|
|
234
410
|
this.reason = '';
|
|
235
411
|
this.configure();
|
|
236
412
|
this.grist?.fetch();
|
|
@@ -253,6 +429,7 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
253
429
|
input() {
|
|
254
430
|
const input = Object.fromEntries(HEAD.map(key => [key, this.form[key] === '' ? null : this.form[key] ?? null]));
|
|
255
431
|
input.items = linesOf(this.grist).map(({ receivedTaxAmount, ...line }) => line);
|
|
432
|
+
input.discountAmount = input.discountAmount === null ? 0 : Number(input.discountAmount);
|
|
256
433
|
return input;
|
|
257
434
|
}
|
|
258
435
|
async save() {
|
|
@@ -276,8 +453,21 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
276
453
|
navigate(`quotation-editor?id=${saved.id}`);
|
|
277
454
|
return saved;
|
|
278
455
|
}
|
|
279
|
-
/**
|
|
456
|
+
/** Asked first - to whom and how much, fixed after - then save what is on screen and send it. */
|
|
280
457
|
async send() {
|
|
458
|
+
const purchase = this.form.direction === 'PURCHASE';
|
|
459
|
+
const words = {
|
|
460
|
+
party: (purchase ? this.form.supplierName : this.form.buyerName) || '—',
|
|
461
|
+
total: documentAmount(this.amounts.totalAmount ?? 0, this.form.currency || 'KRW')
|
|
462
|
+
};
|
|
463
|
+
const confirmed = await OxPrompt.open({
|
|
464
|
+
title: i18next.t('button.quotation-send'),
|
|
465
|
+
text: purchase ? i18next.t('text.quotation-send-confirm-purchase', words) : i18next.t('text.quotation-send-confirm', words),
|
|
466
|
+
confirmButton: { text: i18next.t('button.quotation-send') },
|
|
467
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
468
|
+
});
|
|
469
|
+
if (!confirmed)
|
|
470
|
+
return;
|
|
281
471
|
const saved = await this.save();
|
|
282
472
|
if (saved)
|
|
283
473
|
await this.change('sendQuotation');
|
|
@@ -301,17 +491,81 @@ let QuotationEditor = class QuotationEditor extends localize(i18next)(PageView)
|
|
|
301
491
|
this.show(changed);
|
|
302
492
|
this.updateContext();
|
|
303
493
|
}
|
|
304
|
-
|
|
494
|
+
/**
|
|
495
|
+
* An invoice draft of an accepted quotation (ADR-0170 decision 11): its parties, currency, language, terms, account,
|
|
496
|
+
* notes, discount and lines as the quotation has them, issued today and due the company's days after, the quotation
|
|
497
|
+
* named as its source - then opened to be checked and issued. The counterparty is the one of the other party's name,
|
|
498
|
+
* as the invoice editor links it.
|
|
499
|
+
*/
|
|
500
|
+
async makeInvoice() {
|
|
305
501
|
const quotation = this.quotation;
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
502
|
+
const created = await this.run(async () => {
|
|
503
|
+
const last = await client.query({
|
|
504
|
+
query: gql `
|
|
505
|
+
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
506
|
+
invoices(filters: $filters, pagination: $pagination, sortings: $sortings) { items { number } }
|
|
507
|
+
}
|
|
508
|
+
`,
|
|
509
|
+
variables: { filters: [{ name: 'direction', operator: 'eq', value: 'SALES' }], pagination: { page: 1, limit: 1 }, sortings: [{ name: 'createdAt', desc: true }] },
|
|
510
|
+
fetchPolicy: 'no-cache'
|
|
511
|
+
});
|
|
512
|
+
const issueDate = today();
|
|
513
|
+
const party = await partyOfName(quotation.direction === 'PURCHASE' ? quotation.supplierName : quotation.buyerName);
|
|
514
|
+
const response = await client.mutate({
|
|
515
|
+
mutation: gql `
|
|
516
|
+
mutation ($invoice: NewInvoice!) {
|
|
517
|
+
createInvoice(invoice: $invoice) { id }
|
|
518
|
+
}
|
|
519
|
+
`,
|
|
520
|
+
variables: {
|
|
521
|
+
invoice: {
|
|
522
|
+
number: nextInvoiceNumber(last.data?.invoices?.items?.[0]?.number, Number(issueDate.slice(0, 4))),
|
|
523
|
+
counterpartyId: party?.id ?? null,
|
|
524
|
+
...Object.fromEntries(INVOICE_HEAD.map(key => [key, quotation[key] ?? null])),
|
|
525
|
+
issueDate,
|
|
526
|
+
dueDate: daysAfter(issueDate, this.defaults?.paymentDays ?? PAYMENT_DAYS),
|
|
527
|
+
sourceReference: i18next.t('text.quotation-invoice-source', { number: quotation.number, version: quotation.version }),
|
|
528
|
+
taxBasis: 'CALCULATED',
|
|
529
|
+
discountAmount: Number(quotation.discountAmount ?? 0),
|
|
530
|
+
items: quotation.items.map(({ description, quantity, unit, unitPrice, discount, taxKind, taxRate }) => ({
|
|
531
|
+
description,
|
|
532
|
+
quantity,
|
|
533
|
+
unit: unit ?? null,
|
|
534
|
+
unitPrice,
|
|
535
|
+
discount,
|
|
536
|
+
taxKind,
|
|
537
|
+
taxRate
|
|
538
|
+
}))
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
if (response.errors?.length)
|
|
543
|
+
throw response;
|
|
544
|
+
return response.data.createInvoice;
|
|
545
|
+
});
|
|
546
|
+
if (created)
|
|
547
|
+
navigate(`invoice-editor?id=${created.id}`);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* The saved quotation as it goes out (ADR-0170 decision 10) - the server's page in the language chosen there, the
|
|
551
|
+
* quotation's own first, to download as PDF or print. What is on screen and not saved is not in it.
|
|
552
|
+
*/
|
|
553
|
+
preview() {
|
|
554
|
+
const { id, number, version, language } = this.quotation;
|
|
555
|
+
openDocumentPreview(async (format, lng) => {
|
|
556
|
+
const response = await client.query({
|
|
557
|
+
query: gql `
|
|
558
|
+
query ($id: String!, $format: DocumentFileFormat!, $language: String) {
|
|
559
|
+
quotationDocumentFile(id: $id, format: $format, language: $language) { fileName mimeType content }
|
|
560
|
+
}
|
|
561
|
+
`,
|
|
562
|
+
variables: { id, format, language: lng },
|
|
563
|
+
fetchPolicy: 'no-cache'
|
|
564
|
+
});
|
|
565
|
+
if (!response.data?.quotationDocumentFile)
|
|
566
|
+
throw response.errors?.[0] ?? new Error(i18next.t('text.quotation-not-found'));
|
|
567
|
+
return response.data.quotationDocumentFile;
|
|
568
|
+
}, language, `${i18next.t('title.invoice-preview')} · ${number} · v${version}`);
|
|
315
569
|
}
|
|
316
570
|
};
|
|
317
571
|
__decorate([
|
|
@@ -346,6 +600,22 @@ __decorate([
|
|
|
346
600
|
state(),
|
|
347
601
|
__metadata("design:type", Array)
|
|
348
602
|
], QuotationEditor.prototype, "opportunities", void 0);
|
|
603
|
+
__decorate([
|
|
604
|
+
state(),
|
|
605
|
+
__metadata("design:type", Object)
|
|
606
|
+
], QuotationEditor.prototype, "defaults", void 0);
|
|
607
|
+
__decorate([
|
|
608
|
+
state(),
|
|
609
|
+
__metadata("design:type", Object)
|
|
610
|
+
], QuotationEditor.prototype, "party", void 0);
|
|
611
|
+
__decorate([
|
|
612
|
+
state(),
|
|
613
|
+
__metadata("design:type", Object)
|
|
614
|
+
], QuotationEditor.prototype, "amounts", void 0);
|
|
615
|
+
__decorate([
|
|
616
|
+
state(),
|
|
617
|
+
__metadata("design:type", Object)
|
|
618
|
+
], QuotationEditor.prototype, "invoiceWriter", void 0);
|
|
349
619
|
__decorate([
|
|
350
620
|
query('ox-grist'),
|
|
351
621
|
__metadata("design:type", DataGrist)
|
|
@@ -355,6 +625,6 @@ QuotationEditor = __decorate([
|
|
|
355
625
|
], QuotationEditor);
|
|
356
626
|
export { QuotationEditor };
|
|
357
627
|
function blank() {
|
|
358
|
-
return { number: '', direction: 'SALES', currency: 'KRW', language: 'ko', supplierName: '', buyerName: '', issueDate: today(), validUntil: '', items: [] };
|
|
628
|
+
return { number: '', direction: 'SALES', currency: 'KRW', language: 'ko', supplierName: '', buyerName: '', issueDate: today(), validUntil: '', discountAmount: 0, items: [] };
|
|
359
629
|
}
|
|
360
630
|
//# sourceMappingURL=quotation-editor.js.map
|