Step 9:药品管理(IMP)
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_db_session
|
||||||
|
from app.schemas.imp import TxCreate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.crud import imp_batch as batch_crud
|
||||||
|
from app.crud import study as study_crud
|
||||||
|
from app.schemas.imp import BatchCreate, BatchRead, BatchUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||||
|
study = await study_crud.get(db, study_id)
|
||||||
|
if not study:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||||
|
return study
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=BatchRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "IMP"]))],
|
||||||
|
)
|
||||||
|
async def create_batch(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
batch_in: BatchCreate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> BatchRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
try:
|
||||||
|
batch = await batch_crud.create_batch(db, study_id, batch_in)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="imp_batch",
|
||||||
|
entity_id=batch.id,
|
||||||
|
action="CREATE_IMP_BATCH",
|
||||||
|
detail=f"IMP batch {batch.batch_no} created",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return BatchRead.model_validate(batch)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=list[BatchRead],
|
||||||
|
dependencies=[Depends(require_study_member())],
|
||||||
|
)
|
||||||
|
async def list_batches(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
product_id: uuid.UUID | None = None,
|
||||||
|
status_filter: str | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[BatchRead]:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
batches = await batch_crud.list_batches(db, study_id, product_id=product_id, status=status_filter)
|
||||||
|
return [BatchRead.model_validate(b) for b in batches]
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{batch_id}",
|
||||||
|
response_model=BatchRead,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "IMP"]))],
|
||||||
|
)
|
||||||
|
async def update_batch(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
batch_id: uuid.UUID,
|
||||||
|
batch_in: BatchUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> BatchRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
batch = await batch_crud.get_batch(db, batch_id)
|
||||||
|
if not batch or batch.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Batch not found")
|
||||||
|
updated = await batch_crud.update_batch(db, batch, batch_in)
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="imp_batch",
|
||||||
|
entity_id=batch_id,
|
||||||
|
action="UPDATE_IMP_BATCH",
|
||||||
|
detail=f"IMP batch {batch_id} updated",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return BatchRead.model_validate(updated)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_db_session, require_study_member
|
||||||
|
from app.crud import imp_inventory as inventory_crud
|
||||||
|
from app.crud import study as study_crud
|
||||||
|
from app.schemas.imp import InventoryRead
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||||
|
study = await study_crud.get(db, study_id)
|
||||||
|
return study
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=list[InventoryRead],
|
||||||
|
dependencies=[Depends(require_study_member())],
|
||||||
|
)
|
||||||
|
async def list_inventory(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
site_id: uuid.UUID | None = None,
|
||||||
|
product_id: uuid.UUID | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[InventoryRead]:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
inv = await inventory_crud.list_inventory(db, study_id, site_id=site_id, product_id=product_id)
|
||||||
|
return [InventoryRead.model_validate(i) for i in inv]
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.crud import imp_product as product_crud
|
||||||
|
from app.crud import study as study_crud
|
||||||
|
from app.schemas.imp import ProductCreate, ProductRead, ProductUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||||
|
study = await study_crud.get(db, study_id)
|
||||||
|
if not study:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||||
|
return study
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=ProductRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "IMP"]))],
|
||||||
|
)
|
||||||
|
async def create_product(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
product_in: ProductCreate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> ProductRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
product = await product_crud.create_product(db, study_id, product_in)
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="imp_product",
|
||||||
|
entity_id=product.id,
|
||||||
|
action="CREATE_IMP_PRODUCT",
|
||||||
|
detail=f"IMP product {product.name} created",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return ProductRead.model_validate(product)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=list[ProductRead],
|
||||||
|
dependencies=[Depends(require_study_member())],
|
||||||
|
)
|
||||||
|
async def list_products(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
is_active: bool | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[ProductRead]:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
products = await product_crud.list_products(db, study_id, is_active=is_active)
|
||||||
|
return [ProductRead.model_validate(p) for p in products]
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{product_id}",
|
||||||
|
response_model=ProductRead,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "IMP"]))],
|
||||||
|
)
|
||||||
|
async def update_product(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
product_id: uuid.UUID,
|
||||||
|
product_in: ProductUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> ProductRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
product = await product_crud.get_product(db, product_id)
|
||||||
|
if not product or product.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found")
|
||||||
|
updated = await product_crud.update_product(db, product, product_in)
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="imp_product",
|
||||||
|
entity_id=product_id,
|
||||||
|
action="UPDATE_IMP_PRODUCT",
|
||||||
|
detail=f"IMP product {product_id} updated",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return ProductRead.model_validate(updated)
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.crud import imp_batch as batch_crud
|
||||||
|
from app.crud import imp_tx as tx_crud
|
||||||
|
from app.crud import study as study_crud
|
||||||
|
from app.crud import subject as subject_crud
|
||||||
|
from app.crud import member as member_crud
|
||||||
|
from app.schemas.imp import TxCreate, TxRead
|
||||||
|
from app.services.imp import create_transaction_and_apply_inventory, NEGATIVE_TYPES
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||||
|
study = await study_crud.get(db, study_id)
|
||||||
|
if not study:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||||
|
return study
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_entities(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID, subject_id: uuid.UUID | None):
|
||||||
|
batch = await batch_crud.get_batch(db, batch_id)
|
||||||
|
if not batch or batch.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Batch not found")
|
||||||
|
from app.models.site import Site
|
||||||
|
site = await db.get(Site, site_id)
|
||||||
|
if not site or site.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found")
|
||||||
|
if subject_id:
|
||||||
|
subj = await subject_crud.get_subject(db, subject_id)
|
||||||
|
if not subj or subj.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found")
|
||||||
|
if batch.expiry_date and date.today() > batch.expiry_date and batch_id:
|
||||||
|
# forbid outflow when expired
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=TxRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "IMP"]))],
|
||||||
|
)
|
||||||
|
async def create_transaction(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
tx_in: TxCreate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> TxRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
await _validate_entities(db, study_id, tx_in.site_id, tx_in.batch_id, tx_in.subject_id)
|
||||||
|
if tx_in.tx_type in NEGATIVE_TYPES:
|
||||||
|
batch = await batch_crud.get_batch(db, tx_in.batch_id)
|
||||||
|
if batch and batch.status == "EXPIRED":
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot dispense from expired batch")
|
||||||
|
try:
|
||||||
|
tx = await create_transaction_and_apply_inventory(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
site_id=tx_in.site_id,
|
||||||
|
batch_id=tx_in.batch_id,
|
||||||
|
subject_id=tx_in.subject_id,
|
||||||
|
tx_type=tx_in.tx_type,
|
||||||
|
quantity=tx_in.quantity,
|
||||||
|
tx_date=tx_in.tx_date,
|
||||||
|
reference=tx_in.reference,
|
||||||
|
notes=tx_in.notes,
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
return TxRead.model_validate(tx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=list[TxRead],
|
||||||
|
dependencies=[Depends(require_study_member())],
|
||||||
|
)
|
||||||
|
async def list_transactions(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
site_id: uuid.UUID | None = None,
|
||||||
|
batch_id: uuid.UUID | None = None,
|
||||||
|
subject_id: uuid.UUID | None = None,
|
||||||
|
tx_type: str | None = None,
|
||||||
|
date_from: date | None = None,
|
||||||
|
date_to: date | None = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[TxRead]:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
txs = await tx_crud.list_txs(
|
||||||
|
db,
|
||||||
|
study_id,
|
||||||
|
site_id=site_id,
|
||||||
|
batch_id=batch_id,
|
||||||
|
subject_id=subject_id,
|
||||||
|
tx_type=tx_type,
|
||||||
|
date_from=date_from,
|
||||||
|
date_to=date_to,
|
||||||
|
skip=skip,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
return [TxRead.model_validate(t) for t in txs]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications
|
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
@@ -20,3 +20,7 @@ api_router.include_router(aes.router, prefix="/studies/{study_id}/aes", tags=["a
|
|||||||
api_router.include_router(issues.router, prefix="/studies/{study_id}/issues", tags=["issues"])
|
api_router.include_router(issues.router, prefix="/studies/{study_id}/issues", tags=["issues"])
|
||||||
api_router.include_router(data_queries.router, prefix="/studies/{study_id}/data-queries", tags=["data-queries"])
|
api_router.include_router(data_queries.router, prefix="/studies/{study_id}/data-queries", tags=["data-queries"])
|
||||||
api_router.include_router(verifications.router, prefix="/studies/{study_id}/verifications", tags=["verifications"])
|
api_router.include_router(verifications.router, prefix="/studies/{study_id}/verifications", tags=["verifications"])
|
||||||
|
api_router.include_router(imp_products.router, prefix="/studies/{study_id}/imp/products", tags=["imp-products"])
|
||||||
|
api_router.include_router(imp_batches.router, prefix="/studies/{study_id}/imp/batches", tags=["imp-batches"])
|
||||||
|
api_router.include_router(imp_inventory.router, prefix="/studies/{study_id}/imp/inventory", tags=["imp-inventory"])
|
||||||
|
api_router.include_router(imp_transactions.router, prefix="/studies/{study_id}/imp/transactions", tags=["imp-transactions"])
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select, update as sa_update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.imp_batch import ImpBatch
|
||||||
|
from app.models.imp_product import ImpProduct
|
||||||
|
from app.schemas.imp import BatchCreate, BatchUpdate
|
||||||
|
|
||||||
|
|
||||||
|
async def create_batch(db: AsyncSession, study_id: uuid.UUID, batch_in: BatchCreate) -> ImpBatch:
|
||||||
|
result = await db.execute(select(ImpProduct).where(ImpProduct.id == batch_in.product_id))
|
||||||
|
product = result.scalar_one_or_none()
|
||||||
|
if not product or product.study_id != study_id:
|
||||||
|
raise ValueError("Product not found in study")
|
||||||
|
|
||||||
|
batch = ImpBatch(
|
||||||
|
study_id=study_id,
|
||||||
|
product_id=batch_in.product_id,
|
||||||
|
batch_no=batch_in.batch_no,
|
||||||
|
expiry_date=batch_in.expiry_date,
|
||||||
|
manufacture_date=batch_in.manufacture_date,
|
||||||
|
status="ACTIVE",
|
||||||
|
)
|
||||||
|
db.add(batch)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(batch)
|
||||||
|
return batch
|
||||||
|
|
||||||
|
|
||||||
|
async def get_batch(db: AsyncSession, batch_id: uuid.UUID) -> ImpBatch | None:
|
||||||
|
result = await db.execute(select(ImpBatch).where(ImpBatch.id == batch_id))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def list_batches(db: AsyncSession, study_id: uuid.UUID, product_id: uuid.UUID | None = None, status: str | None = None) -> Sequence[ImpBatch]:
|
||||||
|
stmt = select(ImpBatch).where(ImpBatch.study_id == study_id)
|
||||||
|
if product_id:
|
||||||
|
stmt = stmt.where(ImpBatch.product_id == product_id)
|
||||||
|
if status:
|
||||||
|
stmt = stmt.where(ImpBatch.status == status)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_batch(db: AsyncSession, batch: ImpBatch, batch_in: BatchUpdate) -> ImpBatch:
|
||||||
|
update_data = batch_in.model_dump(exclude_unset=True)
|
||||||
|
if update_data:
|
||||||
|
await db.execute(
|
||||||
|
sa_update(ImpBatch)
|
||||||
|
.where(ImpBatch.id == batch.id)
|
||||||
|
.values(**update_data)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(batch)
|
||||||
|
return batch
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.imp_inventory import ImpInventory
|
||||||
|
from app.models.imp_batch import ImpBatch
|
||||||
|
|
||||||
|
|
||||||
|
async def get_inventory_for_update(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID) -> ImpInventory | None:
|
||||||
|
result = await db.execute(
|
||||||
|
select(ImpInventory)
|
||||||
|
.where(
|
||||||
|
ImpInventory.study_id == study_id,
|
||||||
|
ImpInventory.site_id == site_id,
|
||||||
|
ImpInventory.batch_id == batch_id,
|
||||||
|
)
|
||||||
|
.with_for_update()
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_or_create_inventory(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID) -> ImpInventory:
|
||||||
|
inv = await get_inventory_for_update(db, study_id, site_id, batch_id)
|
||||||
|
if inv:
|
||||||
|
return inv
|
||||||
|
inv = ImpInventory(study_id=study_id, site_id=site_id, batch_id=batch_id, quantity_on_hand=0)
|
||||||
|
db.add(inv)
|
||||||
|
await db.flush()
|
||||||
|
return inv
|
||||||
|
|
||||||
|
|
||||||
|
async def list_inventory(
|
||||||
|
db: AsyncSession,
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
site_id: uuid.UUID | None = None,
|
||||||
|
product_id: uuid.UUID | None = None,
|
||||||
|
) -> Sequence[ImpInventory]:
|
||||||
|
stmt = select(ImpInventory).where(ImpInventory.study_id == study_id)
|
||||||
|
if site_id:
|
||||||
|
stmt = stmt.where(ImpInventory.site_id == site_id)
|
||||||
|
if product_id:
|
||||||
|
stmt = stmt.join(ImpBatch, ImpBatch.id == ImpInventory.batch_id).where(ImpBatch.product_id == product_id)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select, update as sa_update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.imp_product import ImpProduct
|
||||||
|
from app.schemas.imp import ProductCreate, ProductUpdate
|
||||||
|
|
||||||
|
|
||||||
|
async def create_product(db: AsyncSession, study_id: uuid.UUID, product_in: ProductCreate) -> ImpProduct:
|
||||||
|
product = ImpProduct(
|
||||||
|
study_id=study_id,
|
||||||
|
name=product_in.name,
|
||||||
|
form=product_in.form,
|
||||||
|
strength=product_in.strength,
|
||||||
|
unit=product_in.unit,
|
||||||
|
description=product_in.description,
|
||||||
|
is_active=product_in.is_active,
|
||||||
|
)
|
||||||
|
db.add(product)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(product)
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
async def get_product(db: AsyncSession, product_id: uuid.UUID) -> ImpProduct | None:
|
||||||
|
result = await db.execute(select(ImpProduct).where(ImpProduct.id == product_id))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def list_products(db: AsyncSession, study_id: uuid.UUID, is_active: bool | None = None) -> Sequence[ImpProduct]:
|
||||||
|
stmt = select(ImpProduct).where(ImpProduct.study_id == study_id)
|
||||||
|
if is_active is not None:
|
||||||
|
stmt = stmt.where(ImpProduct.is_active == is_active)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_product(db: AsyncSession, product: ImpProduct, product_in: ProductUpdate) -> ImpProduct:
|
||||||
|
update_data = product_in.model_dump(exclude_unset=True)
|
||||||
|
if update_data:
|
||||||
|
await db.execute(
|
||||||
|
sa_update(ImpProduct)
|
||||||
|
.where(ImpProduct.id == product.id)
|
||||||
|
.values(**update_data)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(product)
|
||||||
|
return product
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.imp_transaction import ImpTransaction
|
||||||
|
|
||||||
|
|
||||||
|
async def create_tx(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
site_id: uuid.UUID,
|
||||||
|
batch_id: uuid.UUID,
|
||||||
|
subject_id: uuid.UUID | None,
|
||||||
|
tx_type: str,
|
||||||
|
quantity: int,
|
||||||
|
tx_date: date,
|
||||||
|
reference: str | None,
|
||||||
|
notes: str | None,
|
||||||
|
created_by: uuid.UUID,
|
||||||
|
) -> ImpTransaction:
|
||||||
|
tx = ImpTransaction(
|
||||||
|
study_id=study_id,
|
||||||
|
site_id=site_id,
|
||||||
|
batch_id=batch_id,
|
||||||
|
subject_id=subject_id,
|
||||||
|
tx_type=tx_type,
|
||||||
|
quantity=quantity,
|
||||||
|
tx_date=tx_date,
|
||||||
|
reference=reference,
|
||||||
|
notes=notes,
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
db.add(tx)
|
||||||
|
await db.flush()
|
||||||
|
return tx
|
||||||
|
|
||||||
|
|
||||||
|
async def list_txs(
|
||||||
|
db: AsyncSession,
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
*,
|
||||||
|
site_id: uuid.UUID | None = None,
|
||||||
|
batch_id: uuid.UUID | None = None,
|
||||||
|
subject_id: uuid.UUID | None = None,
|
||||||
|
tx_type: str | None = None,
|
||||||
|
date_from: date | None = None,
|
||||||
|
date_to: date | None = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
) -> Sequence[ImpTransaction]:
|
||||||
|
stmt = select(ImpTransaction).where(ImpTransaction.study_id == study_id)
|
||||||
|
if site_id:
|
||||||
|
stmt = stmt.where(ImpTransaction.site_id == site_id)
|
||||||
|
if batch_id:
|
||||||
|
stmt = stmt.where(ImpTransaction.batch_id == batch_id)
|
||||||
|
if subject_id:
|
||||||
|
stmt = stmt.where(ImpTransaction.subject_id == subject_id)
|
||||||
|
if tx_type:
|
||||||
|
stmt = stmt.where(ImpTransaction.tx_type == tx_type)
|
||||||
|
if date_from:
|
||||||
|
stmt = stmt.where(ImpTransaction.tx_date >= date_from)
|
||||||
|
if date_to:
|
||||||
|
stmt = stmt.where(ImpTransaction.tx_date <= date_to)
|
||||||
|
stmt = stmt.offset(skip).limit(limit)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
@@ -16,3 +16,7 @@ from app.models.ae import AdverseEvent # noqa: F401
|
|||||||
from app.models.issue import Issue # noqa: F401
|
from app.models.issue import Issue # noqa: F401
|
||||||
from app.models.data_query import DataQuery # noqa: F401
|
from app.models.data_query import DataQuery # noqa: F401
|
||||||
from app.models.verification import VerificationProgress # noqa: F401
|
from app.models.verification import VerificationProgress # noqa: F401
|
||||||
|
from app.models.imp_product import ImpProduct # noqa: F401
|
||||||
|
from app.models.imp_batch import ImpBatch # noqa: F401
|
||||||
|
from app.models.imp_inventory import ImpInventory # noqa: F401
|
||||||
|
from app.models.imp_transaction import ImpTransaction # noqa: F401
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, ForeignKey, String, UniqueConstraint, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ImpBatch(Base):
|
||||||
|
__tablename__ = "imp_batches"
|
||||||
|
__table_args__ = (UniqueConstraint("study_id", "batch_no", "product_id", name="uq_imp_batch_no"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False)
|
||||||
|
product_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("imp_products.id"), index=True, nullable=False)
|
||||||
|
batch_no: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
expiry_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
manufacture_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="ACTIVE")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ImpInventory(Base):
|
||||||
|
__tablename__ = "imp_inventory"
|
||||||
|
__table_args__ = (UniqueConstraint("study_id", "site_id", "batch_id", name="uq_imp_inventory_site_batch"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False)
|
||||||
|
site_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=False)
|
||||||
|
batch_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("imp_batches.id"), index=True, nullable=False)
|
||||||
|
quantity_on_hand: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ImpProduct(Base):
|
||||||
|
__tablename__ = "imp_products"
|
||||||
|
__table_args__ = (UniqueConstraint("study_id", "name", name="uq_imp_product_name"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
form: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
strength: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
unit: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, ForeignKey, Integer, String, Text, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ImpTransaction(Base):
|
||||||
|
__tablename__ = "imp_transactions"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False)
|
||||||
|
site_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=False)
|
||||||
|
batch_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("imp_batches.id"), index=True, nullable=False)
|
||||||
|
subject_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("subjects.id"), nullable=True)
|
||||||
|
tx_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||||
|
quantity: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
tx_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||||
|
reference: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
form: Optional[str] = None
|
||||||
|
strength: Optional[str] = None
|
||||||
|
unit: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class ProductUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
form: Optional[str] = None
|
||||||
|
strength: Optional[str] = None
|
||||||
|
unit: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProductRead(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
study_id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
form: Optional[str]
|
||||||
|
strength: Optional[str]
|
||||||
|
unit: str
|
||||||
|
description: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchCreate(BaseModel):
|
||||||
|
product_id: uuid.UUID
|
||||||
|
batch_no: str
|
||||||
|
expiry_date: Optional[date] = None
|
||||||
|
manufacture_date: Optional[date] = None
|
||||||
|
|
||||||
|
|
||||||
|
class BatchUpdate(BaseModel):
|
||||||
|
status: Optional[str] = None
|
||||||
|
expiry_date: Optional[date] = None
|
||||||
|
manufacture_date: Optional[date] = None
|
||||||
|
|
||||||
|
|
||||||
|
class BatchRead(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
study_id: uuid.UUID
|
||||||
|
product_id: uuid.UUID
|
||||||
|
batch_no: str
|
||||||
|
expiry_date: Optional[date]
|
||||||
|
manufacture_date: Optional[date]
|
||||||
|
status: str
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryRead(BaseModel):
|
||||||
|
site_id: uuid.UUID
|
||||||
|
batch_id: uuid.UUID
|
||||||
|
quantity_on_hand: int
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TxCreate(BaseModel):
|
||||||
|
site_id: uuid.UUID
|
||||||
|
batch_id: uuid.UUID
|
||||||
|
subject_id: Optional[uuid.UUID] = None
|
||||||
|
tx_type: str
|
||||||
|
quantity: int = Field(gt=0)
|
||||||
|
tx_date: date
|
||||||
|
reference: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class TxRead(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
study_id: uuid.UUID
|
||||||
|
site_id: uuid.UUID
|
||||||
|
batch_id: uuid.UUID
|
||||||
|
subject_id: Optional[uuid.UUID]
|
||||||
|
tx_type: str
|
||||||
|
quantity: int
|
||||||
|
tx_date: date
|
||||||
|
reference: Optional[str]
|
||||||
|
notes: Optional[str]
|
||||||
|
created_by: uuid.UUID
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.crud import imp_inventory as inventory_crud
|
||||||
|
from app.crud import imp_tx as tx_crud
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.models.imp_batch import ImpBatch
|
||||||
|
from app.models.site import Site
|
||||||
|
from app.models.subject import Subject
|
||||||
|
|
||||||
|
TxType = Literal["RECEIPT", "DISPENSE", "RETURN", "RECONCILE", "DESTROY", "TRANSFER_IN", "TRANSFER_OUT"]
|
||||||
|
|
||||||
|
POSITIVE_TYPES = {"RECEIPT", "RETURN", "TRANSFER_IN"}
|
||||||
|
NEGATIVE_TYPES = {"DISPENSE", "DESTROY", "TRANSFER_OUT"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_entities(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID, subject_id: uuid.UUID | None):
|
||||||
|
site = await db.get(Site, site_id)
|
||||||
|
if not site or site.study_id != study_id:
|
||||||
|
raise ValueError("Site not found in study")
|
||||||
|
batch = await db.get(ImpBatch, batch_id)
|
||||||
|
if not batch or batch.study_id != study_id:
|
||||||
|
raise ValueError("Batch not found in study")
|
||||||
|
# batch status check could be extended
|
||||||
|
if subject_id:
|
||||||
|
subj = await db.get(Subject, subject_id)
|
||||||
|
if not subj or subj.study_id != study_id:
|
||||||
|
raise ValueError("Subject not found in study")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_transaction_and_apply_inventory(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
site_id: uuid.UUID,
|
||||||
|
batch_id: uuid.UUID,
|
||||||
|
subject_id: uuid.UUID | None,
|
||||||
|
tx_type: TxType,
|
||||||
|
quantity: int,
|
||||||
|
tx_date,
|
||||||
|
reference: str | None,
|
||||||
|
notes: str | None,
|
||||||
|
operator_id: uuid.UUID,
|
||||||
|
operator_role: str,
|
||||||
|
) -> tuple:
|
||||||
|
if quantity <= 0:
|
||||||
|
raise ValueError("Quantity must be positive")
|
||||||
|
await _validate_entities(db, study_id, site_id, batch_id, subject_id)
|
||||||
|
|
||||||
|
async with db.begin_nested():
|
||||||
|
inv = await inventory_crud.get_or_create_inventory(db, study_id, site_id, batch_id)
|
||||||
|
delta = quantity if tx_type in POSITIVE_TYPES else -quantity
|
||||||
|
new_qoh = inv.quantity_on_hand + delta
|
||||||
|
if new_qoh < 0:
|
||||||
|
raise ValueError("Insufficient inventory")
|
||||||
|
|
||||||
|
inv.quantity_on_hand = new_qoh
|
||||||
|
tx = await tx_crud.create_tx(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
site_id=site_id,
|
||||||
|
batch_id=batch_id,
|
||||||
|
subject_id=subject_id,
|
||||||
|
tx_type=tx_type,
|
||||||
|
quantity=quantity,
|
||||||
|
tx_date=tx_date,
|
||||||
|
reference=reference,
|
||||||
|
notes=notes,
|
||||||
|
created_by=operator_id,
|
||||||
|
)
|
||||||
|
await db.flush()
|
||||||
|
detail = f"{tx_type} {quantity} units batch {batch_id} at site {site_id}, QOH -> {new_qoh}"
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="imp_tx",
|
||||||
|
entity_id=tx.id,
|
||||||
|
action="CREATE_IMP_TX",
|
||||||
|
detail=detail,
|
||||||
|
operator_id=operator_id,
|
||||||
|
operator_role=operator_role,
|
||||||
|
)
|
||||||
|
return tx
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user