"""成本分析模块测试 — 标准成本/实际成本CRUD + ABC + 看板""" import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from tests.conftest import create_test_user, get_token_for_user, auth_header from app.models import StandardCost, ActualCost, AbcActivity class TestStandardCost: """标准成本卡片CRUD测试""" BASE = "/api/cma/cost" def test_list_standard_costs_empty(self, client: TestClient, db: Session): """空列表""" create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{self.BASE}/standard-costs", headers=auth_header(token)) assert resp.status_code == 200 assert resp.json()["data"] == [] def test_create_standard_cost(self, client: TestClient, db: Session): """创建标准成本卡片""" create_test_user(db) token = get_token_for_user(client) resp = client.post( f"{self.BASE}/standard-costs", headers=auth_header(token), json={ "product_code": "PROD_A", "product_name": "产品A", "cost_type": "material", "item_name": "原材料X", "standard_quantity": 10.0, "standard_price": 5.0, "unit": "kg", }, ) assert resp.status_code == 200 assert resp.json()["message"] == "标准成本已创建" # 验证列表中有数据 list_resp = client.get(f"{self.BASE}/standard-costs", headers=auth_header(token)) assert len(list_resp.json()["data"]) == 1 def test_create_standard_cost_default_client_delivery(self, client: TestClient, db: Session): """未传归集维度 → 默认 client_delivery/research_flag=0(录入默认,客户交付)""" create_test_user(db) token = get_token_for_user(client) resp = client.post( f"{self.BASE}/standard-costs", headers=auth_header(token), json={ "product_code": "PROD_DEF", "product_name": "默认产品", "cost_type": "labor", "item_name": "人工", "standard_quantity": 1.0, "standard_price": 100.0, }, ) assert resp.status_code == 200 row = db.query(StandardCost).filter(StandardCost.product_code == "PROD_DEF").first() assert row is not None assert row.project_type == "client_delivery" assert row.research_flag == 0 def test_create_standard_cost_platform_dev_research_flag_linked(self, client: TestClient, db: Session): """project_type=platform_dev → research_flag 自动联动为 1(研发归集)""" create_test_user(db) token = get_token_for_user(client) resp = client.post( f"{self.BASE}/standard-costs", headers=auth_header(token), json={ "product_code": "PROD_RD", "product_name": "CMA平台开发", "cost_type": "labor", "item_name": "研发工时", "standard_quantity": 10.0, "standard_price": 80.0, "project_type": "platform_dev", }, ) assert resp.status_code == 200 row = db.query(StandardCost).filter(StandardCost.product_code == "PROD_RD").first() assert row.project_type == "platform_dev" assert row.research_flag == 1 def test_list_standard_costs_filter_by_project_type(self, client: TestClient, db: Session): """列表按 project_type 过滤:研发只返回平台研发,不混入客户交付""" create_test_user(db) token = get_token_for_user(client) for code, pt in (("RD_X", "platform_dev"), ("CD_X", "client_delivery"), ("IM_X", "internal")): db.add(StandardCost( product_code=code, product_name=code, cost_type="overhead", item_name=code, standard_quantity=1, standard_price=1.0, standard_cost=1.0, project_type=pt, research_flag=1 if pt == "platform_dev" else 0, )) db.commit() resp = client.get(f"{self.BASE}/standard-costs?project_type=platform_dev", headers=auth_header(token)) assert resp.status_code == 200 codes = [i["product_code"] for i in resp.json()["data"]] assert codes == ["RD_X"] resp2 = client.get(f"{self.BASE}/standard-costs?research_flag=1", headers=auth_header(token)) assert resp2.status_code == 200 codes2 = [i["product_code"] for i in resp2.json()["data"]] assert codes2 == ["RD_X"] def test_update_standard_cost_project_type(self, client: TestClient, db: Session): """更新归集类型 → research_flag 联动(client_delivery 改为 platform_dev)""" create_test_user(db) token = get_token_for_user(client) sc = StandardCost( product_code="PROD_UPD", product_name="产品U", cost_type="labor", item_name="人工", standard_quantity=100, unit="小时", standard_price=50.0, standard_cost=5000.0, project_type="client_delivery", research_flag=0, ) db.add(sc) db.commit() resp = client.put( f"{self.BASE}/standard-costs/{sc.id}", headers=auth_header(token), json={"project_type": "platform_dev"}, ) assert resp.status_code == 200 db.refresh(sc) assert sc.project_type == "platform_dev" assert sc.research_flag == 1 def test_update_standard_cost(self, client: TestClient, db: Session): """修改标准成本卡片""" create_test_user(db) token = get_token_for_user(client) sc = StandardCost( product_code="PROD_B", product_name="产品B", cost_type="labor", item_name="人工", standard_quantity=100, unit="小时", standard_price=50.0, standard_cost=5000.0, ) db.add(sc) db.commit() resp = client.put( f"{self.BASE}/standard-costs/{sc.id}", headers=auth_header(token), json={"standard_price": 55.0, "remark": "调薪后更新"}, ) assert resp.status_code == 200 assert resp.json()["message"] == "已更新" def test_delete_standard_cost(self, client: TestClient, db: Session): """删除(归档)标准成本卡片""" create_test_user(db) token = get_token_for_user(client) sc = StandardCost( product_code="PROD_C", product_name="产品C", cost_type="overhead", item_name="水电费", standard_quantity=1, unit="项", standard_price=1000.0, standard_cost=1000.0, ) db.add(sc) db.commit() resp = client.delete( f"{self.BASE}/standard-costs/{sc.id}", headers=auth_header(token), ) assert resp.status_code == 200 assert resp.json()["message"] == "已归档" class TestActualCost: """实际成本CRUD测试""" BASE = "/api/cma/cost" def test_create_actual_cost(self, client: TestClient, db: Session): """录入实际成本""" create_test_user(db) token = get_token_for_user(client) resp = client.post( f"{self.BASE}/actual-costs", headers=auth_header(token), json={ "period": "2026-06", "product_code": "PROD_A", "product_name": "产品A", "cost_type": "material", "item_name": "原材料X", "actual_quantity": 12.0, "actual_price": 4.8, }, ) assert resp.status_code == 200 assert resp.json()["message"] == "实际成本已录入" def test_list_actual_costs(self, client: TestClient, db: Session): """查询实际成本""" create_test_user(db) token = get_token_for_user(client) ac = ActualCost( period="2026-06", product_code="PROD_A", product_name="产品A", cost_type="material", item_name="原材料X", actual_quantity=10, actual_price=5.0, actual_cost=50.0, ) db.add(ac) db.commit() resp = client.get(f"{self.BASE}/actual-costs?period=2026-06", headers=auth_header(token)) assert resp.status_code == 200 assert len(resp.json()["data"]) == 1 def test_create_actual_cost_research_dims(self, client: TestClient, db: Session): """录入实际成本带归集维度(平台研发 → research_flag=1)""" create_test_user(db) token = get_token_for_user(client) resp = client.post( f"{self.BASE}/actual-costs", headers=auth_header(token), json={ "period": "2026-09", "product_code": "RD_SVC", "product_name": "CMA平台化开发", "cost_type": "labor", "item_name": "平台研发工时", "actual_quantity": 40.0, "actual_price": 100.0, "project_type": "platform_dev", }, ) assert resp.status_code == 200 row = db.query(ActualCost).filter(ActualCost.product_code == "RD_SVC").first() assert row is not None assert row.entity_id == 1 assert row.project_type == "platform_dev" assert row.research_flag == 1 def test_list_actual_costs_filter_by_research(self, client: TestClient, db: Session): """实际成本列表按归集类型/研发标记过滤""" create_test_user(db) token = get_token_for_user(client) db.add(ActualCost( period="2026-09", product_code="RD_SVC", product_name="平台开发", cost_type="labor", item_name="研发工时", actual_quantity=1, actual_price=1.0, actual_cost=1.0, project_type="platform_dev", research_flag=1, )) db.add(ActualCost( period="2026-09", product_code="CD_SVC", product_name="客户交付", cost_type="labor", item_name="实施工时", actual_quantity=1, actual_price=1.0, actual_cost=1.0, project_type="client_delivery", research_flag=0, )) db.commit() resp = client.get(f"{self.BASE}/actual-costs?project_type=platform_dev&period=2026-09", headers=auth_header(token)) assert resp.status_code == 200 codes = [i["product_code"] for i in resp.json()["data"]] assert codes == ["RD_SVC"] resp2 = client.get(f"{self.BASE}/actual-costs?research_flag=0&period=2026-09", headers=auth_header(token)) codes2 = [i["product_code"] for i in resp2.json()["data"]] assert codes2 == ["CD_SVC"] class TestAbcCost: """ABC作业成本测试""" BASE = "/api/cma/cost" def test_create_abc_activity(self, client: TestClient, db: Session): """创建ABC作业中心""" create_test_user(db) token = get_token_for_user(client) resp = client.post( f"{self.BASE}/abc/activities", headers=auth_header(token), json={ "activity_code": "ACT_001", "activity_name": "机器调试", "cost_driver": "调试次数", "driver_unit": "次", "total_cost": 50000.0, "driver_volume": 100, }, ) assert resp.status_code == 200 assert resp.json()["message"] == "作业中心已创建" def test_list_abc_activities(self, client: TestClient, db: Session): """查询ABC作业中心列表""" create_test_user(db) token = get_token_for_user(client) act = AbcActivity( activity_code="ACT_002", activity_name="品质检验", cost_driver="检验批次", driver_unit="批", total_cost=30000.0, driver_volume=200, driver_rate=150.0, ) db.add(act) db.commit() resp = client.get(f"{self.BASE}/abc/activities", headers=auth_header(token)) assert resp.status_code == 200 assert len(resp.json()["data"]) >= 1 assert resp.json()["data"][0]["activity_code"] == "ACT_002" def test_abc_allocate(self, client: TestClient, db: Session): """执行ABC成本分配""" create_test_user(db) token = get_token_for_user(client) act = AbcActivity( activity_code="ACT_003", activity_name="订单处理", cost_driver="订单数", driver_unit="单", total_cost=20000.0, driver_volume=400, driver_rate=50.0, ) db.add(act) db.commit() resp = client.post( f"{self.BASE}/abc/allocate", headers=auth_header(token), json={ "activity_id": act.id, "period": "2026-06", "product_code": "PROD_A", "product_name": "产品A", "driver_consumed": 30, }, ) assert resp.status_code == 200 data = resp.json() # 30 * 50 = 1500 assert data["allocated_cost"] == 1500.0