import json import subprocess from pathlib import Path from test_est_rea_parity import run ROOT = Path(__file__).resolve().parents[2] def test_jdl_special_unit_detail_with_matching_totals_is_editable(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); const empty=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(empty); const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], ['m.txt',table('m.txt',[{F_CODE:'M1',F_DES:'material',F_MPRICE:100}])], ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:300,F_LPRICE:50}])], ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_SEQ:'1',F_CODE1:'M1',F_DES:'normal',F_QQTY:2,F_MPRICE:100,F_MCOST:200}, {F_CODE:'U1',F_SEQ:'2',F_CODE1:'M1',F_DES:'special',F_QQTY:1,F_MCOST:100,F_LCOST:50,F_SPROPER:'1'}])], ['gongjong.txt',table('gongjong.txt',[{F_CODE:'01',F_DES:'work'}])], ['mainpart.txt',table('mainpart.txt',[{F_CODE:'01',F_SEQ:'1',F_CODE1:'U1',F_DES:'unit',F_QQTY:1,F_MPRICE:300,F_LPRICE:50}])]]); const p=j.read(a.pack(files),empty),before=structuredClone(p.tables.HeadMainDat);p.options.AutoCalc=true;p.options.SourceRefresh=true; p.tables.HeadMainDat[2].GC=110;const q=s.calculate(p),out=j.output(q),back=j.read(out.bytes,blank); process.stdout.write(JSON.stringify({before,title:q.tables.HeadTitleDat[0],rows:back.tables.HeadMainDat, total:back.tables.EstMainDat.at(-1).TC,warnings:p.importWarnings,errors:q.calculation.errors}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert len(result["before"]) == 3 assert result["before"][2]["Name"] == "special" assert result["before"][2]["CalcIndex"] == 1 assert result["title"]["TC"] == 360 assert result["rows"][2]["GC"] == 110 assert result["rows"][2]["NC"] == 50 assert result["total"] == 360 assert any("특수 구성" in warning for warning in result["warnings"]) assert not result["errors"] def test_jdl_special_unit_detail_without_source_keeps_recorded_amount(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'); const empty=JSON.parse(fs.readFileSync(0,'utf8')); const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:70}])], ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_SEQ:'1',F_CODE1:'missing',F_DES:'special',F_QQTY:1,F_MCOST:70,F_SPROPER:'1'}])]]); const p=j.read(a.pack(files),empty);process.stdout.write(JSON.stringify({rows:p.tables.HeadMainDat,warnings:p.importWarnings}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert len(result["rows"]) == 2 assert result["rows"][1]["GC"] == 70 assert result["rows"][1]["LinkIndex"] == 0 assert result["rows"][1]["JdlSourceCode"] == "missing" assert any("특수 구성" in warning for warning in result["warnings"]) def test_jdl_special_machine_detail_is_fixed_only_when_amounts_match(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); const empty=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(empty); const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:100,F_LPRICE:20}, {F_CODE:'J2',F_DES:'mismatch',F_MPRICE:50},{F_CODE:'J3',F_DES:'percent',F_MPRICE:50}, {F_CODE:'J4',F_DES:'weighted',F_MPRICE:100}])], ['jpart.txt',table('jpart.txt',[{F_CODE:'J1',F_SEQ:'1',F_DES:'normal',F_QQTY:1,F_MPRICE:60}, {F_CODE:'J1',F_SEQ:'2',F_DES:'special',F_QQTY:1,F_MPRICE:40,F_LPRICE:20,F_SPROPER:'1'}, {F_CODE:'J2',F_SEQ:'1',F_DES:'mismatch',F_QQTY:1,F_MPRICE:10,F_SPROPER:'1'}, {F_CODE:'J3',F_SEQ:'1',F_DES:'percent',F_UNIT:'%',F_QQTY:1,F_MPRICE:50,F_SPROPER:'1'}, {F_CODE:'J4',F_SEQ:'1',F_DES:'weighted part',F_QQTY:2,F_MPRICE:50,F_SPROPER:'1'}])]]); const p=j.read(a.pack(files),empty),before=structuredClone(p.tables.MachMainDat);p.options.AutoCalc=true; p.tables.MachMainDat[2].GC=50;const q=s.calculate(p),out=j.output(q),back=j.read(out.bytes,blank); process.stdout.write(JSON.stringify({before,titles:q.tables.MachTitleDat,back:back.tables.MachMainDat, warnings:p.importWarnings,errors:q.calculation.errors}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert result["before"][2]["Name"] == "special" assert result["before"][2]["CalcIndex"] == 1 assert result["titles"][0]["TC"] == 130 assert result["back"][2]["GC"] == 50 assert result["titles"][3]["TC"] == 100 assert "상세 산식 미변환" in result["before"][4]["Name"] assert "상세 산식 미변환" in result["before"][6]["Name"] assert any("특수 기계 상세" in warning for warning in result["warnings"]) assert not result["errors"] def test_jdl_machine_percent_detail_round_trips_fixed_amounts(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); const empty=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(empty); const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:5},{F_CODE:'J2',F_DES:'ambiguous',F_MPRICE:50}])], ['jpart.txt',table('jpart.txt',[{F_CODE:'J1',F_SEQ:'1',F_DES:'percent',F_UNIT:'%',F_QQTY:10,F_MPRICE:50,F_SPROPER:'1'}, {F_CODE:'J2',F_SEQ:'1',F_DES:'uncertain',F_UNIT:'%',F_QQTY:1,F_MPRICE:50}])]]); const p=j.read(a.pack(files),empty),before=structuredClone(p.tables.MachMainDat);p.options.AutoCalc=true; p.tables.MachMainDat[1].GC=6;const q=s.calculate(p),out=j.output(q),entries=a.unpack(out.bytes),back=j.read(out.bytes,blank); process.stdout.write(JSON.stringify({before,parts:j.parse('jpart.txt',entries.get('jpart.txt')), rows:back.tables.MachMainDat,errors:q.calculation.errors}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert result["before"][1]["GC"] == 5 assert result["before"][1]["GN"] == 10 assert result["before"][1]["JdlSourceQuantity"] == 10 assert result["parts"][0]["F_UNIT"] == "%" assert result["parts"][0]["F_QQTY"] == 100 assert result["rows"][1]["GC"] == 6 assert "상세 산식 미변환" in result["before"][-1]["Name"] assert not result["errors"] def test_jdl_fixed_unit_percent_exports_a_consistent_quantity(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); const p=JSON.parse(fs.readFileSync(0,'utf8')),empty=structuredClone(p);p.name='percent'; p.tables.HeadTitleDat=[{Code:'U',Name:'unit',GC:10,TC:10}]; p.tables.HeadMainDat=[{Index:1,Code:'U'}, {Index:0,Name:'fixed percent',Unite:'%',CalcIndex:1,JdlSummary:true,GC:10,TC:10}]; p.tables.EstMainDat=[{Code:'U',Name:'unit',LinkIndex:103,GN:2,GU:10,GC:20,TC:20},{LinkIndex:54}]; const out=j.output(p),files=a.unpack(out.bytes),parts=j.parse('unitpart.txt',files.get('unitpart.txt')), q=j.read(out.bytes,empty); process.stdout.write(JSON.stringify({part:parts[0],row:q.tables.HeadMainDat[1],total:q.tables.EstMainDat.at(-1).TC}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert result["part"]["F_UNIT"] == "%" assert result["part"]["F_QQTY"] == 100 assert result["part"]["F_MPRICE"] == result["part"]["F_MCOST"] == 10 assert result["row"]["GC"] == 10 assert result["total"] == 20 def test_jdl_estimate_percent_quantity_and_fixed_amount_round_trip(): project = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) for table in project["tables"]: project["tables"][table] = [] script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); const p=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(p); p.tables.StuffcDat=[{Code:'M',Name:'material',GC:200,TC:200}]; p.tables.EstMainDat=[{Code:'M',Name:'rate',Unite:'%',LinkIndex:5,GN:10,GU:200,GC:20,TC:20}, {Name:'fixed',Unite:'%',CalcIndex:1,GC:30,TC:30},{LinkIndex:54}]; const out=j.output(p),files=a.unpack(out.bytes),parts=j.parse('mainpart.txt',files.get('mainpart.txt')), back=j.read(out.bytes,blank); process.stdout.write(JSON.stringify({parts,rows:back.tables.EstMainDat}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert [part["F_QQTY"] for part in result["parts"]] == [10, 100] assert [row["TC"] for row in result["rows"] if not row.get("Level")] == [20, 30, 50] def test_jdl_estimate_preserves_components_when_total_matches(): project = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) for table in project["tables"]: project["tables"][table] = [] script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'); const p=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(p); p.tables.StuffcDat=[{Code:'M',Name:'material',GC:10,TC:10}]; p.tables.EstMainDat=[{Code:'M',Name:'adjusted',LinkIndex:5,GN:2,GU:10,GC:0,NC:20,TC:20},{LinkIndex:54}]; const q=j.read(j.output(p).bytes,blank);process.stdout.write(JSON.stringify(q.tables.EstMainDat.at(-1)));""" result = json.loads( subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) assert result["GC"] == 0 assert result["NC"] == 20 def test_jdl_rejects_case_insensitive_duplicate_tables(): project = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); const p=JSON.parse(fs.readFileSync(0,'utf8')); const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tfirst\\t02\\x05\\r\\n')], ['MAIN.TXT',Buffer.from('0\\tsecond\\t02\\x05\\r\\n')]]); try { j.read(a.pack(files),p); process.stdout.write('accepted'); } catch(error) { process.stdout.write(error.message); }""" result = subprocess.check_output( ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT, text=False ).decode("utf-8") assert "중복" in result