Compare commits
2 Commits
c0d55f8e16
...
2ca5622bbd
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ca5622bbd | |||
| 9dd1f41bbb |
148
app.py
148
app.py
@ -77,6 +77,14 @@ def init_db():
|
||||
if 'reference_notes' not in of_cols:
|
||||
cursor.execute("ALTER TABLE official_formulas ADD COLUMN reference_notes TEXT")
|
||||
|
||||
# herb_items 마이그레이션: product_type, standard_code 컬럼 추가 (OTC 재고 구분)
|
||||
cursor.execute("PRAGMA table_info(herb_items)")
|
||||
hi_cols = {row[1] for row in cursor.fetchall()}
|
||||
if 'product_type' not in hi_cols:
|
||||
cursor.execute("ALTER TABLE herb_items ADD COLUMN product_type TEXT DEFAULT 'HERB'")
|
||||
if 'standard_code' not in hi_cols:
|
||||
cursor.execute("ALTER TABLE herb_items ADD COLUMN standard_code TEXT")
|
||||
|
||||
# 100처방 원방 마스터 시드 데이터 로드
|
||||
cursor.execute("SELECT COUNT(*) FROM official_formulas")
|
||||
if cursor.fetchone()[0] == 0:
|
||||
@ -290,6 +298,8 @@ def get_herbs():
|
||||
h.insurance_code,
|
||||
h.herb_name,
|
||||
h.is_active,
|
||||
h.product_type,
|
||||
h.standard_code,
|
||||
COALESCE(SUM(il.quantity_onhand), 0) as current_stock,
|
||||
GROUP_CONCAT(DISTINCT het.tag_name) as efficacy_tags
|
||||
FROM herb_items h
|
||||
@ -300,7 +310,7 @@ def get_herbs():
|
||||
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
|
||||
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
|
||||
WHERE h.is_active = 1
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name, h.is_active
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name, h.is_active, h.product_type, h.standard_code
|
||||
ORDER BY h.herb_name
|
||||
""")
|
||||
|
||||
@ -1291,6 +1301,127 @@ def create_manual_receipt():
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/purchase-receipts/from-cart', methods=['POST'])
|
||||
def create_receipt_from_cart():
|
||||
"""의약품 마스터 장바구니 → 입고 처리 (standard_code 기반, OTC 포함)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
supplier_id = data.get('supplier_id')
|
||||
receipt_date = data.get('receipt_date')
|
||||
notes = data.get('notes', '')
|
||||
lines = data.get('lines', [])
|
||||
|
||||
if not supplier_id:
|
||||
return jsonify({'success': False, 'error': '도매상을 선택해주세요.'}), 400
|
||||
if not receipt_date:
|
||||
return jsonify({'success': False, 'error': '입고일을 입력해주세요.'}), 400
|
||||
if not lines or len(lines) == 0:
|
||||
return jsonify({'success': False, 'error': '입고 품목을 1개 이상 추가해주세요.'}), 400
|
||||
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 도매상 확인
|
||||
cursor.execute("SELECT name FROM suppliers WHERE supplier_id = ?", (supplier_id,))
|
||||
if not cursor.fetchone():
|
||||
return jsonify({'success': False, 'error': '유효하지 않은 도매상입니다.'}), 400
|
||||
|
||||
# 입고장 번호 생성 (PR-YYYYMMDD-XXXX)
|
||||
date_str = receipt_date.replace('-', '')
|
||||
cursor.execute("""
|
||||
SELECT MAX(CAST(SUBSTR(receipt_no, -4) AS INTEGER))
|
||||
FROM purchase_receipts WHERE receipt_no LIKE ?
|
||||
""", (f'PR-{date_str}-%',))
|
||||
max_num = cursor.fetchone()[0]
|
||||
receipt_no = f"PR-{date_str}-{(max_num or 0) + 1:04d}"
|
||||
|
||||
# 총 금액 계산
|
||||
total_amount = sum(
|
||||
float(line.get('qty', 0)) * float(line.get('unit_price', 0))
|
||||
for line in lines
|
||||
)
|
||||
|
||||
# 입고장 헤더
|
||||
cursor.execute("""
|
||||
INSERT INTO purchase_receipts (supplier_id, receipt_date, receipt_no, total_amount, source_file, notes)
|
||||
VALUES (?, ?, ?, ?, 'CART', ?)
|
||||
""", (supplier_id, receipt_date, receipt_no, total_amount, notes))
|
||||
receipt_id = cursor.lastrowid
|
||||
|
||||
processed_count = 0
|
||||
for line in lines:
|
||||
standard_code = line.get('standard_code')
|
||||
product_name = line.get('product_name', '')
|
||||
company_name = line.get('company_name', '')
|
||||
spec_grams = float(line.get('spec_grams', 0))
|
||||
qty = int(line.get('qty', 0))
|
||||
unit_price = float(line.get('unit_price', 0))
|
||||
origin_country = line.get('origin_country', '')
|
||||
|
||||
if not standard_code or qty <= 0 or unit_price <= 0:
|
||||
continue
|
||||
|
||||
# g 환산
|
||||
quantity_g = spec_grams * qty if spec_grams > 0 else 0
|
||||
unit_price_per_g = unit_price / spec_grams if spec_grams > 0 else 0
|
||||
line_total = qty * unit_price
|
||||
|
||||
# herb_items에서 standard_code로 조회
|
||||
cursor.execute("SELECT herb_item_id FROM herb_items WHERE standard_code = ?", (standard_code,))
|
||||
herb = cursor.fetchone()
|
||||
|
||||
if not herb:
|
||||
# 새 herb_item 자동 생성 (OTC)
|
||||
cursor.execute("""
|
||||
INSERT INTO herb_items (herb_name, specification, product_type, standard_code)
|
||||
VALUES (?, ?, 'OTC', ?)
|
||||
""", (product_name, company_name, standard_code))
|
||||
herb_item_id = cursor.lastrowid
|
||||
else:
|
||||
herb_item_id = herb[0]
|
||||
|
||||
# 입고장 라인
|
||||
cursor.execute("""
|
||||
INSERT INTO purchase_receipt_lines
|
||||
(receipt_id, herb_item_id, origin_country, quantity_g, unit_price_per_g, line_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (receipt_id, herb_item_id, origin_country, quantity_g, unit_price_per_g, line_total))
|
||||
line_id = cursor.lastrowid
|
||||
|
||||
# 재고 로트 생성
|
||||
cursor.execute("""
|
||||
INSERT INTO inventory_lots
|
||||
(herb_item_id, supplier_id, receipt_line_id, received_date, origin_country,
|
||||
unit_price_per_g, quantity_received, quantity_onhand)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (herb_item_id, supplier_id, line_id, receipt_date,
|
||||
origin_country, unit_price_per_g, quantity_g, quantity_g))
|
||||
lot_id = cursor.lastrowid
|
||||
|
||||
# 재고 원장
|
||||
cursor.execute("""
|
||||
INSERT INTO stock_ledger
|
||||
(event_type, herb_item_id, lot_id, quantity_delta, unit_cost_per_g,
|
||||
reference_table, reference_id)
|
||||
VALUES ('RECEIPT', ?, ?, ?, ?, 'purchase_receipts', ?)
|
||||
""", (herb_item_id, lot_id, quantity_g, unit_price_per_g, receipt_id))
|
||||
|
||||
processed_count += 1
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '입고가 완료되었습니다.',
|
||||
'receipt_no': receipt_no,
|
||||
'summary': {
|
||||
'item_count': processed_count,
|
||||
'total_amount': f"{total_amount:,.0f}원"
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
# ==================== 입고장 조회/관리 API ====================
|
||||
|
||||
@app.route('/api/purchase-receipts', methods=['GET'])
|
||||
@ -1406,6 +1537,8 @@ def get_purchase_receipt_detail(receipt_id):
|
||||
h.herb_name,
|
||||
h.insurance_code,
|
||||
h.ingredient_code,
|
||||
h.product_type,
|
||||
h.standard_code,
|
||||
il.lot_id,
|
||||
il.quantity_onhand as current_stock,
|
||||
il.display_name,
|
||||
@ -2467,6 +2600,8 @@ def get_inventory_summary():
|
||||
h.insurance_code,
|
||||
h.herb_name,
|
||||
h.ingredient_code,
|
||||
h.product_type,
|
||||
h.standard_code,
|
||||
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
|
||||
COUNT(DISTINCT il.lot_id) as lot_count,
|
||||
COUNT(DISTINCT il.origin_country) as origin_count,
|
||||
@ -2476,7 +2611,7 @@ def get_inventory_summary():
|
||||
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
|
||||
FROM herb_items h
|
||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND {where_clause}
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name, h.ingredient_code
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name, h.ingredient_code, h.product_type, h.standard_code
|
||||
HAVING total_quantity > 0
|
||||
ORDER BY h.herb_name
|
||||
""")
|
||||
@ -2595,7 +2730,7 @@ def get_inventory_detail(herb_item_id):
|
||||
|
||||
# 약재 기본 정보
|
||||
cursor.execute("""
|
||||
SELECT herb_item_id, insurance_code, herb_name
|
||||
SELECT herb_item_id, insurance_code, herb_name, product_type, standard_code
|
||||
FROM herb_items
|
||||
WHERE herb_item_id = ?
|
||||
""", (herb_item_id,))
|
||||
@ -2719,6 +2854,8 @@ def get_stock_adjustment_detail(adjustment_id):
|
||||
sad.*,
|
||||
h.herb_name,
|
||||
h.insurance_code,
|
||||
h.product_type,
|
||||
h.standard_code,
|
||||
il.origin_country,
|
||||
s.name as supplier_name
|
||||
FROM stock_adjustment_details sad
|
||||
@ -3780,6 +3917,7 @@ def search_medicine_master():
|
||||
try:
|
||||
q = request.args.get('q', '').strip()
|
||||
category = request.args.get('category', '') # 전문일반구분 필터
|
||||
package_type = request.args.get('package_type', '') # 포장형태 필터
|
||||
limit = min(int(request.args.get('limit', 50)), 200)
|
||||
|
||||
if not q or len(q) < 2:
|
||||
@ -3795,6 +3933,10 @@ def search_medicine_master():
|
||||
where_clauses.append("m.category = ?")
|
||||
params.append(category)
|
||||
|
||||
if package_type:
|
||||
where_clauses.append("m.package_type = ?")
|
||||
params.append(package_type)
|
||||
|
||||
# 취소된 제품 제외
|
||||
where_clauses.append("(m.cancel_date IS NULL OR m.cancel_date = '')")
|
||||
|
||||
|
||||
258
database/schema_backup_20260219.sql
Normal file
258
database/schema_backup_20260219.sql
Normal file
@ -0,0 +1,258 @@
|
||||
-- 한약 재고관리 시스템 데이터베이스 스키마
|
||||
-- SQLite 기준
|
||||
|
||||
-- 1) 도매상/공급업체
|
||||
CREATE TABLE IF NOT EXISTS suppliers (
|
||||
supplier_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
business_no TEXT,
|
||||
contact_person TEXT,
|
||||
phone TEXT,
|
||||
address TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 2) 약재 마스터 (보험코드 9자리 기준)
|
||||
CREATE TABLE IF NOT EXISTS herb_items (
|
||||
herb_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
insurance_code TEXT UNIQUE, -- 보험코드 (9자리)
|
||||
herb_name TEXT NOT NULL, -- 약재명
|
||||
specification TEXT, -- 규격/품질
|
||||
default_unit TEXT DEFAULT 'g', -- 기본 단위
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 3) 환자 정보
|
||||
CREATE TABLE IF NOT EXISTS patients (
|
||||
patient_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT NOT NULL,
|
||||
jumin_no TEXT, -- 주민번호 (암호화 필요)
|
||||
gender TEXT CHECK(gender IN ('M', 'F')),
|
||||
birth_date DATE,
|
||||
address TEXT,
|
||||
notes TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(phone, name)
|
||||
);
|
||||
|
||||
-- 4) 입고장 헤더
|
||||
CREATE TABLE IF NOT EXISTS purchase_receipts (
|
||||
receipt_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
supplier_id INTEGER NOT NULL,
|
||||
receipt_date DATE NOT NULL,
|
||||
receipt_no TEXT, -- 입고 번호/전표번호
|
||||
vat_included INTEGER DEFAULT 1, -- 부가세 포함 여부
|
||||
vat_rate REAL DEFAULT 0.10, -- 부가세율
|
||||
total_amount REAL, -- 총 입고액
|
||||
source_file TEXT, -- Excel 파일명
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id)
|
||||
);
|
||||
|
||||
-- 5) 입고장 상세 라인
|
||||
CREATE TABLE IF NOT EXISTS purchase_receipt_lines (
|
||||
line_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
receipt_id INTEGER NOT NULL,
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
origin_country TEXT, -- 원산지
|
||||
quantity_g REAL NOT NULL, -- 구입량(g)
|
||||
unit_price_per_g REAL NOT NULL, -- g당 단가 (VAT 포함)
|
||||
line_total REAL, -- 라인 총액
|
||||
expiry_date DATE, -- 유효기간
|
||||
lot_number TEXT, -- 로트번호
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (receipt_id) REFERENCES purchase_receipts(receipt_id),
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id)
|
||||
);
|
||||
|
||||
-- 6) 재고 로트 (입고 라인별 재고 관리)
|
||||
CREATE TABLE IF NOT EXISTS inventory_lots (
|
||||
lot_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
supplier_id INTEGER NOT NULL,
|
||||
receipt_line_id INTEGER NOT NULL,
|
||||
received_date DATE NOT NULL,
|
||||
origin_country TEXT,
|
||||
unit_price_per_g REAL NOT NULL,
|
||||
quantity_received REAL NOT NULL, -- 입고 수량
|
||||
quantity_onhand REAL NOT NULL, -- 현재 재고
|
||||
expiry_date DATE,
|
||||
lot_number TEXT,
|
||||
is_depleted INTEGER DEFAULT 0, -- 소진 여부
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
|
||||
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id),
|
||||
FOREIGN KEY (receipt_line_id) REFERENCES purchase_receipt_lines(line_id)
|
||||
);
|
||||
|
||||
-- 7) 재고 원장 (모든 재고 변동 기록)
|
||||
CREATE TABLE IF NOT EXISTS stock_ledger (
|
||||
ledger_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
event_type TEXT NOT NULL CHECK(event_type IN ('RECEIPT', 'CONSUME', 'ADJUST', 'DISCARD', 'RETURN')),
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
lot_id INTEGER,
|
||||
quantity_delta REAL NOT NULL, -- 증감량 (+입고, -사용)
|
||||
unit_cost_per_g REAL,
|
||||
reference_table TEXT, -- 참조 테이블 (compounds, adjustments 등)
|
||||
reference_id INTEGER, -- 참조 ID
|
||||
notes TEXT,
|
||||
created_by TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
|
||||
FOREIGN KEY (lot_id) REFERENCES inventory_lots(lot_id)
|
||||
);
|
||||
|
||||
-- 8) 처방 마스터 (약속 처방)
|
||||
-- 8-1) 100처방 원방 마스터
|
||||
CREATE TABLE IF NOT EXISTS official_formulas (
|
||||
official_formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
formula_number INTEGER NOT NULL UNIQUE, -- 연번 (1~100)
|
||||
formula_name TEXT NOT NULL, -- 처방명 (예: 쌍화탕)
|
||||
formula_name_hanja TEXT, -- 한자명 (예: 雙和湯)
|
||||
source_text TEXT, -- 출전 (예: 화제국방)
|
||||
description TEXT, -- 설명/효능
|
||||
reference_notes TEXT, -- 상담참고자료 (OTC 대비 차별점, 구성 해설, 업셀링 포인트 등)
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 8-2) 100처방 원방 구성 약재 (성분코드 기반)
|
||||
CREATE TABLE IF NOT EXISTS official_formula_ingredients (
|
||||
ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
official_formula_id INTEGER NOT NULL,
|
||||
ingredient_code TEXT NOT NULL, -- herb_masters.ingredient_code 기준
|
||||
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수
|
||||
notes TEXT, -- 역할 (예: 군약, 신약, 좌약, 사약)
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (official_formula_id) REFERENCES official_formulas(official_formula_id),
|
||||
UNIQUE (official_formula_id, ingredient_code)
|
||||
);
|
||||
|
||||
-- 8-3) 운영 처방 (조제에 사용)
|
||||
CREATE TABLE IF NOT EXISTS formulas (
|
||||
formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
formula_code TEXT UNIQUE, -- 처방 코드
|
||||
formula_name TEXT NOT NULL, -- 처방명 (예: 쌍화탕)
|
||||
formula_type TEXT DEFAULT 'CUSTOM', -- CUSTOM(약속처방), STANDARD 등
|
||||
official_formula_id INTEGER, -- 100처방 마스터 참조 (원방 기반인 경우)
|
||||
base_cheop INTEGER DEFAULT 20, -- 기본 첩수 (1제 기준)
|
||||
base_pouches INTEGER DEFAULT 30, -- 기본 파우치수 (1제 기준)
|
||||
description TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_by TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (official_formula_id) REFERENCES official_formulas(official_formula_id)
|
||||
);
|
||||
|
||||
-- 9) 처방 구성 약재
|
||||
CREATE TABLE IF NOT EXISTS formula_ingredients (
|
||||
ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
formula_id INTEGER NOT NULL,
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수
|
||||
notes TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (formula_id) REFERENCES formulas(formula_id),
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
|
||||
UNIQUE (formula_id, herb_item_id)
|
||||
);
|
||||
|
||||
-- 10) 조제 작업 (처방 실행)
|
||||
CREATE TABLE IF NOT EXISTS compounds (
|
||||
compound_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
patient_id INTEGER,
|
||||
formula_id INTEGER,
|
||||
compound_date DATE NOT NULL,
|
||||
je_count REAL NOT NULL, -- 제수 (1제, 0.5제 등)
|
||||
cheop_total REAL NOT NULL, -- 총 첩수
|
||||
pouch_total REAL NOT NULL, -- 총 파우치수
|
||||
cost_total REAL, -- 원가 총액
|
||||
sell_price_total REAL, -- 판매 총액
|
||||
prescription_no TEXT, -- 처방전 번호
|
||||
status TEXT DEFAULT 'PREPARED', -- PREPARED, DISPENSED, CANCELLED
|
||||
notes TEXT,
|
||||
created_by TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
|
||||
FOREIGN KEY (formula_id) REFERENCES formulas(formula_id)
|
||||
);
|
||||
|
||||
-- 11) 조제 약재 구성 (실제 조제시 사용된 약재 - 가감 포함)
|
||||
CREATE TABLE IF NOT EXISTS compound_ingredients (
|
||||
compound_ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
compound_id INTEGER NOT NULL,
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수 (가감 반영)
|
||||
total_grams REAL NOT NULL, -- 총 사용량
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (compound_id) REFERENCES compounds(compound_id),
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id)
|
||||
);
|
||||
|
||||
-- 12) 조제 소비 내역 (로트별 차감)
|
||||
CREATE TABLE IF NOT EXISTS compound_consumptions (
|
||||
consumption_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
compound_id INTEGER NOT NULL,
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
lot_id INTEGER NOT NULL,
|
||||
quantity_used REAL NOT NULL, -- 사용량(g)
|
||||
unit_cost_per_g REAL NOT NULL, -- 단가
|
||||
cost_amount REAL, -- 원가액
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (compound_id) REFERENCES compounds(compound_id),
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
|
||||
FOREIGN KEY (lot_id) REFERENCES inventory_lots(lot_id)
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_herb_items_name ON herb_items(herb_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_herb_items_code ON herb_items(insurance_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_lots_herb ON inventory_lots(herb_item_id, is_depleted);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_ledger_herb ON stock_ledger(herb_item_id, event_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_compounds_patient ON compounds(patient_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_compounds_date ON compounds(compound_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_patients_phone ON patients(phone);
|
||||
|
||||
-- 뷰 생성 (자주 사용되는 조회)
|
||||
-- 현재 재고 현황
|
||||
CREATE VIEW IF NOT EXISTS v_current_stock AS
|
||||
SELECT
|
||||
h.herb_item_id,
|
||||
h.insurance_code,
|
||||
h.herb_name,
|
||||
SUM(il.quantity_onhand) as total_quantity,
|
||||
COUNT(DISTINCT il.lot_id) as lot_count,
|
||||
AVG(il.unit_price_per_g) as avg_unit_price
|
||||
FROM herb_items h
|
||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name;
|
||||
|
||||
-- 처방별 구성 약재 뷰
|
||||
CREATE VIEW IF NOT EXISTS v_formula_details AS
|
||||
SELECT
|
||||
f.formula_id,
|
||||
f.formula_name,
|
||||
f.formula_code,
|
||||
h.herb_name,
|
||||
fi.grams_per_cheop,
|
||||
h.insurance_code
|
||||
FROM formulas f
|
||||
JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
|
||||
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
|
||||
WHERE f.is_active = 1
|
||||
ORDER BY f.formula_id, fi.sort_order;
|
||||
124
docs/DB_매핑_의약품마스터_한약재코드.md
Normal file
124
docs/DB_매핑_의약품마스터_한약재코드.md
Normal file
@ -0,0 +1,124 @@
|
||||
# DB 매핑 구조: 의약품 마스터 ↔ 한약재 코드 테이블
|
||||
|
||||
> 의약품 마스터(medicine_master.db)와 기존 한약재 테이블(kdrug.db)은
|
||||
> **`representative_code`(대표코드)** 로 연결된다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 테이블 관계도
|
||||
|
||||
```
|
||||
herb_masters (454개 약재, 공통 성분 마스터)
|
||||
│ ingredient_code (PK) 예: 3033H1AHM = 계지
|
||||
│
|
||||
├─→ herb_products (53,769건, 제품별 코드)
|
||||
│ ingredient_code (FK)
|
||||
│ representative_code ← 매핑 키
|
||||
│ standard_code
|
||||
│ product_code (보험코드 9자리)
|
||||
│
|
||||
└─→ herb_items (29건, 우리 약국 보유 약재)
|
||||
ingredient_code (FK)
|
||||
insurance_code
|
||||
herb_name 예: "휴먼계지"
|
||||
|
||||
medicine_master.db (305,522건, 의약품표준코드 마스터)
|
||||
│ representative_code ← 매핑 키
|
||||
│ standard_code
|
||||
│ item_std_code (품목기준코드)
|
||||
│ product_name, company_name, spec, category, ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 매핑 키: `representative_code` (대표코드)
|
||||
|
||||
| 구분 | herb_products | medicine_master |
|
||||
|------|:---:|:---:|
|
||||
| 대표코드 수 | 17,356 | 13,771 (한약재) |
|
||||
| **매칭 수** | **13,188 (95.8%)** | |
|
||||
| 표준코드 수 | 53,769 | - |
|
||||
| 표준코드 매칭 | **40,364 (75.1%)** | |
|
||||
|
||||
- 풀체인 매칭 약재: **447 / 454개 (98.5%)**
|
||||
- medicine_master에만 있는 한약재 대표코드: 6개 (무시 가능)
|
||||
- herb_products에만 있는 대표코드: 4,168개 (취소/폐지 제품 등)
|
||||
|
||||
---
|
||||
|
||||
## 3. 매핑 예시: 계지
|
||||
|
||||
```sql
|
||||
-- herb_masters: 공통명
|
||||
ingredient_code = '3033H1AHM', herb_name = '계지'
|
||||
|
||||
-- herb_products: 제품들 (57개 제조사)
|
||||
휴먼계지 rep: 8800624003904 std: 8800624003911 (500g)
|
||||
대효계지 rep: 8800628002200 std: 8800628002224 (500g)
|
||||
광명계지 rep: 8800640000505 std: 8800640000512 (500g)
|
||||
...
|
||||
|
||||
-- medicine_master: 동일 대표코드로 매칭
|
||||
휴먼계지 rep: 8800624003904 item_std: 200406389 category: 한약재
|
||||
대효계지 rep: 8800628002200 item_std: 200406525 category: 한약재
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 매핑 SQL
|
||||
|
||||
### 특정 약재의 전체 제품 + 의약품 마스터 정보 조회
|
||||
```sql
|
||||
SELECT hm.herb_name AS 공통명,
|
||||
hp.product_name, hp.company_name,
|
||||
hp.representative_code, hp.standard_code,
|
||||
mm.spec, mm.form_type, mm.package_type,
|
||||
mm.item_std_code, mm.category
|
||||
FROM herb_masters hm
|
||||
JOIN herb_products hp ON hp.ingredient_code = hm.ingredient_code
|
||||
LEFT JOIN med_master.medicine_master mm
|
||||
ON mm.representative_code = hp.representative_code
|
||||
AND (mm.cancel_date IS NULL OR mm.cancel_date = '')
|
||||
WHERE hm.herb_name = '계지';
|
||||
```
|
||||
|
||||
### herb_items(보유 약재) → 의약품 마스터 연결
|
||||
```sql
|
||||
SELECT hi.herb_name, hi.ingredient_code,
|
||||
mm.item_std_code, mm.representative_code, mm.standard_code,
|
||||
mm.spec, mm.package_type
|
||||
FROM herb_items hi
|
||||
JOIN herb_products hp ON hp.ingredient_code = hi.ingredient_code
|
||||
JOIN med_master.medicine_master mm
|
||||
ON mm.representative_code = hp.representative_code
|
||||
AND mm.product_name = hi.herb_name
|
||||
WHERE mm.category = '한약재'
|
||||
AND (mm.cancel_date IS NULL OR mm.cancel_date = '');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 코드 체계 정리
|
||||
|
||||
| 코드 | 출처 | 예시 | 설명 |
|
||||
|------|------|------|------|
|
||||
| `ingredient_code` | herb_masters | 3033H1AHM | 성분코드 (약재 공통) |
|
||||
| `product_code` | herb_products | 062400390 | 보험 제품코드 9자리 |
|
||||
| `representative_code` | 양쪽 | 8800624003904 | **대표 바코드 (매핑 키)** |
|
||||
| `standard_code` | 양쪽 | 8800624003911 | 규격별 바코드 |
|
||||
| `item_std_code` | medicine_master | 200406389 | 식약처 품목기준코드 |
|
||||
| `insurance_code` | herb_items | 062400390 | = product_code와 동일 체계 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 활용 가능 시나리오
|
||||
|
||||
1. **바코드 스캔 입고**: 제품 바코드(standard_code) → herb_products → ingredient_code → herb_items 자동 매칭
|
||||
2. **제품 비교**: 같은 ingredient_code의 다른 업체 제품 가격/규격 비교
|
||||
3. **마스터 검색 → 입고 연동**: 의약품 마스터에서 검색 → representative_code로 herb_products 조회 → 입고 등록
|
||||
4. **유효성 검증**: 취소(cancel_date) 제품 식별, 현행 유통 제품만 필터링
|
||||
|
||||
---
|
||||
|
||||
*최종 수정: 2026-02-19*
|
||||
72
docs/경방신약_주문매핑_20260219.md
Normal file
72
docs/경방신약_주문매핑_20260219.md
Normal file
@ -0,0 +1,72 @@
|
||||
# 경방신약 주문 매핑 결과 (2026-02-19)
|
||||
|
||||
## 조건
|
||||
- 제약사: 경방신약(주)
|
||||
- 규격: 500그램
|
||||
- 포장: 병/통
|
||||
- DB: medicine_master (의약품 표준코드)
|
||||
|
||||
## 매핑 결과: 38/38건 전체 매칭 완료 (한방 OTC 37건 + 일반 OTC 1건)
|
||||
|
||||
### 한방 OTC (37건) — 소계 1,531,300원
|
||||
|
||||
| # | 주문 약품명 | 수량 | 단가(원) | 매칭 제품명 | 표준코드 | 비고 |
|
||||
|---|-----------|------|---------|-----------|---------|------|
|
||||
| 1 | 가미귀비탕 | 1 | 53,400 | 진경안신엑스과립(가미귀비탕엑스과립) | 8806613031439 | 500g 통 (수동매핑) |
|
||||
| 2 | 가미소요산 | 1 | 35,000 | 경방가미소요산엑스과립 | 8806613001012 | |
|
||||
| 3 | 가미온담탕 | 1 | 69,600 | 자양심간탕엑스과립(가미온담탕) | 8806613030616 | |
|
||||
| 4 | 갈근탕 | 1 | 27,700 | 치감엑스과립(갈근탕엑스과립) | 8806613032337 | |
|
||||
| 5 | 계지복령환 | 1 | 35,000 | 모시나엑스과립(계지복령환) | 8806613023816 | |
|
||||
| 6 | 곽향정기산 | 1 | 31,800 | 씨즌쿨엑스과립(곽향정기산) | 8806613027531 | |
|
||||
| 7 | 당귀작약산 | 1 | 36,200 | 경방당귀작약산엑스과립 | 8806613004716 | |
|
||||
| 8 | 대시호탕 | 1 | 37,300 | 경방대시호탕엑스과립 | 8806613004914 | |
|
||||
| 9 | 마행감석탕 | 1 | 28,500 | 사브엑스과립(마행감석탕) | 8806613025438 | |
|
||||
| 10 | 맥문동탕 | 1 | 35,800 | 윤폐탕엑스과립(맥문동탕엑스과립) | 8806613030234 | |
|
||||
| 11 | 반하백출천마탕 | 1 | 57,200 | 경방반하백출천마탕엑스과립 | 8806613007717 | |
|
||||
| 12 | 반하사심탕 | 1 | 49,000 | 스토마큐(반하사심탕엑스과립) | 8806613026541 | |
|
||||
| 13 | 방풍통성산 | 1 | 33,600 | 경방방풍통성산엑스과립 | 8806613008219 | |
|
||||
| 14 | 배농산급탕 | 1 | 26,700 | 오메가엑스과립(배농산급탕) | 8806613028439 | |
|
||||
| 15 | 보중익기탕 | 1 | 58,800 | 경방보중익기탕엑스과립 | 8806613008912 | |
|
||||
| 16 | 사물탕 | 1 | 35,000 | 경방사물탕엑스과립 | 8806613009612 | |
|
||||
| 17 | 소건중탕 | 1 | 25,000 | 포키드엑스과립(소건중탕) | 8806613033617 | |
|
||||
| 18 | 소경활혈탕 | 1 | 40,300 | 경방소경활혈탕엑스과립 | 8806613011110 | |
|
||||
| 19 | 소시호탕 | 1 | 55,000 | 정해탕(소시호탕엑스과립) | 8806613030937 | |
|
||||
| 20 | 소청룡탕 | 1 | 41,400 | 소폐탕엑스과립(소청룡탕) | 8806613026138 | 500g 통 (수동매핑) |
|
||||
| 21 | 시함탕 | 1 | 49,800 | 폐활탕엑스과립(시함탕) | 8806613033433 | |
|
||||
| 22 | 시호가용골모려탕 | 1 | 46,200 | 브아피엑스과립(시호가용골모려탕엑스) | 8806613024738 | |
|
||||
| 23 | 십미패독산 | 1 | 48,700 | 해스킨엑스과립(십미패독탕) | 8806613034430 | 산→탕 명칭차이 (수동매핑) |
|
||||
| 24 | 오령산 | 1 | 37,200 | 자리투엑스과립(오령산엑스과립) | 8806613030531 | |
|
||||
| 25 | 오적산 | 1 | 50,400 | 경방오적산엑스과립 | 8806613014814 | |
|
||||
| 26 | 용담사간탕 | 1 | 37,100 | 용패탕(용담사간탕엑스과립) | 8806613029436 | |
|
||||
| 27 | 월비가출탕 | 1 | 40,400 | 마이진탕엑스과립(월비가출탕) | 8806613023618 | |
|
||||
| 28 | 육미지황탕 | 1 | 40,300 | 경방육미지황탕엑스과립 | 8806613015613 | |
|
||||
| 29 | 인삼패독산 | 1 | 52,300 | 가쁘레엑스과립(인삼패독산) | 8806613000138 | 500g 통 (수동매핑) |
|
||||
| 30 | 자음강화탕 | 1 | 41,500 | 자활탕(자음강화탕엑스과립) | 8806613030715 | |
|
||||
| 31 | 저령탕 | 1 | 36,700 | 바디스엑스과립(저령탕엑스) | 8806613024332 | |
|
||||
| 32 | 청화보음탕 | 1 | 34,200 | 디휘버엑스과립(청화보음탕) | 8806613023038 | |
|
||||
| 33 | 팔미지황환 | 1 | 41,700 | 경방팔미지황환엑스과립 | 8806613019710 | |
|
||||
| 34 | 평위산 | 1 | 28,000 | 위제나과립(평위산엑스과립) | 8806613029818 | |
|
||||
| 35 | 향사육군자탕 | 1 | 58,400 | 스토비엑스과립(향사육군자탕) | 8806613026817 | |
|
||||
| 36 | 형개연교탕 | 1 | 35,600 | 노넥스엑스과립(형개연교탕) | 8806613022314 | |
|
||||
| 37 | 황련해독탕 | 1 | 40,500 | 오브스과립(황련해독탕엑스과립) | 8806613028613 | |
|
||||
|
||||
### 일반 OTC (1건) — 소계 30,500원
|
||||
|
||||
| # | 주문 약품명 | 수량 | 단가(원) | 매칭 제품명 | 표준코드 | 비고 |
|
||||
|---|-----------|------|---------|-----------|---------|------|
|
||||
| 1 | 미소그린에스과립 | 1 | 30,500 | 미소그린에스과립 | 8806613066615 | 420g 병. 한방 OTC 아님 |
|
||||
|
||||
### 총액: 1,561,800원 (한방 OTC 1,531,300 + 일반 OTC 30,500)
|
||||
|
||||
---
|
||||
|
||||
### 수동 매핑 처리 완료 (4건)
|
||||
|
||||
초기 자동매칭 시 누락되었으나, 수동 확인으로 전부 매칭 완료.
|
||||
|
||||
| # | 주문 약품명 | 매칭 제품명 | 표준코드 | 사유 |
|
||||
|---|-----------|-----------|---------|------|
|
||||
| 1 | 가미귀비탕 | 진경안신엑스과립(가미귀비탕엑스과립) | 8806613031439 | 500g 통. 브랜드명이 달라 자동매칭 실패 |
|
||||
| 2 | 소청룡탕 | 소폐탕엑스과립(소청룡탕) | 8806613026138 | 500g 통. 포장이 "통"이라 "병" 필터에서 누락 |
|
||||
| 3 | 십미패독산 | 해스킨엑스과립(십미패독탕) | 8806613034430 | 500g 병. DB에 "산"이 아닌 "탕"으로 등록 |
|
||||
| 4 | 인삼패독산 | 가쁘레엑스과립(인삼패독산) | 8806613000138 | 500g 통. 포장이 "통"이라 "병" 필터에서 누락 |
|
||||
59
docs/주문마이페이지.md
Normal file
59
docs/주문마이페이지.md
Normal file
@ -0,0 +1,59 @@
|
||||
약재명 보험코드 원산지 수량 단가 금액 현재고
|
||||
휴먼갈근
|
||||
갈근.각 062401050 한국 2500g ₩17 ₩42,000 2500g
|
||||
휴먼감초
|
||||
감초.1호[야생](1kg) 062400740 중국 5000g ₩22 ₩110,500 4610g
|
||||
휴먼건강
|
||||
건강 062400730 페루 5000g ₩12 ₩62,000 4880g
|
||||
휴먼건강
|
||||
건강.土 062400730 한국 1500g ₩51 ₩77,100 1390g
|
||||
휴먼계지
|
||||
계지 062400390 베트남 2500g ₩6 ₩14,500 2500g
|
||||
휴먼구기자
|
||||
구기자(영하)(1kg) 062400090 중국 3000g ₩18 ₩53,700 3000g
|
||||
휴먼길경
|
||||
길경.片[특] 062400980 중국 1500g ₩11 ₩15,900 1500g
|
||||
휴먼대추
|
||||
대추(절편)(1kg) 062401120 한국 5000g ₩20 ₩100,000 4650g
|
||||
휴먼마황
|
||||
마황(1kg) 062401400 중국 5000g ₩10 ₩48,000 4970g
|
||||
휴먼반하생강백반제
|
||||
반하생강백반제(1kg) 062401790 중국 3000g ₩34 ₩101,100 2850g
|
||||
휴먼백출
|
||||
백출.당[1kg] 062402150 중국 2000g ₩12 ₩23,600 1560g
|
||||
휴먼복령
|
||||
복령(1kg) 062402310 중국 5000g ₩12 ₩57,500 4760g
|
||||
휴먼석고
|
||||
석고[통포장](kg) 062402650 중국 4000g ₩5 ₩18,800 4000g
|
||||
휴먼세신
|
||||
세신.中 062402860 중국 1500g ₩129 ₩193,500 1500g
|
||||
신흥숙지황
|
||||
숙지황(9증)(신흥.1kg)[완] 060600050 중국 5000g ₩20 ₩100,000 4360g
|
||||
휴먼오미자
|
||||
오미자<토매지>(1kg) 062402800 중국 2000g ₩18 ₩35,000 2000g
|
||||
휴먼용안육
|
||||
용안육.名品(1kg) 062403120 태국 3000g ₩21 ₩62,100 3000g
|
||||
휴먼육계
|
||||
육계.YB 062403210 베트남 2500g ₩15 ₩36,500 2400g
|
||||
휴먼일당귀
|
||||
일당귀.中(1kg) 062403450 중국 5000g ₩13 ₩64,500 4640g
|
||||
휴먼자소엽
|
||||
자소엽.土 062403350 한국 1500g ₩14 ₩20,700 1410g
|
||||
휴먼작약
|
||||
작약(1kg) 062403380 한국 3000g ₩19 ₩56,100 2460g
|
||||
휴먼작약주자
|
||||
작약주자.土[酒炙] 062403470 한국 1500g ₩25 ₩36,900 1500g
|
||||
휴먼전호
|
||||
전호[재배] 062403490 중국 1500g ₩14 ₩21,000 1500g
|
||||
휴먼지각
|
||||
지각 062402220 중국 1500g ₩10 ₩15,000 1500g
|
||||
휴먼지황
|
||||
지황.건[회](1kg) 062402030 중국 1000g ₩12 ₩11,500 1000g
|
||||
휴먼진피(陳皮)
|
||||
진피.비열[非熱](1kg) 062401950 한국 5000g ₩14 ₩68,500 5000g
|
||||
휴먼창출
|
||||
창출[북창출.재배](1kg) 062401830 중국 3000g ₩14 ₩40,500 3000g
|
||||
휴먼천궁
|
||||
천궁.일<토매지>(1kg) 062401810 중국 3000g ₩12 ₩35,700 2560g
|
||||
휴먼황기
|
||||
황기(직절.小)(1kg) 062400040 중국 3000g ₩10 ₩29,700 2500g
|
||||
@ -116,6 +116,9 @@ $(document).ready(function() {
|
||||
case 'herb-info':
|
||||
loadHerbInfo();
|
||||
break;
|
||||
case 'medicine-master':
|
||||
if (typeof loadMedicineMaster === 'function') loadMedicineMaster();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2132,9 +2135,18 @@ $(document).ready(function() {
|
||||
totalValue += item.total_value || 0;
|
||||
if (item.total_quantity > 0) herbsInStock++;
|
||||
|
||||
const isOTC = item.product_type === 'OTC';
|
||||
const typeBadge = isOTC
|
||||
? '<span class="badge bg-warning text-dark">OTC</span>'
|
||||
: '<span class="badge bg-info">한약재</span>';
|
||||
const codeDisplay = isOTC
|
||||
? (item.standard_code || '-')
|
||||
: (item.insurance_code || '-');
|
||||
|
||||
tbody.append(`
|
||||
<tr class="inventory-row" data-herb-id="${item.herb_item_id}">
|
||||
<td>${item.insurance_code || '-'}</td>
|
||||
<td>${typeBadge}</td>
|
||||
<td><small class="text-monospace">${codeDisplay}</small></td>
|
||||
<td>${item.herb_name}${originBadge}${efficacyTags}</td>
|
||||
<td>${item.total_quantity.toFixed(1)}</td>
|
||||
<td>${item.lot_count}</td>
|
||||
@ -2253,7 +2265,8 @@ $(document).ready(function() {
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
${data.herb_name} 재고 상세
|
||||
<small class="text-muted">(${data.insurance_code})</small>
|
||||
<small class="text-muted">(${data.product_type === 'OTC' ? data.standard_code : data.insurance_code})</small>
|
||||
${data.product_type === 'OTC' ? '<span class="badge bg-warning text-dark ms-2">OTC</span>' : ''}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
@ -2293,9 +2306,14 @@ $(document).ready(function() {
|
||||
tbody.empty();
|
||||
|
||||
response.data.forEach(herb => {
|
||||
const hIsOTC = herb.product_type === 'OTC';
|
||||
const hCode = hIsOTC ? (herb.standard_code || '-') : (herb.insurance_code || '-');
|
||||
const hTypeBadge = hIsOTC
|
||||
? '<span class="badge bg-warning text-dark">OTC</span>'
|
||||
: '<span class="badge bg-info">한약재</span>';
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>${herb.insurance_code || '-'}</td>
|
||||
<td>${hTypeBadge} <small class="text-monospace">${hCode}</small></td>
|
||||
<td>${herb.herb_name}</td>
|
||||
<td>${herb.specification || '-'}</td>
|
||||
<td>${herb.current_stock ? herb.current_stock.toFixed(1) + 'g' : '0g'}</td>
|
||||
@ -2394,14 +2412,24 @@ $(document).ready(function() {
|
||||
if (line.processing) variantBadges += `<span class="badge bg-warning ms-1">${line.processing}</span>`;
|
||||
if (line.grade) variantBadges += `<span class="badge bg-success ms-1">${line.grade}</span>`;
|
||||
|
||||
const lineIsOTC = line.product_type === 'OTC';
|
||||
const lineTypeBadge = lineIsOTC
|
||||
? '<span class="badge bg-warning text-dark me-1">OTC</span>'
|
||||
: '<span class="badge bg-info me-1">한약재</span>';
|
||||
const lineCode = lineIsOTC
|
||||
? (line.standard_code || '-')
|
||||
: (line.insurance_code || '-');
|
||||
const lineCodeLabel = lineIsOTC ? '표준' : '보험';
|
||||
|
||||
linesHtml += `
|
||||
<tr>
|
||||
<td>
|
||||
${lineTypeBadge}
|
||||
<div>${line.herb_name}</div>
|
||||
${line.display_name ? `<small class="text-primary">${line.display_name}</small>` : ''}
|
||||
${variantBadges}
|
||||
</td>
|
||||
<td>${line.insurance_code || '-'}</td>
|
||||
<td><small class="text-muted">${lineCodeLabel}</small><br><small class="text-monospace">${lineCode}</small></td>
|
||||
<td>${line.origin_country || '-'}</td>
|
||||
<td>${line.quantity_g}g</td>
|
||||
<td>${formatCurrency(line.unit_price_per_g)}</td>
|
||||
@ -2428,8 +2456,8 @@ $(document).ready(function() {
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>약재명</th>
|
||||
<th>보험코드</th>
|
||||
<th>품목명</th>
|
||||
<th>코드</th>
|
||||
<th>원산지</th>
|
||||
<th>수량</th>
|
||||
<th>단가</th>
|
||||
@ -3468,7 +3496,7 @@ $(document).ready(function() {
|
||||
itemsBody.append(`
|
||||
<tr>
|
||||
<td>${item.herb_name}</td>
|
||||
<td>${item.insurance_code || '-'}</td>
|
||||
<td>${item.product_type === 'OTC' ? (item.standard_code || '-') : (item.insurance_code || '-')}</td>
|
||||
<td>${item.origin_country || '-'}</td>
|
||||
<td>#${item.lot_id}</td>
|
||||
<td>${item.quantity_before.toFixed(1)}g</td>
|
||||
|
||||
496
static/medicine_master.js
Normal file
496
static/medicine_master.js
Normal file
@ -0,0 +1,496 @@
|
||||
/**
|
||||
* 의약품 마스터 검색 + 입고 장바구니 모듈
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 장바구니 데이터 (세션 메모리)
|
||||
let cart = [];
|
||||
|
||||
const ORIGIN_OPTIONS = ['한국','중국','베트남','인도','태국','페루','일본','기타'];
|
||||
|
||||
// ─── 검색 ───────────────────────────────────────────────
|
||||
|
||||
function searchMedicine() {
|
||||
const query = $('#medSearchInput').val().trim();
|
||||
const category = $('#medCategoryFilter').val();
|
||||
const packageType = $('#medPackageFilter').val();
|
||||
|
||||
if (query.length < 2) {
|
||||
alert('검색어는 2자 이상 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ q: query, limit: 100 });
|
||||
if (category) params.append('category', category);
|
||||
if (packageType) params.append('package_type', packageType);
|
||||
|
||||
$('#medSearchResults').html('<tr><td colspan="8" class="text-center py-4"><div class="spinner-border spinner-border-sm"></div> 검색중...</td></tr>');
|
||||
|
||||
$.get(`/api/medicine-master/search?${params}`, function(response) {
|
||||
if (!response.success) {
|
||||
$('#medSearchResults').html(`<tr><td colspan="8" class="text-center text-danger py-4">${response.error}</td></tr>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tbody = $('#medSearchResults');
|
||||
tbody.empty();
|
||||
$('#medResultCount').text(response.count);
|
||||
|
||||
if (response.data.length === 0) {
|
||||
tbody.html('<tr><td colspan="8" class="text-center text-muted py-4">검색 결과가 없습니다.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
response.data.forEach(item => {
|
||||
let categoryBadge = '';
|
||||
switch(item.category) {
|
||||
case '일반의약품':
|
||||
categoryBadge = '<span class="badge bg-success">일반</span>';
|
||||
break;
|
||||
case '전문의약품':
|
||||
categoryBadge = '<span class="badge bg-warning text-dark">전문</span>';
|
||||
break;
|
||||
case '한약재':
|
||||
categoryBadge = '<span class="badge bg-info">한약재</span>';
|
||||
break;
|
||||
case '원료의약품':
|
||||
categoryBadge = '<span class="badge bg-secondary">원료</span>';
|
||||
break;
|
||||
default:
|
||||
categoryBadge = `<span class="badge bg-light text-dark">${item.category || '-'}</span>`;
|
||||
}
|
||||
|
||||
const cleanName = item.product_name.replace(/ /g, ' ').trim();
|
||||
const notes = item.notes ? item.notes.replace(/ /g, ' ').trim() : '';
|
||||
const inCart = cart.some(c => c.standard_code === item.standard_code);
|
||||
const itemJson = JSON.stringify(item).replace(/'/g, "'");
|
||||
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${cleanName}</strong>
|
||||
${item.form_type ? `<br><small class="text-muted">${item.form_type}</small>` : ''}
|
||||
</td>
|
||||
<td><small>${item.company_name || '-'}</small></td>
|
||||
<td><small>${item.spec || '-'}</small></td>
|
||||
<td><small>${item.package_type || '-'}</small></td>
|
||||
<td>${categoryBadge}</td>
|
||||
<td><small class="text-monospace">${item.standard_code || '-'}</small></td>
|
||||
<td><small class="text-muted">${notes.length > 30 ? notes.substring(0, 30) + '...' : notes}</small></td>
|
||||
<td class="text-nowrap">
|
||||
<button class="btn btn-sm btn-outline-info med-detail-btn"
|
||||
data-item='${itemJson}' title="상세보기">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-success med-add-cart-btn"
|
||||
data-item='${itemJson}'
|
||||
data-code="${item.standard_code}"
|
||||
title="장바구니 담기"
|
||||
${inCart ? 'disabled' : ''}>
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
// 상세보기 이벤트
|
||||
$('.med-detail-btn').off('click').on('click', function() {
|
||||
const item = JSON.parse($(this).attr('data-item'));
|
||||
showMedicineDetail(item);
|
||||
});
|
||||
|
||||
// 장바구니 담기 이벤트
|
||||
$('.med-add-cart-btn').off('click').on('click', function() {
|
||||
const item = JSON.parse($(this).attr('data-item'));
|
||||
addToCart(item);
|
||||
$(this).prop('disabled', true);
|
||||
});
|
||||
|
||||
}).fail(function(xhr) {
|
||||
$('#medSearchResults').html(`<tr><td colspan="8" class="text-center text-danger py-4">검색 실패: ${xhr.responseJSON?.error || '서버 오류'}</td></tr>`);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 상세보기 모달 ─────────────────────────────────────
|
||||
|
||||
function showMedicineDetail(item) {
|
||||
const cleanName = item.product_name.replace(/ /g, ' ').trim();
|
||||
$('#medDetailTitle').text(cleanName);
|
||||
|
||||
const fields = [
|
||||
{ label: '상품명', value: cleanName },
|
||||
{ label: '업체명', value: item.company_name },
|
||||
{ label: '규격', value: item.spec },
|
||||
{ label: '제형구분', value: item.form_type },
|
||||
{ label: '포장형태', value: item.package_type },
|
||||
{ label: '전문일반구분', value: item.category },
|
||||
{ label: '품목기준코드', value: item.item_std_code },
|
||||
{ label: '대표코드', value: item.representative_code },
|
||||
{ label: '표준코드', value: item.standard_code },
|
||||
{ label: '일반명코드', value: item.ingredient_name_code },
|
||||
{ label: 'ATC코드', value: item.atc_code },
|
||||
{ label: '비고', value: item.notes?.replace(/ /g, ' ') },
|
||||
];
|
||||
|
||||
let html = '<table class="table table-sm">';
|
||||
fields.forEach(f => {
|
||||
if (f.value) {
|
||||
html += `<tr><th width="140" class="text-muted">${f.label}</th><td>${f.value}</td></tr>`;
|
||||
}
|
||||
});
|
||||
html += '</table>';
|
||||
|
||||
$('#medDetailBody').html(html);
|
||||
$('#medDetailModal').modal('show');
|
||||
}
|
||||
|
||||
// ─── 규격 파싱 ───────────────────────────────────────────
|
||||
|
||||
// 규격 문자열에서 그램 수 추출 (예: "500그램"→500, "1000그램"→1000)
|
||||
function parseSpecToGrams(spec) {
|
||||
if (!spec) return 0;
|
||||
const s = spec.trim();
|
||||
|
||||
// "500그램", "1000그램", "1252.5그램"
|
||||
let m = s.match(/^([\d.]+)\s*그램$/);
|
||||
if (m) return parseFloat(m[1]);
|
||||
|
||||
// "500g", "1000G"
|
||||
m = s.match(/^([\d.]+)\s*[gG]$/);
|
||||
if (m) return parseFloat(m[1]);
|
||||
|
||||
// "1kg", "1.5Kg", "1킬로그램"
|
||||
m = s.match(/^([\d.]+)\s*(kg|킬로그램)$/i);
|
||||
if (m) return parseFloat(m[1]) * 1000;
|
||||
|
||||
// "500밀리그램" → 0.5g
|
||||
m = s.match(/^([\d.]+)\s*밀리그램$/);
|
||||
if (m) return parseFloat(m[1]) / 1000;
|
||||
|
||||
// "500mg"
|
||||
m = s.match(/^([\d.]+)\s*mg$/i);
|
||||
if (m) return parseFloat(m[1]) / 1000;
|
||||
|
||||
return 0; // 파싱 불가 ("없음" 등)
|
||||
}
|
||||
|
||||
// ─── 장바구니 기능 ─────────────────────────────────────
|
||||
|
||||
function addToCart(item) {
|
||||
// 중복 체크
|
||||
if (cart.some(c => c.standard_code === item.standard_code)) {
|
||||
alert('이미 장바구니에 있는 항목입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanName = item.product_name.replace(/ /g, ' ').trim();
|
||||
const specGrams = parseSpecToGrams(item.spec);
|
||||
|
||||
cart.push({
|
||||
standard_code: item.standard_code,
|
||||
product_name: cleanName,
|
||||
company_name: item.company_name || '',
|
||||
spec: item.spec || '',
|
||||
spec_grams: specGrams, // 규격에서 파싱한 g 수
|
||||
qty: 1, // 수량 (포장 단위)
|
||||
unit_price: 0, // 단가 (종이 입고장 가격)
|
||||
origin_country: '한국',
|
||||
_raw: item
|
||||
});
|
||||
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function removeFromCart(index) {
|
||||
const removed = cart.splice(index, 1)[0];
|
||||
if (removed) {
|
||||
$(`.med-add-cart-btn[data-code="${removed.standard_code}"]`).prop('disabled', false);
|
||||
}
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
if (cart.length === 0) return;
|
||||
if (!confirm('장바구니를 비우시겠습니까?')) return;
|
||||
|
||||
const codes = cart.map(c => c.standard_code);
|
||||
cart = [];
|
||||
codes.forEach(code => {
|
||||
$(`.med-add-cart-btn[data-code="${code}"]`).prop('disabled', false);
|
||||
});
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
const panel = $('#cartPanel');
|
||||
const tbody = $('#cartBody');
|
||||
|
||||
if (cart.length === 0) {
|
||||
panel.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
panel.show();
|
||||
$('#cartCount').text(cart.length);
|
||||
tbody.empty();
|
||||
|
||||
let totalQty = 0;
|
||||
let totalAmt = 0;
|
||||
|
||||
cart.forEach((item, idx) => {
|
||||
const amt = (item.qty || 0) * (item.unit_price || 0);
|
||||
const pricePerG = (item.spec_grams && item.unit_price)
|
||||
? (item.unit_price / item.spec_grams) : 0;
|
||||
totalQty += (item.qty || 0);
|
||||
totalAmt += amt;
|
||||
|
||||
const originOptions = ORIGIN_OPTIONS.map(o =>
|
||||
`<option value="${o}" ${item.origin_country === o ? 'selected' : ''}>${o}</option>`
|
||||
).join('');
|
||||
|
||||
const specDisplay = item.spec_grams
|
||||
? `${item.spec} <br><small class="text-success">${item.spec_grams.toLocaleString()}g</small>`
|
||||
: `${item.spec || '-'} <br><small class="text-danger">수동입력</small>`;
|
||||
|
||||
tbody.append(`
|
||||
<tr data-cart-idx="${idx}">
|
||||
<td><small><strong>${item.product_name}</strong></small></td>
|
||||
<td><small>${item.company_name || '-'}</small></td>
|
||||
<td>${specDisplay}</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm text-end cart-qty"
|
||||
data-idx="${idx}" value="${item.qty || 1}"
|
||||
min="1" step="1" placeholder="1">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm text-end cart-unit-price"
|
||||
data-idx="${idx}" value="${item.unit_price || ''}"
|
||||
min="0" step="100" placeholder="35000">
|
||||
</td>
|
||||
<td class="text-end cart-amt" data-idx="${idx}">
|
||||
${amt ? amt.toLocaleString() : '-'}
|
||||
</td>
|
||||
<td class="text-end cart-ppg text-muted" data-idx="${idx}">
|
||||
<small>${pricePerG ? pricePerG.toFixed(1) : '-'}</small>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm cart-origin" data-idx="${idx}">
|
||||
${originOptions}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger cart-remove-btn" data-idx="${idx}" title="삭제">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
$('#cartTotalQty').text(totalQty ? totalQty.toLocaleString() : '0');
|
||||
$('#cartTotalAmt').text(totalAmt ? totalAmt.toLocaleString() + '원' : '0');
|
||||
|
||||
// 이벤트 바인딩
|
||||
tbody.find('.cart-qty').off('input').on('input', function() {
|
||||
const idx = $(this).data('idx');
|
||||
cart[idx].qty = parseInt($(this).val()) || 0;
|
||||
updateCartRow(idx);
|
||||
});
|
||||
|
||||
tbody.find('.cart-unit-price').off('input').on('input', function() {
|
||||
const idx = $(this).data('idx');
|
||||
cart[idx].unit_price = parseFloat($(this).val()) || 0;
|
||||
updateCartRow(idx);
|
||||
});
|
||||
|
||||
tbody.find('.cart-origin').off('change').on('change', function() {
|
||||
const idx = $(this).data('idx');
|
||||
cart[idx].origin_country = $(this).val();
|
||||
});
|
||||
|
||||
tbody.find('.cart-remove-btn').off('click').on('click', function() {
|
||||
removeFromCart($(this).data('idx'));
|
||||
});
|
||||
}
|
||||
|
||||
function updateCartRow(idx) {
|
||||
const item = cart[idx];
|
||||
const amt = (item.qty || 0) * (item.unit_price || 0);
|
||||
const pricePerG = (item.spec_grams && item.unit_price)
|
||||
? (item.unit_price / item.spec_grams) : 0;
|
||||
|
||||
$(`.cart-amt[data-idx="${idx}"]`).text(amt ? amt.toLocaleString() : '-');
|
||||
$(`.cart-ppg[data-idx="${idx}"]`).html(`<small>${pricePerG ? pricePerG.toFixed(1) : '-'}</small>`);
|
||||
updateCartTotals();
|
||||
}
|
||||
|
||||
function updateCartTotals() {
|
||||
let totalQty = 0;
|
||||
let totalAmt = 0;
|
||||
cart.forEach(item => {
|
||||
totalQty += (item.qty || 0);
|
||||
totalAmt += (item.qty || 0) * (item.unit_price || 0);
|
||||
});
|
||||
$('#cartTotalQty').text(totalQty ? totalQty.toLocaleString() : '0');
|
||||
$('#cartTotalAmt').text(totalAmt ? totalAmt.toLocaleString() + '원' : '0');
|
||||
}
|
||||
|
||||
// ─── 입고장 생성 ───────────────────────────────────────
|
||||
|
||||
function createReceipt() {
|
||||
// 검증
|
||||
const supplierId = $('#cartSupplier').val();
|
||||
const receiptDate = $('#cartReceiptDate').val();
|
||||
const notes = $('#cartNotes').val().trim();
|
||||
|
||||
if (!supplierId) {
|
||||
alert('도매상을 선택해주세요.');
|
||||
$('#cartSupplier').focus();
|
||||
return;
|
||||
}
|
||||
if (!receiptDate) {
|
||||
alert('입고일을 입력해주세요.');
|
||||
$('#cartReceiptDate').focus();
|
||||
return;
|
||||
}
|
||||
if (cart.length === 0) {
|
||||
alert('장바구니가 비어있습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 단가 미입력 체크
|
||||
const incomplete = cart.filter(c => !c.unit_price);
|
||||
if (incomplete.length > 0) {
|
||||
alert(`단가가 입력되지 않은 항목이 ${incomplete.length}건 있습니다.\n모든 항목의 단가를 입력해주세요.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 규격 미파싱 경고 (spec_grams=0이면 g 환산 불가)
|
||||
const noSpec = cart.filter(c => !c.spec_grams);
|
||||
if (noSpec.length > 0) {
|
||||
const ok = confirm(
|
||||
`규격(g)을 파싱할 수 없는 항목이 ${noSpec.length}건 있습니다.\n` +
|
||||
`해당 항목은 g당단가가 계산되지 않습니다.\n\n` +
|
||||
noSpec.map(c => '- ' + c.product_name + ' (' + c.spec + ')').join('\n') +
|
||||
'\n\n그래도 진행하시겠습니까?'
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
// 확인 요약
|
||||
const supplierName = $('#cartSupplier option:selected').text();
|
||||
let totalAmt = 0;
|
||||
cart.forEach(item => {
|
||||
totalAmt += (item.qty || 0) * (item.unit_price || 0);
|
||||
});
|
||||
|
||||
const summary = [
|
||||
`도매상: ${supplierName}`,
|
||||
`입고일: ${receiptDate}`,
|
||||
`품목 수: ${cart.length}건`,
|
||||
`총 금액: ${totalAmt.toLocaleString()}원`,
|
||||
].join('\n');
|
||||
|
||||
if (!confirm(`입고장을 생성하시겠습니까?\n\n${summary}`)) return;
|
||||
|
||||
// API 요청 데이터 구성
|
||||
const payload = {
|
||||
supplier_id: parseInt(supplierId),
|
||||
receipt_date: receiptDate,
|
||||
notes: notes,
|
||||
lines: cart.map(c => ({
|
||||
standard_code: c.standard_code,
|
||||
product_name: c.product_name,
|
||||
company_name: c.company_name,
|
||||
spec: c.spec,
|
||||
spec_grams: c.spec_grams,
|
||||
qty: c.qty,
|
||||
unit_price: c.unit_price,
|
||||
origin_country: c.origin_country
|
||||
}))
|
||||
};
|
||||
|
||||
// 버튼 비활성화
|
||||
const btn = $('#createReceiptBtn');
|
||||
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> 처리중...');
|
||||
|
||||
$.ajax({
|
||||
url: '/api/purchase-receipts/from-cart',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert(
|
||||
`입고 완료!\n\n` +
|
||||
`입고장 번호: ${response.receipt_no}\n` +
|
||||
`품목: ${response.summary.item_count}건\n` +
|
||||
`총 금액: ${response.summary.total_amount}`
|
||||
);
|
||||
// 장바구니 초기화
|
||||
cart = [];
|
||||
renderCart();
|
||||
// 검색결과 버튼 전부 재활성화
|
||||
$('.med-add-cart-btn').prop('disabled', false);
|
||||
} else {
|
||||
alert('입고 실패: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
const msg = xhr.responseJSON?.error || '서버 오류';
|
||||
alert('입고 실패: ' + msg);
|
||||
},
|
||||
complete: function() {
|
||||
btn.prop('disabled', false).html('<i class="bi bi-clipboard-check"></i> 입고장 생성');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 도매상 로드 ───────────────────────────────────────
|
||||
|
||||
function loadSuppliers() {
|
||||
$.get('/api/suppliers', function(response) {
|
||||
if (!response.success) return;
|
||||
const sel = $('#cartSupplier');
|
||||
sel.find('option:not(:first)').remove();
|
||||
response.data.forEach(s => {
|
||||
sel.append(`<option value="${s.supplier_id}">${s.name}</option>`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 초기화 ────────────────────────────────────────────
|
||||
|
||||
function init() {
|
||||
// 검색
|
||||
$('#medSearchBtn').off('click').on('click', searchMedicine);
|
||||
$('#medSearchInput').off('keypress').on('keypress', function(e) {
|
||||
if (e.which === 13) searchMedicine();
|
||||
});
|
||||
$('#medCategoryFilter, #medPackageFilter').off('change').on('change', function() {
|
||||
if ($('#medSearchInput').val().trim().length >= 2) {
|
||||
searchMedicine();
|
||||
}
|
||||
});
|
||||
|
||||
// 장바구니
|
||||
$('#cartClearBtn').off('click').on('click', clearCart);
|
||||
$('#createReceiptBtn').off('click').on('click', createReceipt);
|
||||
|
||||
// 입고일 기본값: 오늘
|
||||
$('#cartReceiptDate').val(new Date().toISOString().split('T')[0]);
|
||||
|
||||
// 도매상 목록 로드
|
||||
loadSuppliers();
|
||||
|
||||
// 이전 장바구니 상태 복원 (없으면 숨김)
|
||||
renderCart();
|
||||
}
|
||||
|
||||
// 글로벌 로드 함수 등록
|
||||
window.loadMedicineMaster = function() {
|
||||
init();
|
||||
};
|
||||
})();
|
||||
@ -127,6 +127,11 @@
|
||||
<i class="bi bi-book"></i> 약재 정보
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-page="medicine-master">
|
||||
<i class="bi bi-capsule"></i> 의약품 마스터
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -915,8 +920,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>보험코드</th>
|
||||
<th>약재명</th>
|
||||
<th>구분</th>
|
||||
<th>코드</th>
|
||||
<th>품목명</th>
|
||||
<th>현재 재고(g)</th>
|
||||
<th>로트 수</th>
|
||||
<th>평균 단가</th>
|
||||
@ -1154,8 +1160,8 @@
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>약재명</th>
|
||||
<th>보험코드</th>
|
||||
<th>품목명</th>
|
||||
<th>코드</th>
|
||||
<th>원산지</th>
|
||||
<th>로트ID</th>
|
||||
<th>보정 전</th>
|
||||
@ -1479,6 +1485,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Medicine Master Page -->
|
||||
{% include 'medicine_master.html' %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2107,6 +2117,7 @@
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/app.js?v=20260217"></script>
|
||||
<script src="/static/medicine_master.js?v=20260219"></script>
|
||||
<!-- 재고 자산 계산 설정 모달 -->
|
||||
<div class="modal fade" id="inventorySettingsModal" tabindex="-1" aria-labelledby="inventorySettingsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
|
||||
163
templates/medicine_master.html
Normal file
163
templates/medicine_master.html
Normal file
@ -0,0 +1,163 @@
|
||||
<!-- 의약품 마스터 검색 페이지 -->
|
||||
<div id="medicine-master" class="main-content">
|
||||
<h3 class="mb-4"><i class="bi bi-search"></i> 의약품 마스터 검색</h3>
|
||||
|
||||
<!-- 검색 영역 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">검색어</label>
|
||||
<input type="text" class="form-control" id="medSearchInput"
|
||||
placeholder="상품명, 업체명, 표준코드 검색..." autofocus>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">분류</label>
|
||||
<select class="form-select" id="medCategoryFilter">
|
||||
<option value="">전체</option>
|
||||
<option value="일반의약품">일반의약품</option>
|
||||
<option value="전문의약품">전문의약품</option>
|
||||
<option value="한약재">한약재</option>
|
||||
<option value="원료의약품">원료의약품</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">포장형태</label>
|
||||
<select class="form-select" id="medPackageFilter">
|
||||
<option value="">전체</option>
|
||||
<option value="병">병</option>
|
||||
<option value="포">포</option>
|
||||
<option value="박스">박스</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button class="btn btn-primary w-100" id="medSearchBtn">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<span class="text-muted" id="medSearchCount"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 검색 결과 -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">검색 결과</h5>
|
||||
<div>
|
||||
<span class="badge bg-primary" id="medResultCount">0</span>건
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 600px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>상품명</th>
|
||||
<th>업체명</th>
|
||||
<th>규격</th>
|
||||
<th>포장</th>
|
||||
<th>분류</th>
|
||||
<th>표준코드</th>
|
||||
<th>비고</th>
|
||||
<th width="100">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="medSearchResults">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-5">
|
||||
<i class="bi bi-search" style="font-size: 2rem;"></i>
|
||||
<p class="mt-2">검색어를 입력하세요 (2자 이상)</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 입고 장바구니 -->
|
||||
<div class="card mt-4" id="cartPanel" style="display:none;">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-cart3"></i> 입고 장바구니 (<span id="cartCount">0</span>건)</h5>
|
||||
<button class="btn btn-sm btn-outline-danger" id="cartClearBtn" title="비우기">
|
||||
<i class="bi bi-trash"></i> 비우기
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 입고 정보 -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">도매상 <span class="text-danger">*</span></label>
|
||||
<select class="form-select form-select-sm" id="cartSupplier">
|
||||
<option value="">-- 선택 --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">입고일 <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control form-control-sm" id="cartReceiptDate">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">비고</label>
|
||||
<input type="text" class="form-control form-control-sm" id="cartNotes" placeholder="비고 입력...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 장바구니 테이블 -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>상품명</th>
|
||||
<th>업체</th>
|
||||
<th>규격</th>
|
||||
<th width="70">수량</th>
|
||||
<th width="110">단가(원)</th>
|
||||
<th width="90">금액</th>
|
||||
<th width="80">g당단가</th>
|
||||
<th width="100">원산지</th>
|
||||
<th width="40"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cartBody"></tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr class="fw-bold">
|
||||
<td colspan="3" class="text-end">합계</td>
|
||||
<td id="cartTotalQty" class="text-end">0</td>
|
||||
<td></td>
|
||||
<td id="cartTotalAmt" class="text-end">0</td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 입고장 생성 버튼 -->
|
||||
<div class="text-end mt-3">
|
||||
<button class="btn btn-primary" id="createReceiptBtn">
|
||||
<i class="bi bi-clipboard-check"></i> 입고장 생성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제품 상세 모달 -->
|
||||
<div class="modal fade" id="medDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="medDetailTitle">제품 상세</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="medDetailBody">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user