feat: 增加报工工时归属拆分计算

This commit is contained in:
souplearn
2026-07-25 03:00:07 +08:00
parent 66777fa174
commit b0be660999
4 changed files with 548 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
from datetime import date, datetime
from types import SimpleNamespace
from sqlalchemy import BigInteger, create_engine, select
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models import ProductionReport, ProductionReportAllocation, ProductionReportItem
from app.services.report_allocations import (
allocation_summary_text,
build_report_allocation_drafts,
refresh_report_allocations,
)
@compiles(BigInteger, "sqlite")
def _compile_big_integer_for_sqlite(type_, compiler, **kw):
return "INTEGER"
def _item(
*,
id: int = 1,
good_qty: float = 0,
defect_qty: float = 0,
scrap_qty: float = 0,
changeover_count: float = 0,
process_unit_price_yuan: float = 0,
started_at: datetime | None = None,
):
return SimpleNamespace(
id=id,
device_no="23#",
product_name="23#",
process_name="冲压",
started_at=started_at,
standard_beat=1,
standard_workload=0,
stamping_method=None,
good_qty=good_qty,
defect_qty=defect_qty,
scrap_qty=scrap_qty,
changeover_count=changeover_count,
process_unit_price_yuan=process_unit_price_yuan,
allocated_minutes=0,
)
def _report(start_at: datetime, end_at: datetime, item, *, id: int = 1, report_date: date | None = None):
return SimpleNamespace(
id=id,
attendance_point_name="总厂",
employee_phone="13800000000",
report_date=report_date or start_at.date(),
start_at=start_at,
end_at=end_at,
items=[item],
session=SimpleNamespace(devices=[]),
)
def test_cross_day_after_midnight_allocates_previous_night_and_current_overtime_day():
item = _item(
good_qty=650,
defect_qty=65,
scrap_qty=6.5,
changeover_count=13,
process_unit_price_yuan=1,
)
report = _report(datetime(2026, 7, 25, 0, 10), datetime(2026, 7, 25, 11, 0), item)
rows = build_report_allocation_drafts(report)
assert len(rows) == 2
previous, current = rows
assert previous.allocation_date == date(2026, 7, 24)
assert previous.night_minutes == 350
assert previous.day_minutes == 0
assert previous.overtime_minutes == 0
assert previous.effective_minutes == 350
assert previous.good_qty == 350
assert previous.defect_qty == 35
assert previous.scrap_qty == 3.5
assert previous.changeover_count == 7
assert previous.reference_wage == 350
assert current.allocation_date == date(2026, 7, 25)
assert current.night_minutes == 0
assert current.overtime_minutes == 120
assert current.day_minutes == 180
assert current.effective_minutes == 300
assert current.good_qty == 300
assert current.defect_qty == 30
assert current.scrap_qty == 3
assert current.changeover_count == 6
assert current.reference_wage == 300
def test_one_minute_before_night_end_allocates_to_previous_date():
item = _item(good_qty=301, process_unit_price_yuan=2)
report = _report(datetime(2026, 7, 25, 5, 59), datetime(2026, 7, 25, 11, 0), item)
rows = build_report_allocation_drafts(report)
assert [(row.allocation_date, row.night_minutes, row.overtime_minutes, row.day_minutes) for row in rows] == [
(date(2026, 7, 24), 1, 0, 0),
(date(2026, 7, 25), 0, 120, 180),
]
assert rows[0].good_qty == 1
assert rows[0].reference_wage == 2
assert rows[1].good_qty == 300
assert rows[1].reference_wage == 600
def test_zero_effective_minutes_falls_back_to_original_report_date():
item = _item(good_qty=5, defect_qty=1, scrap_qty=0.5, changeover_count=2, process_unit_price_yuan=3)
report = _report(
datetime(2026, 7, 25, 9, 0),
datetime(2026, 7, 25, 9, 0),
item,
report_date=date(2026, 7, 25),
)
rows = build_report_allocation_drafts(report)
assert len(rows) == 1
row = rows[0]
assert row.allocation_date == date(2026, 7, 25)
assert row.effective_minutes == 0
assert row.day_minutes == 0
assert row.overtime_minutes == 0
assert row.night_minutes == 0
assert row.good_qty == 5
assert row.defect_qty == 1
assert row.scrap_qty == 0.5
assert row.changeover_count == 2
assert row.reference_wage == 15
def test_allocation_summary_text_groups_dates_and_shift_kinds():
item = _item(good_qty=650, process_unit_price_yuan=1)
report = _report(datetime(2026, 7, 25, 0, 10), datetime(2026, 7, 25, 11, 0), item)
rows = build_report_allocation_drafts(report)
assert allocation_summary_text(rows) == "2026-07-24 夜班5.83小时;2026-07-25 白班3小时、加班2小时"
def test_refresh_report_allocations_replaces_existing_rows_and_flushes():
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine, future=True)
db = SessionLocal()
try:
report = ProductionReport(
id=1,
session_id=1,
attendance_point_name="总厂",
employee_phone="13800000000",
report_date=date(2026, 7, 25),
start_at=datetime(2026, 7, 25, 5, 59),
end_at=datetime(2026, 7, 25, 11, 0),
)
item = ProductionReportItem(
id=1,
report_id=1,
attendance_point_name="总厂",
device_no="23#",
project_no="P1",
product_name="23#",
process_name="冲压",
process_unit_price_yuan=2,
good_qty=301,
defect_qty=0,
scrap_qty=0,
changeover_count=0,
standard_beat=1,
standard_workload=0,
)
stale = ProductionReportAllocation(
id=99,
report_id=1,
report_item_id=1,
attendance_point_name="总厂",
employee_phone="13800000000",
allocation_date=date(2026, 7, 20),
effective_minutes=1,
)
report.items.append(item)
db.add_all([report, stale])
db.flush()
rows = refresh_report_allocations(db, report)
assert len(rows) == 2
assert all(row.id is not None for row in rows)
stored = db.scalars(select(ProductionReportAllocation).order_by(ProductionReportAllocation.allocation_date)).all()
assert [row.allocation_date for row in stored] == [date(2026, 7, 24), date(2026, 7, 25)]
assert [row.good_qty for row in stored] == [1, 300]
finally:
db.close()