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,63 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, Query, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from sqlalchemy import select, desc
|
|
4
|
+
from app.core.database import get_db
|
|
5
|
+
from app.core.deps import get_current_user
|
|
6
|
+
from app.modules.auth.models import User
|
|
7
|
+
from app.modules.sales.schemas import SaleCreate, SaleResponse
|
|
8
|
+
from app.modules.sales.service import create_sale, return_sale
|
|
9
|
+
from app.modules.sales.models import Sale
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from app.modules.organizations.models import Branch
|
|
14
|
+
|
|
15
|
+
router = APIRouter(tags=["Sales"])
|
|
16
|
+
|
|
17
|
+
@router.post("", response_model=SaleResponse, include_in_schema=False)
|
|
18
|
+
@router.post("/", response_model=SaleResponse)
|
|
19
|
+
async def process_sale(sale_in: SaleCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
20
|
+
branch_res = await db.execute(select(Branch).limit(1))
|
|
21
|
+
branch = branch_res.scalars().first()
|
|
22
|
+
if not branch:
|
|
23
|
+
branch = Branch(name="Main Branch", code="MAIN")
|
|
24
|
+
db.add(branch)
|
|
25
|
+
await db.commit()
|
|
26
|
+
await db.refresh(branch)
|
|
27
|
+
branch_id = branch.id
|
|
28
|
+
sale = await create_sale(db, sale_in, branch_id, current_user.id)
|
|
29
|
+
return sale
|
|
30
|
+
|
|
31
|
+
@router.get("", response_model=List[SaleResponse], include_in_schema=False)
|
|
32
|
+
@router.get("/", response_model=List[SaleResponse])
|
|
33
|
+
async def list_sales(
|
|
34
|
+
start_date: Optional[str] = None,
|
|
35
|
+
end_date: Optional[str] = None,
|
|
36
|
+
status: Optional[str] = None,
|
|
37
|
+
search: Optional[str] = None,
|
|
38
|
+
db: AsyncSession = Depends(get_db),
|
|
39
|
+
current_user: User = Depends(get_current_user)
|
|
40
|
+
):
|
|
41
|
+
query = select(Sale).order_by(desc(Sale.created_at))
|
|
42
|
+
|
|
43
|
+
if status and status != 'ALL':
|
|
44
|
+
query = query.where(Sale.status == status)
|
|
45
|
+
if search:
|
|
46
|
+
query = query.where(Sale.invoice_number.ilike(f"%{search}%"))
|
|
47
|
+
|
|
48
|
+
result = await db.execute(query)
|
|
49
|
+
return result.scalars().all()
|
|
50
|
+
|
|
51
|
+
@router.get("/{sale_id}", response_model=SaleResponse)
|
|
52
|
+
async def get_sale(sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
53
|
+
result = await db.execute(select(Sale).where(Sale.id == sale_id))
|
|
54
|
+
sale = result.scalars().first()
|
|
55
|
+
if not sale:
|
|
56
|
+
raise HTTPException(status_code=404, detail="Sale not found")
|
|
57
|
+
return sale
|
|
58
|
+
|
|
59
|
+
@router.post("/{sale_id}/return", response_model=SaleResponse)
|
|
60
|
+
async def process_return(sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
61
|
+
sale = await return_sale(db, sale_id, current_user.id)
|
|
62
|
+
return sale
|
|
63
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
import uuid
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
class SaleItemCreate(BaseModel):
|
|
8
|
+
medicine_id: uuid.UUID
|
|
9
|
+
quantity: Decimal
|
|
10
|
+
unit_price: Decimal
|
|
11
|
+
|
|
12
|
+
class SaleCreate(BaseModel):
|
|
13
|
+
payment_method: str
|
|
14
|
+
items: List[SaleItemCreate]
|
|
15
|
+
customer_id: Optional[uuid.UUID] = None
|
|
16
|
+
cash_session_id: Optional[uuid.UUID] = None
|
|
17
|
+
|
|
18
|
+
class CustomerSimple(BaseModel):
|
|
19
|
+
id: uuid.UUID
|
|
20
|
+
name: str
|
|
21
|
+
model_config = {"from_attributes": True}
|
|
22
|
+
|
|
23
|
+
class SaleItemResponse(BaseModel):
|
|
24
|
+
id: uuid.UUID
|
|
25
|
+
medicine_id: uuid.UUID
|
|
26
|
+
batch_id: uuid.UUID
|
|
27
|
+
quantity: Decimal
|
|
28
|
+
unit_price: Decimal
|
|
29
|
+
line_total: Decimal
|
|
30
|
+
model_config = {"from_attributes": True}
|
|
31
|
+
|
|
32
|
+
class SaleResponse(BaseModel):
|
|
33
|
+
id: uuid.UUID
|
|
34
|
+
invoice_number: str
|
|
35
|
+
total_amount: Decimal
|
|
36
|
+
status: str
|
|
37
|
+
payment_method: str
|
|
38
|
+
created_at: datetime
|
|
39
|
+
customer: Optional[CustomerSimple] = None
|
|
40
|
+
items: List[SaleItemResponse] = []
|
|
41
|
+
model_config = {"from_attributes": True}
|
|
42
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from sqlalchemy import select
|
|
4
|
+
from app.modules.medicines.models import MedicineBatch
|
|
5
|
+
from app.modules.sales.models import Sale, SaleItem
|
|
6
|
+
from app.modules.inventory.models import InventoryMovement
|
|
7
|
+
from app.core.exceptions import ValidationError
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
async def create_sale(db: AsyncSession, sale_data, branch_id: uuid.UUID, cashier_id: uuid.UUID):
|
|
12
|
+
total_amount = Decimal('0.0')
|
|
13
|
+
sale_items = []
|
|
14
|
+
movements = []
|
|
15
|
+
|
|
16
|
+
for item in sale_data.items:
|
|
17
|
+
result = await db.execute(
|
|
18
|
+
select(MedicineBatch)
|
|
19
|
+
.where(MedicineBatch.medicine_id == item.medicine_id)
|
|
20
|
+
.where(MedicineBatch.quantity_remaining > 0)
|
|
21
|
+
.where(MedicineBatch.is_active == True)
|
|
22
|
+
.where(MedicineBatch.expiry_date > datetime.utcnow())
|
|
23
|
+
.where(MedicineBatch.status.in_(["ACTIVE", "NEAR_EXPIRY"]))
|
|
24
|
+
.order_by(MedicineBatch.expiry_date.asc())
|
|
25
|
+
)
|
|
26
|
+
batches = result.scalars().all()
|
|
27
|
+
|
|
28
|
+
qty_needed = Decimal(str(item.quantity))
|
|
29
|
+
|
|
30
|
+
for batch in batches:
|
|
31
|
+
if qty_needed <= 0:
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
available = Decimal(str(batch.quantity_remaining))
|
|
35
|
+
take = min(available, qty_needed)
|
|
36
|
+
|
|
37
|
+
batch.quantity_remaining = float(available - take)
|
|
38
|
+
qty_needed -= take
|
|
39
|
+
|
|
40
|
+
line_total = take * Decimal(str(item.unit_price))
|
|
41
|
+
sale_items.append(SaleItem(
|
|
42
|
+
medicine_id=item.medicine_id,
|
|
43
|
+
batch_id=batch.id,
|
|
44
|
+
quantity=float(take),
|
|
45
|
+
unit_price=float(item.unit_price),
|
|
46
|
+
line_total=float(line_total)
|
|
47
|
+
))
|
|
48
|
+
total_amount += line_total
|
|
49
|
+
|
|
50
|
+
movements.append(InventoryMovement(
|
|
51
|
+
medicine_id=item.medicine_id,
|
|
52
|
+
batch_id=batch.id,
|
|
53
|
+
branch_id=branch_id,
|
|
54
|
+
movement_type="SALE_OUT",
|
|
55
|
+
quantity=float(-take),
|
|
56
|
+
created_by=cashier_id
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
if qty_needed > 0:
|
|
60
|
+
raise ValidationError(f"Insufficient stock for medicine {item.medicine_id}")
|
|
61
|
+
|
|
62
|
+
invoice_number = f"INV-{uuid.uuid4().hex[:8].upper()}"
|
|
63
|
+
|
|
64
|
+
sale = Sale(
|
|
65
|
+
invoice_number=invoice_number,
|
|
66
|
+
branch_id=branch_id,
|
|
67
|
+
cashier_id=cashier_id,
|
|
68
|
+
status="COMPLETED",
|
|
69
|
+
subtotal=float(total_amount),
|
|
70
|
+
total_amount=float(total_amount),
|
|
71
|
+
payment_method=sale_data.payment_method,
|
|
72
|
+
customer_id=sale_data.customer_id,
|
|
73
|
+
cash_session_id=sale_data.cash_session_id
|
|
74
|
+
)
|
|
75
|
+
db.add(sale)
|
|
76
|
+
await db.flush()
|
|
77
|
+
|
|
78
|
+
for si in sale_items:
|
|
79
|
+
si.sale_id = sale.id
|
|
80
|
+
db.add(si)
|
|
81
|
+
|
|
82
|
+
for mv in movements:
|
|
83
|
+
mv.reference_id = str(sale.id)
|
|
84
|
+
mv.reference_type = "SALE"
|
|
85
|
+
db.add(mv)
|
|
86
|
+
|
|
87
|
+
await db.commit()
|
|
88
|
+
await db.refresh(sale)
|
|
89
|
+
return sale
|
|
90
|
+
|
|
91
|
+
async def return_sale(db: AsyncSession, sale_id: uuid.UUID, cashier_id: uuid.UUID):
|
|
92
|
+
result = await db.execute(select(Sale).where(Sale.id == sale_id))
|
|
93
|
+
sale = result.scalars().first()
|
|
94
|
+
|
|
95
|
+
if not sale:
|
|
96
|
+
raise ValidationError(f"Sale {sale_id} not found")
|
|
97
|
+
|
|
98
|
+
if sale.status == "RETURNED":
|
|
99
|
+
raise ValidationError(f"Sale {sale_id} is already returned")
|
|
100
|
+
|
|
101
|
+
items_result = await db.execute(select(SaleItem).where(SaleItem.sale_id == sale_id))
|
|
102
|
+
sale_items = items_result.scalars().all()
|
|
103
|
+
|
|
104
|
+
from decimal import Decimal
|
|
105
|
+
for item in sale_items:
|
|
106
|
+
# Restore stock
|
|
107
|
+
batch_result = await db.execute(select(MedicineBatch).where(MedicineBatch.id == item.batch_id))
|
|
108
|
+
batch = batch_result.scalars().first()
|
|
109
|
+
if batch:
|
|
110
|
+
batch.quantity_remaining += Decimal(str(item.quantity))
|
|
111
|
+
|
|
112
|
+
# Create return movement
|
|
113
|
+
mv = InventoryMovement(
|
|
114
|
+
medicine_id=item.medicine_id,
|
|
115
|
+
batch_id=item.batch_id,
|
|
116
|
+
branch_id=sale.branch_id,
|
|
117
|
+
movement_type="RETURN_IN",
|
|
118
|
+
quantity=Decimal(str(item.quantity)),
|
|
119
|
+
created_by=cashier_id,
|
|
120
|
+
reference_id=str(sale.id),
|
|
121
|
+
reference_type="SALE_RETURN"
|
|
122
|
+
)
|
|
123
|
+
db.add(mv)
|
|
124
|
+
|
|
125
|
+
sale.status = "RETURNED"
|
|
126
|
+
await db.commit()
|
|
127
|
+
await db.refresh(sale)
|
|
128
|
+
return sale
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
2
|
+
from sqlalchemy import String, Boolean
|
|
3
|
+
from app.core.database import Base
|
|
4
|
+
|
|
5
|
+
class Supplier(Base):
|
|
6
|
+
__tablename__ = "suppliers"
|
|
7
|
+
name: Mapped[str] = mapped_column(String)
|
|
8
|
+
name_ar: Mapped[str] = mapped_column(String, nullable=True)
|
|
9
|
+
contact_person: Mapped[str] = mapped_column(String, nullable=True)
|
|
10
|
+
phone: Mapped[str] = mapped_column(String, nullable=True)
|
|
11
|
+
email: Mapped[str] = mapped_column(String, nullable=True)
|
|
12
|
+
address: Mapped[str] = mapped_column(String, nullable=True)
|
|
13
|
+
payment_terms: Mapped[str] = mapped_column(String, nullable=True)
|
|
14
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from sqlalchemy import select
|
|
4
|
+
from typing import List
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from app.core.database import get_db
|
|
8
|
+
from app.core.deps import get_current_user
|
|
9
|
+
from app.modules.auth.models import User
|
|
10
|
+
from app.modules.suppliers.models import Supplier
|
|
11
|
+
from app.modules.suppliers.schemas import SupplierCreate, SupplierUpdate, SupplierResponse
|
|
12
|
+
|
|
13
|
+
router = APIRouter(tags=["suppliers"])
|
|
14
|
+
|
|
15
|
+
@router.get("/", response_model=List[SupplierResponse])
|
|
16
|
+
async def list_suppliers(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
17
|
+
result = await db.execute(select(Supplier))
|
|
18
|
+
return result.scalars().all()
|
|
19
|
+
|
|
20
|
+
@router.get("/{id}", response_model=SupplierResponse)
|
|
21
|
+
async def get_supplier(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
22
|
+
supplier = await db.get(Supplier, id)
|
|
23
|
+
if not supplier:
|
|
24
|
+
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
25
|
+
return supplier
|
|
26
|
+
|
|
27
|
+
@router.post("/", response_model=SupplierResponse)
|
|
28
|
+
async def create_supplier(data: SupplierCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
29
|
+
supplier = Supplier(**data.model_dump())
|
|
30
|
+
db.add(supplier)
|
|
31
|
+
await db.commit()
|
|
32
|
+
await db.refresh(supplier)
|
|
33
|
+
return supplier
|
|
34
|
+
|
|
35
|
+
@router.put("/{id}", response_model=SupplierResponse)
|
|
36
|
+
async def update_supplier(id: UUID, data: SupplierUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
37
|
+
supplier = await db.get(Supplier, id)
|
|
38
|
+
if not supplier:
|
|
39
|
+
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
40
|
+
|
|
41
|
+
for key, value in data.model_dump(exclude_unset=True).items():
|
|
42
|
+
setattr(supplier, key, value)
|
|
43
|
+
|
|
44
|
+
await db.commit()
|
|
45
|
+
await db.refresh(supplier)
|
|
46
|
+
return supplier
|
|
47
|
+
|
|
48
|
+
@router.delete("/{id}")
|
|
49
|
+
async def delete_supplier(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
50
|
+
supplier = await db.get(Supplier, id)
|
|
51
|
+
if not supplier:
|
|
52
|
+
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
53
|
+
|
|
54
|
+
supplier.is_active = False
|
|
55
|
+
await db.commit()
|
|
56
|
+
return {"message": "Supplier deleted successfully"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pydantic import BaseModel, EmailStr
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
class SupplierBase(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
name_ar: Optional[str] = None
|
|
9
|
+
contact_person: Optional[str] = None
|
|
10
|
+
phone: Optional[str] = None
|
|
11
|
+
email: Optional[EmailStr] = None
|
|
12
|
+
address: Optional[str] = None
|
|
13
|
+
payment_terms: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
class SupplierCreate(SupplierBase):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class SupplierUpdate(SupplierBase):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class SupplierResponse(SupplierBase):
|
|
22
|
+
id: UUID
|
|
23
|
+
is_active: bool
|
|
24
|
+
created_at: datetime
|
|
25
|
+
|
|
26
|
+
class Config:
|
|
27
|
+
from_attributes = True
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from sqlalchemy import select
|
|
4
|
+
from typing import List
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from app.core.database import get_db
|
|
8
|
+
from app.core.deps import get_current_user
|
|
9
|
+
from app.modules.auth.models import User
|
|
10
|
+
from pydantic import BaseModel, EmailStr
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
class UserUpdate(BaseModel):
|
|
14
|
+
full_name: Optional[str] = None
|
|
15
|
+
full_name_ar: Optional[str] = None
|
|
16
|
+
phone: Optional[str] = None
|
|
17
|
+
role: Optional[str] = None
|
|
18
|
+
is_active: Optional[bool] = None
|
|
19
|
+
|
|
20
|
+
class UserResponse(BaseModel):
|
|
21
|
+
id: UUID
|
|
22
|
+
email: str
|
|
23
|
+
full_name: str
|
|
24
|
+
full_name_ar: Optional[str] = None
|
|
25
|
+
role: str
|
|
26
|
+
phone: Optional[str] = None
|
|
27
|
+
is_active: bool
|
|
28
|
+
|
|
29
|
+
model_config = {"from_attributes": True}
|
|
30
|
+
|
|
31
|
+
router = APIRouter(tags=["users"])
|
|
32
|
+
|
|
33
|
+
@router.get("/", response_model=List[UserResponse])
|
|
34
|
+
async def list_users(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
35
|
+
if current_user.role != "admin":
|
|
36
|
+
raise HTTPException(status_code=403, detail="Not authorized")
|
|
37
|
+
result = await db.execute(select(User))
|
|
38
|
+
return result.scalars().all()
|
|
39
|
+
|
|
40
|
+
@router.get("/{id}", response_model=UserResponse)
|
|
41
|
+
async def get_user(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
42
|
+
user = await db.get(User, id)
|
|
43
|
+
if not user:
|
|
44
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
45
|
+
return user
|
|
46
|
+
|
|
47
|
+
@router.put("/{id}", response_model=UserResponse)
|
|
48
|
+
async def update_user(id: UUID, data: UserUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
49
|
+
if current_user.role != "admin" and current_user.id != id:
|
|
50
|
+
raise HTTPException(status_code=403, detail="Not authorized")
|
|
51
|
+
|
|
52
|
+
user = await db.get(User, id)
|
|
53
|
+
if not user:
|
|
54
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
55
|
+
|
|
56
|
+
for key, value in data.model_dump(exclude_unset=True).items():
|
|
57
|
+
setattr(user, key, value)
|
|
58
|
+
|
|
59
|
+
await db.commit()
|
|
60
|
+
await db.refresh(user)
|
|
61
|
+
return user
|
|
62
|
+
|
|
63
|
+
@router.delete("/{id}")
|
|
64
|
+
async def delete_user(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
65
|
+
if current_user.role != "admin":
|
|
66
|
+
raise HTTPException(status_code=403, detail="Not authorized")
|
|
67
|
+
|
|
68
|
+
user = await db.get(User, id)
|
|
69
|
+
if not user:
|
|
70
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
71
|
+
|
|
72
|
+
user.is_active = False
|
|
73
|
+
await db.commit()
|
|
74
|
+
return {"message": "User deleted successfully"}
|