commit 8e6552724a1b16ba0dcf1749edc6d6cfc416681f Author: 청춘약국 Date: Wed Apr 1 22:10:15 2026 +0900 Initial commit: Flask stats API (v1 PharmIT3000, v2 PMPLUS20) diff --git a/README.md b/README.md new file mode 100644 index 0000000..37d961d --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Pharmacy Stats API + +QT-POS 통계 다이얼로그 API 버전 + +## 구조 + +``` +pharmacy-stats-api/ +├── app.py # Flask 메인 +├── config.py # DB 연결 설정 +├── queries/ +│ ├── v1_pharmit3000.py # PharmIT3000 쿼리 +│ └── v2_pmplus20.py # PMPLUS20 쿼리 +└── templates/ + └── stats.html # 비교 UI +``` + +## API 엔드포인트 + +### v1 - PharmIT3000 + +| Endpoint | 설명 | +|----------|------| +| `/v1/api/stats` | 전체 통계 | +| `/v1/api/stats/insurance` | 보험별 통계 | +| `/v1/api/stats/time` | 시간가산별 통계 | +| `/v1/api/stats/payment` | 결제수단별 통계 | +| `/v1/api/stats/hospital` | 병원별 통계 | + +### v2 - PMPLUS20 + +동일한 구조의 `/v2/api/stats/...` 엔드포인트 + +### 비교 + +| Endpoint | 설명 | +|----------|------| +| `/api/compare` | v1 vs v2 비교 | + +## 쿼리 파라미터 + +- `from`: 시작일 (YYYYMMDD) +- `to`: 종료일 (YYYYMMDD) + +예: `/v1/api/stats?from=20260401&to=20260401` + +## 테이블 매핑 + +| 역할 | PharmIT3000 (v1) | PMPLUS20 (v2) | +|------|-----------------|---------------| +| 판매 헤더 | PM_PRES.PS_MAIN | PM_MAIN.TBSIM040_28 | +| 판매 상세 | PM_PRES.PS_SUB_PHARM | PM_MAIN.TBSIM040_29 | +| 수납 정보 | PM_PRES.CD_SUNAB | PM_MAIN.TBSIR000_01 | +| 약품 마스터 | PM_DRUG.CD_GOODS | PM_MAIN.TBSIM040_01 | + +## 실행 + +```bash +pip install -r requirements.txt +python app.py +``` + +서버: http://0.0.0.0:5060 + +## 환경변수 + +```bash +PHARMIT_SERVER=192.168.0.4\PM2014 +PHARMIT_USER=sa +PHARMIT_PASS=password + +PMPLUS_SERVER=192.168.0.4\PM2014 +PMPLUS_USER=sa +PMPLUS_PASS=password +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..94d0f81 --- /dev/null +++ b/app.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +""" +Pharmacy Stats API - Flask 서버 + +QT-POS 통계 다이얼로그 API 버전 +v1: PharmIT3000 +v2: PMPLUS20 +""" +from flask import Flask, jsonify, request, render_template +from datetime import date, timedelta + +from queries import v1_pharmit3000 as v1 +from queries import v2_pmplus20 as v2 + +app = Flask(__name__) + +# 보험구분 라벨 +GUBUN_LABEL = { + '0': '건강보험', '1': '의료급여', '2': '산재', '3': '자동차', + '4': '보훈', '5': '공상', '6': '본인', '7': '차상위1', + '8': '희귀', '9': '비급여', 'E': '차상위2', 'F': '차상위2', +} + + +@app.route('/') +def index(): + return render_template('stats.html') + + +# ============== v1 API (PharmIT3000) ============== + +@app.route('/v1/api/stats') +def v1_stats(): + """v1 전체 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + try: + result = v1.get_sales_stats(date_from, date_to) + result['version'] = 'v1' + result['source'] = 'PharmIT3000' + result['date_from'] = date_from + result['date_to'] = date_to + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/v1/api/stats/insurance') +def v1_stats_insurance(): + """v1 보험별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v1.get_sales_stats(date_from, date_to) + + by_gubun = [] + for code, data in result.get('by_gubun', {}).items(): + by_gubun.append({ + 'code': code, + 'label': GUBUN_LABEL.get(code, code), + **data + }) + + return jsonify({ + 'version': 'v1', + 'source': 'PharmIT3000', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_gubun': sorted(by_gubun, key=lambda x: x['code']) + }) + + +@app.route('/v1/api/stats/time') +def v1_stats_time(): + """v1 시간가산별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v1.get_sales_stats(date_from, date_to) + + return jsonify({ + 'version': 'v1', + 'source': 'PharmIT3000', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_time': result['by_time'] + }) + + +@app.route('/v1/api/stats/payment') +def v1_stats_payment(): + """v1 결제수단별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v1.get_sales_stats(date_from, date_to) + + return jsonify({ + 'version': 'v1', + 'source': 'PharmIT3000', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_pay': result['by_pay'] + }) + + +@app.route('/v1/api/stats/hospital') +def v1_stats_hospital(): + """v1 병원별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v1.get_sales_stats(date_from, date_to) + + by_hosp = [] + for name, data in result.get('by_hosp', {}).items(): + by_hosp.append({'name': name, **data}) + + return jsonify({ + 'version': 'v1', + 'source': 'PharmIT3000', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_hosp': sorted(by_hosp, key=lambda x: -x['cnt'])[:50] # 상위 50개 + }) + + +# ============== v2 API (PMPLUS20) ============== + +@app.route('/v2/api/stats') +def v2_stats(): + """v2 전체 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + try: + result = v2.get_sales_stats(date_from, date_to) + result['version'] = 'v2' + result['source'] = 'PMPLUS20' + result['date_from'] = date_from + result['date_to'] = date_to + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/v2/api/stats/insurance') +def v2_stats_insurance(): + """v2 보험별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v2.get_sales_stats(date_from, date_to) + + if 'error' in result: + return jsonify(result), 500 + + by_gubun = [] + for code, data in result.get('by_gubun', {}).items(): + by_gubun.append({ + 'code': code, + 'label': GUBUN_LABEL.get(code, code), + **data + }) + + return jsonify({ + 'version': 'v2', + 'source': 'PMPLUS20', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_gubun': sorted(by_gubun, key=lambda x: x['code']) + }) + + +@app.route('/v2/api/stats/time') +def v2_stats_time(): + """v2 시간가산별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v2.get_sales_stats(date_from, date_to) + if 'error' in result: + return jsonify(result), 500 + + return jsonify({ + 'version': 'v2', + 'source': 'PMPLUS20', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_time': result['by_time'] + }) + + +@app.route('/v2/api/stats/payment') +def v2_stats_payment(): + """v2 결제수단별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v2.get_sales_stats(date_from, date_to) + if 'error' in result: + return jsonify(result), 500 + + return jsonify({ + 'version': 'v2', + 'source': 'PMPLUS20', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_pay': result['by_pay'] + }) + + +@app.route('/v2/api/stats/hospital') +def v2_stats_hospital(): + """v2 병원별 통계""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + result = v2.get_sales_stats(date_from, date_to) + if 'error' in result: + return jsonify(result), 500 + + by_hosp = [] + for name, data in result.get('by_hosp', {}).items(): + by_hosp.append({'name': name, **data}) + + return jsonify({ + 'version': 'v2', + 'source': 'PMPLUS20', + 'date_from': date_from, + 'date_to': date_to, + 'total': result['total'], + 'by_hosp': sorted(by_hosp, key=lambda x: -x['cnt'])[:50] + }) + + +# ============== 비교 API ============== + +@app.route('/api/compare') +def compare(): + """v1 vs v2 비교""" + date_from = request.args.get('from', _default_from()) + date_to = request.args.get('to', _default_to()) + + v1_result = v1.get_sales_stats(date_from, date_to) + v2_result = v2.get_sales_stats(date_from, date_to) + + # 차이 계산 + diff = {} + if 'total' in v1_result and 'total' in v2_result: + v1_total = v1_result['total'] + v2_total = v2_result['total'] + for key in v1_total: + v1_val = v1_total.get(key, 0) + v2_val = v2_total.get(key, 0) + diff[key] = v2_val - v1_val + + return jsonify({ + 'date_from': date_from, + 'date_to': date_to, + 'v1': { + 'source': 'PharmIT3000', + 'total': v1_result.get('total', {}), + 'error': v1_result.get('error') + }, + 'v2': { + 'source': 'PMPLUS20', + 'total': v2_result.get('total', {}), + 'error': v2_result.get('error') + }, + 'diff': diff + }) + + +def _default_from(): + """기본 시작일: 이번 달 1일""" + today = date.today() + return today.replace(day=1).strftime('%Y%m%d') + + +def _default_to(): + """기본 종료일: 오늘""" + return date.today().strftime('%Y%m%d') + + +if __name__ == '__main__': + print("🏥 Pharmacy Stats API") + print("http://0.0.0.0:5060") + print("") + print("Endpoints:") + print(" /v1/api/stats - PharmIT3000 전체 통계") + print(" /v1/api/stats/insurance - PharmIT3000 보험별") + print(" /v1/api/stats/time - PharmIT3000 시간가산별") + print(" /v1/api/stats/payment - PharmIT3000 결제수단별") + print(" /v1/api/stats/hospital - PharmIT3000 병원별") + print("") + print(" /v2/api/stats/... - PMPLUS20 (동일 구조)") + print("") + print(" /api/compare - v1 vs v2 비교") + app.run(host='0.0.0.0', port=5060, debug=True) diff --git a/config.py b/config.py new file mode 100644 index 0000000..93e2b7d --- /dev/null +++ b/config.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +"""DB 연결 설정""" +import os + +# PharmIT3000 (v1) +PHARMIT3000_CONFIG = { + 'server': os.getenv('PHARMIT_SERVER', '192.168.0.4\\PM2014'), + 'database': 'PM_PRES', + 'username': os.getenv('PHARMIT_USER', 'sa'), + 'password': os.getenv('PHARMIT_PASS', 'tmddls214!%('), +} + +# PMPLUS20 (v2) +PMPLUS20_CONFIG = { + 'server': os.getenv('PMPLUS_SERVER', '192.168.0.4\\PM2014'), + 'database': 'PM_MAIN', + 'username': os.getenv('PMPLUS_USER', 'sa'), + 'password': os.getenv('PMPLUS_PASS', 'tmddls214!%('), +} diff --git a/queries/__init__.py b/queries/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/queries/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/queries/v1_pharmit3000.py b/queries/v1_pharmit3000.py new file mode 100644 index 0000000..10fdeef --- /dev/null +++ b/queries/v1_pharmit3000.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +""" +PharmIT3000 통계 쿼리 (v1) + +테이블: +- PM_PRES.PS_MAIN: 처방 헤더 +- PM_PRES.PS_SUB_PHARM: 처방 상세 +- PM_PRES.CD_SUNAB: 수납 정보 +- PM_DRUG.CD_GOODS: 약품 마스터 + +QT-POS sales_stats_dialog.py 기반 +""" +import pyodbc +from datetime import date +from config import PHARMIT3000_CONFIG as CFG + + +def get_connection(): + conn_str = ( + f"DRIVER={{ODBC Driver 17 for SQL Server}};" + f"SERVER={CFG['server']};" + f"DATABASE={CFG['database']};" + f"UID={CFG['username']};" + f"PWD={CFG['password']};" + f"TrustServerCertificate=yes;" + f"Connection Timeout=30;" + ) + return pyodbc.connect(conn_str) + + +def query(sql, params=None): + """쿼리 실행 후 dict 리스트 반환""" + conn = get_connection() + cursor = conn.cursor() + cursor.execute(sql, params or ()) + columns = [col[0] for col in cursor.description] + rows = [dict(zip(columns, row)) for row in cursor.fetchall()] + conn.close() + return rows + + +def get_sales_stats(date_from: str, date_to: str) -> dict: + """ + 매출 통계 조회 (QT-POS sales_stats_dialog.py 기준) + + Args: + date_from: 시작일 (YYYYMMDD) + date_to: 종료일 (YYYYMMDD) + + Returns: + dict: {total, by_gubun, by_age, by_time, by_pay, by_hosp} + """ + sql = """ + SELECT + m.PreSerial, + m.PreGubun, + m.PaNum, + m.OrderName, + m.Holiday, + m.PRICE_T, + m.Drug_T4, + m.S_Prep, + m.PRICE_C, + m.PRICE_P, + m.PRICE_N, + ISNULL(n.Appr_Gubun, '') AS Appr_Gubun, + ISNULL(n.nAPPROVAL_NUM, '') AS nAPPROVAL_NUM + FROM PM_PRES..PS_MAIN m + LEFT JOIN PM_PRES..CD_SUNAB n ON n.PRESERIAL = m.PreSerial + WHERE m.INDATE BETWEEN ? AND ? + """ + rows = query(sql, (date_from, date_to)) + + ref_date = date( + int(date_to[:4]), + int(date_to[4:6]), + int(date_to[6:8]), + ) + + # 집계 + result = { + 'total': _empty_row(), + 'by_gubun': {}, + 'by_age': {'old': _empty_row(), 'infant': _empty_row(), 'mid': _empty_row()}, + 'by_time': {'overtime': _empty_row(), 'saturday': _empty_row(), 'normal': _empty_row()}, + 'by_pay': {'card': _empty_row(), 'cash': _empty_row(), 'paper': _empty_row()}, + 'by_hosp': {}, + } + + for r in rows: + gubun = str(r.get('PreGubun') or '0') + holiday = str(r.get('Holiday') or '1') + price_t = int(r.get('PRICE_T') or 0) + drug_t4 = int(r.get('Drug_T4') or 0) + s_prep = int(r.get('S_Prep') or 0) + price_c = int(r.get('PRICE_C') or 0) + price_p = int(r.get('PRICE_P') or 0) + price_n = int(r.get('PRICE_N') or 0) + appr_gubun = str(r.get('Appr_Gubun') or '') + order_name = str(r.get('OrderName') or '기타') + panum = str(r.get('PaNum') or '') + + sales_amt = price_t + drug_t4 + + # 급여/비급여 분리 + if gubun == '9': + ins_prep, ins_drug = 0, 0 + nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0 + nonins_drug = drug_t4 + else: + ins_prep, ins_drug = s_prep, price_t - s_prep + nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0 + nonins_drug = drug_t4 + + args = (sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug, 0, price_c, price_p, price_n) + + # 전체 + _add(result['total'], *args) + + # 보험별 + if gubun not in result['by_gubun']: + result['by_gubun'][gubun] = _empty_row() + _add(result['by_gubun'][gubun], *args) + + # 연령가산별 + age = _calc_age(panum, ref_date) + if age is None or (6 <= age < 65): + _add(result['by_age']['mid'], *args) + elif age >= 65: + _add(result['by_age']['old'], *args) + else: + _add(result['by_age']['infant'], *args) + + # 시간가산별 + hd_add = holiday in ('2', '4') # 공휴가산 + overtime = holiday in ('3', '4') # 시간외 + if hd_add: + _add(result['by_time']['saturday'], *args) + elif overtime: + _add(result['by_time']['overtime'], *args) + else: + _add(result['by_time']['normal'], *args) + + # 결제수단별 + if appr_gubun == '1': + _add(result['by_pay']['card'], *args) + elif appr_gubun == '2': + _add(result['by_pay']['cash'], *args) + else: + _add(result['by_pay']['paper'], *args) + + # 병원별 + if order_name not in result['by_hosp']: + result['by_hosp'][order_name] = _empty_row() + _add(result['by_hosp'][order_name], *args) + + return result + + +def _empty_row(): + return dict(cnt=0, sales_amt=0, ins_prep=0, ins_drug=0, + nonins_prep=0, nonins_drug=0, nonins_margin=0, + claim_amt=0, copay=0, receipt=0) + + +def _add(d, sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug, + nonins_margin, claim_amt, copay, receipt): + d['cnt'] += 1 + d['sales_amt'] += sales_amt + d['ins_prep'] += ins_prep + d['ins_drug'] += ins_drug + d['nonins_prep'] += nonins_prep + d['nonins_drug'] += nonins_drug + d['nonins_margin'] += nonins_margin + d['claim_amt'] += claim_amt + d['copay'] += copay + d['receipt'] += receipt + + +def _calc_age(panum: str, ref_date: date) -> int | None: + """주민번호 → 나이""" + if not panum or len(panum) < 7: + return None + birth6 = panum[:6] + gender = panum[6] + if not birth6.isdigit(): + return None + yy, mm, dd = int(birth6[:2]), int(birth6[2:4]), int(birth6[4:6]) + if gender in ('1', '2'): + year = 1900 + yy + elif gender in ('3', '4'): + year = 2000 + yy + else: + year = 1900 + yy + try: + bday = date(year, mm, dd) + return ref_date.year - bday.year - ( + (ref_date.month, ref_date.day) < (bday.month, bday.day)) + except ValueError: + return None diff --git a/queries/v2_pmplus20.py b/queries/v2_pmplus20.py new file mode 100644 index 0000000..3c7efdd --- /dev/null +++ b/queries/v2_pmplus20.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +""" +PMPLUS20 통계 쿼리 (v2) + +테이블 매핑 (PharmIT3000 → PMPLUS20): +- PM_PRES.PS_MAIN → PM_MAIN.TBSIM040_28 +- PM_PRES.PS_SUB_PHARM → PM_MAIN.TBSIM040_29 +- PM_PRES.CD_SUNAB → PM_MAIN.TBSIR000_01 +- PM_DRUG.CD_GOODS → PM_MAIN.TBSIM040_01 + +주의: +- DrugCode vs Drug_Code 대소문자 차이 +- TBSIR000_01.RECP_DT는 datetime (변환 필요) +""" +import pyodbc +from datetime import date +from config import PMPLUS20_CONFIG as CFG + + +def get_connection(): + conn_str = ( + f"DRIVER={{ODBC Driver 17 for SQL Server}};" + f"SERVER={CFG['server']};" + f"DATABASE={CFG['database']};" + f"UID={CFG['username']};" + f"PWD={CFG['password']};" + f"TrustServerCertificate=yes;" + f"Connection Timeout=30;" + ) + return pyodbc.connect(conn_str) + + +def query(sql, params=None): + """쿼리 실행 후 dict 리스트 반환""" + conn = get_connection() + cursor = conn.cursor() + cursor.execute(sql, params or ()) + columns = [col[0] for col in cursor.description] + rows = [dict(zip(columns, row)) for row in cursor.fetchall()] + conn.close() + return rows + + +def get_sales_stats(date_from: str, date_to: str) -> dict: + """ + 매출 통계 조회 (PMPLUS20 버전) + + Args: + date_from: 시작일 (YYYYMMDD) + date_to: 종료일 (YYYYMMDD) + + Returns: + dict: {total, by_gubun, by_age, by_time, by_pay, by_hosp} + + Note: + 컬럼명/테이블명은 실제 PMPLUS20 스키마 확인 후 수정 필요 + """ + # TODO: 실제 PMPLUS20 스키마 확인 후 쿼리 수정 + # 현재는 PharmIT3000과 동일한 컬럼명 가정 + sql = """ + SELECT + m.PreSerial, + m.PreGubun, + m.PaNum, + m.OrderName, + m.Holiday, + m.PRICE_T, + m.Drug_T4, + m.S_Prep, + m.PRICE_C, + m.PRICE_P, + m.PRICE_N, + ISNULL(n.Appr_Gubun, '') AS Appr_Gubun, + ISNULL(n.nAPPROVAL_NUM, '') AS nAPPROVAL_NUM + FROM PM_MAIN..TBSIM040_28 m + LEFT JOIN PM_MAIN..TBSIR000_01 n ON n.PRESERIAL = m.PreSerial + WHERE m.INDATE BETWEEN ? AND ? + """ + + try: + rows = query(sql, (date_from, date_to)) + except Exception as e: + return { + 'error': str(e), + 'note': 'PMPLUS20 스키마 확인 필요. 컬럼명/테이블명이 다를 수 있음.' + } + + ref_date = date( + int(date_to[:4]), + int(date_to[4:6]), + int(date_to[6:8]), + ) + + # 집계 + result = { + 'total': _empty_row(), + 'by_gubun': {}, + 'by_age': {'old': _empty_row(), 'infant': _empty_row(), 'mid': _empty_row()}, + 'by_time': {'overtime': _empty_row(), 'saturday': _empty_row(), 'normal': _empty_row()}, + 'by_pay': {'card': _empty_row(), 'cash': _empty_row(), 'paper': _empty_row()}, + 'by_hosp': {}, + } + + for r in rows: + gubun = str(r.get('PreGubun') or '0') + holiday = str(r.get('Holiday') or '1') + price_t = int(r.get('PRICE_T') or 0) + drug_t4 = int(r.get('Drug_T4') or 0) + s_prep = int(r.get('S_Prep') or 0) + price_c = int(r.get('PRICE_C') or 0) + price_p = int(r.get('PRICE_P') or 0) + price_n = int(r.get('PRICE_N') or 0) + appr_gubun = str(r.get('Appr_Gubun') or '') + order_name = str(r.get('OrderName') or '기타') + panum = str(r.get('PaNum') or '') + + sales_amt = price_t + drug_t4 + + if gubun == '9': + ins_prep, ins_drug = 0, 0 + nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0 + nonins_drug = drug_t4 + else: + ins_prep, ins_drug = s_prep, price_t - s_prep + nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0 + nonins_drug = drug_t4 + + args = (sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug, 0, price_c, price_p, price_n) + + _add(result['total'], *args) + + if gubun not in result['by_gubun']: + result['by_gubun'][gubun] = _empty_row() + _add(result['by_gubun'][gubun], *args) + + age = _calc_age(panum, ref_date) + if age is None or (6 <= age < 65): + _add(result['by_age']['mid'], *args) + elif age >= 65: + _add(result['by_age']['old'], *args) + else: + _add(result['by_age']['infant'], *args) + + hd_add = holiday in ('2', '4') + overtime = holiday in ('3', '4') + if hd_add: + _add(result['by_time']['saturday'], *args) + elif overtime: + _add(result['by_time']['overtime'], *args) + else: + _add(result['by_time']['normal'], *args) + + if appr_gubun == '1': + _add(result['by_pay']['card'], *args) + elif appr_gubun == '2': + _add(result['by_pay']['cash'], *args) + else: + _add(result['by_pay']['paper'], *args) + + if order_name not in result['by_hosp']: + result['by_hosp'][order_name] = _empty_row() + _add(result['by_hosp'][order_name], *args) + + return result + + +def _empty_row(): + return dict(cnt=0, sales_amt=0, ins_prep=0, ins_drug=0, + nonins_prep=0, nonins_drug=0, nonins_margin=0, + claim_amt=0, copay=0, receipt=0) + + +def _add(d, sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug, + nonins_margin, claim_amt, copay, receipt): + d['cnt'] += 1 + d['sales_amt'] += sales_amt + d['ins_prep'] += ins_prep + d['ins_drug'] += ins_drug + d['nonins_prep'] += nonins_prep + d['nonins_drug'] += nonins_drug + d['nonins_margin'] += nonins_margin + d['claim_amt'] += claim_amt + d['copay'] += copay + d['receipt'] += receipt + + +def _calc_age(panum: str, ref_date: date) -> int | None: + """주민번호 → 나이""" + if not panum or len(panum) < 7: + return None + birth6 = panum[:6] + gender = panum[6] + if not birth6.isdigit(): + return None + yy, mm, dd = int(birth6[:2]), int(birth6[2:4]), int(birth6[4:6]) + if gender in ('1', '2'): + year = 1900 + yy + elif gender in ('3', '4'): + year = 2000 + yy + else: + year = 1900 + yy + try: + bday = date(year, mm, dd) + return ref_date.year - bday.year - ( + (ref_date.month, ref_date.day) < (bday.month, bday.day)) + except ValueError: + return None diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8043ae4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=2.0 +pyodbc>=4.0 diff --git a/templates/stats.html b/templates/stats.html new file mode 100644 index 0000000..5ba6de1 --- /dev/null +++ b/templates/stats.html @@ -0,0 +1,308 @@ + + + + + Pharmacy Stats API - QT-POS 통계 비교 + + + +

🏥 Pharmacy Stats API - QT-POS 통계 비교

+ +
+ + + ~ + + + +
+ +
+ + + + + +
+ +
+
+

v1 - PharmIT3000

+
조회 버튼을 클릭하세요
+
+
+

v2 - PMPLUS20

+
조회 버튼을 클릭하세요
+
+
+ +
+ + + +