초기 정리: PIT3000 분석 자료 업로드
- 좌표/SQL/API 분석 문서화 - 재현 스크립트와 예시 응답 추가 - 개인정보/원본 실행파일 제외
This commit is contained in:
216
scripts/render_a4_quickreport_receipt.py
Normal file
216
scripts/render_a4_quickreport_receipt.py
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import csv, re, json, zipfile
|
||||
from pathlib import Path
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib import colors
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
|
||||
|
||||
ROOT=Path('/root/work/pharmit-ghidra')
|
||||
LAYOUT=ROOT/'out/a4_receipt_template_candidates/quickreport_receipt_deep/quickreport_receipt_canonical_top.csv'
|
||||
RAW=ROOT/'out/a4_receipt_three_raw.txt'
|
||||
OUT=ROOT/'out/a4_receipt_quickreport_render_samples'
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
SCALE=72/96
|
||||
FONT='HYGothic-Medium'
|
||||
try:
|
||||
pdfmetrics.registerFont(UnicodeCIDFont(FONT))
|
||||
except Exception:
|
||||
FONT='Helvetica'
|
||||
|
||||
# QuickReport labels found in PIT template -> actual SQL field names from A4 amount query.
|
||||
FIELD_MAP={
|
||||
# common receipt form
|
||||
'Q_paname':'PANAME', 'Q_name2':'PANAME', 'Q_num':'PANUM',
|
||||
'Q_InDate':'INDATE', 'Q_Year':'_YEAR', 'Q_year':'_YEAR', 'Q_year1':'_YEAR',
|
||||
'Q_Month':'_MONTH', 'Q_mon':'_MONTH', 'Q_mon1':'_MONTH',
|
||||
'Q_day':'_DAY', 'Q_day1':'_DAY', 'Q_Days':'_DAYS',
|
||||
'Q_PreSerial':'PRESERIAL', 'QR_PreSerial':'PRESERIAL',
|
||||
'Q_Price_A':'TOT_C', 'QR_Price_a':'TOT_C',
|
||||
'Q_Price_p':'BON_C', 'QR_PriceP':'BON_C', 'QR_Price_psum':'BI_CNT',
|
||||
'Q_Price_c':'CHUNG_C', 'QR_PriceC':'CHUNG_C', 'QR_Pricec1':'CHUNG_C',
|
||||
'Q_s_FastMon':'BI_PRICE', 'QR_PriceN':'BI_PRICE',
|
||||
'Q_Price_n':'_PAY_TOTAL',
|
||||
'Q_Card':'CARD_C', 'Q_Cash':'CHASH_C', 'Q_PCash':'PAPER_C',
|
||||
'Q_TotGum':'_PAY_TOTAL',
|
||||
# company/pharmacy placeholders; real pharmacy master query is separate, seed for visual proof
|
||||
'Q_iscode':'_PHARM_ISCODE', 'Q_isname':'_PHARM_NAME', 'Q_istel':'_PHARM_TEL',
|
||||
'Q_tel':'_PHARM_TEL', 'Q_add':'_PHARM_ADDR', 'Q_post':'_PHARM_POST',
|
||||
'Q_name':'_PHARM_NAME', 'Q_comcode':'_BIZNO', 'Q_comname':'_PHARMACIST', 'Q_name1':'_PHARMACIST',
|
||||
'Q_COMMIT_CODE':'_COMMIT_CODE', 'Q_COMMIT_NAME':'_COMMIT_NAME', 'Q_Unit_Gubun':'_UNIT_GUBUN',
|
||||
'qrlApproval_Num':'_CASH_APPROVAL',
|
||||
}
|
||||
MONEY_FIELDS={'TOT_C','CHUNG_C','BON_C','CARD_C','CHASH_C','PAPER_C','BI_PRICE','BI_CNT','_PAY_TOTAL'}
|
||||
# Keep only the actual receipt/certificate templates in the canonical blob, excluding prescription-review form tail.
|
||||
KEEP_KEYWORDS=['약제비','영수증','진료비','납입 확인서','소득공제','환자성명','조제일자','총 수 납','요양기관','사업자','상 호','대 표 자','전 화 번 호','현금','카드']
|
||||
KEEP_NAMES=set(FIELD_MAP)|{'Q_tag1','Q_tag2'}
|
||||
|
||||
def load_layout(form='certificate'):
|
||||
with LAYOUT.open(encoding='utf-8-sig') as f:
|
||||
rows=list(csv.DictReader(f))
|
||||
# The selected binary is a concatenated QR blob. Band indices reveal independent forms:
|
||||
# 0..77 = 진료비(약제비) 납입 확인서 summary/table
|
||||
# 78..169 = 약제비 계산서·영수증
|
||||
# 171..394/396..626 = 청구/기관확인서 variants
|
||||
ranges={
|
||||
'certificate': (0,77),
|
||||
'receipt': (78,169),
|
||||
'claim1': (171,394),
|
||||
'claim2': (396,626),
|
||||
}
|
||||
lo,hi=ranges.get(form,ranges['certificate'])
|
||||
out=[]
|
||||
for r in rows:
|
||||
idx=int(r['idx'])
|
||||
if not (lo <= idx <= hi):
|
||||
continue
|
||||
cls=r.get('class','')
|
||||
cap=(r.get('caption') or r.get('text') or '')
|
||||
name=r.get('name','')
|
||||
if cls in {'TQRShape','TQRBand'}:
|
||||
out.append(r)
|
||||
elif cls in {'TQRLabel','TQRDBText'}:
|
||||
# In an isolated form keep most labels; mapped dynamic placeholders are replaced.
|
||||
if cap or name in KEEP_NAMES or name.startswith(('QR_Price','QR_PreSerial')):
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
def parse_rows():
|
||||
lines=RAW.read_text(encoding='utf-8', errors='replace').splitlines()
|
||||
data=[ln for ln in lines if '|' in ln and not set(ln.replace('|','').strip()) <= {'-'}]
|
||||
reader=csv.DictReader(data, delimiter='|')
|
||||
rows=[]
|
||||
for r in reader:
|
||||
if r.get('CUSCODE') and not r['CUSCODE'].startswith('-'):
|
||||
rows.append({k:(v or '').strip() for k,v in r.items()})
|
||||
return rows
|
||||
|
||||
def money(v):
|
||||
try: return f"{int(float(str(v).replace(',','') or '0')):,}"
|
||||
except Exception: return str(v or '')
|
||||
|
||||
def ymd_parts(s):
|
||||
s=re.sub(r'\D','',s or '')
|
||||
if len(s)>=8: return s[:4],s[4:6],s[6:8],f'{s[:4]}-{s[4:6]}-{s[6:8]}'
|
||||
return '','','',s
|
||||
|
||||
def enrich(r):
|
||||
r=dict(r)
|
||||
y,m,d,fmt=ymd_parts(r.get('INDATE'))
|
||||
r['_YEAR']=y; r['_MONTH']=m; r['_DAY']=d; r['_DATE_FMT']=fmt
|
||||
# Actual SQL sample had payment method columns zero in these rows; PIT sometimes updates CD_SELL_MASTE later.
|
||||
pay=sum(int(float(r.get(k) or 0)) for k in ['CARD_C','CHASH_C','PAPER_C'])
|
||||
if pay==0:
|
||||
pay=int(float(r.get('BON_C') or 0))
|
||||
r['CHASH_C']=str(pay)
|
||||
r['_PAY_TOTAL']=str(pay)
|
||||
r['_DAYS']=''
|
||||
# raw names are codepage-garbled in saved text; mask/use generic visual proof, keep ids real.
|
||||
r['PANAME']='홍길동' if '<EFBFBD>' in r.get('PANAME','') else r.get('PANAME') or '홍길동'
|
||||
panum=r.get('PANUM') or ''
|
||||
r['PANUM']=(panum[:6]+'-'+panum[6:7]+'******') if len(re.sub(r'\D','',panum))>=7 else '******-*******'
|
||||
r['_PHARM_ISCODE']='12345678'
|
||||
r['_PHARM_NAME']='샘플약국'
|
||||
r['_PHARM_TEL']='02-1234-5678'
|
||||
r['_PHARM_ADDR']='서울특별시 샘플구 약국로 1'
|
||||
r['_PHARM_POST']='01234'
|
||||
r['_BIZNO']='123-45-67890'
|
||||
r['_PHARMACIST']='김약사'
|
||||
r['_COMMIT_CODE']=''
|
||||
r['_COMMIT_NAME']=''
|
||||
r['_UNIT_GUBUN']='약국'
|
||||
r['_CASH_APPROVAL']=''
|
||||
return r
|
||||
|
||||
def obj_value(r, o):
|
||||
name=o['name']; cap=o.get('caption') or o.get('text') or ''
|
||||
fld=FIELD_MAP.get(name)
|
||||
if fld:
|
||||
v=r.get(fld,'')
|
||||
return money(v) if fld in MONEY_FIELDS else str(v)
|
||||
# TQRDBText from actual sqlMain in certificate table
|
||||
df=o.get('datafield') or ''
|
||||
if df and df in r:
|
||||
return money(r[df]) if df in MONEY_FIELDS else r[df]
|
||||
# drop design placeholders that are dynamic but not mapped
|
||||
if name.startswith(('Q_','QR_Price','QR_PreSerial','qrlApproval')) and cap==name:
|
||||
return ''
|
||||
return cap
|
||||
|
||||
def draw_text(c, r, row, page_h):
|
||||
try:
|
||||
x=float(row['left'] or 0)*SCALE; top=float(row['top'] or 0)*SCALE
|
||||
w=float(row['width'] or 0)*SCALE; h=float(row['height'] or 0)*SCALE
|
||||
except Exception: return
|
||||
txt=obj_value(r,row)
|
||||
if not txt: return
|
||||
y=page_h-top-h
|
||||
fh=abs(float(row.get('font_height') or -10))*0.72
|
||||
fh=max(5.5,min(fh,17))
|
||||
c.setFont(FONT,fh)
|
||||
c.setFillColor(colors.black)
|
||||
# right-align amount/name-ish labels by PIT naming convention
|
||||
if row['name'] in FIELD_MAP and FIELD_MAP[row['name']] in MONEY_FIELDS:
|
||||
c.drawRightString(x+w-1,y+max(1,(h-fh)/2)+1,txt)
|
||||
else:
|
||||
# simplistic wrap for long notes
|
||||
if len(txt)>70 and w>100:
|
||||
words=list(txt)
|
||||
line=''; yy=y+h-fh
|
||||
maxchars=max(10,int(w/(fh*0.55)))
|
||||
for ch in words:
|
||||
line+=ch
|
||||
if len(line)>=maxchars or ch=='\n':
|
||||
c.drawString(x+1,yy,line.strip()); yy-=fh*1.15; line=''
|
||||
if yy<y: break
|
||||
if line and yy>=y: c.drawString(x+1,yy,line)
|
||||
else:
|
||||
c.drawString(x+1,y+max(1,(h-fh)/2)+1,txt)
|
||||
|
||||
def draw_shape(c,row,page_h):
|
||||
try:
|
||||
x=float(row['left'] or 0)*SCALE; top=float(row['top'] or 0)*SCALE
|
||||
w=float(row['width'] or 0)*SCALE; h=float(row['height'] or 0)*SCALE
|
||||
except Exception: return
|
||||
if w<=0 or h<=0: return
|
||||
y=page_h-top-h
|
||||
c.setStrokeColor(colors.HexColor('#444444'))
|
||||
c.setLineWidth(0.35)
|
||||
if w<=2*SCALE or h<=2*SCALE:
|
||||
c.line(x,y,x+w,y+h)
|
||||
else:
|
||||
c.rect(x,y,w,h,stroke=1,fill=0)
|
||||
|
||||
def render_one(data_row, idx, layout, form):
|
||||
r=enrich(data_row)
|
||||
out=OUT/f'a4_quickreport_{form}_{idx}_{r["PRESERIAL"]}.pdf'
|
||||
c=canvas.Canvas(str(out), pagesize=A4)
|
||||
W,H=A4
|
||||
# QuickReport object order: shapes first, text on top.
|
||||
for o in layout:
|
||||
if o['class']=='TQRShape': draw_shape(c,o,H)
|
||||
for o in layout:
|
||||
if o['class'] in {'TQRLabel','TQRDBText'}: draw_text(c,r,o,H)
|
||||
c.setFont(FONT,6); c.setFillColor(colors.HexColor('#777777'))
|
||||
c.drawString(4,4,f'PIT QuickReport mapped render / CUSCODE={r.get("CUSCODE")} PRESERIAL={r.get("PRESERIAL")} / source SQL fields: TOT_C CHUNG_C BON_C CARD_C CHASH_C PAPER_C BI_PRICE')
|
||||
c.save()
|
||||
return out
|
||||
|
||||
def main():
|
||||
rows=parse_rows()
|
||||
outs=[]
|
||||
for form in ['certificate','receipt']:
|
||||
layout=load_layout(form)
|
||||
for i,r in enumerate(rows[:3],1):
|
||||
outs.append(render_one(r,i,layout,form))
|
||||
z=ROOT/'out/a4_quickreport_receipt_sql_mapped_20260628.zip'
|
||||
with zipfile.ZipFile(z,'w',zipfile.ZIP_DEFLATED) as zz:
|
||||
for p in outs+[Path(__file__),LAYOUT,RAW]:
|
||||
zz.write(p,p.relative_to(ROOT))
|
||||
print('\n'.join(str(p) for p in outs))
|
||||
print(z)
|
||||
print(z.stat().st_size)
|
||||
|
||||
if __name__=='__main__': main()
|
||||
Reference in New Issue
Block a user