1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
|
/*
* kintone 見積書AI異常値検出機能
* Copyright (c) 2025 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
*/
(() => {
'use strict';
// 設定
const CONFIG = {
OPENAI_API_KEY: 'your-api-key-here', // OpenAI APIキーを設定してください
// フィールドコード
FIELDS: {
CUSTOMER_NAME: '顧客名',
INDUSTRY: '業界',
EMPLOYEE_COUNT: '従業員数',
CUSTOMER_RANK: '顧客ランク',
PRODUCT_NAME: '商品・サービス名',
UNIT_PRICE: '単価',
QUANTITY: '数量',
ESTIMATED_AMOUNT: '見積金額',
DISCOUNT_RATE: '値引率',
FINAL_AMOUNT: '最終金額',
REMARKS: '備考',
PAST_ORDERS: '過去取引回数',
AI_CHECK_SPACE: 'ai_check_space'
}
};
// 現在の四半期を取得
const getCurrentQuarter = () => {
const month = new Date().getMonth() + 1;
const quarter = Math.ceil(month / 3);
return `第${quarter}四半期`;
};
// 数値をカンマ区切りでフォーマット
const formatNumber = (num) => {
return new Intl.NumberFormat('ja-JP').format(num);
};
// ChatGPT APIを使用した異常値検出
const performAnomalyDetection = async (record) => {
try {
// レコードから必要な情報を抽出
const data = {
customerName: record[CONFIG.FIELDS.CUSTOMER_NAME]?.value || '',
industry: record[CONFIG.FIELDS.INDUSTRY]?.value || '',
employeeCount: record[CONFIG.FIELDS.EMPLOYEE_COUNT]?.value || 0,
customerRank: record[CONFIG.FIELDS.CUSTOMER_RANK]?.value || '新規',
productName: record[CONFIG.FIELDS.PRODUCT_NAME]?.value || '',
unitPrice: parseFloat(record[CONFIG.FIELDS.UNIT_PRICE]?.value || 0),
quantity: parseFloat(record[CONFIG.FIELDS.QUANTITY]?.value || 0),
estimatedAmount: parseFloat(record[CONFIG.FIELDS.ESTIMATED_AMOUNT]?.value || 0),
discountRate: parseFloat(record[CONFIG.FIELDS.DISCOUNT_RATE]?.value || 0),
finalAmount: parseFloat(record[CONFIG.FIELDS.FINAL_AMOUNT]?.value || 0),
remarks: record[CONFIG.FIELDS.REMARKS]?.value || '',
pastOrders: parseInt(record[CONFIG.FIELDS.PAST_ORDERS]?.value || 0, 10)
};
// ChatGPTに送信するプロンプトを作成
const prompt = `見積書の異常値検出を行ってください。実務の観点で、おかしい箇所は指摘し、妥当な範囲は「ok」としてください。
【評価方針】
- 指摘には簡潔な理由を添えること
【基本情報】
- 顧客名: ${data.customerName}
- 業界: ${data.industry}
- 従業員数: ${data.employeeCount}人
- 顧客ランク: ${data.customerRank}
- 過去取引回数: ${data.pastOrders}回
【見積内容】
- 商品・サービス: ${data.productName}
- 単価: ${formatNumber(data.unitPrice)}円
- 数量: ${data.quantity}
- 見積金額: ${formatNumber(data.estimatedAmount)}円
- 値引率: ${data.discountRate}%
- 最終金額: ${formatNumber(data.finalAmount)}円
【追加情報】
- 時期: ${getCurrentQuarter()}
- 備考: ${data.remarks}
【出力形式(JSONのみ、マークダウン不可)】
{
"overallRisk": "high|medium|low|normal",
"checks": [
{"item": "単価|値引率|数量|総合評価", "status": "error|warning|ok", "message": "理由", "suggestion": "改善提案(あれば)"}
],
"summary": "総合的な判断とアドバイス"
}`;
// ChatGPT APIに送信
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${CONFIG.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: 'あなたは見積書の異常値検出の専門家です。重要な点:' +
'1) 一般的な商慣習の範囲内であれば「ok」と判定する、' +
'2) 複合的にみて、注意するべきときは「warning」、ミスなどの可能性が高い場合は「error」、' +
'3) 健全な見積書に対しては「normal」のoverallRiskを返す、' +
'4) 正常な項目には「status": "ok"」を設定する。' +
'5) 見積書の内容に対して、業界相場や顧客属性、時期を考慮して判断してください。' +
'過度に厳しい判定は避け、実用的な観点で分析してください。'
},
{
role: 'user',
content: prompt
}
],
temperature: 0.3,
max_tokens: 1000
})
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const responseData = await response.json();
const content = responseData.choices[0].message.content;
// JSON解析(マークダウン対応)
let result;
try {
// マークダウンコードブロックを削除してJSONを抽出
const jsonContent = content
.replace(/^```(json)?\s*/, '') // 先頭のコードブロック削除
.replace(/\s*```$/, '') // 末尾のコードブロック削除
.slice(content.indexOf('{'), content.lastIndexOf('}') + 1); // JSON部分のみ抽出
result = JSON.parse(jsonContent);
} catch (e) {
throw new Error('AI応答の解析に失敗しました');
}
return result;
} catch (error) {
console.error('AI異常値検出エラー:', error);
throw error;
}
};
// チェックボタンを表示
const showCheckButton = () => {
const spaceElement = kintone.app.record.getSpaceElement(CONFIG.FIELDS.AI_CHECK_SPACE);
if (!spaceElement) {
console.warn('スペースフィールドが見つかりません');
return;
}
spaceElement.innerHTML = `
<div class="ai-check-container">
<div class="ai-check-header">
<span class="ai-check-icon">🤖</span>
<span class="ai-check-title">AI異常値検出</span>
</div>
<div class="ai-check-description">
ChatGPTが見積内容の妥当性を分析します
<small>業界相場・顧客属性・時期を考慮した総合判断</small>
</div>
<button id="perform-ai-check" class="ai-check-button">
<span style="margin-right: 8px;">✨</span>
異常値チェック実行
</button>
</div>`;
const btn = document.getElementById('perform-ai-check');
if (btn) btn.addEventListener('click', executeAnomalyCheck);
};
// ローディング表示
const showLoading = () => {
const spaceElement = kintone.app.record.getSpaceElement(CONFIG.FIELDS.AI_CHECK_SPACE);
if (!spaceElement) return;
spaceElement.innerHTML = `
<div class="ai-loading-container">
<div class="ai-loading-spinner"></div>
<div class="ai-loading-text">
<strong>ChatGPTで分析中...</strong>
<small>業界相場・顧客属性・ビジネスコンテキストを考慮しています</small>
</div>
</div>`;
};
// 結果表示
const showResults = (result) => {
const spaceElement = kintone.app.record.getSpaceElement(CONFIG.FIELDS.AI_CHECK_SPACE);
if (!spaceElement) return;
// リスクレベルに応じたアイコン
const riskStyles = {
high: {icon: '🚨', label: '高リスク'},
medium: {icon: '⚠️', label: '要確認'},
low: {icon: '💡', label: '低リスク'},
normal: {icon: '✅', label: '正常'}
};
const statusStyles = {
error: {icon: '❌'},
warning: {icon: '⚠️'},
ok: {icon: '✅'}
};
const riskStyle = riskStyles[result.overallRisk] || riskStyles.normal;
const riskClassMap = {high: 'risk-high', medium: 'risk-medium', low: 'risk-low', normal: 'risk-normal'};
const riskClass = riskClassMap[result.overallRisk] || riskClassMap.normal;
const checksHtml = result.checks.map(check => {
const statusStyle = statusStyles[check.status] || statusStyles.ok;
const statusClass = `status-${check.status || 'ok'}`;
return `
<div class="ai-check-item ${statusClass}">
<div class="ai-check-item-header">
<span class="ai-check-item-icon">${statusStyle.icon}</span>
<strong class="ai-check-item-title">${check.item}</strong>
</div>
<div class="ai-check-item-message">${check.message}</div>
${check.suggestion ? `<div class="ai-check-item-suggestion">💡 ${check.suggestion}</div>` : ''}
</div>`;
}).join('');
const html = `
<div class="ai-result-container ${riskClass}">
<div class="ai-result-header">
<div class="ai-result-status">
<span class="ai-result-icon">${riskStyle.icon}</span>
<span class="ai-result-label">総合評価: ${riskStyle.label}</span>
</div>
<button id="recheck-button" class="ai-result-reload-button">再チェック</button>
</div>
<div class="ai-result-content">
${checksHtml}
<div class="ai-summary-container">
<div class="ai-summary-header"><span>📊</span>総合判断</div>
<div class="ai-summary-content">${result.summary}</div>
</div>
</div>
</div>`;
spaceElement.innerHTML = html;
const re = document.getElementById('recheck-button');
if (re) re.addEventListener('click', showCheckButton);
};
// 異常値チェック実行
const executeAnomalyCheck = async () => {
try {
const record = kintone.app.record.get().record;
// 必須項目チェック
if (!record[CONFIG.FIELDS.UNIT_PRICE]?.value || !record[CONFIG.FIELDS.QUANTITY]?.value) {
alert('単価と数量を入力してください');
return;
}
showLoading();
const result = await performAnomalyDetection(record);
showResults(result);
} catch (error) {
console.error('異常値チェックエラー:', error);
alert('エラーが発生しました: ' + error.message);
}
};
// イベント登録
kintone.events.on([
'app.record.create.show',
'app.record.edit.show',
'app.record.detail.show'
], (event) => {
console.log('💰 見積書AI異常値検出機能が有効です');
showCheckButton();
return event;
});
})();
|