fix: 统一报工归属金额舍入
This commit is contained in:
parent
4c0cbc9815
commit
4db517ac49
@ -1,6 +1,8 @@
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
@ -24,6 +26,8 @@ from app.services.metrics import (
|
||||
)
|
||||
from app.services.work_schedule import DEFAULT_WORK_SCHEDULE_CONFIG, WorkScheduleConfig
|
||||
|
||||
SPLIT_QUANT = Decimal("0.01")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReportAllocationDraft:
|
||||
@ -126,8 +130,34 @@ def _report_devices(report) -> list | None:
|
||||
return list(devices) if devices is not None else None
|
||||
|
||||
|
||||
def _item_reference_wage(item) -> float:
|
||||
return as_float(getattr(item, "good_qty", 0)) * as_float(getattr(item, "process_unit_price_yuan", 0))
|
||||
def _decimal_value(value: Any) -> Decimal:
|
||||
if value is None:
|
||||
return Decimal("0")
|
||||
if isinstance(value, Decimal):
|
||||
return value
|
||||
try:
|
||||
return Decimal(str(value))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
return Decimal("0")
|
||||
|
||||
|
||||
def _round_split_value(value: Any) -> float:
|
||||
return float(_decimal_value(value).quantize(SPLIT_QUANT, rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _split_cents(value: Any) -> int:
|
||||
rounded = _decimal_value(value).quantize(SPLIT_QUANT, rounding=ROUND_HALF_UP)
|
||||
return int(rounded * 100)
|
||||
|
||||
|
||||
def _scaled_split_value(value: Any, ratio: float) -> float:
|
||||
return _round_split_value(_decimal_value(value) * _decimal_value(ratio))
|
||||
|
||||
|
||||
def _item_reference_wage(item) -> Decimal:
|
||||
return _decimal_value(getattr(item, "good_qty", 0)) * _decimal_value(
|
||||
getattr(item, "process_unit_price_yuan", 0)
|
||||
)
|
||||
|
||||
|
||||
def _allocation_targets(report, items: list, schedule: WorkScheduleConfig | None = None):
|
||||
@ -398,11 +428,11 @@ def _fallback_row(report, item) -> ReportAllocationDraft:
|
||||
attendance_point_name=str(getattr(report, "attendance_point_name", "") or ""),
|
||||
employee_phone=str(getattr(report, "employee_phone", "") or ""),
|
||||
allocation_date=getattr(report, "report_date"),
|
||||
good_qty=round2(getattr(item, "good_qty", 0)),
|
||||
defect_qty=round2(getattr(item, "defect_qty", 0)),
|
||||
scrap_qty=round2(getattr(item, "scrap_qty", 0)),
|
||||
changeover_count=round2(getattr(item, "changeover_count", 0)),
|
||||
reference_wage=round2(_item_reference_wage(item)),
|
||||
good_qty=_round_split_value(getattr(item, "good_qty", 0)),
|
||||
defect_qty=_round_split_value(getattr(item, "defect_qty", 0)),
|
||||
scrap_qty=_round_split_value(getattr(item, "scrap_qty", 0)),
|
||||
changeover_count=_round_split_value(getattr(item, "changeover_count", 0)),
|
||||
reference_wage=_round_split_value(_item_reference_wage(item)),
|
||||
)
|
||||
|
||||
|
||||
@ -421,11 +451,11 @@ def _draft_from_minutes(report, item, allocation_date: date, minutes: dict[str,
|
||||
overtime_minutes=overtime_minutes,
|
||||
night_minutes=night_minutes,
|
||||
effective_minutes=round2(day_minutes + overtime_minutes + night_minutes),
|
||||
good_qty=round2(as_float(getattr(item, "good_qty", 0)) * ratio),
|
||||
defect_qty=round2(as_float(getattr(item, "defect_qty", 0)) * ratio),
|
||||
scrap_qty=round2(as_float(getattr(item, "scrap_qty", 0)) * ratio),
|
||||
changeover_count=round2(as_float(getattr(item, "changeover_count", 0)) * ratio),
|
||||
reference_wage=round2(reference_wage * ratio),
|
||||
good_qty=_scaled_split_value(getattr(item, "good_qty", 0), ratio),
|
||||
defect_qty=_scaled_split_value(getattr(item, "defect_qty", 0), ratio),
|
||||
scrap_qty=_scaled_split_value(getattr(item, "scrap_qty", 0), ratio),
|
||||
changeover_count=_scaled_split_value(getattr(item, "changeover_count", 0), ratio),
|
||||
reference_wage=_round_split_value(reference_wage * _decimal_value(ratio)),
|
||||
)
|
||||
|
||||
|
||||
@ -440,11 +470,11 @@ SPLIT_RESIDUAL_FIELDS = (
|
||||
|
||||
def _item_split_targets(item) -> dict[str, float]:
|
||||
return {
|
||||
"good_qty": round2(getattr(item, "good_qty", 0)),
|
||||
"defect_qty": round2(getattr(item, "defect_qty", 0)),
|
||||
"scrap_qty": round2(getattr(item, "scrap_qty", 0)),
|
||||
"changeover_count": round2(getattr(item, "changeover_count", 0)),
|
||||
"reference_wage": round2(_item_reference_wage(item)),
|
||||
"good_qty": _round_split_value(getattr(item, "good_qty", 0)),
|
||||
"defect_qty": _round_split_value(getattr(item, "defect_qty", 0)),
|
||||
"scrap_qty": _round_split_value(getattr(item, "scrap_qty", 0)),
|
||||
"changeover_count": _round_split_value(getattr(item, "changeover_count", 0)),
|
||||
"reference_wage": _round_split_value(_item_reference_wage(item)),
|
||||
}
|
||||
|
||||
|
||||
@ -456,12 +486,12 @@ def _balance_item_split_residuals(
|
||||
return drafts
|
||||
|
||||
mutable_values = [
|
||||
{field: int(round(as_float(getattr(draft, field)) * 100)) for field in SPLIT_RESIDUAL_FIELDS}
|
||||
{field: _split_cents(getattr(draft, field)) for field in SPLIT_RESIDUAL_FIELDS}
|
||||
for draft in drafts
|
||||
]
|
||||
|
||||
for field in SPLIT_RESIDUAL_FIELDS:
|
||||
target_cents = int(round(round2(targets[field]) * 100))
|
||||
target_cents = _split_cents(targets[field])
|
||||
current_cents = sum(values[field] for values in mutable_values)
|
||||
residual = target_cents - current_cents
|
||||
if residual == 0:
|
||||
|
||||
@ -230,6 +230,26 @@ def test_item_split_fields_keep_original_totals_after_rounding_residual():
|
||||
assert round(sum(row.reference_wage for row in rows), 2) == 1
|
||||
|
||||
|
||||
def test_reference_wage_uses_half_up_rounding_for_allocation_totals():
|
||||
item = _item(good_qty=115, process_unit_price_yuan=Decimal("0.867"))
|
||||
report = _report(datetime(2026, 7, 25, 8, 0), datetime(2026, 7, 25, 10, 0), item)
|
||||
|
||||
rows = build_report_allocation_drafts(report)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].reference_wage == 99.71
|
||||
|
||||
|
||||
def test_split_reference_wage_residual_keeps_half_up_item_total():
|
||||
item = _item(good_qty=115, process_unit_price_yuan=Decimal("0.867"))
|
||||
report = _report(datetime(2026, 7, 25, 0, 10), datetime(2026, 7, 25, 11, 0), item)
|
||||
|
||||
rows = build_report_allocation_drafts(report)
|
||||
|
||||
assert [row.allocation_date for row in rows] == [date(2026, 7, 24), date(2026, 7, 25)]
|
||||
assert round(sum(row.reference_wage for row in rows), 2) == 99.71
|
||||
|
||||
|
||||
def test_items_sharing_same_started_at_split_the_segment_minutes():
|
||||
started_at = datetime(2026, 7, 25, 8, 0)
|
||||
first_item = _item(id=1, good_qty=10, started_at=started_at)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user