From 9b02963d45e17229ae550b228271442d018de518 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 8 Dec 2025 15:46:11 +0700 Subject: [PATCH] add acquisition data CRUD API --- src/__pycache__/api.cpython-311.pyc | Bin 4463 -> 4677 bytes src/acquisition_cost/router.py | 95 ++++++++++++++ src/acquisition_cost/schema.py | 33 +++++ src/acquisition_cost/service.py | 119 ++++++++++++++++++ src/api.py | 5 + .../__pycache__/model.cpython-311.pyc | Bin 5550 -> 5772 bytes .../__pycache__/router.cpython-311.pyc | Bin 11536 -> 11559 bytes .../__pycache__/schema.cpython-311.pyc | Bin 12098 -> 13535 bytes .../__pycache__/service.cpython-311.pyc | Bin 17516 -> 18419 bytes src/equipment/router.py | 4 +- .../__pycache__/Prediksi.cpython-311.pyc | Bin 50755 -> 50532 bytes .../insert_actual_data.cpython-311.pyc | Bin 49368 -> 49467 bytes 12 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/acquisition_cost/router.py create mode 100644 src/acquisition_cost/schema.py create mode 100644 src/acquisition_cost/service.py diff --git a/src/__pycache__/api.cpython-311.pyc b/src/__pycache__/api.cpython-311.pyc index 11ddfb59859d92684da6398fac9deeb894f17905..71794e2ec8936178b22e192caf597a0ef7abf5be 100644 GIT binary patch delta 539 zcmaE_bX0|RIWI340}xpEnPvW+$ScXXWTN^?t`vq8_8i$<{wTSL=VTe>C%%!kS72gD zWl3dE;Yn4 z^NUM1_pzHXGwM%1%B9O?2sF0{M1)NK!}XL=WAY>JAV$;4raXJ}q>GD^_0aX|73G(f zq!!&0N0tCtGx;UYV@AWtXL)6X!+^$tTwJ_?nStR0Gb1D8jY)hWlLh(K33y&$KtY@L M@>w%YJ}a;Y0C%C0h5!Hn delta 443 zcmX@A@?ME|IWI340}vd{<;gUe$ScX1Hc@@$#5>Z_vP=x_3@KbK3@O~HT+5gl7*+!@ z1VqU(F{HAjvZwH*%4IU*BzfnsuHjq81k?${P@VFr@(UCuGcpNJPG!_!RGi$$Xw9mW zs)~aVVTRgi z%#fm-BCtjf)lDfXsVZqqDXJ|j%Ycqt4Rb0(lq%4zV0TF}q$(|A1j+$11TZqBum&?| zs%^f&tj5SFGWk2pc}9uJ$65a|YHU8n#?H9;6T2xhquyiTZlX-=wLQ817TvbDH)axdTM%~|}G KOq0(EmIDC5?_m}I diff --git a/src/acquisition_cost/router.py b/src/acquisition_cost/router.py new file mode 100644 index 0000000..86e201d --- /dev/null +++ b/src/acquisition_cost/router.py @@ -0,0 +1,95 @@ +from typing import Optional +from fastapi import APIRouter, HTTPException, status, Query + +from src.acquisition_cost.model import AcquisitionData +from src.acquisition_cost.schema import AcquisitionCostDataPagination, AcquisitionCostDataRead, AcquisitionCostDataCreate, AcquisitionCostDataUpdate +from src.acquisition_cost.service import get, get_all, create, update, delete + +from src.database.service import CommonParameters, search_filter_sort_paginate +from src.database.core import DbSession +from src.auth.service import CurrentUser +from src.models import StandardResponse + +router = APIRouter() + + +@router.get("", response_model=StandardResponse[AcquisitionCostDataPagination]) +async def get_yeardatas( + db_session: DbSession, + common: CommonParameters, + items_per_page: Optional[int] = Query(5), + search: Optional[str] = Query(None), +): + """Get all acquisition_cost_data pagination.""" + get_acquisition_cost_data = await get_all( + db_session=db_session, + items_per_page=items_per_page, + search=search, + common=common, + ) + # return + return StandardResponse( + data=get_acquisition_cost_data, + message="Data retrieved successfully", + ) + + +@router.get("/{acquisition_cost_data_id}", response_model=StandardResponse[AcquisitionCostDataRead]) +async def get_acquisition_cost_data(db_session: DbSession, acquisition_cost_data_id: str): + acquisition_cost_data = await get(db_session=db_session, acquisition_cost_data_id=acquisition_cost_data_id) + if not acquisition_cost_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="A data with this id does not exist.", + ) + + return StandardResponse(data=acquisition_cost_data, message="Data retrieved successfully") + + +@router.post("", response_model=StandardResponse[AcquisitionCostDataRead]) +async def create_acquisition_cost_data( + db_session: DbSession, acquisition_cost_data_in: AcquisitionCostDataCreate, current_user: CurrentUser +): + acquisition_cost_data_in.created_by = current_user.name + acquisition_cost_data = await create(db_session=db_session, acquisition_data_in=acquisition_cost_data_in) + + return StandardResponse(data=acquisition_cost_data, message="Data created successfully") + + +@router.put("/{acquisition_cost_data_id}", response_model=StandardResponse[AcquisitionCostDataRead]) +async def update_acquisition_cost_data( + db_session: DbSession, + acquisition_cost_data_id: str, + acquisition_cost_data_in: AcquisitionCostDataUpdate, + current_user: CurrentUser, +): + acquisition_cost_data = await get(db_session=db_session, acquisition_cost_data_id=acquisition_cost_data_id) + + if not acquisition_cost_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="A data with this id does not exist.", + ) + acquisition_cost_data_in.updated_by = current_user.name + + return StandardResponse( + data=await update( + db_session=db_session, acquisition_data=acquisition_cost_data, acquisition_data_in=acquisition_cost_data_in + ), + message="Data updated successfully", + ) + + +@router.delete("/{acquisition_cost_data_id}", response_model=StandardResponse[AcquisitionCostDataRead]) +async def delete_acquisition_cost_data(db_session: DbSession, acquisition_cost_data_id: str): + acquisition_cost_data = await get(db_session=db_session, acquisition_cost_data_id=acquisition_cost_data_id) + + if not acquisition_cost_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=[{"msg": "A data with this id does not exist."}], + ) + + await delete(db_session=db_session, acquisition_cost_data_id=acquisition_cost_data_id) + + return StandardResponse(message="Data deleted successfully", data=acquisition_cost_data) diff --git a/src/acquisition_cost/schema.py b/src/acquisition_cost/schema.py new file mode 100644 index 0000000..4c80f8a --- /dev/null +++ b/src/acquisition_cost/schema.py @@ -0,0 +1,33 @@ +from datetime import datetime +from typing import List, Optional +from uuid import UUID + +from pydantic import Field +from src.models import DefaultBase, Pagination + + +class AcquisitionCostDataBase(DefaultBase): + category_no: Optional[str] = Field(None, nullable=True) + name: Optional[str] = Field(None, nullable=True) + cost_unit_3_n_4: Optional[float] = Field(None, nullable=True) + cost_unit_3: Optional[float] = Field(None, nullable=True) + created_at: Optional[datetime] = Field(None, nullable=True) + updated_at: Optional[datetime] = Field(None, nullable=True) + created_by: Optional[str] = Field(None, nullable=True) + updated_by: Optional[str] = Field(None, nullable=True) + + +class AcquisitionCostDataCreate(AcquisitionCostDataBase): + pass + + +class AcquisitionCostDataUpdate(AcquisitionCostDataBase): + pass + + +class AcquisitionCostDataRead(AcquisitionCostDataBase): + id: UUID + + +class AcquisitionCostDataPagination(Pagination): + items: List[AcquisitionCostDataRead] = [] diff --git a/src/acquisition_cost/service.py b/src/acquisition_cost/service.py new file mode 100644 index 0000000..a41dcf0 --- /dev/null +++ b/src/acquisition_cost/service.py @@ -0,0 +1,119 @@ +from sqlalchemy import Select, Delete, cast, String +from src.acquisition_cost.model import AcquisitionData +from src.acquisition_cost.schema import AcquisitionCostDataCreate, AcquisitionCostDataUpdate +from src.database.service import search_filter_sort_paginate +from typing import Optional + +from src.database.core import DbSession +from src.auth.service import CurrentUser +from src.equipment.model import Equipment + + +def _calculate_cost_unit_3(cost_unit_3_n_4: Optional[float]) -> Optional[float]: + """Derive cost_unit_3 by splitting the combined unit 3&4 cost evenly.""" + if cost_unit_3_n_4 is None: + return None + return cost_unit_3_n_4 / 2 + + +async def _sync_equipment_acquisition_costs( + *, db_session: DbSession, category_no: Optional[str], cost_unit_3: Optional[float] +): + """Keep equipment acquisition cost in sync for the affected category.""" + if not category_no or cost_unit_3 is None: + return + + equipment_query = Select(Equipment).filter(Equipment.category_no == category_no) + equipment_result = await db_session.execute(equipment_query) + equipments = equipment_result.scalars().all() + + for equipment in equipments: + if equipment.proportion is None: + continue + equipment.acquisition_cost = (equipment.proportion * 0.01) * cost_unit_3 + + +async def get(*, db_session: DbSession, acquisition_cost_data_id: str) -> Optional[AcquisitionData]: + """Returns a document based on the given document id.""" + query = Select(AcquisitionData).filter(AcquisitionData.id == acquisition_cost_data_id) + result = await db_session.execute(query) + return result.scalars().one_or_none() + + +async def get_all( + *, + db_session: DbSession, + items_per_page: Optional[int], + search: Optional[str] = None, + common, +): + """Returns all documents.""" + query = Select(AcquisitionData).order_by(AcquisitionData.name.asc()) + if search: + query = query.filter(cast(AcquisitionData.name, String).ilike(f"%{search}%")) + + common["items_per_page"] = items_per_page + results = await search_filter_sort_paginate(model=query, **common) + + # return results.scalars().all() + return results + + +async def create(*, db_session: DbSession, acquisition_data_in: AcquisitionCostDataCreate): + """Creates a new document.""" + data = acquisition_data_in.model_dump() + cost_unit_changed = False + + if data.get("cost_unit_3_n_4") is not None: + derived_cost_unit = _calculate_cost_unit_3(data["cost_unit_3_n_4"]) + data["cost_unit_3"] = derived_cost_unit + cost_unit_changed = derived_cost_unit is not None + + acquisition_data = AcquisitionData(**data) + db_session.add(acquisition_data) + + if cost_unit_changed: + await _sync_equipment_acquisition_costs( + db_session=db_session, + category_no=acquisition_data.category_no, + cost_unit_3=acquisition_data.cost_unit_3, + ) + + await db_session.commit() + return acquisition_data + + +async def update( + *, db_session: DbSession, acquisition_data: AcquisitionData, acquisition_data_in: AcquisitionCostDataUpdate +): + """Updates a document.""" + data = acquisition_data_in.model_dump() + update_data = acquisition_data_in.model_dump(exclude_defaults=True) + + cost_unit_changed = False + if "cost_unit_3_n_4" in update_data: + derived_cost_unit = _calculate_cost_unit_3(update_data.get("cost_unit_3_n_4")) + update_data["cost_unit_3"] = derived_cost_unit + cost_unit_changed = derived_cost_unit is not None + + for field in data: + if field in update_data: + setattr(acquisition_data, field, update_data[field]) + + if cost_unit_changed: + await _sync_equipment_acquisition_costs( + db_session=db_session, + category_no=acquisition_data.category_no, + cost_unit_3=acquisition_data.cost_unit_3, + ) + + await db_session.commit() + + return acquisition_data + + +async def delete(*, db_session: DbSession, acquisition_cost_data_id: str): + """Deletes a document.""" + query = Delete(AcquisitionData).where(AcquisitionData.id == acquisition_cost_data_id) + await db_session.execute(query) + await db_session.commit() diff --git a/src/api.py b/src/api.py index 3f33702..e9b38c1 100644 --- a/src/api.py +++ b/src/api.py @@ -12,6 +12,7 @@ from src.masterdata.router import router as masterdata_router from src.plant_masterdata.router import router as plant_masterdata from src.plant_transaction_data.router import router as plant_transaction_data from src.equipment.router import router as equipment_router +from src.acquisition_cost.router import router as acquisition_data_router from src.yeardata.router import router as yeardata_router from src.equipment_master.router import router as equipment_master_router from src.uploaded_file.router import router as uploaded_file_router @@ -72,6 +73,10 @@ authenticated_api_router.include_router( equipment_master_router, prefix="/equipment-master", tags=["equipment_master"] ) +authenticated_api_router.include_router( + acquisition_data_router, prefix="/acquisition-data", tags=["acquisition_cost"] +) + authenticated_api_router.include_router( yeardata_router, prefix="/yeardata", tags=["yeardata"] ) diff --git a/src/equipment/__pycache__/model.cpython-311.pyc b/src/equipment/__pycache__/model.cpython-311.pyc index a8e1524526e71999948e69510cbe9a2f382bbad8..7b84e7eadfeddd9a9bf947b2f980ec3ac0a2ee1d 100644 GIT binary patch delta 390 zcmZ3d-J{F5oR^o20SF9=%`#ntHu4!UGFdTgc4FMjIJuZfj8S580h45?Bojj_ODby$ z%NpioObiUGffxd!q`)$qSY)JuGBPQwDQqe1DJ&^WYdDs%0(Am0RHy7@A?EFja+^;v z$1zU+$!{-ti#s{7BsD$1s4_k;{}xw4QGP*wQAuWg-ee8d2*#_M8(E_mCw~{#(zqcc zF+pvH*b0>kLi!hl^sfl%H+bI=6rUhAgLMVV1wq}5g1T1(bsM}U-)7TgG}_F`{((cK zNC#+bkuHcZ1QA9c!WcxD!U)aHK?2v9*}yWUll4R@HNaA+%71a#&AN^yL}ZJ}_Vs6IdqXEb#pZlKFy=b^(U2s5uMBE&v_HUAh1O delta 220 zcmeCtU8l{roR^o20SF|w=w&7fZsap!WV*z-*@_OYM!n6_93MC)Phho|D$)fSQltkW^g)Cnj8NUIFL;fa4J>0g znOn3{11yEA{1=B!ZhlH>PO4o|Do`UM5EoycyhgN4PnwbK0|O>8fn`F@0^g4ynJ)-w Mo5_J<<}4tq0DQMIA^-pY diff --git a/src/equipment/__pycache__/router.cpython-311.pyc b/src/equipment/__pycache__/router.cpython-311.pyc index e6f5bd1a960a844af536f82f6bdfccdd5550939d..a8404e004ee015c873efd4fc4173996f592b55da 100644 GIT binary patch delta 2231 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 diff --git a/src/equipment/__pycache__/schema.cpython-311.pyc b/src/equipment/__pycache__/schema.cpython-311.pyc index 2cb302b84241bcad38030e97da0f92f6b981341d..2b051e61ee03552f71bed49bb44e643debd2d633 100644 GIT binary patch delta 1894 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 WzPlgf-@5`3i$IL99ecV=XzTx#Ky#M> delta 2888 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 XVQ&?aL?R=j><0!6(qpsG0Zm2#xHh1(