375 lines
14 KiB
JavaScript
375 lines
14 KiB
JavaScript
// REA 전체점검 4~5: 변경 보존, 기성 3입력, 품목조정률, 연차 사본 계산.
|
|
const EstBusiness = (() => {
|
|
const { num, components, units, floating, sum } = EstMath;
|
|
const copy = (value) => structuredClone(value);
|
|
const rowKey = (row, index) => String(row.PkID ?? row.Code ?? index);
|
|
const special = (row) => [57, 58, 62, 67].includes(num(row.LinkIndex));
|
|
function baseline(project) {
|
|
const next = copy(project);
|
|
next.baseline = { tables: copy(project.tables), name: project.name };
|
|
for (const rows of Object.values(next.baseline.tables))
|
|
for (const row of rows) row.NewItem = false;
|
|
for (const rows of Object.values(next.tables))
|
|
for (const row of rows) {
|
|
row.NewItem = false;
|
|
for (const key of [
|
|
"Code",
|
|
"Name",
|
|
"Size",
|
|
"Unite",
|
|
"GN",
|
|
"TU",
|
|
"TC",
|
|
...units,
|
|
...components,
|
|
"LineData",
|
|
"VSusic",
|
|
"Cost",
|
|
])
|
|
if (Object.hasOwn(row, key)) row[`${key}_`] = copy(row[key]);
|
|
}
|
|
next.options.Mode = "설계변경";
|
|
return next;
|
|
}
|
|
function changes(project) {
|
|
const old = project.baseline?.tables.EstMainDat || [];
|
|
return project.tables.EstMainDat.map((row, index) => {
|
|
const original = old.find((r, i) => rowKey(r, i) === rowKey(row, index));
|
|
return {
|
|
key: rowKey(row, index),
|
|
Code: row.Code,
|
|
Name: row.Name,
|
|
GN: num(row.GN),
|
|
originalGN: num(original?.GN),
|
|
TC: num(row.TC),
|
|
originalTC: num(original?.TC),
|
|
difference: num(row.TC) - num(original?.TC),
|
|
NewItem: !original,
|
|
};
|
|
});
|
|
}
|
|
function bid(project, rates) {
|
|
const next = copy(project);
|
|
for (const key of ["HeadMainDat", "EstMainDat"])
|
|
for (const row of next.tables[key]) {
|
|
if (
|
|
num(row.Level) > 0 ||
|
|
num(row.Index) > 0 ||
|
|
[51, 52, 54, 55, 57, 58, 59, 62, 67, 68].includes(num(row.LinkIndex))
|
|
)
|
|
continue;
|
|
units.forEach((field, i) => {
|
|
row[`O${field}`] ??= num(row[field]);
|
|
row[field] = floating((num(row[`O${field}`]) * num(rates[i] ?? rates[0])) / 100);
|
|
});
|
|
row.TU = units.reduce((n, k) => n + num(row[k]), 0);
|
|
row.ApplyNacChal = 1;
|
|
}
|
|
next.options.SourceRefresh = false;
|
|
return EstNative.calculate(next);
|
|
}
|
|
function original(project, method = 0) {
|
|
if (!project.baseline) throw Error("당초 내역을 먼저 확정하세요.");
|
|
const old = {
|
|
...EstNative.empty(),
|
|
name: project.baseline.name,
|
|
tables: copy(project.baseline.tables),
|
|
options: {
|
|
...project.options,
|
|
RecalculateOriginal: true,
|
|
OldRecalcMethod: method,
|
|
SourceRefresh: true,
|
|
TotalFix: false,
|
|
},
|
|
};
|
|
for (const table of ["HeadMainDat", "CostMainDat", "EstMainDat"]) {
|
|
const rows = old.tables[table];
|
|
let previous = -1;
|
|
project.tables[table].forEach((row, index) => {
|
|
const existing = rows.findIndex((r, i) => rowKey(r, i) === rowKey(row, index));
|
|
if (existing >= 0) previous = existing;
|
|
else if (row.NewItem) rows.splice(++previous, 0, copy(row));
|
|
});
|
|
}
|
|
for (const table of [
|
|
"StuffcDat",
|
|
"LaborcDat",
|
|
"SilJukDat",
|
|
"EstiMateDat",
|
|
"MachGijunDat",
|
|
"MachMainDat",
|
|
])
|
|
old.tables[table] = copy(project.tables[table]);
|
|
const calculated = EstNative.calculate(old);
|
|
if (calculated.calculation.errors.length) throw Error(calculated.calculation.errors.join("\n"));
|
|
return { name: old.name, tables: calculated.tables };
|
|
}
|
|
function inputValue(row, input, prefix) {
|
|
const mode = input[`${prefix}Mode`] || "quantity",
|
|
value = num(input[prefix]);
|
|
if (mode === "amount") return num(row.TC) ? (value / num(row.TC)) * num(row.GN) : 0;
|
|
if (mode === "ratio")
|
|
return floating((value / 100) * num(row.GN), input.QuantityDigits ?? 4, 1);
|
|
return value;
|
|
}
|
|
function progress(project, roundId) {
|
|
const round = project.progress.find((r) => r.id === roundId);
|
|
if (!round) throw Error("기성 회차 없음");
|
|
const position = project.progress.indexOf(round),
|
|
rows = copy(project.tables.EstMainDat),
|
|
errors = [];
|
|
const previousRounds = project.progress.slice(0, position);
|
|
if (previousRounds.some((r) => !r.locked)) errors.push("전회 기성 회차를 먼저 확정하세요.");
|
|
const previous = previousRounds.at(-1)?.result?.rows || [];
|
|
const segment = { Gum: [], Sa: [] };
|
|
rows.forEach((row, index) => {
|
|
const key = rowKey(row, index),
|
|
input = round.inputs?.[key] || {};
|
|
const old = previous.find((r, i) => rowKey(r, i) === key);
|
|
row.JunGN = num(old?.TotalGN);
|
|
row.JunTC = num(old?.TotalTC);
|
|
row.JunRatio = num(old?.TotalRatio);
|
|
for (const k of components) row[`Jun${k}`] = num(old?.[`Total${k}`]);
|
|
if (num(row.Level) > 0) {
|
|
segment.Gum = [];
|
|
segment.Sa = [];
|
|
return;
|
|
}
|
|
for (const prefix of ["Gum", "Sa"]) {
|
|
const quantity = inputValue(row, input, prefix),
|
|
factor = special(row) ? quantity / 100 : quantity;
|
|
row[`${prefix}GN`] = quantity;
|
|
components.forEach(
|
|
(k, i) =>
|
|
(row[`${prefix}${k}`] = EstMath.fixed(
|
|
num(row[units[i]]) * factor,
|
|
input.AmountDigits || 0,
|
|
)),
|
|
);
|
|
if ([62, 67].includes(num(row.LinkIndex))) {
|
|
const percent = { ...row, GN: num(row.GN), CalcIndex: 0 };
|
|
const converted = segment[prefix].map((r) =>
|
|
Object.fromEntries(
|
|
[...components, "TC"]
|
|
.map((k) => [k, num(r[`${prefix}${k}`])])
|
|
.concat([["SKind", r.SKind]]),
|
|
),
|
|
);
|
|
EstRows.percent(percent, converted);
|
|
components.forEach((k) => (row[`${prefix}${k}`] = num(percent[k])));
|
|
}
|
|
row[`${prefix}TC`] = components.reduce((n, k) => n + num(row[`${prefix}${k}`]), 0);
|
|
row[`${prefix}Ratio`] = num(row.TC)
|
|
? floating((row[`${prefix}TC`] / num(row.TC)) * 100, 2, 1)
|
|
: 0;
|
|
segment[prefix].push(row);
|
|
}
|
|
const accepted = round.approved ? "Sa" : "Gum";
|
|
for (const k of ["GN", "TC", "Ratio", ...components])
|
|
row[`Total${k}`] = num(row[`Jun${k}`]) + num(row[`${accepted}${k}`]);
|
|
});
|
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
const row = rows[i];
|
|
if (num(row.Level) <= 0) continue;
|
|
const children = [];
|
|
for (
|
|
let j = i + 1;
|
|
j < rows.length &&
|
|
num(rows[j].Level) < num(row.Level) &&
|
|
![54, 59].includes(num(rows[j].LinkIndex));
|
|
j++
|
|
)
|
|
if (num(rows[j].Level) === 0 && num(rows[j].CalcIndex) < 2) children.push(rows[j]);
|
|
for (const prefix of ["Jun", "Gum", "Sa", "Total"])
|
|
for (const k of ["TC", ...components])
|
|
row[`${prefix}${k}`] = children.reduce((n, r) => n + num(r[`${prefix}${k}`]), 0);
|
|
}
|
|
const boundary = rows.findIndex((r) => num(r.LinkIndex) === 54);
|
|
const direct = rows
|
|
.slice(0, boundary < 0 ? rows.length : boundary)
|
|
.filter((r) => !num(r.Level) && num(r.CalcIndex) < 2);
|
|
const accepted = round.approved ? "Sa" : "Gum";
|
|
const current = direct.reduce((n, r) => n + num(r[`${accepted}TC`]), 0);
|
|
const contract = direct.reduce((n, r) => n + num(r.TC), 0),
|
|
ratio = contract ? current / contract : 0;
|
|
function costsFor(prefix) {
|
|
const part = direct.reduce((n, r) => n + num(r[`${prefix}TC`]), 0);
|
|
const proportion = contract ? part / contract : 0;
|
|
const bases = Object.fromEntries(
|
|
[...components, "TC"].map((key) => [
|
|
key,
|
|
direct.reduce((n, r) => n + num(r[`${prefix}${key}`]), 0),
|
|
]),
|
|
);
|
|
const material = project.options.GCToWonga
|
|
? bases.GC
|
|
: direct
|
|
.filter((r) => num(r.LinkIndex) === 5)
|
|
.reduce((n, r) => n + num(r[`${prefix}GC`]), 0);
|
|
const wonga = copy(project.tables.Wonga);
|
|
for (const row of wonga) {
|
|
if (row.Youl && !String(row.Name).replace(/\s/g, "").includes("부가")) {
|
|
row.Cost = Math.trunc(num(row.Cost) * proportion);
|
|
row.VSusic = "";
|
|
} else if (!row.VSusic && !row.BaseField && ![1, 3].includes(num(row.CalcIndex)))
|
|
row.Cost = Math.trunc(num(row.Cost) * proportion);
|
|
if (row.BaseField) {
|
|
row.Cost = num(bases[row.BaseField]);
|
|
row.VSusic = "";
|
|
} else if (!row.VSusic && !row.Youl && ![1, 3].includes(num(row.CalcIndex))) {
|
|
const name = EstNative.normalize(row.Name);
|
|
if (name.includes("직접재료")) row.Cost = material;
|
|
else if (name.includes("간접재료")) row.Cost = bases.GC - material;
|
|
else if (name.includes("직접노무")) row.Cost = bases.NC;
|
|
else if (name.includes("산출경비")) row.Cost = bases.JC;
|
|
}
|
|
}
|
|
const costs = EstWonga.calculate(wonga, { ...project.options, TotalFix: false }, bases);
|
|
errors.push(...costs.errors);
|
|
const byName = new Map(
|
|
costs.rows
|
|
.filter((r) => num(r.CalcIndex) < 2 && r.Gubun)
|
|
.map((r) => [EstNative.normalize(r.Name), r]),
|
|
);
|
|
for (const row of rows) {
|
|
if (num(row.LinkIndex) === 54) {
|
|
for (const key of [...components, "TC"]) row[`${prefix}${key}`] = bases[key];
|
|
} else if (
|
|
[55, 58].includes(num(row.LinkIndex)) &&
|
|
byName.has(EstNative.normalize(row.Name))
|
|
)
|
|
row[`${prefix}TC`] = num(byName.get(EstNative.normalize(row.Name)).Cost);
|
|
}
|
|
return costs;
|
|
}
|
|
const gumCosts = costsFor("Gum"),
|
|
saCosts = costsFor("Sa"),
|
|
costs = round.approved ? saCosts : gumCosts;
|
|
for (const row of rows)
|
|
if ([54, 55, 58].includes(num(row.LinkIndex))) {
|
|
for (const key of [...components, "TC"])
|
|
row[`Total${key}`] = num(row[`Jun${key}`]) + num(row[`${accepted}${key}`]);
|
|
}
|
|
return {
|
|
rows,
|
|
wonga: costs.rows,
|
|
total: costs.total || current,
|
|
ratio: floating(ratio * 100, 2, 1),
|
|
errors,
|
|
};
|
|
}
|
|
function escalation(project) {
|
|
const rows = copy(project.tables.EstMainDat),
|
|
errors = [];
|
|
const contractRows = project.baseline?.tables.EstMainDat || rows;
|
|
for (let index = 0; index < rows.length; index++) {
|
|
const row = rows[index],
|
|
old = contractRows.find((r, i) => rowKey(r, i) === rowKey(row, index)) || row;
|
|
if (num(row.Level) > 0 || [1, 3].includes(num(row.CalcIndex))) continue;
|
|
let total = 0;
|
|
units.forEach((unit, i) => {
|
|
const A = num(row[`A${unit}`]),
|
|
C = num(old[unit]),
|
|
B = num(row[unit]);
|
|
const D = A && B ? floating(((B - A) / A) * 100, 2) : 0;
|
|
let E = 0;
|
|
if (A && B && A >= C && B >= A) E = Math.trunc((C * D) / 100);
|
|
else if (A && B && A < C && B >= C) E = Math.trunc(B - C);
|
|
row[`D${unit}`] = D;
|
|
row[`E${unit}`] = E;
|
|
row[`F${unit}`] = C + E;
|
|
row[`E${components[i]}`] = Math.trunc(E * num(row.GN));
|
|
row[components[i]] = Math.trunc((C + E) * num(row.GN));
|
|
total += row[components[i]];
|
|
});
|
|
row.TC = total;
|
|
const excluded = Math.max(num(row.EscPlanned), num(row.EscActual), num(row.EscProgress));
|
|
row.EscExcluded = excluded;
|
|
row.EscApplied = num(old.GN) - excluded;
|
|
}
|
|
// 내역 재집계는 조정 단가를 사용한다.
|
|
for (const row of rows)
|
|
units.forEach((unit) => {
|
|
if (Object.hasOwn(row, `F${unit}`)) row[unit] = row[`F${unit}`];
|
|
});
|
|
const adjusted = EstRows.estimate(rows, { ...project.options, SourceRefresh: false });
|
|
function costCopy(estimateRows) {
|
|
const next = copy(project);
|
|
next.tables.EstMainDat = estimateRows;
|
|
next.options = { ...next.options, SourceRefresh: false, TotalFix: false };
|
|
return EstNative.calculate(next);
|
|
}
|
|
const adjustedProject = costCopy(adjusted.rows);
|
|
const contractProject = costCopy(copy(contractRows));
|
|
const excludedRows = copy(contractRows);
|
|
excludedRows.forEach((row, index) => {
|
|
if (num(row.Level) > 0 || [54, 55, 57, 58, 59, 62, 67].includes(num(row.LinkIndex))) return;
|
|
const current = rows.find((r, i) => rowKey(r, i) === rowKey(row, index));
|
|
row.GN = num(current?.EscExcluded);
|
|
});
|
|
const excludedProject = costCopy(excludedRows);
|
|
const contractTotal = num(contractProject.calculation.total);
|
|
const applied = contractTotal - num(excludedProject.calculation.total);
|
|
const rate = contractTotal
|
|
? floating(
|
|
((num(adjustedProject.calculation.total) - contractTotal) / contractTotal) * 100,
|
|
2,
|
|
)
|
|
: 0;
|
|
return {
|
|
rows: adjusted.rows,
|
|
direct: adjusted.direct,
|
|
wonga: adjustedProject.tables.Wonga,
|
|
contractTotal,
|
|
adjustedTotal: num(adjustedProject.calculation.total),
|
|
excludedTotal: num(excludedProject.calculation.total),
|
|
applied,
|
|
rate,
|
|
adjustment: Math.trunc((applied * rate) / 100),
|
|
errors: [
|
|
...new Set([
|
|
...errors,
|
|
...adjusted.errors,
|
|
...adjustedProject.calculation.errors,
|
|
...contractProject.calculation.errors,
|
|
...excludedProject.calculation.errors,
|
|
]),
|
|
],
|
|
};
|
|
}
|
|
function annual(project) {
|
|
const errors = [],
|
|
results = [];
|
|
for (const year of project.annual || []) {
|
|
const next = copy(project);
|
|
next.progress = [];
|
|
next.annual = [];
|
|
next.options.TotalFix = false;
|
|
for (let index = 0; index < next.tables.EstMainDat.length; index++) {
|
|
const row = next.tables.EstMainDat[index];
|
|
if (num(row.LinkIndex) === 57) {
|
|
row.CalcIndex = 3;
|
|
components.forEach((k) => (row[k] = 0));
|
|
row.TC = 0;
|
|
continue;
|
|
}
|
|
if (num(row.Level) > 0 || [54, 55, 58, 59, 62, 67].includes(num(row.LinkIndex))) continue;
|
|
try {
|
|
row.GN = EstExpression.evaluate(year.formula || "[계약수량]", (name) => {
|
|
if (name === "계약수량") return num(project.tables.EstMainDat[index].GN);
|
|
const previous = results.find((r) => r.name === name);
|
|
return previous ? num(previous.project.tables.EstMainDat[index]?.GN) : undefined;
|
|
});
|
|
} catch (error) {
|
|
errors.push(`${year.name}: ${error.message}`);
|
|
}
|
|
}
|
|
const calculated = EstNative.calculate(next);
|
|
errors.push(...calculated.calculation.errors);
|
|
results.push({ name: year.name, project: calculated });
|
|
}
|
|
return { results, errors };
|
|
}
|
|
return { baseline, changes, bid, original, progress, escalation, annual, rowKey };
|
|
})();
|