pharmacy-erp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -0
- package/LICENSE +21 -0
- package/README.md +479 -0
- package/backend/Dockerfile +44 -0
- package/backend/app/__init__.py +1 -0
- package/backend/app/core/__init__.py +1 -0
- package/backend/app/core/audit.py +33 -0
- package/backend/app/core/config.py +16 -0
- package/backend/app/core/database.py +26 -0
- package/backend/app/core/deps.py +49 -0
- package/backend/app/core/exceptions.py +43 -0
- package/backend/app/core/security.py +32 -0
- package/backend/app/main.py +54 -0
- package/backend/app/modules/__init__.py +0 -0
- package/backend/app/modules/admin/__init__.py +1 -0
- package/backend/app/modules/admin/models.py +15 -0
- package/backend/app/modules/admin/router.py +353 -0
- package/backend/app/modules/auth/__init__.py +0 -0
- package/backend/app/modules/auth/models.py +16 -0
- package/backend/app/modules/auth/router.py +73 -0
- package/backend/app/modules/auth/schemas.py +29 -0
- package/backend/app/modules/cash_sessions/__init__.py +0 -0
- package/backend/app/modules/cash_sessions/models.py +19 -0
- package/backend/app/modules/cash_sessions/router.py +85 -0
- package/backend/app/modules/cash_sessions/schemas.py +31 -0
- package/backend/app/modules/customers/__init__.py +0 -0
- package/backend/app/modules/customers/models.py +37 -0
- package/backend/app/modules/customers/router.py +296 -0
- package/backend/app/modules/customers/schemas.py +72 -0
- package/backend/app/modules/inventory/__init__.py +0 -0
- package/backend/app/modules/inventory/models.py +16 -0
- package/backend/app/modules/inventory/router.py +150 -0
- package/backend/app/modules/inventory/schemas.py +24 -0
- package/backend/app/modules/medicines/__init__.py +0 -0
- package/backend/app/modules/medicines/models.py +54 -0
- package/backend/app/modules/medicines/router.py +631 -0
- package/backend/app/modules/medicines/schemas.py +153 -0
- package/backend/app/modules/medicines/template_generator.py +118 -0
- package/backend/app/modules/notifications/__init__.py +1 -0
- package/backend/app/modules/notifications/models.py +15 -0
- package/backend/app/modules/notifications/router.py +89 -0
- package/backend/app/modules/notifications/schemas.py +22 -0
- package/backend/app/modules/organizations/__init__.py +0 -0
- package/backend/app/modules/organizations/models.py +16 -0
- package/backend/app/modules/organizations/router.py +49 -0
- package/backend/app/modules/organizations/schemas.py +31 -0
- package/backend/app/modules/reports/__init__.py +0 -0
- package/backend/app/modules/reports/router.py +409 -0
- package/backend/app/modules/roles/__init__.py +1 -0
- package/backend/app/modules/roles/models.py +43 -0
- package/backend/app/modules/roles/router.py +89 -0
- package/backend/app/modules/roles/schemas.py +55 -0
- package/backend/app/modules/sales/__init__.py +0 -0
- package/backend/app/modules/sales/models.py +44 -0
- package/backend/app/modules/sales/router.py +63 -0
- package/backend/app/modules/sales/schemas.py +42 -0
- package/backend/app/modules/sales/service.py +128 -0
- package/backend/app/modules/suppliers/__init__.py +0 -0
- package/backend/app/modules/suppliers/models.py +14 -0
- package/backend/app/modules/suppliers/router.py +56 -0
- package/backend/app/modules/suppliers/schemas.py +27 -0
- package/backend/app/modules/users/__init__.py +0 -0
- package/backend/app/modules/users/router.py +74 -0
- package/backend/app/seed.py +275 -0
- package/backend/data/.gitkeep +1 -0
- package/backend/requirements.txt +17 -0
- package/bin/cli.js +288 -0
- package/docker-compose.dev.yml +42 -0
- package/docker-compose.yml +59 -0
- package/frontend/Dockerfile +56 -0
- package/frontend/next.config.ts +19 -0
- package/frontend/package.json +60 -0
- package/frontend/pnpm-lock.yaml +5678 -0
- package/frontend/pnpm-workspace.yaml +6 -0
- package/frontend/postcss.config.mjs +6 -0
- package/frontend/public/logo.png +0 -0
- package/frontend/src/app/[locale]/(app)/admin/page.tsx +1426 -0
- package/frontend/src/app/[locale]/(app)/catalog/products/[id]/page.tsx +505 -0
- package/frontend/src/app/[locale]/(app)/catalog/products/page.tsx +753 -0
- package/frontend/src/app/[locale]/(app)/customers/[id]/page.tsx +500 -0
- package/frontend/src/app/[locale]/(app)/customers/page.tsx +538 -0
- package/frontend/src/app/[locale]/(app)/dashboard/page.tsx +175 -0
- package/frontend/src/app/[locale]/(app)/inventory/page.tsx +765 -0
- package/frontend/src/app/[locale]/(app)/layout.tsx +60 -0
- package/frontend/src/app/[locale]/(app)/purchasing/page.tsx +1 -0
- package/frontend/src/app/[locale]/(app)/reports/page.tsx +794 -0
- package/frontend/src/app/[locale]/(app)/sales/page.tsx +296 -0
- package/frontend/src/app/[locale]/(auth)/login/page.tsx +100 -0
- package/frontend/src/app/[locale]/(pos)/layout.tsx +33 -0
- package/frontend/src/app/[locale]/(pos)/pos/page.tsx +463 -0
- package/frontend/src/app/[locale]/(pos)/pos/pos-data.ts +89 -0
- package/frontend/src/app/[locale]/layout.tsx +30 -0
- package/frontend/src/app/[locale]/page.tsx +6 -0
- package/frontend/src/app/globals.css +77 -0
- package/frontend/src/app/layout.tsx +23 -0
- package/frontend/src/app/print.css +18 -0
- package/frontend/src/components/pos/CartPanel.tsx +525 -0
- package/frontend/src/components/pos/CashSessionGuard.tsx +255 -0
- package/frontend/src/components/pos/CloseSessionModal.tsx +160 -0
- package/frontend/src/components/pos/PaymentModal.tsx +266 -0
- package/frontend/src/components/pos/PaymentSettingsModal.tsx +472 -0
- package/frontend/src/components/pos/ProductCatalog.tsx +140 -0
- package/frontend/src/components/pos/SaleReceipt.tsx +133 -0
- package/frontend/src/components/ui/badge.tsx +28 -0
- package/frontend/src/components/ui/bulk-import-modal.tsx +400 -0
- package/frontend/src/components/ui/button.tsx +60 -0
- package/frontend/src/components/ui/card.tsx +13 -0
- package/frontend/src/components/ui/confirm-dialog.tsx +50 -0
- package/frontend/src/components/ui/input.tsx +32 -0
- package/frontend/src/components/ui/notification-center.tsx +228 -0
- package/frontend/src/components/ui/select.tsx +59 -0
- package/frontend/src/components/ui/sidebar.tsx +91 -0
- package/frontend/src/components/ui/slide-over.tsx +43 -0
- package/frontend/src/components/ui/stat-card.tsx +40 -0
- package/frontend/src/components/ui/switch.tsx +38 -0
- package/frontend/src/components/ui/topbar.tsx +97 -0
- package/frontend/src/i18n/request.ts +13 -0
- package/frontend/src/i18n/routing.ts +9 -0
- package/frontend/src/lib/api.ts +240 -0
- package/frontend/src/lib/constants.ts +30 -0
- package/frontend/src/lib/utils.ts +32 -0
- package/frontend/src/messages/ar.json +106 -0
- package/frontend/src/messages/en.json +106 -0
- package/frontend/src/middleware.ts +8 -0
- package/frontend/src/store/app-store.ts +37 -0
- package/frontend/src/store/auth-store.ts +52 -0
- package/frontend/src/store/payment-methods-store.ts +144 -0
- package/frontend/src/store/pos-store.ts +344 -0
- package/frontend/tailwind.config.ts +85 -0
- package/frontend/tsconfig.json +27 -0
- package/img/backgraund-logo.png +0 -0
- package/img/full-logo.png +0 -0
- package/img/logo.png +0 -0
- package/nginx/nginx.conf +67 -0
- package/package.json +88 -0
- package/start.bat +90 -0
- package/start.sh +64 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from app.core.database import AsyncSessionLocal, init_db
|
|
6
|
+
from sqlalchemy import select
|
|
7
|
+
|
|
8
|
+
from app.modules.auth.models import User
|
|
9
|
+
from app.modules.organizations.models import Branch
|
|
10
|
+
from app.modules.medicines.models import MedicineCategory, Medicine, MedicineBatch
|
|
11
|
+
from app.modules.suppliers.models import Supplier
|
|
12
|
+
from app.modules.customers.models import Customer
|
|
13
|
+
from app.modules.roles.models import Role, Permission
|
|
14
|
+
from app.core.security import hash_password
|
|
15
|
+
|
|
16
|
+
async def seed_roles_and_permissions(db):
|
|
17
|
+
res = await db.execute(select(Role).limit(1))
|
|
18
|
+
if res.scalars().first():
|
|
19
|
+
print("Roles already seeded. Skipping.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
permissions_data = [
|
|
23
|
+
("dashboard", ["view"]),
|
|
24
|
+
("pos", ["view", "create_sale", "void_sale", "apply_discount", "hold_sale"]),
|
|
25
|
+
("sales", ["view", "create", "void", "return_sale", "export"]),
|
|
26
|
+
("inventory", ["view", "adjust", "transfer", "receive"]),
|
|
27
|
+
("catalog", ["view", "create", "edit", "delete"]),
|
|
28
|
+
("customers", ["view", "create", "edit", "delete"]),
|
|
29
|
+
("reports", ["view", "export"]),
|
|
30
|
+
("settings", ["view", "manage_users", "manage_roles", "manage_branches", "manage_system"]),
|
|
31
|
+
("purchases", ["view", "create", "approve", "receive"]),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
all_permissions = []
|
|
35
|
+
perm_map = {}
|
|
36
|
+
for module, actions in permissions_data:
|
|
37
|
+
for action in actions:
|
|
38
|
+
code = f"{module}.{action}"
|
|
39
|
+
p = Permission(
|
|
40
|
+
code=code,
|
|
41
|
+
module=module,
|
|
42
|
+
action=action,
|
|
43
|
+
display_name=f"{action.replace('_', ' ').title()} {module.title()}"
|
|
44
|
+
)
|
|
45
|
+
all_permissions.append(p)
|
|
46
|
+
perm_map[code] = p
|
|
47
|
+
|
|
48
|
+
db.add_all(all_permissions)
|
|
49
|
+
await db.commit()
|
|
50
|
+
|
|
51
|
+
roles_data = [
|
|
52
|
+
{
|
|
53
|
+
"name": "SUPER_ADMIN", "display_name": "Super Admin",
|
|
54
|
+
"sort_order": 0, "color": "red", "is_system": True,
|
|
55
|
+
"permissions": list(perm_map.keys())
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "OWNER", "display_name": "Owner",
|
|
59
|
+
"sort_order": 1, "color": "purple", "is_system": True,
|
|
60
|
+
"permissions": [k for k in perm_map.keys() if k != "settings.manage_system"]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "BRANCH_MANAGER", "display_name": "Branch Manager",
|
|
64
|
+
"sort_order": 2, "color": "blue", "is_system": True,
|
|
65
|
+
"permissions": ["dashboard.view"] +
|
|
66
|
+
[k for k in perm_map.keys() if k.startswith("pos.") or k.startswith("sales.") or k.startswith("inventory.") or k.startswith("customers.") or k.startswith("reports.")] +
|
|
67
|
+
["catalog.view", "catalog.edit", "settings.view"]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "PHARMACIST", "display_name": "Pharmacist",
|
|
71
|
+
"sort_order": 3, "color": "green", "is_system": True,
|
|
72
|
+
"permissions": ["dashboard.view", "pos.view", "pos.create_sale", "pos.hold_sale", "sales.view", "inventory.view", "catalog.view", "customers.view"]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "CASHIER", "display_name": "Cashier",
|
|
76
|
+
"sort_order": 4, "color": "amber", "is_system": True,
|
|
77
|
+
"permissions": ["pos.view", "pos.create_sale", "pos.hold_sale", "pos.apply_discount", "sales.view", "customers.view", "customers.create"]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "INVENTORY_STAFF", "display_name": "Inventory Staff",
|
|
81
|
+
"sort_order": 5, "color": "teal", "is_system": True,
|
|
82
|
+
"permissions": ["dashboard.view", "catalog.view", "catalog.edit", "purchases.view", "purchases.receive"] +
|
|
83
|
+
[k for k in perm_map.keys() if k.startswith("inventory.")]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "ACCOUNTANT", "display_name": "Accountant",
|
|
87
|
+
"sort_order": 6, "color": "indigo", "is_system": True,
|
|
88
|
+
"permissions": ["dashboard.view", "sales.view", "sales.export", "purchases.view"] +
|
|
89
|
+
[k for k in perm_map.keys() if k.startswith("reports.")]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
for r_data in roles_data:
|
|
94
|
+
r = Role(
|
|
95
|
+
name=r_data["name"],
|
|
96
|
+
display_name=r_data["display_name"],
|
|
97
|
+
sort_order=r_data["sort_order"],
|
|
98
|
+
color=r_data["color"],
|
|
99
|
+
is_system=r_data["is_system"]
|
|
100
|
+
)
|
|
101
|
+
r.permissions = [perm_map[code] for code in r_data["permissions"] if code in perm_map]
|
|
102
|
+
db.add(r)
|
|
103
|
+
|
|
104
|
+
await db.commit()
|
|
105
|
+
print("Successfully seeded roles and permissions.")
|
|
106
|
+
|
|
107
|
+
async def seed():
|
|
108
|
+
await init_db()
|
|
109
|
+
async with AsyncSessionLocal() as db:
|
|
110
|
+
await seed_roles_and_permissions(db)
|
|
111
|
+
|
|
112
|
+
res = await db.execute(select(User).where(User.email == "admin"))
|
|
113
|
+
if res.scalars().first():
|
|
114
|
+
print("Database already seeded. Skipping.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
admin_user = User(
|
|
118
|
+
email="admin",
|
|
119
|
+
password_hash=hash_password("admin"),
|
|
120
|
+
full_name="System Administrator",
|
|
121
|
+
full_name_ar="مدير النظام",
|
|
122
|
+
role="SUPER_ADMIN",
|
|
123
|
+
is_active=True
|
|
124
|
+
)
|
|
125
|
+
cashier = User(email="cashier@pharmacy.com", password_hash=hash_password("cashier123"), full_name="Cashier", role="cashier")
|
|
126
|
+
db.add_all([admin_user, cashier])
|
|
127
|
+
|
|
128
|
+
branch = Branch(name="Main Branch", code="B001", name_ar="الفرع الرئيسي")
|
|
129
|
+
db.add(branch)
|
|
130
|
+
|
|
131
|
+
await db.commit()
|
|
132
|
+
|
|
133
|
+
# Item Type categories (level 1)
|
|
134
|
+
item_types = [
|
|
135
|
+
("Prescription Medicines", "أدوية وصفية", "pill"),
|
|
136
|
+
("Over-the-Counter (OTC)", "أدوية بدون وصفة", "pill-bottle"),
|
|
137
|
+
("Medical Devices", "أجهزة طبية", "monitor"),
|
|
138
|
+
("Surgical Supplies", "مستلزمات جراحية", "scissors"),
|
|
139
|
+
("Vitamins & Supplements", "فيتامينات ومكملات", "apple"),
|
|
140
|
+
("Baby Care", "العناية بالطفل", "baby"),
|
|
141
|
+
("Personal Care", "العناية الشخصية", "heart"),
|
|
142
|
+
("Cosmetics", "مستحضرات تجميل", "sparkles"),
|
|
143
|
+
("First Aid", "الإسعافات الأولية", "cross"),
|
|
144
|
+
("Laboratory Supplies", "مستلزمات مخبرية", "flask")
|
|
145
|
+
]
|
|
146
|
+
level_1_cats = {}
|
|
147
|
+
for en, ar, icon in item_types:
|
|
148
|
+
c = MedicineCategory(name_en=en, name_ar=ar, icon=icon, category_level=1, sort_order=0)
|
|
149
|
+
db.add(c)
|
|
150
|
+
level_1_cats[en] = c
|
|
151
|
+
await db.commit()
|
|
152
|
+
|
|
153
|
+
# Therapeutic sub-categories (level 2) under Prescription Medicines
|
|
154
|
+
rx_parent_id = level_1_cats["Prescription Medicines"].id
|
|
155
|
+
therapeutics = [
|
|
156
|
+
("Antibiotics", "مضادات حيوية"),
|
|
157
|
+
("Pain Relief / Analgesics", "مسكنات الألم"),
|
|
158
|
+
("Anti-inflammatory", "مضادات الالتهاب"),
|
|
159
|
+
("Antihistamines", "مضادات الهيستامين"),
|
|
160
|
+
("Antidiabetics", "أدوية السكري"),
|
|
161
|
+
("Cardiovascular", "أدوية القلب والأوعية"),
|
|
162
|
+
("Hypertension", "أدوية الضغط"),
|
|
163
|
+
("Gastrointestinal", "أدوية الجهاز الهضمي"),
|
|
164
|
+
("Respiratory", "أدوية الجهاز التنفسي"),
|
|
165
|
+
("Dermatology", "أدوية الجلدية"),
|
|
166
|
+
("Ophthalmology", "أدوية العيون"),
|
|
167
|
+
("ENT", "أدوية الأنف والأذن والحنجرة"),
|
|
168
|
+
("Neurology", "أدوية الأعصاب"),
|
|
169
|
+
("Psychiatry", "أدوية نفسية"),
|
|
170
|
+
("Endocrinology", "أدوية الغدد الصماء"),
|
|
171
|
+
("Oncology", "أدوية الأورام"),
|
|
172
|
+
("Urology", "أدوية المسالك البولية"),
|
|
173
|
+
("Gynaecology", "أدوية النساء والتوليد"),
|
|
174
|
+
("Paediatrics", "أدوية الأطفال")
|
|
175
|
+
]
|
|
176
|
+
level_2_cats = {}
|
|
177
|
+
for idx, (en, ar) in enumerate(therapeutics):
|
|
178
|
+
c = MedicineCategory(name_en=en, name_ar=ar, parent_id=rx_parent_id, category_level=2, sort_order=idx)
|
|
179
|
+
db.add(c)
|
|
180
|
+
level_2_cats[en] = c
|
|
181
|
+
await db.commit()
|
|
182
|
+
|
|
183
|
+
# Suppliers
|
|
184
|
+
sup_names = ["Al-Dawaa Medical", "Nahdi Wholesale", "Gulf Pharma Supply"]
|
|
185
|
+
suppliers = []
|
|
186
|
+
for name in sup_names:
|
|
187
|
+
s = Supplier(name=name, contact_person="John Doe", phone="0500000000", email="sup@example.com")
|
|
188
|
+
db.add(s)
|
|
189
|
+
suppliers.append(s)
|
|
190
|
+
await db.commit()
|
|
191
|
+
|
|
192
|
+
# Customers
|
|
193
|
+
cust_names = [("Mohammed Al-Rashid", "محمد الراشد"), ("Fatima Al-Zahrani", "فاطمة الزهراني")]
|
|
194
|
+
customers = []
|
|
195
|
+
for en, ar in cust_names:
|
|
196
|
+
c = Customer(name=en, name_ar=ar, phone="0511111111")
|
|
197
|
+
db.add(c)
|
|
198
|
+
customers.append(c)
|
|
199
|
+
await db.commit()
|
|
200
|
+
|
|
201
|
+
# Medicines
|
|
202
|
+
meds_data = [
|
|
203
|
+
("Panadol", "بانادول", "Paracetamol", "Panadol", "500mg", "Tablet", "GSK", "Pain Relief / Analgesics", 15.0),
|
|
204
|
+
("Amoxil", "أموكسيل", "Amoxicillin", "Amoxil", "500mg", "Capsule", "GSK", "Antibiotics", 25.5),
|
|
205
|
+
("Augmentin", "أوجمنتين", "Amoxicillin/Clavulanate", "Augmentin", "1g", "Tablet", "GSK", "Antibiotics", 80.0),
|
|
206
|
+
("Brufen", "بروفين", "Ibuprofen", "Brufen", "400mg", "Tablet", "Abbott", "Anti-inflammatory", 12.0),
|
|
207
|
+
("Voltaren", "فولتارين", "Diclofenac", "Voltaren", "50mg", "Tablet", "Novartis", "Anti-inflammatory", 18.0),
|
|
208
|
+
("Zyrtec", "زيرتيك", "Cetirizine", "Zyrtec", "10mg", "Tablet", "UCB Pharma", "Antihistamines", 22.0),
|
|
209
|
+
("Crestor", "كريستور", "Rosuvastatin", "Crestor", "10mg", "Tablet", "AstraZeneca", "Cardiovascular", 150.0),
|
|
210
|
+
("Lipitor", "ليبيتور", "Atorvastatin", "Lipitor", "20mg", "Tablet", "Pfizer", "Cardiovascular", 120.0),
|
|
211
|
+
("Ventolin", "فنتولين", "Salbutamol", "Ventolin", "100mcg", "Inhaler", "GSK", "Respiratory", 35.0),
|
|
212
|
+
("Nexium", "نيكسيوم", "Esomeprazole", "Nexium", "40mg", "Capsule", "AstraZeneca", "Gastrointestinal", 65.0),
|
|
213
|
+
("Glucophage", "جلوكوفاج", "Metformin", "Glucophage", "500mg", "Tablet", "Merck", "Antidiabetics", 15.0),
|
|
214
|
+
("Concor", "كونكور", "Bisoprolol", "Concor", "5mg", "Tablet", "Merck", "Hypertension", 40.0),
|
|
215
|
+
("Losartan", "لوسارتان", "Losartan", "Losartan", "50mg", "Tablet", "MSD", "Hypertension", 30.0),
|
|
216
|
+
("Omeprazole", "أوميبرازول", "Omeprazole", "Omeprazole", "20mg", "Capsule", "Various", "Gastrointestinal", 45.0),
|
|
217
|
+
("Aspirin", "أسبرين", "Acetylsalicylic Acid", "Aspirin", "100mg", "Tablet", "Bayer", "Cardiovascular", 10.0),
|
|
218
|
+
("Cetrizine", "سيتريزين", "Cetirizine", "Cetrizine", "10mg", "Syrup", "Various", "Antihistamines", 14.0),
|
|
219
|
+
("Metformin XR", "ميتفورمين", "Metformin", "Metformin XR", "1000mg", "Tablet", "Merck", "Antidiabetics", 12.0),
|
|
220
|
+
("Amlodipine", "أملوديبين", "Amlodipine", "Amlodipine", "5mg", "Tablet", "Pfizer", "Hypertension", 28.0),
|
|
221
|
+
("Azithromycin", "أزيثروميسين", "Azithromycin", "Azithromycin", "500mg", "Tablet", "Pfizer", "Antibiotics", 55.0),
|
|
222
|
+
("Ibuprofen Susp", "إيبوبروفين", "Ibuprofen", "Ibuprofen Susp", "100mg/5ml", "Suspension", "Abbott", "Paediatrics", 11.0)
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
medicines = []
|
|
226
|
+
for idx, (en, ar, gen, brand, st, form, man, cat_key, price) in enumerate(meds_data):
|
|
227
|
+
m = Medicine(
|
|
228
|
+
sku=f"SKU{idx:04d}",
|
|
229
|
+
barcode=f"123456789{idx:03d}",
|
|
230
|
+
name_en=en,
|
|
231
|
+
name_ar=ar,
|
|
232
|
+
generic_name=gen,
|
|
233
|
+
brand_name=brand,
|
|
234
|
+
strength=st,
|
|
235
|
+
dosage_form=form,
|
|
236
|
+
manufacturer=man,
|
|
237
|
+
category_id=level_2_cats[cat_key].id,
|
|
238
|
+
base_unit="Pack",
|
|
239
|
+
selling_price=price,
|
|
240
|
+
reorder_level=10
|
|
241
|
+
)
|
|
242
|
+
db.add(m)
|
|
243
|
+
medicines.append(m)
|
|
244
|
+
await db.commit()
|
|
245
|
+
|
|
246
|
+
# Batches
|
|
247
|
+
now = datetime.utcnow()
|
|
248
|
+
for idx, m in enumerate(medicines):
|
|
249
|
+
b1 = MedicineBatch(
|
|
250
|
+
medicine_id=m.id,
|
|
251
|
+
batch_number=f"BATCH-A-{idx}",
|
|
252
|
+
production_date=now - timedelta(days=120), # approx 6 months before expiry (if expiry is +60 days)
|
|
253
|
+
expiry_date=now + timedelta(days=60),
|
|
254
|
+
purchase_price=float(m.selling_price) * 0.7,
|
|
255
|
+
quantity_received=50,
|
|
256
|
+
quantity_remaining=50,
|
|
257
|
+
supplier_id=suppliers[0].id
|
|
258
|
+
)
|
|
259
|
+
b2 = MedicineBatch(
|
|
260
|
+
medicine_id=m.id,
|
|
261
|
+
batch_number=f"BATCH-B-{idx}",
|
|
262
|
+
production_date=now - timedelta(days=185), # approx 6 months before expiry (if expiry is +180 days)
|
|
263
|
+
expiry_date=now + timedelta(days=180),
|
|
264
|
+
purchase_price=float(m.selling_price) * 0.7,
|
|
265
|
+
quantity_received=100,
|
|
266
|
+
quantity_remaining=100,
|
|
267
|
+
supplier_id=suppliers[1].id
|
|
268
|
+
)
|
|
269
|
+
db.add_all([b1, b2])
|
|
270
|
+
await db.commit()
|
|
271
|
+
|
|
272
|
+
print("Successfully seeded full pharmacy taxonomy and data.")
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__":
|
|
275
|
+
asyncio.run(seed())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
fastapi>=0.115.0
|
|
2
|
+
uvicorn[standard]>=0.32.0
|
|
3
|
+
sqlalchemy[asyncio]>=2.0.0
|
|
4
|
+
aiosqlite>=0.20.0
|
|
5
|
+
alembic>=1.13.0
|
|
6
|
+
pydantic>=2.0.0
|
|
7
|
+
pydantic-settings>=2.0.0
|
|
8
|
+
python-jose[cryptography]>=3.3.0
|
|
9
|
+
passlib[argon2]>=1.7.4
|
|
10
|
+
python-multipart>=0.0.9
|
|
11
|
+
httpx>=0.27.0
|
|
12
|
+
pillow>=10.0.0
|
|
13
|
+
openpyxl>=3.1.0
|
|
14
|
+
python-dateutil>=2.9.0
|
|
15
|
+
aiofiles>=24.0.0
|
|
16
|
+
|
|
17
|
+
email-validator>=2.0.0
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const BOLD = "\x1b[1m";
|
|
8
|
+
const GREEN = "\x1b[32m";
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
const YELLOW = "\x1b[33m";
|
|
11
|
+
const RED = "\x1b[31m";
|
|
12
|
+
const RESET = "\x1b[0m";
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
|
|
15
|
+
const LOGO = `
|
|
16
|
+
${CYAN}╔══════════════════════════════════════════════╗
|
|
17
|
+
║ ║
|
|
18
|
+
║ 💊 ${BOLD}Pharmacy ERP & POS System${RESET}${CYAN} ║
|
|
19
|
+
║ ${DIM}Enterprise-grade pharmacy management${RESET}${CYAN} ║
|
|
20
|
+
║ ║
|
|
21
|
+
╚══════════════════════════════════════════════╝${RESET}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
function log(msg) {
|
|
25
|
+
console.log(` ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function logStep(step, total, msg) {
|
|
29
|
+
console.log(` ${GREEN}[${step}/${total}]${RESET} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function logError(msg) {
|
|
33
|
+
console.error(` ${RED}✗ ${msg}${RESET}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function logSuccess(msg) {
|
|
37
|
+
console.log(` ${GREEN}✓ ${msg}${RESET}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function checkCommand(cmd) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(`${cmd} --version`, { stdio: "ignore" });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function showHelp() {
|
|
50
|
+
console.log(LOGO);
|
|
51
|
+
console.log(`${BOLD}Usage:${RESET}`);
|
|
52
|
+
log(`npx pharmacy-erp ${CYAN}<command>${RESET}`);
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(`${BOLD}Commands:${RESET}`);
|
|
55
|
+
log(`${CYAN}init${RESET} Scaffold a new Pharmacy ERP project in the current directory`);
|
|
56
|
+
log(`${CYAN}start${RESET} Start the development servers (frontend + backend)`);
|
|
57
|
+
log(`${CYAN}setup${RESET} Install all dependencies (frontend + backend)`);
|
|
58
|
+
log(`${CYAN}seed${RESET} Seed the database with demo data`);
|
|
59
|
+
log(`${CYAN}docker${RESET} Start with Docker Compose (production mode)`);
|
|
60
|
+
log(`${CYAN}help${RESET} Show this help message`);
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(`${BOLD}Quick Start:${RESET}`);
|
|
63
|
+
log(`${DIM}$ npx pharmacy-erp init${RESET}`);
|
|
64
|
+
log(`${DIM}$ cd pharmacy-erp${RESET}`);
|
|
65
|
+
log(`${DIM}$ npx pharmacy-erp start${RESET}`);
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(`${DIM}Docs: https://github.com/omersx/pharmacy-erp${RESET}`);
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkPrerequisites() {
|
|
72
|
+
let ok = true;
|
|
73
|
+
|
|
74
|
+
if (!checkCommand("node")) {
|
|
75
|
+
logError("Node.js is not installed. Download from https://nodejs.org");
|
|
76
|
+
ok = false;
|
|
77
|
+
} else {
|
|
78
|
+
logSuccess("Node.js found");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!checkCommand("pnpm")) {
|
|
82
|
+
logError("pnpm is not installed. Run: npm install -g pnpm");
|
|
83
|
+
ok = false;
|
|
84
|
+
} else {
|
|
85
|
+
logSuccess("pnpm found");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!checkCommand("python") && !checkCommand("python3")) {
|
|
89
|
+
logError("Python is not installed. Download from https://python.org");
|
|
90
|
+
ok = false;
|
|
91
|
+
} else {
|
|
92
|
+
logSuccess("Python found");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return ok;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cmdInit() {
|
|
99
|
+
console.log(LOGO);
|
|
100
|
+
log(`${BOLD}Scaffolding Pharmacy ERP project...${RESET}\n`);
|
|
101
|
+
|
|
102
|
+
const targetDir = path.join(process.cwd(), "pharmacy-erp");
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(targetDir)) {
|
|
105
|
+
logError(`Directory "pharmacy-erp" already exists. Remove it or use a different location.`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check prerequisites
|
|
110
|
+
log(`${BOLD}Checking prerequisites...${RESET}`);
|
|
111
|
+
if (!checkPrerequisites()) {
|
|
112
|
+
console.log();
|
|
113
|
+
logError("Please install missing prerequisites and try again.");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
// Check for git
|
|
119
|
+
if (!checkCommand("git")) {
|
|
120
|
+
logError("Git is not installed. Download from https://git-scm.com");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logStep(1, 4, "Cloning repository...");
|
|
125
|
+
try {
|
|
126
|
+
execSync("git clone https://github.com/omersx/pharmacy-erp.git", {
|
|
127
|
+
cwd: process.cwd(),
|
|
128
|
+
stdio: "inherit",
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
logError("Failed to clone repository.");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logStep(2, 4, "Creating environment file...");
|
|
136
|
+
const envExample = path.join(targetDir, ".env.example");
|
|
137
|
+
const envFile = path.join(targetDir, ".env");
|
|
138
|
+
if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
|
|
139
|
+
fs.copyFileSync(envExample, envFile);
|
|
140
|
+
logSuccess(".env created from .env.example");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logStep(3, 4, "Installing dependencies...");
|
|
144
|
+
try {
|
|
145
|
+
execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
|
|
146
|
+
execSync("pnpm run setup:frontend", { cwd: targetDir, stdio: "inherit" });
|
|
147
|
+
execSync("pnpm run setup:backend", { cwd: targetDir, stdio: "inherit" });
|
|
148
|
+
} catch {
|
|
149
|
+
logError("Dependency installation failed. Try running manually:");
|
|
150
|
+
log(` cd pharmacy-erp && pnpm run setup`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logStep(4, 4, "Seeding demo data...");
|
|
155
|
+
try {
|
|
156
|
+
execSync("pnpm run seed", { cwd: targetDir, stdio: "inherit" });
|
|
157
|
+
} catch {
|
|
158
|
+
logError("Seeding failed. Try running manually:");
|
|
159
|
+
log(` cd pharmacy-erp && pnpm run seed`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log();
|
|
163
|
+
console.log(`${GREEN}${BOLD} ✅ Pharmacy ERP is ready!${RESET}`);
|
|
164
|
+
console.log();
|
|
165
|
+
log(`${BOLD}Next steps:${RESET}`);
|
|
166
|
+
log(` ${CYAN}cd pharmacy-erp${RESET}`);
|
|
167
|
+
log(` ${CYAN}pnpm dev${RESET} ${DIM}# or: npx pharmacy-erp start${RESET}`);
|
|
168
|
+
console.log();
|
|
169
|
+
log(`${BOLD}Login:${RESET} admin@pharmacy.com / admin123`);
|
|
170
|
+
log(`${BOLD}Web:${RESET} http://localhost:3000`);
|
|
171
|
+
log(`${BOLD}API:${RESET} http://localhost:8000/docs`);
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cmdStart() {
|
|
176
|
+
console.log(LOGO);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync("package.json")) {
|
|
179
|
+
logError("No package.json found. Run this from the pharmacy-erp project directory.");
|
|
180
|
+
log(`Or run ${CYAN}npx pharmacy-erp init${RESET} to create a new project.`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
log(`${BOLD}Starting Pharmacy ERP...${RESET}\n`);
|
|
185
|
+
log(`${CYAN}Frontend:${RESET} http://localhost:3000`);
|
|
186
|
+
log(`${CYAN}Backend:${RESET} http://localhost:8000`);
|
|
187
|
+
log(`${CYAN}API Docs:${RESET} http://localhost:8000/docs`);
|
|
188
|
+
log(`${DIM}Press Ctrl+C to stop${RESET}\n`);
|
|
189
|
+
|
|
190
|
+
const child = spawn("pnpm", ["dev"], { cwd: process.cwd(), stdio: "inherit", shell: true });
|
|
191
|
+
child.on("exit", (code) => process.exit(code));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cmdSetup() {
|
|
195
|
+
console.log(LOGO);
|
|
196
|
+
log(`${BOLD}Installing dependencies...${RESET}\n`);
|
|
197
|
+
|
|
198
|
+
if (!checkPrerequisites()) {
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
execSync("pnpm install", { stdio: "inherit" });
|
|
204
|
+
execSync("pnpm run setup:frontend", { stdio: "inherit" });
|
|
205
|
+
execSync("pnpm run setup:backend", { stdio: "inherit" });
|
|
206
|
+
logSuccess("All dependencies installed!");
|
|
207
|
+
} catch {
|
|
208
|
+
logError("Setup failed.");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cmdSeed() {
|
|
214
|
+
console.log(LOGO);
|
|
215
|
+
log(`${BOLD}Seeding demo data...${RESET}\n`);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
execSync("pnpm run seed", { stdio: "inherit" });
|
|
219
|
+
console.log();
|
|
220
|
+
logSuccess("Database seeded!");
|
|
221
|
+
log(`Login: ${CYAN}admin@pharmacy.com${RESET} / ${CYAN}admin123${RESET}`);
|
|
222
|
+
} catch {
|
|
223
|
+
logError("Seeding failed.");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function cmdDocker() {
|
|
229
|
+
console.log(LOGO);
|
|
230
|
+
log(`${BOLD}Starting with Docker Compose...${RESET}\n`);
|
|
231
|
+
|
|
232
|
+
if (!checkCommand("docker")) {
|
|
233
|
+
logError("Docker is not installed. Download from https://docker.com");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const child = spawn("docker", ["compose", "up", "-d"], {
|
|
239
|
+
cwd: process.cwd(),
|
|
240
|
+
stdio: "inherit",
|
|
241
|
+
shell: true,
|
|
242
|
+
});
|
|
243
|
+
child.on("exit", (code) => {
|
|
244
|
+
if (code === 0) {
|
|
245
|
+
console.log();
|
|
246
|
+
logSuccess("Pharmacy ERP is running!");
|
|
247
|
+
log(`${CYAN}Web:${RESET} http://localhost`);
|
|
248
|
+
log(`${CYAN}API:${RESET} http://localhost:8000/docs`);
|
|
249
|
+
}
|
|
250
|
+
process.exit(code);
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
logError("Docker Compose failed.");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Main ─────────────────────────────────────────────────
|
|
259
|
+
const command = process.argv[2] || "help";
|
|
260
|
+
|
|
261
|
+
switch (command) {
|
|
262
|
+
case "init":
|
|
263
|
+
cmdInit();
|
|
264
|
+
break;
|
|
265
|
+
case "start":
|
|
266
|
+
case "dev":
|
|
267
|
+
cmdStart();
|
|
268
|
+
break;
|
|
269
|
+
case "setup":
|
|
270
|
+
case "install":
|
|
271
|
+
cmdSetup();
|
|
272
|
+
break;
|
|
273
|
+
case "seed":
|
|
274
|
+
cmdSeed();
|
|
275
|
+
break;
|
|
276
|
+
case "docker":
|
|
277
|
+
cmdDocker();
|
|
278
|
+
break;
|
|
279
|
+
case "help":
|
|
280
|
+
case "--help":
|
|
281
|
+
case "-h":
|
|
282
|
+
showHelp();
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
logError(`Unknown command: ${command}`);
|
|
286
|
+
showHelp();
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Pharmacy ERP — Development Docker Compose
|
|
3
|
+
# Builds from local source code (for contributors)
|
|
4
|
+
# ============================================================
|
|
5
|
+
# Usage:
|
|
6
|
+
# docker compose -f docker-compose.dev.yml up --build
|
|
7
|
+
# ============================================================
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
backend:
|
|
11
|
+
build:
|
|
12
|
+
context: ./backend
|
|
13
|
+
dockerfile: Dockerfile
|
|
14
|
+
container_name: pharmacy-erp-backend-dev
|
|
15
|
+
restart: unless-stopped
|
|
16
|
+
ports:
|
|
17
|
+
- "8000:8000"
|
|
18
|
+
environment:
|
|
19
|
+
- APP_ENV=development
|
|
20
|
+
- DEBUG=true
|
|
21
|
+
- DATABASE_URL=sqlite+aiosqlite:///./data/pharmacy.db
|
|
22
|
+
- SECRET_KEY=dev-secret-key-change-in-production-minimum-32-characters-long
|
|
23
|
+
- CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
|
24
|
+
volumes:
|
|
25
|
+
- backend_data:/app/data
|
|
26
|
+
|
|
27
|
+
frontend:
|
|
28
|
+
build:
|
|
29
|
+
context: ./frontend
|
|
30
|
+
dockerfile: Dockerfile
|
|
31
|
+
container_name: pharmacy-erp-frontend-dev
|
|
32
|
+
restart: unless-stopped
|
|
33
|
+
ports:
|
|
34
|
+
- "3000:3000"
|
|
35
|
+
environment:
|
|
36
|
+
- NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
|
|
37
|
+
depends_on:
|
|
38
|
+
- backend
|
|
39
|
+
|
|
40
|
+
volumes:
|
|
41
|
+
backend_data:
|
|
42
|
+
driver: local
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Pharmacy ERP — Production Docker Compose
|
|
3
|
+
# Uses pre-built images from Docker Hub
|
|
4
|
+
# ============================================================
|
|
5
|
+
# Usage:
|
|
6
|
+
# docker compose up -d
|
|
7
|
+
#
|
|
8
|
+
# Prerequisites:
|
|
9
|
+
# Copy .env.example to .env and update values
|
|
10
|
+
# ============================================================
|
|
11
|
+
|
|
12
|
+
services:
|
|
13
|
+
backend:
|
|
14
|
+
image: omersx/pharmacy-erp-backend:latest
|
|
15
|
+
container_name: pharmacy-erp-backend
|
|
16
|
+
restart: unless-stopped
|
|
17
|
+
ports:
|
|
18
|
+
- "8000:8000"
|
|
19
|
+
env_file:
|
|
20
|
+
- .env
|
|
21
|
+
environment:
|
|
22
|
+
- APP_ENV=production
|
|
23
|
+
- DEBUG=false
|
|
24
|
+
volumes:
|
|
25
|
+
- backend_data:/app/data
|
|
26
|
+
healthcheck:
|
|
27
|
+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')"]
|
|
28
|
+
interval: 30s
|
|
29
|
+
timeout: 10s
|
|
30
|
+
retries: 3
|
|
31
|
+
start_period: 10s
|
|
32
|
+
|
|
33
|
+
frontend:
|
|
34
|
+
image: omersx/pharmacy-erp-frontend:latest
|
|
35
|
+
container_name: pharmacy-erp-frontend
|
|
36
|
+
restart: unless-stopped
|
|
37
|
+
ports:
|
|
38
|
+
- "3000:3000"
|
|
39
|
+
environment:
|
|
40
|
+
- NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
|
|
41
|
+
depends_on:
|
|
42
|
+
backend:
|
|
43
|
+
condition: service_healthy
|
|
44
|
+
|
|
45
|
+
nginx:
|
|
46
|
+
image: nginx:alpine
|
|
47
|
+
container_name: pharmacy-erp-nginx
|
|
48
|
+
restart: unless-stopped
|
|
49
|
+
ports:
|
|
50
|
+
- "80:80"
|
|
51
|
+
volumes:
|
|
52
|
+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
53
|
+
depends_on:
|
|
54
|
+
- backend
|
|
55
|
+
- frontend
|
|
56
|
+
|
|
57
|
+
volumes:
|
|
58
|
+
backend_data:
|
|
59
|
+
driver: local
|