217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
#!/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()
|