chore: initialize mini program backend
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services.continuous_die import (
|
||||
CONTINUOUS_DIE_STAMPING_METHOD,
|
||||
is_continuous_die_stamping_method,
|
||||
)
|
||||
from app.services.serializers import product_option
|
||||
|
||||
|
||||
def test_continuous_die_stamping_method_is_trimmed_exact_match():
|
||||
assert CONTINUOUS_DIE_STAMPING_METHOD == "连续模"
|
||||
assert is_continuous_die_stamping_method("连续模") is True
|
||||
assert is_continuous_die_stamping_method(" 连续模 ") is True
|
||||
assert is_continuous_die_stamping_method("连续模具") is False
|
||||
assert is_continuous_die_stamping_method("清洗") is False
|
||||
assert is_continuous_die_stamping_method(None) is False
|
||||
|
||||
|
||||
def test_product_option_marks_continuous_die():
|
||||
product = SimpleNamespace(
|
||||
attendance_point_name="嘉恒",
|
||||
project_no="P001",
|
||||
product_name="测试产品",
|
||||
profile_no=None,
|
||||
material_code=None,
|
||||
material_name=None,
|
||||
supplier=None,
|
||||
product_net_weight_kg=None,
|
||||
product_gross_weight_kg=None,
|
||||
scrap_loss_rate=None,
|
||||
waste_price_yuan_per_kg=None,
|
||||
device_no="",
|
||||
process_name="1",
|
||||
stamping_method=" 连续模 ",
|
||||
operator_count=1,
|
||||
process_unit_price_yuan=0,
|
||||
standard_beat=10,
|
||||
standard_workload=0,
|
||||
)
|
||||
|
||||
option = product_option(product)
|
||||
|
||||
assert option.stamping_method == " 连续模 "
|
||||
assert option.is_continuous_die is True
|
||||
assert option.is_cleaning is False
|
||||
@@ -0,0 +1,282 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.services.metrics import calculate_report_metrics
|
||||
from app.services.work_schedule import WorkScheduleConfig
|
||||
|
||||
|
||||
class Item:
|
||||
def __init__(
|
||||
self,
|
||||
device_no: str = "23#",
|
||||
good_qty: float = 2,
|
||||
standard_beat: float = 21.12,
|
||||
standard_workload: float = 1513,
|
||||
started_at: datetime | None = None,
|
||||
) -> None:
|
||||
self.device_no = device_no
|
||||
self.started_at = started_at
|
||||
self.standard_beat = standard_beat
|
||||
self.standard_workload = standard_workload
|
||||
self.good_qty = good_qty
|
||||
self.defect_qty = 0
|
||||
self.scrap_qty = 0
|
||||
self.allocated_minutes = 0
|
||||
|
||||
|
||||
class Device:
|
||||
def __init__(
|
||||
self,
|
||||
device_no: str,
|
||||
scanned_at: datetime,
|
||||
sort_order: int,
|
||||
released_at: datetime | None = None,
|
||||
release_reason: str | None = None,
|
||||
) -> None:
|
||||
self.device_no = device_no
|
||||
self.scanned_at = scanned_at
|
||||
self.sort_order = sort_order
|
||||
self.released_at = released_at
|
||||
self.release_reason = release_reason
|
||||
|
||||
|
||||
def test_short_report_uses_actual_seconds_without_minute_floor():
|
||||
start_at = datetime(2026, 5, 3, 1, 4, 50)
|
||||
end_at = start_at + timedelta(seconds=2)
|
||||
|
||||
metrics = calculate_report_metrics(start_at, end_at, [Item()])
|
||||
|
||||
assert metrics["duration_minutes"] == 0.03
|
||||
assert metrics["effective_minutes"] == 0.03
|
||||
assert metrics["actual_beat"] == 1.0
|
||||
|
||||
|
||||
def test_multi_device_report_allocates_minutes_by_switch_time():
|
||||
start_at = datetime(2026, 5, 3, 1, 25, 55)
|
||||
switch_at = start_at + timedelta(seconds=30)
|
||||
end_at = switch_at + timedelta(seconds=30)
|
||||
first_item = Item(device_no="23#", good_qty=3)
|
||||
second_item = Item(device_no="28#", good_qty=6)
|
||||
|
||||
metrics = calculate_report_metrics(
|
||||
start_at,
|
||||
end_at,
|
||||
[first_item, second_item],
|
||||
devices=[
|
||||
Device("23#", start_at, 0),
|
||||
Device("28#", switch_at, 1),
|
||||
],
|
||||
)
|
||||
|
||||
assert first_item.allocated_minutes == 0.5
|
||||
assert second_item.allocated_minutes == 0.5
|
||||
assert first_item.allocated_minutes * 60 / first_item.good_qty == 10
|
||||
assert second_item.allocated_minutes * 60 / second_item.good_qty == 5
|
||||
assert metrics["duration_minutes"] == 1
|
||||
|
||||
|
||||
def test_multi_device_report_last_segment_uses_report_end_time():
|
||||
start_at = datetime(2026, 5, 3, 1, 56, 57)
|
||||
switch_at = start_at + timedelta(seconds=30)
|
||||
end_at = start_at + timedelta(seconds=44)
|
||||
first_item = Item(device_no="23#", good_qty=3)
|
||||
second_item = Item(device_no="28#", good_qty=5)
|
||||
|
||||
metrics = calculate_report_metrics(
|
||||
start_at,
|
||||
end_at,
|
||||
[first_item, second_item],
|
||||
devices=[
|
||||
Device("23#", start_at, 0),
|
||||
Device("28#", switch_at, 1),
|
||||
],
|
||||
)
|
||||
|
||||
total_allocated_seconds = round((first_item.allocated_minutes + second_item.allocated_minutes) * 60)
|
||||
assert metrics["duration_minutes"] == 0.73
|
||||
assert round(first_item.allocated_minutes * 60) == 30
|
||||
assert round(second_item.allocated_minutes * 60) == 14
|
||||
assert total_allocated_seconds == 44
|
||||
|
||||
|
||||
def test_same_device_items_split_by_item_start_time():
|
||||
start_at = datetime(2026, 5, 3, 21, 51, 35)
|
||||
product_switch_at = start_at + timedelta(seconds=20)
|
||||
end_at = start_at + timedelta(seconds=50)
|
||||
first_item = Item(device_no="28#", good_qty=1, started_at=start_at)
|
||||
second_item = Item(device_no="28#", good_qty=3, started_at=product_switch_at)
|
||||
|
||||
metrics = calculate_report_metrics(
|
||||
start_at,
|
||||
end_at,
|
||||
[first_item, second_item],
|
||||
devices=[
|
||||
Device("28#", start_at, 0),
|
||||
],
|
||||
)
|
||||
|
||||
assert round(first_item.allocated_minutes * 60) == 20
|
||||
assert round(second_item.allocated_minutes * 60) == 30
|
||||
assert metrics["duration_minutes"] == 0.83
|
||||
|
||||
|
||||
def test_meal_break_is_deducted_and_shift_distribution_is_marked():
|
||||
start_at = datetime(2026, 5, 4, 8, 0, 0)
|
||||
end_at = datetime(2026, 5, 4, 20, 0, 0)
|
||||
item = Item(good_qty=10)
|
||||
|
||||
metrics = calculate_report_metrics(start_at, end_at, [item])
|
||||
|
||||
assert metrics["duration_minutes"] == 720
|
||||
assert metrics["shift_meal_break_minutes"] == 100
|
||||
assert metrics["effective_minutes"] == 620
|
||||
assert metrics["shift_day_minutes"] == 500
|
||||
assert metrics["shift_overtime_minutes"] == 120
|
||||
assert metrics["shift_distribution_text"] == "工时 10.33小时(白班8.33小时、加班2小时)"
|
||||
|
||||
|
||||
def test_partial_meal_tail_is_not_deducted_when_report_ends_during_meal():
|
||||
schedule = WorkScheduleConfig(
|
||||
day_start="08:00",
|
||||
day_end="17:00",
|
||||
lunch_start="11:40",
|
||||
lunch_end="12:40",
|
||||
dinner_start="17:00",
|
||||
dinner_end="17:30",
|
||||
overtime_start="17:00",
|
||||
overtime_end="20:00",
|
||||
night_start="20:00",
|
||||
night_end="06:00",
|
||||
)
|
||||
start_at = datetime(2026, 5, 4, 16, 0, 0)
|
||||
end_at = datetime(2026, 5, 4, 17, 10, 0)
|
||||
|
||||
metrics = calculate_report_metrics(start_at, end_at, [Item()], schedule=schedule)
|
||||
|
||||
assert metrics["duration_minutes"] == 70
|
||||
assert metrics["shift_meal_break_minutes"] == 0
|
||||
assert metrics["effective_minutes"] == 70
|
||||
assert metrics["shift_day_minutes"] == 60
|
||||
assert metrics["shift_overtime_minutes"] == 10
|
||||
|
||||
|
||||
def test_meal_end_grace_keeps_minutes_until_five_minutes_after_meal():
|
||||
schedule = WorkScheduleConfig(
|
||||
day_start="08:00",
|
||||
day_end="17:00",
|
||||
lunch_start="11:40",
|
||||
lunch_end="12:40",
|
||||
dinner_start="17:00",
|
||||
dinner_end="17:30",
|
||||
overtime_start="17:00",
|
||||
overtime_end="20:00",
|
||||
night_start="20:00",
|
||||
night_end="06:00",
|
||||
)
|
||||
start_at = datetime(2026, 5, 4, 16, 0, 0)
|
||||
end_at = datetime(2026, 5, 4, 17, 35, 0)
|
||||
|
||||
metrics = calculate_report_metrics(start_at, end_at, [Item()], schedule=schedule)
|
||||
|
||||
assert metrics["duration_minutes"] == 95
|
||||
assert metrics["shift_meal_break_minutes"] == 0
|
||||
assert metrics["effective_minutes"] == 95
|
||||
assert metrics["shift_day_minutes"] == 60
|
||||
assert metrics["shift_overtime_minutes"] == 35
|
||||
|
||||
|
||||
def test_meal_break_is_deducted_after_grace_window():
|
||||
schedule = WorkScheduleConfig(
|
||||
day_start="08:00",
|
||||
day_end="17:00",
|
||||
lunch_start="11:40",
|
||||
lunch_end="12:40",
|
||||
dinner_start="17:00",
|
||||
dinner_end="17:30",
|
||||
overtime_start="17:00",
|
||||
overtime_end="20:00",
|
||||
night_start="20:00",
|
||||
night_end="06:00",
|
||||
)
|
||||
start_at = datetime(2026, 5, 4, 16, 0, 0)
|
||||
end_at = datetime(2026, 5, 4, 17, 36, 0)
|
||||
|
||||
metrics = calculate_report_metrics(start_at, end_at, [Item()], schedule=schedule)
|
||||
|
||||
assert metrics["duration_minutes"] == 96
|
||||
assert metrics["shift_meal_break_minutes"] == 30
|
||||
assert metrics["effective_minutes"] == 66
|
||||
assert metrics["shift_day_minutes"] == 60
|
||||
assert metrics["shift_overtime_minutes"] == 6
|
||||
|
||||
|
||||
def test_full_meal_break_is_deducted_when_report_covers_whole_meal():
|
||||
schedule = WorkScheduleConfig(
|
||||
day_start="08:00",
|
||||
day_end="17:00",
|
||||
lunch_start="11:40",
|
||||
lunch_end="12:40",
|
||||
dinner_start="17:00",
|
||||
dinner_end="17:30",
|
||||
overtime_start="17:00",
|
||||
overtime_end="20:00",
|
||||
night_start="20:00",
|
||||
night_end="06:00",
|
||||
)
|
||||
start_at = datetime(2026, 5, 4, 16, 0, 0)
|
||||
end_at = datetime(2026, 5, 4, 18, 0, 0)
|
||||
|
||||
metrics = calculate_report_metrics(start_at, end_at, [Item()], schedule=schedule)
|
||||
|
||||
assert metrics["duration_minutes"] == 120
|
||||
assert metrics["shift_meal_break_minutes"] == 30
|
||||
assert metrics["effective_minutes"] == 90
|
||||
assert metrics["shift_day_minutes"] == 60
|
||||
assert metrics["shift_overtime_minutes"] == 30
|
||||
|
||||
|
||||
def test_admin_corrected_start_uses_first_device_from_report_start():
|
||||
start_at = datetime(2026, 5, 27, 10, 0, 0)
|
||||
scanned_at = datetime(2026, 5, 27, 13, 56, 11)
|
||||
submitted_at = datetime(2026, 5, 27, 13, 56, 53)
|
||||
end_at = datetime(2026, 5, 27, 13, 59, 0)
|
||||
item = Item(device_no="开口挡圈-3528221-05", good_qty=2700, standard_beat=1, started_at=scanned_at)
|
||||
|
||||
metrics = calculate_report_metrics(
|
||||
start_at,
|
||||
end_at,
|
||||
[item],
|
||||
devices=[
|
||||
Device(
|
||||
"开口挡圈-3528221-05",
|
||||
scanned_at,
|
||||
0,
|
||||
released_at=submitted_at,
|
||||
release_reason="报工提交释放占用",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
assert metrics["duration_minutes"] == 239
|
||||
assert metrics["effective_minutes"] == 179
|
||||
assert item.allocated_minutes == 179
|
||||
assert metrics["shift_distribution_text"] == "工时 2.98小时(白班2.98小时)"
|
||||
|
||||
|
||||
def test_admin_forced_release_still_caps_work_time():
|
||||
start_at = datetime(2026, 5, 27, 8, 0, 0)
|
||||
release_at = datetime(2026, 5, 27, 9, 0, 0)
|
||||
end_at = datetime(2026, 5, 27, 12, 0, 0)
|
||||
item = Item(device_no="A", good_qty=10)
|
||||
|
||||
metrics = calculate_report_metrics(
|
||||
start_at,
|
||||
end_at,
|
||||
[item],
|
||||
devices=[
|
||||
Device("A", start_at, 0, released_at=release_at, release_reason="管理员强制解除占用"),
|
||||
],
|
||||
)
|
||||
|
||||
assert metrics["duration_minutes"] == 60
|
||||
assert metrics["effective_minutes"] == 60
|
||||
assert item.allocated_minutes == 60
|
||||
@@ -0,0 +1,126 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.database import Base
|
||||
from app.models import (
|
||||
AttendancePoint,
|
||||
DeviceQRCode,
|
||||
Personnel,
|
||||
Product,
|
||||
ProductionReport,
|
||||
ProductionReportItem,
|
||||
ReportStatus,
|
||||
Role,
|
||||
SessionStatus,
|
||||
WorkSession,
|
||||
WorkSessionDevice,
|
||||
)
|
||||
from app.routers.products import delete_product
|
||||
|
||||
|
||||
def _db_session():
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||
Base.metadata.create_all(engine)
|
||||
SessionLocal = sessionmaker(bind=engine, future=True)
|
||||
return SessionLocal()
|
||||
|
||||
|
||||
def test_delete_product_removes_qrcode_but_keeps_history():
|
||||
db = _db_session()
|
||||
try:
|
||||
point = "测试考勤点"
|
||||
user = Personnel(phone="13900000000", name="管理员")
|
||||
user.role = Role.manager
|
||||
db.add_all([
|
||||
AttendancePoint(name=point, radius_meters=500, is_active=True),
|
||||
user,
|
||||
Product(
|
||||
attendance_point_name=point,
|
||||
project_no="测试",
|
||||
product_name="测试",
|
||||
device_no="",
|
||||
process_name="1",
|
||||
stamping_method="普通",
|
||||
operator_count=1,
|
||||
standard_beat=1,
|
||||
standard_workload=0,
|
||||
),
|
||||
DeviceQRCode(
|
||||
attendance_point_name=point,
|
||||
device_no="测试",
|
||||
process_name="1",
|
||||
qr_scene="mold=old",
|
||||
created_by=user.phone,
|
||||
),
|
||||
])
|
||||
db.flush()
|
||||
session = WorkSession(
|
||||
id=1,
|
||||
attendance_point_name=point,
|
||||
employee_phone=user.phone,
|
||||
start_at=datetime(2026, 6, 23, 8, 0),
|
||||
status=SessionStatus.submitted,
|
||||
)
|
||||
db.add(session)
|
||||
db.flush()
|
||||
report = ProductionReport(
|
||||
id=1,
|
||||
attendance_point_name=point,
|
||||
session_id=session.id,
|
||||
employee_phone=user.phone,
|
||||
report_date=date(2026, 6, 23),
|
||||
start_at=datetime(2026, 6, 23, 8, 0),
|
||||
end_at=datetime(2026, 6, 23, 9, 0),
|
||||
status=ReportStatus.pending,
|
||||
)
|
||||
db.add(report)
|
||||
db.flush()
|
||||
history_device = WorkSessionDevice(
|
||||
id=1,
|
||||
session_id=session.id,
|
||||
attendance_point_name=point,
|
||||
device_no="测试",
|
||||
process_name="1",
|
||||
scanned_at=datetime(2026, 6, 23, 8, 0),
|
||||
sort_order=0,
|
||||
)
|
||||
history_item = ProductionReportItem(
|
||||
id=1,
|
||||
report_id=report.id,
|
||||
attendance_point_name=point,
|
||||
device_no="28#",
|
||||
project_no="测试",
|
||||
product_name="测试",
|
||||
process_name="1",
|
||||
standard_beat=1,
|
||||
)
|
||||
db.add_all([history_device, history_item])
|
||||
db.commit()
|
||||
|
||||
delete_product(
|
||||
attendance_point_name=point,
|
||||
project_no="测试",
|
||||
product_name="测试",
|
||||
process_name="1",
|
||||
user=user,
|
||||
db=db,
|
||||
)
|
||||
|
||||
assert db.get(Product, {
|
||||
"attendance_point_name": point,
|
||||
"project_no": "测试",
|
||||
"product_name": "测试",
|
||||
"device_no": "",
|
||||
"process_name": "1",
|
||||
}) is None
|
||||
assert db.get(DeviceQRCode, {
|
||||
"attendance_point_name": point,
|
||||
"device_no": "测试",
|
||||
"process_name": "1",
|
||||
}) is None
|
||||
assert db.scalar(select(ProductionReportItem).where(ProductionReportItem.id == history_item.id)) is not None
|
||||
assert db.scalar(select(WorkSessionDevice).where(WorkSessionDevice.id == history_device.id)) is not None
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,96 @@
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.routers.reviews import _apply_device_corrections
|
||||
from app.routers.reviews import _effective_review_time
|
||||
from app.schemas import ApproveReportRequest, ReportDeviceCorrection
|
||||
from app.services.serializers import _datetime_changed
|
||||
|
||||
|
||||
def _device(id_: int, name: str, scanned_at: datetime, sort_order: int):
|
||||
return SimpleNamespace(
|
||||
id=id_,
|
||||
device_no=name,
|
||||
process_name="1",
|
||||
scanned_at=scanned_at,
|
||||
sort_order=sort_order,
|
||||
)
|
||||
|
||||
|
||||
def _report(devices):
|
||||
return SimpleNamespace(session=SimpleNamespace(devices=devices))
|
||||
|
||||
|
||||
def test_first_device_correction_syncs_report_start_time():
|
||||
first_scan = datetime(2026, 5, 27, 8, 0)
|
||||
next_start = datetime(2026, 5, 27, 8, 0)
|
||||
next_end = datetime(2026, 5, 27, 10, 0)
|
||||
first = _device(1, "A", first_scan, 0)
|
||||
second = _device(2, "B", datetime(2026, 5, 27, 9, 30), 1)
|
||||
payload = ApproveReportRequest(
|
||||
device_corrections=[
|
||||
ReportDeviceCorrection(id=1, scanned_at=datetime(2026, 5, 27, 8, 30))
|
||||
]
|
||||
)
|
||||
|
||||
changed, corrected_start = _apply_device_corrections(None, _report([first, second]), payload, next_start, next_end)
|
||||
|
||||
assert changed is True
|
||||
assert corrected_start == datetime(2026, 5, 27, 8, 30)
|
||||
assert first.scanned_at == corrected_start
|
||||
|
||||
|
||||
def test_top_start_time_syncs_first_device_when_device_corrections_are_empty():
|
||||
first = _device(1, "A", datetime(2026, 5, 27, 13, 56, 11), 0)
|
||||
payload = ApproveReportRequest()
|
||||
|
||||
changed, corrected_start = _apply_device_corrections(
|
||||
None,
|
||||
_report([first]),
|
||||
payload,
|
||||
datetime(2026, 5, 27, 10, 0),
|
||||
datetime(2026, 5, 27, 13, 59),
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
assert corrected_start == datetime(2026, 5, 27, 10, 0)
|
||||
assert first.scanned_at == corrected_start
|
||||
|
||||
|
||||
def test_non_first_device_correction_does_not_change_report_start_time():
|
||||
first = _device(1, "A", datetime(2026, 5, 27, 8, 0), 0)
|
||||
second = _device(2, "B", datetime(2026, 5, 27, 9, 0), 1)
|
||||
payload = ApproveReportRequest(
|
||||
device_corrections=[
|
||||
ReportDeviceCorrection(id=2, scanned_at=datetime(2026, 5, 27, 9, 30))
|
||||
]
|
||||
)
|
||||
|
||||
changed, corrected_start = _apply_device_corrections(
|
||||
None,
|
||||
_report([first, second]),
|
||||
payload,
|
||||
datetime(2026, 5, 27, 8, 0),
|
||||
datetime(2026, 5, 27, 10, 0),
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
assert corrected_start == datetime(2026, 5, 27, 8, 0)
|
||||
assert first.scanned_at == datetime(2026, 5, 27, 8, 0)
|
||||
assert second.scanned_at == datetime(2026, 5, 27, 9, 30)
|
||||
|
||||
|
||||
def test_same_minute_picker_value_keeps_original_seconds():
|
||||
current = datetime(2026, 5, 29, 15, 10, 6)
|
||||
payload = datetime(2026, 5, 29, 15, 10, 0)
|
||||
|
||||
assert _effective_review_time(payload, current) == current
|
||||
assert _datetime_changed(current.isoformat(), payload.isoformat()) is False
|
||||
|
||||
|
||||
def test_adjacent_minute_picker_value_is_real_correction():
|
||||
current = datetime(2026, 5, 29, 15, 10, 6)
|
||||
payload = datetime(2026, 5, 29, 15, 11, 0)
|
||||
|
||||
assert _effective_review_time(payload, current) == payload
|
||||
assert _datetime_changed(current.isoformat(), payload.isoformat()) is True
|
||||
@@ -0,0 +1,648 @@
|
||||
from io import BytesIO
|
||||
from types import SimpleNamespace
|
||||
import json
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from app.routers.usage_stats import (
|
||||
_build_usage_stats_export_response,
|
||||
_filter_usage_rows,
|
||||
_matches_target,
|
||||
_sort_usage_rows,
|
||||
_to_detail_out,
|
||||
_to_schema_row,
|
||||
)
|
||||
from app.schemas import UsageStatsDetailOut, UsageStatsRow
|
||||
from app.services.usage_stats import UsageStatRow
|
||||
from app.services.usage_stats import build_usage_stats, split_cleaning_device_nos
|
||||
from app.services.usage_stats_export import export_usage_stats_rows
|
||||
|
||||
|
||||
def _report(*items):
|
||||
return SimpleNamespace(items=list(items))
|
||||
|
||||
|
||||
def _item(**overrides):
|
||||
values = {
|
||||
"attendance_point_name": "嘉恒",
|
||||
"product_name": "产品A",
|
||||
"process_name": "1",
|
||||
"stamping_method": "普通",
|
||||
"operator_count": 1,
|
||||
"device_no": "28#",
|
||||
"allocated_minutes": 0,
|
||||
"good_qty": 0,
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
def test_split_cleaning_device_nos_supports_chinese_and_ascii_commas():
|
||||
assert split_cleaning_device_nos("清洗机A、清洗机B, 清洗机C") == ["清洗机A", "清洗机B", "清洗机C"]
|
||||
assert split_cleaning_device_nos(None) == []
|
||||
assert split_cleaning_device_nos("") == []
|
||||
|
||||
|
||||
def test_split_cleaning_device_nos_deduplicates_in_order():
|
||||
assert split_cleaning_device_nos("A、A、B") == ["A", "B"]
|
||||
|
||||
|
||||
def test_build_usage_stats_for_stamping_device_uses_allocated_minutes():
|
||||
rows = build_usage_stats(
|
||||
reports=[_report(_item(device_no="28#", allocated_minutes=75, good_qty=12))],
|
||||
category="device",
|
||||
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
row = rows[0]
|
||||
assert row.name == "28#"
|
||||
assert row.metric_kind == "minutes"
|
||||
assert row.value == 75
|
||||
assert row.report_count == 1
|
||||
assert row.object_type == "冲压设备"
|
||||
assert row.id
|
||||
assert "device" in row.id
|
||||
assert "嘉恒" in row.id
|
||||
assert "28#" in row.id
|
||||
assert "minutes" in row.id
|
||||
|
||||
|
||||
def test_build_usage_stats_for_cleaning_device_splits_quantity_evenly():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(
|
||||
device_no="清洗机A、清洗机B",
|
||||
stamping_method="清洗",
|
||||
good_qty=100,
|
||||
allocated_minutes=0,
|
||||
)
|
||||
)
|
||||
],
|
||||
category="device",
|
||||
equipment_type_by_key={("嘉恒", "清洗机A"): "清洗设备", ("嘉恒", "清洗机B"): "清洗设备"},
|
||||
)
|
||||
|
||||
assert [(row.name, row.metric_kind, row.value, row.report_count) for row in rows] == [
|
||||
("清洗机A", "quantity", 50, 1),
|
||||
("清洗机B", "quantity", 50, 1),
|
||||
]
|
||||
assert [row.object_type for row in rows] == ["清洗设备", "清洗设备"]
|
||||
|
||||
|
||||
def test_build_usage_stats_for_cleaning_device_deduplicates_before_splitting_quantity():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(
|
||||
device_no="清洗机A、清洗机A、清洗机B",
|
||||
stamping_method="清洗",
|
||||
good_qty=100,
|
||||
)
|
||||
)
|
||||
],
|
||||
category="device",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert [(row.name, row.value, row.report_count) for row in rows] == [
|
||||
("清洗机A", 50, 1),
|
||||
("清洗机B", 50, 1),
|
||||
]
|
||||
|
||||
|
||||
def test_build_usage_stats_for_device_skips_misc_items():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(
|
||||
product_name="处理杂活",
|
||||
process_name="杂活",
|
||||
stamping_method="处理杂活",
|
||||
device_no="28#",
|
||||
allocated_minutes=30,
|
||||
)
|
||||
)
|
||||
],
|
||||
category="device",
|
||||
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
|
||||
)
|
||||
|
||||
assert rows == []
|
||||
|
||||
|
||||
def test_build_usage_stats_for_non_cleaning_device_ignores_cleaning_equipment_type():
|
||||
rows = build_usage_stats(
|
||||
reports=[_report(_item(device_no="清洗机A", allocated_minutes=75, good_qty=12))],
|
||||
category="device",
|
||||
equipment_type_by_key={("嘉恒", "清洗机A"): "清洗设备"},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].name == "清洗机A"
|
||||
assert rows[0].metric_kind == "minutes"
|
||||
assert rows[0].value == 75
|
||||
assert rows[0].object_type == "清洗设备"
|
||||
|
||||
|
||||
def test_build_usage_stats_for_cleaning_device_marks_cleaning_tag():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(
|
||||
device_no="清洗机A",
|
||||
stamping_method="清洗",
|
||||
good_qty=100,
|
||||
allocated_minutes=0,
|
||||
)
|
||||
)
|
||||
],
|
||||
category="device",
|
||||
equipment_type_by_key={("嘉恒", "清洗机A"): "清洗设备"},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert "清洗" in rows[0].tags
|
||||
assert "清洗设备" not in rows[0].tags
|
||||
assert rows[0].object_type == "清洗设备"
|
||||
|
||||
|
||||
def test_build_usage_stats_for_mold_merges_by_point_product_process_and_stamping_method():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(stamping_method="连续模", allocated_minutes=30),
|
||||
_item(stamping_method="连续模", allocated_minutes=45),
|
||||
)
|
||||
],
|
||||
category="mold",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
row = rows[0]
|
||||
assert row.attendance_point_name == "嘉恒"
|
||||
assert row.product_name == "产品A"
|
||||
assert row.process_name == "1"
|
||||
assert row.stamping_method == "连续模"
|
||||
assert row.metric_kind == "minutes"
|
||||
assert row.value == 75
|
||||
assert row.report_count == 2
|
||||
assert row.object_type == "连续模"
|
||||
assert "连续模" in row.tags
|
||||
|
||||
|
||||
def test_build_usage_stats_for_mold_ids_distinguish_point_and_process():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(attendance_point_name="嘉恒", process_name="1", allocated_minutes=30),
|
||||
_item(attendance_point_name="二厂", process_name="1", allocated_minutes=40),
|
||||
_item(attendance_point_name="嘉恒", process_name="2", allocated_minutes=50),
|
||||
)
|
||||
],
|
||||
category="mold",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
ids = {row.id for row in rows}
|
||||
assert len(rows) == 3
|
||||
assert len(ids) == 3
|
||||
|
||||
|
||||
def test_build_usage_stats_preserves_numeric_zero_identity_fields():
|
||||
rows = build_usage_stats(
|
||||
reports=[_report(_item(product_name=0, process_name=0, allocated_minutes=10))],
|
||||
category="mold",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].product_name == "0"
|
||||
assert rows[0].process_name == "0"
|
||||
assert json.loads(rows[0].id)[3] == "0"
|
||||
assert json.loads(rows[0].id)[4] == "0"
|
||||
|
||||
|
||||
def test_build_usage_stats_uses_structured_ids_when_fields_contain_delimiter():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(attendance_point_name="A||B", product_name="C", process_name="1", allocated_minutes=10),
|
||||
_item(attendance_point_name="A", product_name="B||C", process_name="1", allocated_minutes=20),
|
||||
)
|
||||
],
|
||||
category="mold",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
ids = {row.id for row in rows}
|
||||
assert len(ids) == 2
|
||||
assert all(json.loads(row_id) for row_id in ids)
|
||||
assert {tuple(json.loads(row_id)[:5]) for row_id in ids} == {
|
||||
("mold", "A||B", "A||B / C / 1 / 普通", "C", "1"),
|
||||
("mold", "A", "A / B||C / 1 / 普通", "B||C", "1"),
|
||||
}
|
||||
|
||||
|
||||
def test_build_usage_stats_for_cleaning_mold_uses_good_quantity():
|
||||
rows = build_usage_stats(
|
||||
reports=[_report(_item(stamping_method="清洗", allocated_minutes=80, good_qty=64))],
|
||||
category="mold",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].metric_kind == "quantity"
|
||||
assert rows[0].value == 64
|
||||
assert rows[0].object_type == "清洗模具"
|
||||
assert "清洗" in rows[0].tags
|
||||
|
||||
|
||||
def test_build_usage_stats_for_mold_skips_misc_and_marks_multi_person():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(
|
||||
product_name="处理杂活",
|
||||
process_name="杂活",
|
||||
stamping_method="处理杂活",
|
||||
allocated_minutes=30,
|
||||
),
|
||||
_item(operator_count=2, allocated_minutes=40),
|
||||
)
|
||||
],
|
||||
category="mold",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].product_name == "产品A"
|
||||
assert rows[0].value == 40
|
||||
assert rows[0].object_type == "多人工序"
|
||||
assert "多人协作" in rows[0].tags
|
||||
|
||||
|
||||
def test_build_usage_stats_for_non_cleaning_device_defaults_object_type_to_stamping_device():
|
||||
rows = build_usage_stats(
|
||||
reports=[_report(_item(device_no="29#", allocated_minutes=25))],
|
||||
category="device",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].object_type == "冲压设备"
|
||||
|
||||
|
||||
def test_build_usage_stats_for_cleaning_device_defaults_object_type_to_cleaning_device():
|
||||
rows = build_usage_stats(
|
||||
reports=[
|
||||
_report(
|
||||
_item(
|
||||
device_no="清洗机Z",
|
||||
stamping_method="清洗",
|
||||
good_qty=10,
|
||||
)
|
||||
)
|
||||
],
|
||||
category="device",
|
||||
equipment_type_by_key={},
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0].object_type == "清洗设备"
|
||||
|
||||
|
||||
def test_filter_usage_rows_searches_all_visible_fields():
|
||||
rows = [
|
||||
UsageStatRow(
|
||||
id="device-1",
|
||||
category="device",
|
||||
name="28#",
|
||||
metric_kind="minutes",
|
||||
object_type="冲压设备",
|
||||
value=360,
|
||||
report_count=2,
|
||||
attendance_point_name="嘉恒",
|
||||
),
|
||||
UsageStatRow(
|
||||
id="mold-1",
|
||||
category="mold",
|
||||
name="旭升 / 产品B / 2 / 连续模",
|
||||
metric_kind="minutes",
|
||||
object_type="连续模",
|
||||
value=45,
|
||||
report_count=9,
|
||||
attendance_point_name="旭升",
|
||||
product_name="产品B",
|
||||
process_name="2",
|
||||
stamping_method="连续模",
|
||||
tags=["连续模", "多人协作"],
|
||||
),
|
||||
]
|
||||
|
||||
assert _filter_usage_rows(rows, "") == rows
|
||||
assert [row.id for row in _filter_usage_rows(rows, "嘉恒")] == ["device-1"]
|
||||
assert [row.id for row in _filter_usage_rows(rows, "冲压")] == ["device-1"]
|
||||
assert [row.id for row in _filter_usage_rows(rows, "产品B")] == ["mold-1"]
|
||||
assert [row.id for row in _filter_usage_rows(rows, "多人协作")] == ["mold-1"]
|
||||
assert [row.id for row in _filter_usage_rows(rows, "360")] == ["device-1"]
|
||||
assert [row.id for row in _filter_usage_rows(rows, "9")] == ["mold-1"]
|
||||
|
||||
|
||||
def test_export_usage_stats_rows_writes_device_and_mold_sheets():
|
||||
device_rows = [
|
||||
UsageStatRow(
|
||||
id="device-minutes",
|
||||
category="device",
|
||||
name="28#",
|
||||
metric_kind="minutes",
|
||||
object_type="冲压设备",
|
||||
value=360,
|
||||
report_count=2,
|
||||
attendance_point_name="嘉恒",
|
||||
),
|
||||
UsageStatRow(
|
||||
id="device-quantity",
|
||||
category="device",
|
||||
name="清洗机A",
|
||||
metric_kind="quantity",
|
||||
object_type="清洗设备",
|
||||
value=30,
|
||||
report_count=1,
|
||||
attendance_point_name="嘉恒",
|
||||
),
|
||||
]
|
||||
mold_rows = [
|
||||
UsageStatRow(
|
||||
id="mold-minutes",
|
||||
category="mold",
|
||||
name="嘉恒 / 产品A / 1 / 普通",
|
||||
metric_kind="minutes",
|
||||
object_type="普通模具",
|
||||
value=45,
|
||||
report_count=3,
|
||||
attendance_point_name="嘉恒",
|
||||
product_name="产品A",
|
||||
process_name="1",
|
||||
stamping_method="普通",
|
||||
),
|
||||
UsageStatRow(
|
||||
id="mold-quantity",
|
||||
category="mold",
|
||||
name="嘉恒 / 产品B / 2 / 清洗",
|
||||
metric_kind="quantity",
|
||||
object_type="清洗模具",
|
||||
value=18,
|
||||
report_count=1,
|
||||
attendance_point_name="嘉恒",
|
||||
product_name="产品B",
|
||||
process_name="2",
|
||||
stamping_method="清洗",
|
||||
),
|
||||
]
|
||||
|
||||
workbook = load_workbook(BytesIO(export_usage_stats_rows(device_rows, mold_rows)))
|
||||
|
||||
assert workbook.sheetnames == ["设备统计", "模具统计"]
|
||||
device_sheet = workbook["设备统计"]
|
||||
mold_sheet = workbook["模具统计"]
|
||||
assert [cell.value for cell in device_sheet[1]] == ["考勤点", "设备号", "设备类型", "使用时长(分钟)", "使用时长(小时)", "使用时长(天)", "清洗数量", "报工次数"]
|
||||
assert [cell.value for cell in mold_sheet[1]] == ["考勤点", "产品名称", "工序", "冲压方式", "使用时长(分钟)", "使用时长(小时)", "使用时长(天)", "清洗数量", "报工次数"]
|
||||
assert [cell.value for cell in device_sheet[2]] == ["嘉恒", "28#", "冲压设备", 360, 6, 0.25, 0, 2]
|
||||
assert [cell.value for cell in device_sheet[3]] == ["嘉恒", "清洗机A", "清洗设备", 0, 0, 0, 30, 1]
|
||||
assert [cell.value for cell in mold_sheet[2]] == ["嘉恒", "产品A", "1", "普通", 45, 0.75, 0.03, 0, 3]
|
||||
assert [cell.value for cell in mold_sheet[3]] == ["嘉恒", "产品B", "2", "清洗", 0, 0, 0, 18, 1]
|
||||
|
||||
|
||||
def test_build_usage_stats_export_response_returns_xlsx_attachment():
|
||||
device_rows = [
|
||||
UsageStatRow(
|
||||
id="device-minutes",
|
||||
category="device",
|
||||
name="28#",
|
||||
metric_kind="minutes",
|
||||
object_type="冲压设备",
|
||||
value=75,
|
||||
report_count=2,
|
||||
attendance_point_name="嘉恒",
|
||||
)
|
||||
]
|
||||
mold_rows = [
|
||||
UsageStatRow(
|
||||
id="mold-minutes",
|
||||
category="mold",
|
||||
name="嘉恒 / 产品A / 1 / 普通",
|
||||
metric_kind="minutes",
|
||||
object_type="普通模具",
|
||||
value=45,
|
||||
report_count=3,
|
||||
attendance_point_name="嘉恒",
|
||||
product_name="产品A",
|
||||
process_name="1",
|
||||
stamping_method="普通",
|
||||
)
|
||||
]
|
||||
|
||||
response = _build_usage_stats_export_response(device_rows, mold_rows)
|
||||
|
||||
assert response.media_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
assert "usage-stats.xlsx" in response.headers["Content-Disposition"]
|
||||
workbook = load_workbook(BytesIO(response.body))
|
||||
assert workbook.sheetnames == ["设备统计", "模具统计"]
|
||||
|
||||
|
||||
def test_sort_usage_rows_by_value_desc_then_report_count_desc_then_name():
|
||||
rows = [
|
||||
UsageStatRow(id="b", category="device", name="B", metric_kind="minutes", object_type="冲压设备", value=10, report_count=1),
|
||||
UsageStatRow(id="c", category="device", name="C", metric_kind="minutes", object_type="冲压设备", value=20, report_count=1),
|
||||
UsageStatRow(id="a", category="device", name="A", metric_kind="minutes", object_type="冲压设备", value=20, report_count=3),
|
||||
]
|
||||
|
||||
sorted_rows = _sort_usage_rows(rows, "value")
|
||||
|
||||
assert [row.name for row in sorted_rows] == ["A", "C", "B"]
|
||||
|
||||
|
||||
def test_sort_usage_rows_by_count_desc_then_value_desc_then_name():
|
||||
rows = [
|
||||
UsageStatRow(id="b", category="device", name="B", metric_kind="minutes", object_type="冲压设备", value=20, report_count=2),
|
||||
UsageStatRow(id="c", category="device", name="C", metric_kind="minutes", object_type="冲压设备", value=30, report_count=1),
|
||||
UsageStatRow(id="a", category="device", name="A", metric_kind="minutes", object_type="冲压设备", value=30, report_count=2),
|
||||
]
|
||||
|
||||
sorted_rows = _sort_usage_rows(rows, "count")
|
||||
|
||||
assert [row.name for row in sorted_rows] == ["A", "B", "C"]
|
||||
|
||||
|
||||
def test_to_schema_row_maps_usage_stat_fields():
|
||||
row = UsageStatRow(
|
||||
id="row-1",
|
||||
category="mold",
|
||||
name="嘉恒 / 产品A / 1 / 普通",
|
||||
metric_kind="minutes",
|
||||
object_type="普通模具",
|
||||
value=12.34,
|
||||
report_count=5,
|
||||
tags=["多人协作"],
|
||||
attendance_point_name="嘉恒",
|
||||
product_name="产品A",
|
||||
process_name="1",
|
||||
stamping_method="普通",
|
||||
)
|
||||
|
||||
schema_row = _to_schema_row(row)
|
||||
|
||||
assert isinstance(schema_row, UsageStatsRow)
|
||||
assert schema_row.model_dump() == {
|
||||
"id": "row-1",
|
||||
"category": "mold",
|
||||
"name": "嘉恒 / 产品A / 1 / 普通",
|
||||
"metric_kind": "minutes",
|
||||
"object_type": "普通模具",
|
||||
"value": 12.34,
|
||||
"report_count": 5,
|
||||
"tags": ["多人协作"],
|
||||
"attendance_point_name": "嘉恒",
|
||||
"product_name": "产品A",
|
||||
"process_name": "1",
|
||||
"stamping_method": "普通",
|
||||
}
|
||||
|
||||
|
||||
def test_to_detail_out_maps_target_identity_fields():
|
||||
row = UsageStatRow(
|
||||
id="target-1",
|
||||
category="mold",
|
||||
name="嘉恒 / 产品A / 1 / 连续模",
|
||||
metric_kind="minutes",
|
||||
object_type="连续模",
|
||||
value=88,
|
||||
report_count=4,
|
||||
tags=["连续模"],
|
||||
attendance_point_name="嘉恒",
|
||||
product_name="产品A",
|
||||
process_name="1",
|
||||
stamping_method="连续模",
|
||||
)
|
||||
|
||||
detail = _to_detail_out(row)
|
||||
|
||||
assert isinstance(detail, UsageStatsDetailOut)
|
||||
assert detail.model_dump() == {
|
||||
"id": "target-1",
|
||||
"category": "mold",
|
||||
"attendance_point_name": "嘉恒",
|
||||
"name": "嘉恒 / 产品A / 1 / 连续模",
|
||||
"object_type": "连续模",
|
||||
"metric_kind": "minutes",
|
||||
"value": 88,
|
||||
"report_count": 4,
|
||||
"product_name": "产品A",
|
||||
"process_name": "1",
|
||||
"stamping_method": "连续模",
|
||||
"tags": ["连续模"],
|
||||
"daily_rows": [],
|
||||
"report_rows": [],
|
||||
}
|
||||
|
||||
|
||||
def test_usage_stats_detail_out_identity_fields_have_empty_defaults():
|
||||
detail = UsageStatsDetailOut(category="device")
|
||||
|
||||
assert detail.id == ""
|
||||
assert detail.product_name == ""
|
||||
assert detail.process_name == ""
|
||||
assert detail.stamping_method == ""
|
||||
assert detail.tags == []
|
||||
|
||||
|
||||
def test_matches_target_allows_accessible_point_when_attendance_point_name_is_empty():
|
||||
row = UsageStatRow(
|
||||
id="row-1",
|
||||
category="device",
|
||||
name="28#",
|
||||
metric_kind="minutes",
|
||||
object_type="冲压设备",
|
||||
attendance_point_name="嘉恒",
|
||||
)
|
||||
|
||||
assert _matches_target(
|
||||
row,
|
||||
id="",
|
||||
category="device",
|
||||
attendance_point_name="",
|
||||
name="28#",
|
||||
product_name="",
|
||||
process_name="",
|
||||
stamping_method="",
|
||||
metric_kind="",
|
||||
)
|
||||
|
||||
|
||||
def test_matches_target_uses_metric_kind_to_disambiguate_same_device_name():
|
||||
minutes_row = UsageStatRow(
|
||||
id="minutes-id",
|
||||
category="device",
|
||||
name="清洗机A",
|
||||
metric_kind="minutes",
|
||||
object_type="清洗设备",
|
||||
attendance_point_name="嘉恒",
|
||||
)
|
||||
quantity_row = UsageStatRow(
|
||||
id="quantity-id",
|
||||
category="device",
|
||||
name="清洗机A",
|
||||
metric_kind="quantity",
|
||||
object_type="清洗设备",
|
||||
attendance_point_name="嘉恒",
|
||||
)
|
||||
|
||||
target_args = {
|
||||
"id": "",
|
||||
"category": "device",
|
||||
"attendance_point_name": "",
|
||||
"name": "清洗机A",
|
||||
"product_name": "",
|
||||
"process_name": "",
|
||||
"stamping_method": "",
|
||||
"metric_kind": "quantity",
|
||||
}
|
||||
|
||||
assert not _matches_target(minutes_row, **target_args)
|
||||
assert _matches_target(quantity_row, **target_args)
|
||||
|
||||
|
||||
def test_matches_target_uses_id_before_ambiguous_device_fields():
|
||||
minutes_row = UsageStatRow(
|
||||
id="minutes-id",
|
||||
category="device",
|
||||
name="清洗机A",
|
||||
metric_kind="minutes",
|
||||
object_type="清洗设备",
|
||||
attendance_point_name="嘉恒",
|
||||
)
|
||||
quantity_row = UsageStatRow(
|
||||
id="quantity-id",
|
||||
category="device",
|
||||
name="清洗机A",
|
||||
metric_kind="quantity",
|
||||
object_type="清洗设备",
|
||||
attendance_point_name="嘉恒",
|
||||
)
|
||||
|
||||
target_args = {
|
||||
"id": "minutes-id",
|
||||
"category": "device",
|
||||
"attendance_point_name": "",
|
||||
"name": "清洗机A",
|
||||
"product_name": "",
|
||||
"process_name": "",
|
||||
"stamping_method": "",
|
||||
"metric_kind": "quantity",
|
||||
}
|
||||
|
||||
assert _matches_target(minutes_row, **target_args)
|
||||
assert not _matches_target(quantity_row, **target_args)
|
||||
@@ -0,0 +1,181 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.routers import reports
|
||||
|
||||
|
||||
def _session():
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text("create table md_item (id integer primary key, item_code text, item_name text)"))
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
create table pp_production_batch_ledger (
|
||||
id integer primary key,
|
||||
material_lot_no text,
|
||||
product_item_id integer,
|
||||
status text,
|
||||
miniapp_selectable integer
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
SessionLocal = sessionmaker(bind=engine, future=True)
|
||||
return SessionLocal()
|
||||
|
||||
|
||||
def test_raw_material_batch_options_returns_material_lot_numbers_for_product_process(monkeypatch):
|
||||
class FrozenDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None): # noqa: ANN001
|
||||
_ = tz
|
||||
return cls(2026, 6, 2, 10, 0, 0)
|
||||
|
||||
monkeypatch.setattr(reports, "datetime", FrozenDateTime)
|
||||
db = _session()
|
||||
try:
|
||||
db.execute(
|
||||
text("insert into md_item (id, item_code, item_name) values (1, 'FG-001', '钢制碗'), (2, 'FG-002', '钢制盆')")
|
||||
)
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
insert into pp_production_batch_ledger (id, material_lot_no, product_item_id, status, miniapp_selectable)
|
||||
values
|
||||
(1, 'YL0001', 1, '在生产', 1),
|
||||
(2, 'YL0002', 1, '在生产', 1),
|
||||
(3, 'YL0003', 1, '锁单', 0),
|
||||
(4, '', 1, '在生产', 1),
|
||||
(5, null, 1, '在生产', 1),
|
||||
(6, 'YL0004', 2, '在生产', 1)
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
options = reports._raw_material_batch_options(
|
||||
db,
|
||||
project_no="FG-001",
|
||||
product_name="钢制碗",
|
||||
process_name="1序",
|
||||
)
|
||||
|
||||
assert options == ["YL0001", "YL0002"]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_raw_material_batch_options_matches_project_no_when_product_name_is_empty(monkeypatch):
|
||||
class FrozenDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None): # noqa: ANN001
|
||||
_ = tz
|
||||
return cls(2026, 6, 2, 10, 0, 0)
|
||||
|
||||
monkeypatch.setattr(reports, "datetime", FrozenDateTime)
|
||||
db = _session()
|
||||
try:
|
||||
db.execute(text("insert into md_item (id, item_code, item_name) values (1, 'FG-001', '钢制碗')"))
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
insert into pp_production_batch_ledger (id, material_lot_no, product_item_id, status, miniapp_selectable)
|
||||
values (1, 'YL0001', 1, '在生产', 1)
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
options = reports._raw_material_batch_options(
|
||||
db,
|
||||
project_no="FG-001",
|
||||
product_name="",
|
||||
process_name="1序",
|
||||
)
|
||||
|
||||
assert options == ["YL0001"]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_raw_material_batch_options_uses_project_no_to_narrow_duplicate_product_names(monkeypatch):
|
||||
class FrozenDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None): # noqa: ANN001
|
||||
_ = tz
|
||||
return cls(2026, 6, 2, 10, 0, 0)
|
||||
|
||||
monkeypatch.setattr(reports, "datetime", FrozenDateTime)
|
||||
db = _session()
|
||||
try:
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
insert into md_item (id, item_code, item_name)
|
||||
values
|
||||
(1, 'FG-001', '钢制碗'),
|
||||
(2, 'FG-002', '钢制碗')
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
insert into pp_production_batch_ledger (id, material_lot_no, product_item_id, status, miniapp_selectable)
|
||||
values
|
||||
(1, 'YL0001', 1, '在生产', 1),
|
||||
(2, 'YL0002', 2, '在生产', 1)
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
options = reports._raw_material_batch_options(
|
||||
db,
|
||||
project_no="FG-001",
|
||||
product_name="钢制碗",
|
||||
process_name="1序",
|
||||
)
|
||||
|
||||
assert options == ["YL0001"]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_raw_material_batch_options_excludes_locked_and_unselectable_ledgers(monkeypatch):
|
||||
class FrozenDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None): # noqa: ANN001
|
||||
_ = tz
|
||||
return cls(2026, 6, 2, 10, 0, 0)
|
||||
|
||||
monkeypatch.setattr(reports, "datetime", FrozenDateTime)
|
||||
db = _session()
|
||||
try:
|
||||
db.execute(text("insert into md_item (id, item_code, item_name) values (1, 'FG-001', '钢制碗')"))
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
insert into pp_production_batch_ledger (id, material_lot_no, product_item_id, status, miniapp_selectable)
|
||||
values
|
||||
(1, 'YL0001', 1, '锁单', 0),
|
||||
(2, 'YL0002', 1, '在生产', 0),
|
||||
(3, 'YL0003', 1, '在生产', 1)
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
options = reports._raw_material_batch_options(
|
||||
db,
|
||||
project_no="FG-001",
|
||||
product_name="钢制碗",
|
||||
process_name="1序",
|
||||
)
|
||||
|
||||
assert options == ["YL0003"]
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user