187 lines
8.9 KiB
Python
187 lines
8.9 KiB
Python
import hashlib
|
|
import json
|
|
from uuid import uuid4
|
|
|
|
from B08_wf5_Quantity.B08_wf5_Quantity_Schema_Quantity import (
|
|
DesignQuantity,
|
|
WorkBreakdown,
|
|
)
|
|
|
|
|
|
class QuantityRepository:
|
|
def __init__(self, connection):
|
|
self.db = connection
|
|
|
|
async def workspace(self, project_id: str) -> dict:
|
|
async with self.db.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT * FROM b08_work_breakdown WHERE project_id=%s ORDER BY level_no,sort_order",
|
|
(project_id,),
|
|
)
|
|
work_breakdown = list(await cursor.fetchall())
|
|
await cursor.execute(self._quantity_query(), (project_id,))
|
|
quantities = list(await cursor.fetchall())
|
|
await cursor.execute(
|
|
"""SELECT quantity_version, input_hash, confirmed_by, confirmed_at
|
|
FROM b08_quantity_confirmations WHERE project_id=%s
|
|
ORDER BY quantity_version DESC LIMIT 1""",
|
|
(project_id,),
|
|
)
|
|
confirmation = await cursor.fetchone()
|
|
return {
|
|
"work_breakdown": work_breakdown,
|
|
"quantities": quantities,
|
|
"confirmation": confirmation,
|
|
}
|
|
|
|
@staticmethod
|
|
def _quantity_query() -> str:
|
|
return """
|
|
SELECT q.*,
|
|
CASE q.reference_type
|
|
WHEN 'CATALOG' THEN IF(ci.cost_type='LABOR',ap.applied_price,0)
|
|
ELSE COALESCE(uc.labor_price,eq.labor_price,cb.labor_price,0)
|
|
END AS unit_labor,
|
|
CASE q.reference_type
|
|
WHEN 'CATALOG' THEN IF(ci.cost_type='MATERIAL',ap.applied_price,0)
|
|
ELSE COALESCE(uc.material_price,eq.material_price,cb.material_price,0)
|
|
END AS unit_material,
|
|
CASE q.reference_type
|
|
WHEN 'CATALOG' THEN IF(ci.cost_type='EXPENSE',ap.applied_price,0)
|
|
ELSE COALESCE(uc.expense_price,eq.expense_price,cb.expense_price,0)
|
|
END AS unit_expense,
|
|
pb.price_version
|
|
FROM b08_design_quantity_items q
|
|
LEFT JOIN b08_catalog_items ci
|
|
ON q.reference_type='CATALOG' AND ci.id=q.reference_id
|
|
LEFT JOIN b08_price_books pb
|
|
ON pb.project_id=q.project_id AND pb.status='CONFIRMED'
|
|
AND pb.effective_date=(
|
|
SELECT MAX(x.effective_date) FROM b08_price_books x
|
|
WHERE x.project_id=q.project_id AND x.status='CONFIRMED'
|
|
)
|
|
LEFT JOIN b08_applied_prices ap
|
|
ON ap.project_id=q.project_id AND ap.item_id=ci.id
|
|
AND ap.price_version=pb.price_version
|
|
LEFT JOIN b08_unit_costs uc
|
|
ON q.reference_type='UNIT_COST' AND uc.id=q.reference_id
|
|
AND uc.status='CONFIRMED'
|
|
LEFT JOIN b08_equipment_rates eq
|
|
ON q.reference_type='EQUIPMENT' AND eq.id=q.reference_id
|
|
AND eq.status='CONFIRMED'
|
|
LEFT JOIN b08_cost_basis cb
|
|
ON q.reference_type='COST_BASIS' AND cb.id=q.reference_id
|
|
AND cb.status='CONFIRMED'
|
|
WHERE q.project_id=%s
|
|
ORDER BY q.wbs_id,q.sort_order
|
|
"""
|
|
|
|
async def save_wbs(self, project_id: str, row: WorkBreakdown) -> str:
|
|
row_id = row.id or str(uuid4())
|
|
async with self.db.cursor() as cursor:
|
|
await cursor.execute(
|
|
"""INSERT INTO b08_work_breakdown(
|
|
id,project_id,parent_id,wbs_code,name,level_no,sort_order
|
|
) VALUES(%s,%s,%s,%s,%s,%s,%s)
|
|
ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id),
|
|
name=VALUES(name),level_no=VALUES(level_no),sort_order=VALUES(sort_order)""",
|
|
(row_id, project_id, row.parent_id, row.code, row.name,
|
|
row.level_no, row.sort_order),
|
|
)
|
|
await self.db.commit()
|
|
return row_id
|
|
|
|
async def save_quantity(
|
|
self, project_id: str, row: DesignQuantity, user_id: int | None
|
|
) -> str:
|
|
row_id = row.id or str(uuid4())
|
|
async with self.db.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT * FROM b08_design_quantity_items WHERE id=%s FOR UPDATE",
|
|
(row_id,),
|
|
)
|
|
before = await cursor.fetchone()
|
|
await cursor.execute(
|
|
"""INSERT INTO b08_design_quantity_items(
|
|
id,project_id,wbs_id,reference_type,reference_id,item_name,
|
|
specification,unit,design_quantity,adjusted_quantity,
|
|
confirmed_quantity,adjustment_reason,procurement_type,
|
|
excluded,status,sort_order,updated_by
|
|
) VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
ON DUPLICATE KEY UPDATE
|
|
wbs_id=VALUES(wbs_id),reference_type=VALUES(reference_type),
|
|
reference_id=VALUES(reference_id),item_name=VALUES(item_name),
|
|
specification=VALUES(specification),unit=VALUES(unit),
|
|
design_quantity=VALUES(design_quantity),
|
|
adjusted_quantity=VALUES(adjusted_quantity),
|
|
confirmed_quantity=NULL,adjustment_reason=VALUES(adjustment_reason),
|
|
procurement_type=VALUES(procurement_type),excluded=VALUES(excluded),
|
|
status=VALUES(status),sort_order=VALUES(sort_order),updated_by=VALUES(updated_by)""",
|
|
(row_id, project_id, row.wbs_id, row.reference_type, row.reference_id,
|
|
row.item_name, row.specification, row.unit, row.design_quantity,
|
|
row.adjusted_quantity, row.confirmed_quantity, row.adjustment_reason,
|
|
row.procurement_type, row.excluded, row.status, row.sort_order, user_id),
|
|
)
|
|
if before:
|
|
await cursor.execute(
|
|
"""SELECT COALESCE(MAX(revision_no),0)+1 AS n
|
|
FROM b08_quantity_revisions WHERE quantity_item_id=%s""",
|
|
(row_id,),
|
|
)
|
|
revision_no = (await cursor.fetchone())["n"]
|
|
await cursor.execute(
|
|
"""INSERT INTO b08_quantity_revisions(
|
|
quantity_item_id,revision_no,before_json,after_json,
|
|
change_reason,changed_by
|
|
) VALUES(%s,%s,%s,%s,%s,%s)""",
|
|
(row_id, revision_no,
|
|
json.dumps(before, default=str, ensure_ascii=False),
|
|
row.model_dump_json(), row.adjustment_reason or "수량 변경", user_id),
|
|
)
|
|
await self.db.commit()
|
|
return row_id
|
|
|
|
async def confirm(self, project_id: str, user_id: int | None) -> int:
|
|
async with self.db.cursor() as cursor:
|
|
await cursor.execute(
|
|
"""SELECT COUNT(*) AS missing FROM b08_design_quantity_items
|
|
WHERE project_id=%s AND design_quantity IS NULL""",
|
|
(project_id,),
|
|
)
|
|
missing = (await cursor.fetchone())["missing"]
|
|
if missing:
|
|
raise ValueError(f"수량 미입력 항목 {missing}건이 있습니다.")
|
|
await cursor.execute(self._quantity_query(), (project_id,))
|
|
rows = list(await cursor.fetchall())
|
|
unavailable = [row for row in rows if all(
|
|
row.get(key) in (None, 0) for key in ("unit_labor", "unit_material", "unit_expense")
|
|
)]
|
|
if unavailable:
|
|
raise ValueError(f"확정 적용단가가 없는 항목 {len(unavailable)}건이 있습니다.")
|
|
await cursor.execute(
|
|
"""UPDATE b08_design_quantity_items
|
|
SET confirmed_quantity=COALESCE(adjusted_quantity,design_quantity),
|
|
status='CONFIRMED',updated_by=%s
|
|
WHERE project_id=%s""",
|
|
(user_id, project_id),
|
|
)
|
|
snapshot = [
|
|
{"id": row["id"], "quantity": row["adjusted_quantity"] if row["adjusted_quantity"] is not None else row["design_quantity"]}
|
|
for row in sorted(rows, key=lambda item: item["id"])
|
|
]
|
|
payload = json.dumps(snapshot, default=str, sort_keys=True).encode()
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
await cursor.execute(
|
|
"""SELECT COALESCE(MAX(quantity_version),0)+1 AS version
|
|
FROM b08_quantity_confirmations WHERE project_id=%s""",
|
|
(project_id,),
|
|
)
|
|
version = (await cursor.fetchone())["version"]
|
|
await cursor.execute(
|
|
"""INSERT INTO b08_quantity_confirmations(
|
|
project_id,quantity_version,input_hash,confirmed_by
|
|
) VALUES(%s,%s,%s,%s)""",
|
|
(project_id, version, digest, user_id),
|
|
)
|
|
await self.db.commit()
|
|
return version |