From 9f49cf34b2ce179b5c4abe1b5efeb0abab208709 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 4 Feb 2025 16:17:46 +0700 Subject: [PATCH] feat: update bulk on master data --- .../__pycache__/router.cpython-311.pyc | Bin 4644 -> 6349 bytes .../__pycache__/schema.cpython-311.pyc | Bin 3129 -> 3413 bytes .../__pycache__/service.cpython-311.pyc | Bin 3667 -> 10445 bytes src/masterdata/router.py | 40 ++++- src/masterdata/schema.py | 4 + src/masterdata/service.py | 165 +++++++++++++++++- 6 files changed, 201 insertions(+), 8 deletions(-) diff --git a/src/masterdata/__pycache__/router.cpython-311.pyc b/src/masterdata/__pycache__/router.cpython-311.pyc index 2d6a93fed82dbb4c4e249e6935137441dcf060cd..c59cd02df7a8f5750d749b868925542a4393f12c 100644 GIT binary patch literal 6349 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 delta 1721 zcmbVM%WoS+7@ze%yk5T_Yden9IDMr~p(Hev@=#T1RHC9Jh^7e5#WLP;YH6o2vks^n z2#5XwR3m}PL*jr^RYhEadO|`-T#*blo4viGO0>qP}xNCdG8)oDvGnAx_ zp`{DND}kltXv1Dh>L8a*b`7t5!`PGB6zCkr)Lx5H`!AgeOLssE}KCL65briK; zKd>jS!qQ65_Gr zl>MtQ1F<-X@DxDvu}#zxDE)z4N{?@aq{c0-*9YmYt4KMPZ5j{qqS12nCF z^;p1pY_pgJ*j4hSV#PTJ)Z0jZB3JCE<>y7dISpZRI*a|vV8E*^xj*28g3L%Yi@qmf2=^)p-I diff --git a/src/masterdata/__pycache__/schema.cpython-311.pyc b/src/masterdata/__pycache__/schema.cpython-311.pyc index 0dca9795e0d9f5919fab6922ec5fe1081d238312..dd4ecf919ae36e93ca2327c53dcd55bd693c2b57 100644 GIT binary patch delta 403 zcmdlfaaD?UIWI340}yPuxG??GL|#e8e-qU&81pV;W?)zi#1IfA$i(2zkRsT^kRp`I zg(fGIA{@-1DYBW5aW{Lt8c@S@hE#?q#uSDqrcj1d=2R9*m@y2gtSO8(Kwb(HPzUoZ z5vS6eY~RGIv%kO9%BHhBX_C6^XRH;B-j zY|N?7s09?1oLs`0z^w<80NGlkHCd2Tf=d^~1QEKE8M#c^K`cK_;mNjK#cW_RbSH1* olI1c13Nr$6ar5RITuh8=5+4{C8PSOub{|2aU*J-cuW=^>0HLZ>k^lez delta 175 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 R457BcXT1l1zLsD?_#b0NJ%0cI delta 1112 zcmZWn%WD%s7~knW^4Q%p*)(bG!&KWeO)N!354Pg7da&S=id3YonQ4t|nmW7jfsxh+ zA_c`V2M>zWlNSY{7ykthLRt&TiV$yJlt3@)$!{hKRwwy>Gv9B&`OSRad~c7x>x?`% z&9DTcH84N*GO!S7YpO{L{fB0ege4qsjG{SiignP56r0DJVI3kS4wER>iB7Z)e6tvn z8aD1I<1uXF$P*-@nu&#@A~L=%GKgb$wDA@ZW0Bybg5$6=A^aAiVT+_fAeR6#`R6zY zN`l{t&685Lm7PE{8RQ3#&Ul5=wC!Y(9~wD-lDICo;OGa+l#*$0%q4WGOIbu4O}HeD z0Ql;qGNIRejS{!)c&-9mH#;T|hvg~aQJ#>&zNwd*)+ANzSwaxEesW7_^N?1VDMhb{yIZUYyCndDPAx1izwgRtfcO z6?$nN7%tkbM<}*E+m#z44FxSvi49-20J({+gf8~9!;0nr@?c`{4FRy*U~4M?%nqOK zy`fa(5$SB#4Yi_Hl*@2Kj|cu96~tNgG2EqYtVoMZY&p@wX3bbkzAm4V9?5qVBvlZU zTw%}5*@cfjIcTa%qhix{d3Ax%UCS!0+6gR1ZqVhWC2Uir%xVr&sm#+(^wzy|;2JR_^sk z)ygef`&O-ebECCbqAoS5!vgN9AN1(M(W;)RODHt_$%sEZyr?c-f3;&Zvvb)PS~Z5M z#?bc~dz0?bYkH!pBq%R1+p8p(26{EL-&e0(Ae2xGSQ_UbvXmQH^2QSH|Ie#o;t+VX z9q@UG(ezD?i)P!{kMuT_Vgr4dSwH5|{2;H!dD|s}`4XI=Ly#Zkyj}J#3_=fIF60R{ zI4!$SVBVy@T$uKJ#VG*6+e);CD495pc5_ZN32(ul!Cl1?E<0p5-2*NHb8i8%&wXd2 t9aU-FpPk6`vH8s7fe1=|hW diff --git a/src/masterdata/router.py b/src/masterdata/router.py index 6307275..f10682d 100644 --- a/src/masterdata/router.py +++ b/src/masterdata/router.py @@ -1,14 +1,16 @@ -from typing import Optional +from typing import Optional, List from fastapi import APIRouter, HTTPException, status, Query +from sqlalchemy import Select from .model import MasterData from .schema import ( MasterDataPagination, MasterDataRead, MasterDataCreate, MasterDataUpdate, + BulkMasterDataUpdate, ) -from .service import get, get_all, create, update, delete +from .service import get, get_all, create, update, bulk_update, delete from src.database.service import CommonParameters, search_filter_sort_paginate from src.database.core import DbSession @@ -17,7 +19,6 @@ from src.models import StandardResponse router = APIRouter() - @router.get("", response_model=StandardResponse[MasterDataPagination]) async def get_masterdatas( db_session: DbSession, @@ -61,6 +62,39 @@ async def create_masterdata( return StandardResponse(data=masterdata, message="Data created successfully") +@router.put("/bulk", response_model=StandardResponse[List[MasterDataRead]]) +async def update_masterdata( + db_session: DbSession, + data: BulkMasterDataUpdate, + current_user: CurrentUser, +): + # Extract IDs and updates + updates = [] + ids = [] + + for item in data.updates: + masterdata_id = item.pop("id") # remove id from update data + # Create MasterDataUpdate object with remaining data + update = MasterDataUpdate(**item, updated_by=current_user.name) + updates.append(update) + ids.append(masterdata_id) + + # Verify all records exist + query = Select(MasterData).where(MasterData.id.in_(ids)) + result = await db_session.execute(query) + + existing_records = result.scalars().all() + if len(existing_records) != len(ids): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Some records do not exist", + ) + + return StandardResponse( + data=await bulk_update(db_session=db_session, updates=updates, ids=ids), + message="Data updated successfully", + ) + @router.put("/{masterdata_id}", response_model=StandardResponse[MasterDataRead]) async def update_masterdata( db_session: DbSession, diff --git a/src/masterdata/schema.py b/src/masterdata/schema.py index 57fb0df..33b1dbb 100644 --- a/src/masterdata/schema.py +++ b/src/masterdata/schema.py @@ -34,6 +34,10 @@ class MasterDataUpdate(MasterdataBase): pass +class BulkMasterDataUpdate(MasterdataBase): + updates: List[dict] # each dict contains id and update data + + class MasterDataRead(MasterdataBase): id: UUID diff --git a/src/masterdata/service.py b/src/masterdata/service.py index 4f3e9d4..3e6cd39 100644 --- a/src/masterdata/service.py +++ b/src/masterdata/service.py @@ -3,12 +3,28 @@ from sqlalchemy import Select, Delete from src.database.service import search_filter_sort_paginate from .model import MasterData from .schema import MasterDataCreate, MasterDataUpdate -from typing import Optional +from typing import Optional, List from src.database.core import DbSession from src.auth.service import CurrentUser +def calculate_pmt(rate, nper, pv): + """ + rate: interest rate per period + nper: total number of payment periods + pv: present value (loan amount) + """ + # Convert percentage to decimal if needed (e.g., 5% to 0.05) + rate = float(rate) / 100 if rate > 1 else float(rate) + + # PMT formula: PMT = PV * (r * (1 + r)^n) / ((1 + r)^n - 1) + if rate == 0: + return -pv / nper + else: + return -pv * (rate * (1 + rate) ** nper) / ((1 + rate) ** nper - 1) + + async def get(*, db_session: DbSession, masterdata_id: str) -> Optional[MasterData]: """Returns a document based on the given document id.""" query = Select(MasterData).filter(MasterData.id == masterdata_id) @@ -43,16 +59,155 @@ async def update( ): """Updates a document.""" data = masterdata_in.model_dump() - update_data = masterdata_in.model_dump(exclude_defaults=True) - for field in data: - if field in update_data: + def get_value(data_list, name): + return next((m.value_num for m in data_list if m.name == name), 0) + + # First update the direct values from update_data + for field in update_data: + setattr(masterdata, field, update_data[field]) + + # Then check which formulas need to be recalculated based on updated fields + if "loan_portion" in update_data: + # Update equity_portion when loan_portion changes + equity_portion = 100 - get_value(masterdata, "loan_portion") + setattr(masterdata, "equity_portion", equity_portion) + + # Update loan amount when loan_portion changes + total_project_cost = get_value(masterdata, "total_project_cost") + loan = total_project_cost * (get_value(masterdata, "loan_portion") / 100) + setattr(masterdata, "loan", loan) + + # Update equity when loan_portion changes + equity = total_project_cost * (equity_portion / 100) + setattr(masterdata, "equity", equity) + + if any(field in update_data for field in ["loan", "interest_rate", "loan_tenor"]): + # Recalculate PMT when loan, interest_rate, or loan_tenor changes + pmt = calculate_pmt( + rate=get_value(masterdata, "interest_rate"), + nper=get_value(masterdata, "loan_tenor"), + pv=get_value(masterdata, "loan"), + ) + setattr(masterdata, "principal_interest_payment", pmt) + + if any( + field in update_data + for field in [ + "loan_portion", + "interest_rate", + "corporate_tax_rate", + "wacc_on_equity", + "equity_portion", + ] + ): + # Recalculate WACC when any of its components change + wacc = ( + get_value(masterdata, "loan_portion") + * ( + get_value(masterdata, "interest_rate") + * (1 - get_value(masterdata, "corporate_tax_rate")) + ) + ) + ( + get_value(masterdata, "wacc_on_equity") + * get_value(masterdata, "equity_portion") + ) + setattr(masterdata, "wacc_on_project", wacc) + + await db_session.commit() + return masterdata + + +async def bulk_update( + *, db_session: DbSession, updates: List[MasterDataUpdate], ids: List[str] +) -> List[MasterData]: + """ + Performs bulk update on multiple MasterData records. + + Args: + db_session: Database session + updates: List of MasterDataUpdate objects containing the updates + ids: List of MasterData IDs to update + + Returns: + List of updated MasterData objects + """ + # Fetch all records to be updated in one query + query = Select(MasterData).where(MasterData.id.in_(ids)) + result = await db_session.execute(query) + records = result.scalars().all() + + # Create a mapping of id to record for easier access + records_map = {record.id: record for record in records} + + # Process updates in batches + updated_records = [] + for masterdata_id, masterdata_in in zip(ids, updates): + masterdata = records_map.get(masterdata_id) + if not masterdata: + continue + + data = masterdata_in.model_dump() + update_data = masterdata_in.model_dump(exclude_defaults=True) + + def get_value(obj, name): + return next((m.value_num for m in obj if m.name == name), 0) + + # Update direct values + for field in update_data: setattr(masterdata, field, update_data[field]) + # Handle interdependent calculations + if "loan_portion" in update_data: + equity_portion = 100 - get_value(masterdata, "loan_portion") + setattr(masterdata, "equity_portion", equity_portion) + + total_project_cost = get_value(masterdata, "total_project_cost") + loan = total_project_cost * (get_value(masterdata, "loan_portion") / 100) + setattr(masterdata, "loan", loan) + + equity = total_project_cost * (equity_portion / 100) + setattr(masterdata, "equity", equity) + + if any( + field in update_data for field in ["loan", "interest_rate", "loan_tenor"] + ): + pmt = calculate_pmt( + rate=get_value(masterdata, "interest_rate"), + nper=get_value(masterdata, "loan_tenor"), + pv=get_value(masterdata, "loan"), + ) + setattr(masterdata, "principal_interest_payment", pmt) + + if any( + field in update_data + for field in [ + "loan_portion", + "interest_rate", + "corporate_tax_rate", + "wacc_on_equity", + "equity_portion", + ] + ): + wacc = ( + get_value(masterdata, "loan_portion") + * ( + get_value(masterdata, "interest_rate") + * (1 - get_value(masterdata, "corporate_tax_rate")) + ) + ) + ( + get_value(masterdata, "wacc_on_equity") + * get_value(masterdata, "equity_portion") + ) + setattr(masterdata, "wacc_on_project", wacc) + + updated_records.append(masterdata) + + # Commit all changes in a single transaction await db_session.commit() - return masterdata + return updated_records async def delete(*, db_session: DbSession, masterdata_id: str):