GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。 NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは 別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
572 lines
23 KiB
JavaScript
572 lines
23 KiB
JavaScript
/**
|
||
* batch-refresh-507570.js
|
||
* ------------------------------------------------------------
|
||
* SiteId 507570(事業計画v2)専用のワンオフスクリプト。
|
||
* ブラウザで手動実施している「1) レコードを開く→実績計上(263191)読込→
|
||
* 2) 保存→集計テーブル(507676)へ反映」を、scripts/1_事業計画テーブル.jsの
|
||
* getJissekiData()・summaryCalc()・summaryRecord()/bulkUpsertRecords()と
|
||
* 同一ロジックでAPI経由で再現し、対象レコードを一括処理する。
|
||
*
|
||
* 処理内容(1レコードあたり):
|
||
* Phase A: 263191から受注(本契約/設計契約)・着工・完成を取得し、
|
||
* 月別にDescription021-032(受注実績)/081-092(着工実績)/141-152(完成実績)へ格納
|
||
* ※ getJissekiData()と同一ロジック(年度範囲10/1-9/30、月境界チェック等)
|
||
* Phase B: 既存の計画(Description001-012等)+Phase Aの実績から、
|
||
* 受注/着工/完成それぞれの月別合計・累計をsummaryCalc()と同一ロジックで計算し
|
||
* Description041-052/101-112/161-172(合計)へ格納
|
||
* Phase C: Phase Bの合計(12ヶ月×3系統=36件)をsummaryRecord()と同一ロジックで
|
||
* ClassHash/NumHashに組み立て、507676へbulkupsertで1回送信
|
||
* Phase D: Phase A+Bの結果を507570へ1回のupdateでまとめて保存
|
||
*
|
||
* 使い方:
|
||
* node batch-refresh-507570.js … 全レコード対象、プレビューのみ(送信なし)
|
||
* node batch-refresh-507570.js --only=507580 … 指定ResultIdのみ対象(テスト用)
|
||
* node batch-refresh-507570.js --execute … 実際に507570 update + 507676 bulkupsert を送信
|
||
* --only と --execute は併用可
|
||
* ------------------------------------------------------------
|
||
*/
|
||
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const https = require("https");
|
||
|
||
const PROJECT_NAME = "NEXTGROUP経営計画";
|
||
const ENV = "production";
|
||
const SITE_ID = 507570;
|
||
const JISSEKI_SITE_ID = 263191;
|
||
const SHUKEI_SITE_ID = 507676;
|
||
|
||
const REPO_ROOT = path.join(__dirname, "..", "..", "..", "..");
|
||
const baseDir = path.join(REPO_ROOT, PROJECT_NAME);
|
||
const siteDir = path.join(baseDir, "configs", ENV, `site-${SITE_ID}_事業計画v2`);
|
||
const modifyDir = path.join(siteDir, "modify", "batch-refresh");
|
||
|
||
const configPath = path.join(REPO_ROOT, `config_${ENV}.json`);
|
||
const serverConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||
const apiKey = serverConfig.ApiKey;
|
||
const baseUrl = serverConfig.BaseUrl;
|
||
|
||
const argv = process.argv.slice(2);
|
||
const execute = argv.includes("--execute");
|
||
const onlyArg = argv.find((a) => a.startsWith("--only="));
|
||
const onlyId = onlyArg ? parseInt(onlyArg.split("=")[1], 10) : null;
|
||
|
||
function post(pathName, body) {
|
||
return new Promise((resolve, reject) => {
|
||
const url = new URL(baseUrl + pathName);
|
||
const payload = JSON.stringify(body);
|
||
const req = https.request(
|
||
{
|
||
hostname: url.hostname,
|
||
path: url.pathname,
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
|
||
},
|
||
(res) => {
|
||
let c = "";
|
||
res.on("data", (d) => (c += d));
|
||
res.on("end", () => {
|
||
try {
|
||
resolve({ status: res.statusCode, body: JSON.parse(c) });
|
||
} catch (e) {
|
||
resolve({ status: res.statusCode, body: c });
|
||
}
|
||
});
|
||
}
|
||
);
|
||
req.on("error", reject);
|
||
req.write(payload);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
function pad3(n) {
|
||
return String(n).padStart(3, "0");
|
||
}
|
||
|
||
// getJissekiData()の空行テンプレート(1-10)
|
||
function emptyJissekiTemplate() {
|
||
const rows = [];
|
||
for (let k = 1; k <= 10; k++) {
|
||
rows.push({ 0: String(k), 1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: "" });
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
// jissekiA/B/C[m]配列(最大10件超は元コードも考慮していないため同様に踏襲)を
|
||
// 10行テンプレートへ整形する(getJissekiData()の394-462行と同一ロジック)
|
||
function buildJissekiRows(rows) {
|
||
if (!rows.length) return emptyJissekiTemplate();
|
||
const out = rows.map((r, idx) => ({ ...r, 0: String(idx + 1) }));
|
||
for (let k = 0; k < 10; k++) {
|
||
if (k >= out.length) {
|
||
out[k] = { 0: String(k + 1), 1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: "" };
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// SiteSettings.Columnsを見ずに済むよう、getJigyouKeikakuData同様Description番号直指定で読み書きする
|
||
|
||
async function fetchJisseki(classE, sYear, eYear, sDate, eDate, eDate1) {
|
||
const jissekiA = {}, jissekiB = {}, jissekiC = {};
|
||
for (let m = 1; m <= 12; m++) {
|
||
jissekiA[m] = [];
|
||
jissekiB[m] = [];
|
||
jissekiC[m] = [];
|
||
}
|
||
|
||
// 受注(本契約のみ)
|
||
{
|
||
const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, {
|
||
ApiVersion: 1.1,
|
||
ApiKey: apiKey,
|
||
View: {
|
||
ColumnFilterHash: {
|
||
ClassE: `["${classE}"]`,
|
||
ClassO: `["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]`,
|
||
DateA: `["${sDate},${eDate}"]`,
|
||
DateB: `["\t"]`,
|
||
},
|
||
ColumnSorterHash: { DateA: "asc" },
|
||
},
|
||
});
|
||
for (const value of res.body.Response.Data) {
|
||
const countMonth = parseFloat(value.ClassHash.ClassO.replace("月", ""));
|
||
const date0 = new Date(value.DateHash.DateA);
|
||
date0.setHours(date0.getHours() + 9);
|
||
const countYear = date0.getFullYear();
|
||
if (countMonth <= 9 && countYear == sYear) continue;
|
||
if (countMonth == 10 && countYear == eYear) continue;
|
||
const kubun = value.ClassHash.ClassD;
|
||
const cname = value.ClassHash.ClassC;
|
||
const ukeoi = parseFloat(value.NumHash.NumA);
|
||
const arari = parseFloat(value.NumHash.NumB);
|
||
const arariPer = Math.floor((arari / ukeoi) * 1000) / 10;
|
||
const tanto = value.ClassHash.ClassF;
|
||
jissekiA[countMonth].push({ 0: "0", 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto });
|
||
}
|
||
}
|
||
|
||
// 受注(設計契約)
|
||
{
|
||
const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, {
|
||
ApiVersion: 1.1,
|
||
ApiKey: apiKey,
|
||
View: {
|
||
ColumnFilterHash: {
|
||
ClassE: `["${classE}"]`,
|
||
ClassO: `["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]`,
|
||
DateB: `["${sDate},${eDate}"]`,
|
||
},
|
||
ColumnSorterHash: { DateB: "asc" },
|
||
},
|
||
});
|
||
for (const value of res.body.Response.Data) {
|
||
const countMonth = parseFloat(value.ClassHash.ClassO.replace("月", ""));
|
||
const date0 = new Date(value.DateHash.DateB);
|
||
date0.setHours(date0.getHours() + 9);
|
||
const countYear = date0.getFullYear();
|
||
if (countMonth <= 9 && countYear == sYear) continue;
|
||
if (countMonth == 10 && countYear == eYear) continue;
|
||
const kubun = value.ClassHash.ClassD;
|
||
let cname, ukeoi, arari;
|
||
if (value.DateHash.DateA != "1899-12-30T00:00:00") {
|
||
cname = "[本]" + value.ClassHash.ClassC;
|
||
ukeoi = parseFloat(value.NumHash.NumA);
|
||
arari = parseFloat(value.NumHash.NumB);
|
||
} else {
|
||
cname = "[設]" + value.ClassHash.ClassC;
|
||
ukeoi = parseFloat(value.NumHash.NumC);
|
||
arari = parseFloat(value.NumHash.NumD);
|
||
}
|
||
const tanto = value.ClassHash.ClassF;
|
||
const arariPer = Math.floor((arari / ukeoi) * 1000) / 10;
|
||
jissekiA[countMonth].push({ 0: "0", 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto });
|
||
}
|
||
}
|
||
|
||
// 着工
|
||
{
|
||
const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, {
|
||
ApiVersion: 1.1,
|
||
ApiKey: apiKey,
|
||
View: {
|
||
ColumnFilterHash: { ClassE: `["${classE}"]`, DateC: `["${sDate},${eDate1}"]` },
|
||
ColumnSorterHash: { DateC: "asc" },
|
||
},
|
||
});
|
||
let num = 1, month0 = 0;
|
||
res.body.Response.Data.forEach((value, index) => {
|
||
const date0 = new Date(value.DateHash.DateC);
|
||
date0.setHours(date0.getHours() + 9);
|
||
const month = date0.getMonth() + 1;
|
||
const kubun = value.ClassHash.ClassD;
|
||
const cname = value.ClassHash.ClassC;
|
||
const ukeoi = parseFloat(value.NumHash.NumA);
|
||
const arari = parseFloat(value.NumHash.NumB);
|
||
const arariPer = Math.floor((arari / ukeoi) * 1000) / 10;
|
||
const tanto = value.ClassHash.ClassF;
|
||
if (index > 0 && month0 != month) num = 1;
|
||
jissekiB[month].push({ 0: num, 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto });
|
||
num++;
|
||
month0 = month;
|
||
});
|
||
}
|
||
|
||
// 完成
|
||
{
|
||
const res = await post(`/api/items/${JISSEKI_SITE_ID}/get`, {
|
||
ApiVersion: 1.1,
|
||
ApiKey: apiKey,
|
||
View: {
|
||
ColumnFilterHash: { ClassE: `["${classE}"]`, DateD: `["${sDate},${eDate1}"]` },
|
||
ColumnSorterHash: { DateD: "asc" },
|
||
},
|
||
});
|
||
let num = 1, month0 = 0;
|
||
res.body.Response.Data.forEach((value, index) => {
|
||
const date0 = new Date(value.DateHash.DateD);
|
||
date0.setHours(date0.getHours() + 9);
|
||
const month = date0.getMonth() + 1;
|
||
const kubun = value.ClassHash.ClassD;
|
||
const cname = value.ClassHash.ClassC;
|
||
const ukeoi = parseFloat(value.NumHash.NumA);
|
||
const arari = parseFloat(value.NumHash.NumB);
|
||
const arariPer = Math.floor((arari / ukeoi) * 1000) / 10;
|
||
const tanto = value.ClassHash.ClassF;
|
||
if (index > 0 && month0 != month) num = 1;
|
||
jissekiC[month].push({ 0: num, 1: date0, 2: kubun, 3: cname, 4: ukeoi, 5: arariPer, 6: arari, 7: tanto });
|
||
num++;
|
||
month0 = month;
|
||
});
|
||
}
|
||
|
||
return { jissekiA, jissekiB, jissekiC };
|
||
}
|
||
|
||
// summaryCalc()と同一ロジック。DescriptionHash(既存の計画+今回の実績)からgoukeiDataを計算する
|
||
function calcGoukei(descriptionHash) {
|
||
const keikakuCumulative = {};
|
||
const jissekiCumulative = {};
|
||
for (let month1 = 10; month1 < 22; month1++) {
|
||
const month2 = month1 >= 13 ? month1 - 12 : month1;
|
||
for (let i = 40; i <= 160; i += 60) {
|
||
const tableId = pad3(month2 + i);
|
||
keikakuCumulative[tableId] = [0, 0, 0, 0, 0, 0];
|
||
jissekiCumulative[tableId] = [0, 0, 0, 0, 0, 0];
|
||
}
|
||
}
|
||
|
||
const goukeiResult = {}; // tableId -> 6行配列
|
||
|
||
for (let month1 = 10; month1 < 22; month1++) {
|
||
const month2 = month1 >= 13 ? month1 - 12 : month1;
|
||
|
||
for (let i = 0; i <= 120; i += 60) {
|
||
const keikakuTableId = pad3(month2 + i);
|
||
const jissekiTableId = pad3(month2 + i + 20);
|
||
const goukeiTableId = pad3(month2 + i + 40);
|
||
|
||
const keikakuData = JSON.parse(descriptionHash["Description" + keikakuTableId] || "[]");
|
||
const jissekiDataArr = JSON.parse(descriptionHash["Description" + jissekiTableId] || "[]");
|
||
const existingGoukei = descriptionHash["Description" + goukeiTableId]
|
||
? JSON.parse(descriptionHash["Description" + goukeiTableId])
|
||
: JSON.parse(
|
||
'[{"0":"当月","1":"計画","2":"","3":"","4":"","5":"","6":"請負金額"},{"0":"","1":"実績","2":"","3":"","4":"","5":"","6":"0%"},{"0":"","1":"差異","2":"","3":"","4":"","5":"","6":""},{"0":"累計","1":"計画","2":"","3":"","4":"","5":"","6":"棟数"},{"0":"","1":"実績","2":"","3":"","4":"","5":"","6":"0%"},{"0":"","1":"差異","2":"","3":"","4":"","5":"","6":""}]'
|
||
);
|
||
const goukeiData = existingGoukei; // ラベル([0],[1]列、[0][6]/[3][6])を保持したまま数値部分だけ上書きする
|
||
|
||
const keikakuSum = [0, 0, 0, 0, 0, 0];
|
||
const jissekiSum = [0, 0, 0, 0, 0, 0];
|
||
|
||
for (const row of keikakuData) {
|
||
const v4 = parseFloat(String(row[4]).toString().replace(/[¥,]/g, ""));
|
||
if (v4 > 0) {
|
||
const v6 = parseFloat(String(row[6]).toString().replace(/[¥,]/g, ""));
|
||
keikakuSum[1] += v4;
|
||
keikakuSum[3] += v6;
|
||
if (row[2] != "カウント外") {
|
||
keikakuSum[0]++;
|
||
keikakuSum[4] += v4;
|
||
keikakuSum[5] += v6;
|
||
}
|
||
}
|
||
}
|
||
if (keikakuSum[4] > 0 && keikakuSum[5] > 0) {
|
||
keikakuSum[2] = Math.floor((keikakuSum[5] / keikakuSum[4]) * 10000) / 100;
|
||
}
|
||
|
||
for (const row of jissekiDataArr) {
|
||
const v4 = parseFloat(String(row[4]).toString().replace(/[¥,]/g, ""));
|
||
if (v4 > 0) {
|
||
const v6 = parseFloat(String(row[6]).toString().replace(/[¥,]/g, ""));
|
||
jissekiSum[1] += v4;
|
||
jissekiSum[3] += v6;
|
||
if (row[2] != "カウント外") {
|
||
jissekiSum[0]++;
|
||
jissekiSum[4] += v4;
|
||
jissekiSum[5] += v6;
|
||
}
|
||
}
|
||
}
|
||
if (jissekiSum[4] > 0 && jissekiSum[5] > 0) {
|
||
jissekiSum[2] = Math.floor((jissekiSum[5] / jissekiSum[4]) * 10000) / 100;
|
||
}
|
||
|
||
goukeiData[0][2] = keikakuSum[0];
|
||
goukeiData[0][3] = keikakuSum[1];
|
||
goukeiData[0][4] = keikakuSum[2];
|
||
goukeiData[0][5] = keikakuSum[3];
|
||
|
||
goukeiData[1][2] = jissekiSum[0];
|
||
goukeiData[1][3] = jissekiSum[1];
|
||
goukeiData[1][4] = jissekiSum[2];
|
||
goukeiData[1][5] = jissekiSum[3];
|
||
|
||
goukeiData[2][2] = jissekiSum[0] - keikakuSum[0];
|
||
goukeiData[2][3] = jissekiSum[1] - keikakuSum[1];
|
||
goukeiData[2][4] = jissekiSum[2] - keikakuSum[2];
|
||
goukeiData[2][5] = jissekiSum[3] - keikakuSum[3];
|
||
|
||
for (let month3 = month1; month3 < 22; month3++) {
|
||
const month2b = month3 >= 13 ? month3 - 12 : month3;
|
||
const tableId = pad3(month2b + i + 40);
|
||
|
||
keikakuCumulative[tableId][0] += keikakuSum[0];
|
||
keikakuCumulative[tableId][1] += keikakuSum[1];
|
||
keikakuCumulative[tableId][3] += keikakuSum[3];
|
||
keikakuCumulative[tableId][4] += keikakuSum[4];
|
||
keikakuCumulative[tableId][5] += keikakuSum[5];
|
||
keikakuCumulative[tableId][2] =
|
||
keikakuCumulative[tableId][1] > 0 ? Math.floor((keikakuCumulative[tableId][5] / keikakuCumulative[tableId][4]) * 10000) / 100 : 0;
|
||
|
||
jissekiCumulative[tableId][0] += jissekiSum[0];
|
||
jissekiCumulative[tableId][1] += jissekiSum[1];
|
||
jissekiCumulative[tableId][3] += jissekiSum[3];
|
||
jissekiCumulative[tableId][4] += jissekiSum[4];
|
||
jissekiCumulative[tableId][5] += jissekiSum[5];
|
||
jissekiCumulative[tableId][2] =
|
||
jissekiCumulative[tableId][1] > 0 ? Math.floor((jissekiCumulative[tableId][5] / jissekiCumulative[tableId][4]) * 10000) / 100 : 0;
|
||
}
|
||
|
||
goukeiData[3][2] = keikakuCumulative[goukeiTableId][0];
|
||
goukeiData[3][3] = keikakuCumulative[goukeiTableId][1];
|
||
goukeiData[3][4] = keikakuCumulative[goukeiTableId][2];
|
||
goukeiData[3][5] = keikakuCumulative[goukeiTableId][3];
|
||
|
||
goukeiData[4][2] = jissekiCumulative[goukeiTableId][0];
|
||
goukeiData[4][3] = jissekiCumulative[goukeiTableId][1];
|
||
goukeiData[4][4] = jissekiCumulative[goukeiTableId][2];
|
||
goukeiData[4][5] = jissekiCumulative[goukeiTableId][3];
|
||
|
||
goukeiData[5][2] = jissekiCumulative[goukeiTableId][0] - keikakuCumulative[goukeiTableId][0];
|
||
goukeiData[5][3] = jissekiCumulative[goukeiTableId][1] - keikakuCumulative[goukeiTableId][1];
|
||
goukeiData[5][4] = jissekiCumulative[goukeiTableId][2] - keikakuCumulative[goukeiTableId][2];
|
||
goukeiData[5][5] = jissekiCumulative[goukeiTableId][3] - keikakuCumulative[goukeiTableId][3];
|
||
|
||
if (keikakuCumulative[goukeiTableId][1] > 0 && jissekiCumulative[goukeiTableId][1] > 0) {
|
||
goukeiData[1][6] = (Math.floor((jissekiCumulative[goukeiTableId][1] / keikakuCumulative[goukeiTableId][1]) * 10000) / 100).toString() + "%";
|
||
} else {
|
||
goukeiData[1][6] = "0%";
|
||
}
|
||
if (keikakuCumulative[goukeiTableId][0] > 0 && jissekiCumulative[goukeiTableId][0] > 0) {
|
||
goukeiData[4][6] = (Math.floor((jissekiCumulative[goukeiTableId][0] / keikakuCumulative[goukeiTableId][0]) * 10000) / 100).toString() + "%";
|
||
} else {
|
||
goukeiData[4][6] = "0%";
|
||
}
|
||
|
||
goukeiResult[goukeiTableId] = goukeiData;
|
||
// 直接descriptionHashへも書き戻す(次のmonth1ループが最新の計画/実績を参照できるよう。
|
||
// ただしkeikaku/jissekiは今回変更しないので実質goukei追記のみ)
|
||
descriptionHash["Description" + goukeiTableId] = JSON.stringify(goukeiData);
|
||
}
|
||
}
|
||
|
||
return goukeiResult;
|
||
}
|
||
|
||
// summaryRecord()と同一ロジック。goukeiResult(受注/着工/完成 x 12ヶ月)からbulkupsert用Dataを組み立てる
|
||
function buildBulkData(goukeiResult, classHashBase) {
|
||
const data = [];
|
||
for (let i = 1; i <= 12; i++) {
|
||
let sortKey;
|
||
if (i >= 10) sortKey = pad3(i - 9);
|
||
else sortKey = pad3(i + 3);
|
||
|
||
let count = 1;
|
||
for (let j = 40; j <= 160; j += 60) {
|
||
const tableId = pad3(i + j);
|
||
const goukeiData = goukeiResult[tableId];
|
||
|
||
const classHash = {
|
||
ClassA: classHashBase.ClassA,
|
||
ClassB: classHashBase.ClassB,
|
||
ClassC: classHashBase.ClassC,
|
||
ClassD: classHashBase.ClassD,
|
||
ClassE: classHashBase.ClassE,
|
||
ClassH: classHashBase.ClassH,
|
||
ClassK: sortKey,
|
||
ClassY: i,
|
||
ClassV: count,
|
||
ClassZ: classHashBase.ClassZ,
|
||
};
|
||
const numHash = {
|
||
Num001: goukeiData[0][2],
|
||
Num002: goukeiData[0][3],
|
||
Num003: goukeiData[0][4],
|
||
Num004: goukeiData[0][5],
|
||
Num011: goukeiData[1][2],
|
||
Num012: goukeiData[1][3],
|
||
Num013: goukeiData[1][4],
|
||
Num014: goukeiData[1][5],
|
||
Num021: goukeiData[3][2],
|
||
Num022: goukeiData[3][3],
|
||
Num023: goukeiData[3][4],
|
||
Num024: goukeiData[3][5],
|
||
Num031: goukeiData[4][2],
|
||
Num032: goukeiData[4][3],
|
||
Num033: goukeiData[4][4],
|
||
Num034: goukeiData[4][5],
|
||
};
|
||
data.push({ ClassHash: classHash, NumHash: numHash });
|
||
count++;
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
|
||
async function processRecord(it, log) {
|
||
const resultId = it.ResultId;
|
||
const classHashBase = it.ClassHash;
|
||
const classE = classHashBase.ClassB; // getJissekiData()同様、自レコードのClassB値を263191のClassEフィルタに使う
|
||
const sYear = classHashBase.ClassZ;
|
||
const eYear = parseFloat(classHashBase.ClassZ) + 1;
|
||
const sDate = `${sYear}/10/01 00:00:00`;
|
||
const eDate = `${eYear}/09/30 23:59:59`;
|
||
|
||
const sDate0 = new Date(sDate);
|
||
const eDate0 = new Date(eDate);
|
||
const thisMonthLastDay = new Date();
|
||
thisMonthLastDay.setMonth(thisMonthLastDay.getMonth() + 1, 0);
|
||
let eDate1 = `${thisMonthLastDay.getFullYear()}/${String(thisMonthLastDay.getMonth() + 1).padStart(2, "0")}/${String(
|
||
thisMonthLastDay.getDate()
|
||
).padStart(2, "0")} 23:59:59`;
|
||
const eDate1D = new Date(eDate1);
|
||
if (eDate0.getTime() < eDate1D.getTime()) {
|
||
eDate1 = eDate;
|
||
}
|
||
|
||
const { jissekiA, jissekiB, jissekiC } = await fetchJisseki(classE, sYear, eYear, sDate, eDate, eDate1);
|
||
|
||
// Phase A: DescriptionHashへ実績を組み立て(既存の計画等はit.DescriptionHashからコピーして保持)
|
||
const descriptionHash = { ...it.DescriptionHash };
|
||
for (let i = 1; i <= 12; i++) {
|
||
descriptionHash["Description" + pad3(i + 20)] = JSON.stringify(buildJissekiRows(jissekiA[i]));
|
||
descriptionHash["Description" + pad3(i + 80)] = JSON.stringify(buildJissekiRows(jissekiB[i]));
|
||
descriptionHash["Description" + pad3(i + 140)] = JSON.stringify(buildJissekiRows(jissekiC[i]));
|
||
}
|
||
|
||
// Phase B: 合計(累計込み)を計算し、descriptionHashへ反映(calcGoukei内で書き戻す)
|
||
const goukeiResult = calcGoukei(descriptionHash);
|
||
|
||
// 更新するDescriptionフィールドだけを送信用に抜き出す(実績36 + 合計36 = 72)
|
||
const updateDescriptionHash = {};
|
||
for (let i = 1; i <= 12; i++) {
|
||
for (const off of [20, 80, 140]) updateDescriptionHash["Description" + pad3(i + off)] = descriptionHash["Description" + pad3(i + off)];
|
||
for (const off of [40, 100, 160]) updateDescriptionHash["Description" + pad3(i + off)] = descriptionHash["Description" + pad3(i + off)];
|
||
}
|
||
|
||
// Phase C: 507676向けbulkupsertデータ
|
||
const bulkData = buildBulkData(goukeiResult, classHashBase);
|
||
|
||
log(`ResultId ${resultId} (${classHashBase.ClassB}): 受注/着工/完成 実績${["A", "B", "C"].map((x) => "OK").join("/")}, 合計36件, bulkupsert対象${bulkData.length}件`);
|
||
|
||
return { resultId, updateDescriptionHash, bulkData };
|
||
}
|
||
|
||
(async () => {
|
||
fs.mkdirSync(modifyDir, { recursive: true });
|
||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||
const logLines = [];
|
||
const log = (s) => {
|
||
console.log(s);
|
||
logLines.push(s);
|
||
};
|
||
|
||
log(`=== batch-refresh-507570.js 実行開始 (execute=${execute}, only=${onlyId || "全件"}) ===`);
|
||
|
||
const listRes = await post(`/api/items/${SITE_ID}/get`, { ApiVersion: 1.1, ApiKey: apiKey });
|
||
let items = listRes.body.Response.Data;
|
||
if (onlyId) items = items.filter((it) => it.ResultId === onlyId);
|
||
log(`対象レコード数: ${items.length}`);
|
||
|
||
const perRecordResults = [];
|
||
for (const it of items) {
|
||
try {
|
||
const r = await processRecord(it, log);
|
||
perRecordResults.push(r);
|
||
} catch (e) {
|
||
log(`ResultId ${it.ResultId}: エラー ${e.message}`);
|
||
}
|
||
}
|
||
|
||
const previewPath = path.join(modifyDir, `preview_${ts}.json`);
|
||
fs.writeFileSync(
|
||
previewPath,
|
||
JSON.stringify(
|
||
{
|
||
targetCount: items.length,
|
||
updates: perRecordResults.map((r) => ({ resultId: r.resultId, descriptionKeys: Object.keys(r.updateDescriptionHash), bulkDataCount: r.bulkData.length })),
|
||
sampleUpdate: perRecordResults[0],
|
||
},
|
||
null,
|
||
1
|
||
)
|
||
);
|
||
log(`[OK] プレビュー保存: ${previewPath}`);
|
||
|
||
if (!execute) {
|
||
log("[注意] --execute が指定されていないため、送信は行っていません。");
|
||
fs.writeFileSync(path.join(modifyDir, `log_${ts}.txt`), logLines.join("\n"));
|
||
return;
|
||
}
|
||
|
||
// レコードごとに 507570 update → 507676 bulkupsert(36件)を実行
|
||
// (全レコード分をまとめて1回のbulkupsertにすると件数超過でPleasanter側のBulkUpsertMaxに
|
||
// 引っかかる恐れがあるため、レコード単位=36件ずつに区切って送信する)
|
||
let updateOk = 0, updateNg = [];
|
||
let bulkOk = 0, bulkNg = [];
|
||
for (const r of perRecordResults) {
|
||
const updateRes = await post(`/api/items/${r.resultId}/update`, {
|
||
ApiVersion: 1.1,
|
||
ApiKey: apiKey,
|
||
DescriptionHash: r.updateDescriptionHash,
|
||
});
|
||
if (updateRes.status === 200 && updateRes.body && updateRes.body.StatusCode === 200) updateOk++;
|
||
else updateNg.push({ resultId: r.resultId, res: updateRes });
|
||
|
||
const bulkRes = await post(`/api/items/${SHUKEI_SITE_ID}/bulkupsert`, {
|
||
ApiVersion: 1.1,
|
||
ApiKey: apiKey,
|
||
Keys: ["ClassA", "ClassB", "ClassV", "ClassY", "ClassZ"],
|
||
KeyNotFoundCreate: true,
|
||
Data: r.bulkData,
|
||
});
|
||
if (bulkRes.status === 200 && bulkRes.body && bulkRes.body.StatusCode === 200) bulkOk++;
|
||
else bulkNg.push({ resultId: r.resultId, res: bulkRes });
|
||
|
||
log(`ResultId ${r.resultId}: 507570update=${updateRes.status} / 507676bulkupsert=${bulkRes.status} ${bulkRes.body && bulkRes.body.Message ? bulkRes.body.Message : ""}`);
|
||
}
|
||
log(`507570 update: 成功${updateOk}件 / 失敗${updateNg.length}件`);
|
||
log(`507676 bulkupsert: 成功${bulkOk}件 / 失敗${bulkNg.length}件`);
|
||
if (updateNg.length) log("updateNg: " + JSON.stringify(updateNg, null, 1).slice(0, 2000));
|
||
if (bulkNg.length) log("bulkNg: " + JSON.stringify(bulkNg, null, 1).slice(0, 2000));
|
||
|
||
fs.writeFileSync(
|
||
path.join(modifyDir, `result_${ts}.json`),
|
||
JSON.stringify({ updateOk, updateNg, bulkOk, bulkNg }, null, 1)
|
||
);
|
||
fs.writeFileSync(path.join(modifyDir, `log_${ts}.txt`), logLines.join("\n"));
|
||
})();
|