|
|
|
@@ -0,0 +1,233 @@
|
|
|
|
|
// 원본 견적서작성(mnuEstiMatePrn → TfrmEstiMatePrn 0x10c1994, 그리기 0x10c4768).
|
|
|
|
|
// 설정은 공사마다 project.estimatePrint(원본 EstiPrintInfo.einf 세 구역), 공급자정보는 원본과 같은 INI(.einf)로 내보내기·불러오기
|
|
|
|
|
// (불러오기는 EstiPrnCompanyInfo·EstiPrnInfo 두 구역만). 행: 레벨 0 = 전 행, N = Level ≥ N 공종 머리 + LinkIndex 54·55·58.
|
|
|
|
|
// 열: 품명(Pr+Name)·규격·수량·단위·단가·공급가액·비고(선택). 1쪽만 머리·공급자·합계금액, 1쪽 F_PageCount·이후 L_PageCount 줄.
|
|
|
|
|
// 합계금액 = 원가계산서를 아래에서 위로 훑어 Checked 이고 금액 있는 첫 행(없으면 도급액 55 행) — 「일금: …원정 (₩…원정)」.
|
|
|
|
|
const EstEstimatePrint = (() => {
|
|
|
|
|
const num = (value) => Number(value) || 0;
|
|
|
|
|
const defaults = {
|
|
|
|
|
Title_Name: "견 적 서",
|
|
|
|
|
Project_Name: "",
|
|
|
|
|
Order_name: "",
|
|
|
|
|
Esti_Date: "",
|
|
|
|
|
Reg_Num: "",
|
|
|
|
|
ComPany_Name: "",
|
|
|
|
|
Ceo_Name: "",
|
|
|
|
|
Addr: "",
|
|
|
|
|
Uptae: "",
|
|
|
|
|
Jongmog: "",
|
|
|
|
|
Tel: "",
|
|
|
|
|
Company_Show: true,
|
|
|
|
|
F_PageCount: 27,
|
|
|
|
|
L_PageCount: 36,
|
|
|
|
|
Aligment: 0,
|
|
|
|
|
Bigo_Show: false,
|
|
|
|
|
Level: 0,
|
|
|
|
|
};
|
|
|
|
|
const sections = {
|
|
|
|
|
EstiProjectInfo: ["Title_Name", "Project_Name", "Order_name", "Esti_Date"],
|
|
|
|
|
EstiPrnCompanyInfo: [
|
|
|
|
|
"Reg_Num",
|
|
|
|
|
"ComPany_Name",
|
|
|
|
|
"Ceo_Name",
|
|
|
|
|
"Addr",
|
|
|
|
|
"Uptae",
|
|
|
|
|
"Jongmog",
|
|
|
|
|
"Tel",
|
|
|
|
|
"Company_Show",
|
|
|
|
|
],
|
|
|
|
|
EstiPrnInfo: ["F_PageCount", "L_PageCount", "Aligment", "Bigo_Show"],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function settings(project) {
|
|
|
|
|
const today = new Date();
|
|
|
|
|
return {
|
|
|
|
|
...defaults,
|
|
|
|
|
Project_Name: project.name || "",
|
|
|
|
|
Esti_Date: `${today.getFullYear()}년 ${String(today.getMonth() + 1).padStart(2, "0")}월 ${String(today.getDate()).padStart(2, "0")}일`,
|
|
|
|
|
...(project.estimatePrint || {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rows(project, level) {
|
|
|
|
|
const list = project.tables.EstMainDat || [];
|
|
|
|
|
return level > 0
|
|
|
|
|
? list.filter((r) => num(r.Level) >= level || [54, 55, 58].includes(num(r.LinkIndex)))
|
|
|
|
|
: list;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function total(project) {
|
|
|
|
|
const wonga = project.tables.Wonga || [];
|
|
|
|
|
for (let i = wonga.length - 1; i >= 0; i--)
|
|
|
|
|
if (wonga[i].Checked === true && num(wonga[i].Cost)) return num(wonga[i].Cost);
|
|
|
|
|
return num(wonga.find((r) => num(r.LinkIndex) === 55)?.Cost ?? project.calculation?.total);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function totalText(value) {
|
|
|
|
|
return ` 일금:${EstDocs.korean(value)}원정 (₩${Math.round(value).toLocaleString("en-US")}원정)`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pages(list, first, rest) {
|
|
|
|
|
if (!(first > 0 && rest > 0)) throw Error("쪽당 줄 수는 1 이상입니다.");
|
|
|
|
|
const out = [list.slice(0, first)];
|
|
|
|
|
for (let at = first; at < list.length; at += rest) out.push(list.slice(at, at + rest));
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 원본 TIniFile 형식(.einf)
|
|
|
|
|
function toIni(s, keys = Object.keys(sections)) {
|
|
|
|
|
return keys
|
|
|
|
|
.map(
|
|
|
|
|
(section) =>
|
|
|
|
|
`[${section}]\r\n${sections[section].map((key) => `${key}=${typeof s[key] === "boolean" ? (s[key] ? "1" : "0") : (s[key] ?? "")}`).join("\r\n")}`,
|
|
|
|
|
)
|
|
|
|
|
.join("\r\n\r\n");
|
|
|
|
|
}
|
|
|
|
|
function fromIni(text, keys = ["EstiPrnCompanyInfo", "EstiPrnInfo"]) {
|
|
|
|
|
const out = {};
|
|
|
|
|
let section = "";
|
|
|
|
|
for (const line of String(text).split(/\r?\n/)) {
|
|
|
|
|
const head = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
|
|
|
if (head) {
|
|
|
|
|
section = head[1];
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const pair = line.match(/^\s*([^=]+?)\s*=(.*)$/);
|
|
|
|
|
if (!pair || !keys.includes(section) || !sections[section].includes(pair[1])) continue;
|
|
|
|
|
const key = pair[1],
|
|
|
|
|
value = pair[2].trim(),
|
|
|
|
|
base = defaults[key];
|
|
|
|
|
out[key] =
|
|
|
|
|
typeof base === "boolean"
|
|
|
|
|
? ["1", "true", "-1"].includes(value.toLowerCase())
|
|
|
|
|
: typeof base === "number"
|
|
|
|
|
? Number(value) || 0
|
|
|
|
|
: value;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function html(project, s) {
|
|
|
|
|
const esc = (v) =>
|
|
|
|
|
String(v ?? "").replace(
|
|
|
|
|
/[&<>"]/g,
|
|
|
|
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
|
|
|
|
);
|
|
|
|
|
const fmt = (v) => (num(v) ? num(v).toLocaleString("ko-KR", { maximumFractionDigits: 4 }) : "");
|
|
|
|
|
const lines = (text) => esc(text).split("|").join("<br>");
|
|
|
|
|
const align = num(s.Aligment) ? "center" : "left";
|
|
|
|
|
const head = [
|
|
|
|
|
"품 명",
|
|
|
|
|
"규 격",
|
|
|
|
|
"수 량",
|
|
|
|
|
"단위",
|
|
|
|
|
"단 가",
|
|
|
|
|
"공급가액",
|
|
|
|
|
...(s.Bigo_Show ? ["비 고"] : []),
|
|
|
|
|
];
|
|
|
|
|
const table = (list) =>
|
|
|
|
|
`<table class="esti"><thead><tr>${head.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${list
|
|
|
|
|
.map((r) => {
|
|
|
|
|
const bold =
|
|
|
|
|
num(r.Level) > 0 || num(r.LinkIndex) === 54 ? ' style="font-weight:bold"' : "";
|
|
|
|
|
return `<tr${bold}><td style="text-align:${align}">${esc(`${r.Pr ?? ""}${r.Name ?? ""}`)}</td><td style="text-align:${align}">${esc(r.Size)}</td><td class="n">${fmt(r.GN)}</td><td style="text-align:center">${esc(r.Unite)}</td><td class="n">${fmt(r.TU)}</td><td class="n">${fmt(r.TC)}</td>${s.Bigo_Show ? `<td>${esc(typeof r.Bigo === "string" ? r.Bigo : "")}</td>` : ""}</tr>`;
|
|
|
|
|
})
|
|
|
|
|
.join("")}</tbody></table>`;
|
|
|
|
|
const company = s.Company_Show
|
|
|
|
|
? `<table class="supplier"><tr><th rowspan="7">공<br>급<br>자</th><th>등록번호</th><td>${esc(s.Reg_Num)}</td></tr>${[
|
|
|
|
|
["상 호", s.ComPany_Name],
|
|
|
|
|
["성 명", s.Ceo_Name],
|
|
|
|
|
["사업장주소", s.Addr],
|
|
|
|
|
["업 태", s.Uptae],
|
|
|
|
|
["종 목", s.Jongmog],
|
|
|
|
|
["전화(FAX)", s.Tel],
|
|
|
|
|
]
|
|
|
|
|
.map(([k, v]) => `<tr><th>${k}</th><td>${esc(v)}</td></tr>`)
|
|
|
|
|
.join("")}</table>`
|
|
|
|
|
: "";
|
|
|
|
|
const all = pages(rows(project, num(s.Level)), num(s.F_PageCount), num(s.L_PageCount));
|
|
|
|
|
const sum = total(project);
|
|
|
|
|
const body = all
|
|
|
|
|
.map(
|
|
|
|
|
(list, i) =>
|
|
|
|
|
`<section class="page">${
|
|
|
|
|
i === 0
|
|
|
|
|
? `<h1>${esc(s.Title_Name)}</h1><div class="top"><div><p>공사명: ${lines(s.Project_Name)}</p><p>${lines(s.Order_name)} 귀하</p><p>${esc(s.Esti_Date)}</p><p>아래와 같이 견적합니다</p></div>${company}</div><p class="sum">합계금액 ${esc(totalText(sum))}</p>`
|
|
|
|
|
: ""
|
|
|
|
|
}${table(list)}<div class="footer">${i + 1} / ${all.length}</div></section>`,
|
|
|
|
|
)
|
|
|
|
|
.join("");
|
|
|
|
|
return `<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:굴림체,monospace;font-size:10pt}.page{page-break-after:always;padding:10mm}h1{text-align:center;letter-spacing:8px}.top{display:flex;justify-content:space-between;gap:12px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #333;padding:2px 4px}.n{text-align:right}.supplier{width:55%}.sum{font-weight:bold;font-size:12pt;border:1px solid #333;padding:6px}.footer{text-align:center;margin-top:6px}</style></head><body>${body}</body></html>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 화면
|
|
|
|
|
const field = (name) => document.querySelector(`#dialog-content [name="ep-${name}"]`);
|
|
|
|
|
function open(state) {
|
|
|
|
|
const s = settings(state.project),
|
|
|
|
|
{ esc } = EstApp;
|
|
|
|
|
const input = (k, label, type = "text") =>
|
|
|
|
|
`<label>${label}<input name="ep-${k}" type="${type}" value="${esc(s[k])}"></label>`;
|
|
|
|
|
const check = (k, label) =>
|
|
|
|
|
`<label><input name="ep-${k}" type="checkbox" ${s[k] ? "checked" : ""}>${label}</label>`;
|
|
|
|
|
EstApp.showDialog(
|
|
|
|
|
"견적서 작성",
|
|
|
|
|
`<div class="form-fields">${input("Title_Name", "제목")}${input("Project_Name", "공사명(| 줄바꿈)")}${input("Order_name", "발주처")}${input("Esti_Date", "견적일자")}${input("Level", "설계내역 레벨표시(0~9)", "number")}${input("F_PageCount", "첫 페이지 줄수", "number")}${input("L_PageCount", "나머지 페이지 줄수", "number")}<label>품명·규격 정렬<select name="ep-Aligment"><option value="0" ${num(s.Aligment) ? "" : "selected"}>왼쪽</option><option value="1" ${num(s.Aligment) ? "selected" : ""}>가운데</option></select></label>${check("Bigo_Show", "비고란 보기")}${check("Company_Show", "공급자 정보 인쇄")}</div><h4>공급자</h4><div class="form-fields">${input("Reg_Num", "등록번호")}${input("ComPany_Name", "상호")}${input("Ceo_Name", "대표자")}${input("Addr", "사업장주소")}${input("Uptae", "업태")}${input("Jongmog", "종목")}${input("Tel", "전화,(FAX)")}</div><button data-action="estimate-print-preview">미리보기</button> <button data-action="estimate-print-print">인쇄</button> <button data-action="estimate-print-save">공급자정보 저장하기(.einf)</button> <button data-action="estimate-print-load">공급자정보 불러오기</button><iframe id="estimate-print-frame" style="width:100%;height:420px;border:1px solid #ccc;margin-top:8px"></iframe>`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
function read(state) {
|
|
|
|
|
const s = settings(state.project);
|
|
|
|
|
for (const key of Object.keys(defaults)) {
|
|
|
|
|
const node = field(key);
|
|
|
|
|
if (!node) continue;
|
|
|
|
|
s[key] =
|
|
|
|
|
node.type === "checkbox"
|
|
|
|
|
? node.checked
|
|
|
|
|
: typeof defaults[key] === "number"
|
|
|
|
|
? Number(node.value) || 0
|
|
|
|
|
: node.value;
|
|
|
|
|
}
|
|
|
|
|
if (!(s.Level >= 0 && s.Level <= 9)) throw Error("레벨은 0~9 입니다.");
|
|
|
|
|
if (JSON.stringify(s) !== JSON.stringify(settings(state.project)))
|
|
|
|
|
EstApp.change((project) => (project.estimatePrint = s), false);
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
function preview(state) {
|
|
|
|
|
const s = read(state),
|
|
|
|
|
frame = EstApp.byId("estimate-print-frame");
|
|
|
|
|
frame.srcdoc = html(state.project, s);
|
|
|
|
|
return frame;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handle(name, state) {
|
|
|
|
|
if (name === "estimate-print") open(state);
|
|
|
|
|
else if (name === "estimate-print-preview") preview(state);
|
|
|
|
|
else if (name === "estimate-print-print") {
|
|
|
|
|
const frame = preview(state);
|
|
|
|
|
frame.onload = () => frame.contentWindow.print();
|
|
|
|
|
} else if (name === "estimate-print-save")
|
|
|
|
|
EstApp.download("견적서 기본정보.einf", toIni(read(state)), "text/plain");
|
|
|
|
|
else if (name === "estimate-print-load") {
|
|
|
|
|
const picker = document.createElement("input");
|
|
|
|
|
picker.type = "file";
|
|
|
|
|
picker.accept = ".einf,.ini,.txt";
|
|
|
|
|
picker.addEventListener("change", async () => {
|
|
|
|
|
const file = picker.files[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
|
|
|
let text;
|
|
|
|
|
try {
|
|
|
|
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
|
|
|
} catch {
|
|
|
|
|
text = new TextDecoder("euc-kr").decode(bytes);
|
|
|
|
|
}
|
|
|
|
|
const next = { ...read(state), ...fromIni(text) };
|
|
|
|
|
EstApp.change((project) => (project.estimatePrint = next), false);
|
|
|
|
|
open(state);
|
|
|
|
|
});
|
|
|
|
|
picker.click();
|
|
|
|
|
} else return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { settings, rows, total, totalText, pages, toIni, fromIni, html, handle };
|
|
|
|
|
})();
|