388 lines
13 KiB
Python
388 lines
13 KiB
Python
import csv
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from article_data_manager.enrichment.erp import (
|
|
enrich_rollcalc_articles_from_erp,
|
|
write_enrichment_outputs,
|
|
)
|
|
|
|
|
|
def write_rollcalc(path: Path, article_numbers: list[str]) -> Path:
|
|
payload = [
|
|
{
|
|
"nr": nr,
|
|
"name": f"RollCalc {nr}",
|
|
"thickness": float(index + 1),
|
|
"area_weight": 100.0 + index,
|
|
"core_type": 0.0,
|
|
}
|
|
for index, nr in enumerate(article_numbers)
|
|
]
|
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_erp(path: Path, rows: list[dict[str, str]]) -> Path:
|
|
fieldnames = [
|
|
"SL_ITEM_NO",
|
|
"ROP_PRODUCT_WIDTH",
|
|
"ROP_RATE_OF_PRODUCTION",
|
|
"SL_MINIMUM_PRODUCTION_QUANTITY",
|
|
"WPL_WORKPLACE_TEXT",
|
|
"SL_PRODUCTION_SPEED",
|
|
"kg_qm",
|
|
]
|
|
with path.open("w", encoding="utf-8", newline="") as csv_file:
|
|
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
for row in rows:
|
|
writer.writerow({field: row.get(field, "") for field in fieldnames})
|
|
return path
|
|
|
|
|
|
def test_unique_erp_match_enriches_selected_fields(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{
|
|
"SL_ITEM_NO": "214700",
|
|
"ROP_PRODUCT_WIDTH": "6,00",
|
|
"ROP_RATE_OF_PRODUCTION": "18.5",
|
|
"SL_MINIMUM_PRODUCTION_QUANTITY": "5000",
|
|
"WPL_WORKPLACE_TEXT": "Anlage 3",
|
|
}
|
|
],
|
|
)
|
|
|
|
article = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0]
|
|
|
|
assert article.status == "complete"
|
|
assert article.to_output_dict()["erp"] == {
|
|
"product_width_m": 6.0,
|
|
"line_speed_m_min": 18.5,
|
|
"minimum_production_quantity": 5000.0,
|
|
"workplace": "Anlage 3",
|
|
}
|
|
|
|
|
|
def test_no_erp_match_keeps_rollcalc_article_with_null_erp_object(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["999999"])
|
|
erp_path = write_erp(tmp_path / "erp.csv", [{"SL_ITEM_NO": "214700"}])
|
|
|
|
article = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0]
|
|
|
|
assert article.status == "no_erp_match"
|
|
assert article.erp_match_count == 0
|
|
assert article.to_output_dict()["erp"] == {
|
|
"product_width_m": None,
|
|
"line_speed_m_min": None,
|
|
"minimum_production_quantity": None,
|
|
"workplace": None,
|
|
}
|
|
assert {field.status for field in article.fields.values()} == {"no_erp_match"}
|
|
|
|
|
|
def test_multiple_erp_rows_with_identical_width_are_collapsed(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6,00"},
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6.00"},
|
|
],
|
|
)
|
|
|
|
field = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].fields[
|
|
"product_width_m"
|
|
]
|
|
|
|
assert field.value == 6.0
|
|
assert field.status == "same_value_multiple_rows"
|
|
|
|
|
|
def test_conflicting_width_is_reported_as_null_field(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6,00"},
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6.10"},
|
|
],
|
|
)
|
|
|
|
article = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0]
|
|
|
|
assert article.status == "conflict"
|
|
assert article.fields["product_width_m"].value is None
|
|
assert article.fields["product_width_m"].status == "conflict"
|
|
|
|
|
|
def test_identical_line_speeds_in_multiple_rows_are_collapsed(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_RATE_OF_PRODUCTION": "18,5"},
|
|
{"SL_ITEM_NO": "214700", "ROP_RATE_OF_PRODUCTION": "18.50"},
|
|
],
|
|
)
|
|
|
|
field = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].fields[
|
|
"line_speed_m_min"
|
|
]
|
|
|
|
assert field.value == 18.5
|
|
assert field.status == "same_value_multiple_rows"
|
|
|
|
|
|
def test_conflicting_line_speeds_are_reported(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_RATE_OF_PRODUCTION": "18,5"},
|
|
{"SL_ITEM_NO": "214700", "ROP_RATE_OF_PRODUCTION": "19,0"},
|
|
],
|
|
)
|
|
|
|
article = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0]
|
|
|
|
assert article.status == "conflict"
|
|
assert article.fields["line_speed_m_min"].status == "conflict"
|
|
|
|
|
|
def test_missing_minimum_production_quantity_is_reported(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(tmp_path / "erp.csv", [{"SL_ITEM_NO": "214700"}])
|
|
|
|
field = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].fields[
|
|
"minimum_production_quantity"
|
|
]
|
|
|
|
assert field.value is None
|
|
assert field.status == "missing"
|
|
|
|
|
|
def test_identical_workplaces_in_multiple_rows_are_collapsed(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "WPL_WORKPLACE_TEXT": "Anlage 3"},
|
|
{"SL_ITEM_NO": "214700", "WPL_WORKPLACE_TEXT": "Anlage 3"},
|
|
],
|
|
)
|
|
|
|
field = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].fields[
|
|
"workplace"
|
|
]
|
|
|
|
assert field.value == "Anlage 3"
|
|
assert field.status == "same_value_multiple_rows"
|
|
|
|
|
|
def test_different_workplaces_are_reported_as_conflict(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "WPL_WORKPLACE_TEXT": "Anlage 3"},
|
|
{"SL_ITEM_NO": "214700", "WPL_WORKPLACE_TEXT": "Anlage 4"},
|
|
],
|
|
)
|
|
|
|
field = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].fields[
|
|
"workplace"
|
|
]
|
|
|
|
assert field.value is None
|
|
assert field.status == "conflict"
|
|
|
|
|
|
def test_decimal_values_with_comma_and_point_are_supported(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700", "000123"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6,00"},
|
|
{"SL_ITEM_NO": "000123", "ROP_PRODUCT_WIDTH": "1.20"},
|
|
],
|
|
)
|
|
|
|
articles = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles
|
|
|
|
assert articles[0].fields["product_width_m"].value == 6.0
|
|
assert articles[1].fields["product_width_m"].value == 1.2
|
|
|
|
|
|
def test_invalid_numeric_value_sets_field_to_null(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "not-a-number"}],
|
|
)
|
|
|
|
article = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0]
|
|
|
|
assert article.status == "conflict"
|
|
assert article.fields["product_width_m"].value is None
|
|
assert article.fields["product_width_m"].status == "invalid"
|
|
|
|
|
|
def test_empty_erp_fields_do_not_create_conflicts(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": ""},
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6.00"},
|
|
],
|
|
)
|
|
|
|
field = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].fields[
|
|
"product_width_m"
|
|
]
|
|
|
|
assert field.value == 6.0
|
|
assert field.status == "unique"
|
|
|
|
|
|
def test_leading_zero_article_number_matches_exactly(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["000123"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[{"SL_ITEM_NO": "000123", "ROP_PRODUCT_WIDTH": "1.20"}],
|
|
)
|
|
|
|
article = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0]
|
|
|
|
assert article.nr == "000123"
|
|
assert article.fields["product_width_m"].value == 1.2
|
|
|
|
|
|
def test_erp_article_without_rollcalc_match_is_not_output(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "214700", "ROP_PRODUCT_WIDTH": "6.00"},
|
|
{"SL_ITEM_NO": "ERPONLY", "ROP_PRODUCT_WIDTH": "9.99"},
|
|
],
|
|
)
|
|
|
|
result = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path)
|
|
|
|
assert [article.nr for article in result.articles] == ["214700"]
|
|
|
|
|
|
def test_rollcalc_order_is_preserved(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["300", "100", "200"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{"SL_ITEM_NO": "100"},
|
|
{"SL_ITEM_NO": "200"},
|
|
{"SL_ITEM_NO": "300"},
|
|
],
|
|
)
|
|
|
|
result = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path)
|
|
|
|
assert [article.nr for article in result.articles] == ["300", "100", "200"]
|
|
|
|
|
|
def test_existing_area_weight_remains_unchanged_and_erp_weight_fields_are_ignored(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{
|
|
"SL_ITEM_NO": "214700",
|
|
"kg_qm": "999",
|
|
"SL_PRODUCTION_SPEED": "999",
|
|
"ROP_PRODUCT_WIDTH": "6.00",
|
|
}
|
|
],
|
|
)
|
|
|
|
output = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).articles[0].to_output_dict()
|
|
|
|
assert output["area_weight"] == 100.0
|
|
assert "kg_qm" not in output["erp"]
|
|
assert "SL_PRODUCTION_SPEED" not in output["erp"]
|
|
|
|
|
|
def test_source_files_remain_unchanged(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(tmp_path / "erp.csv", [{"SL_ITEM_NO": "214700"}])
|
|
original_rollcalc = rollcalc_path.read_bytes()
|
|
original_erp = erp_path.read_bytes()
|
|
|
|
result = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path)
|
|
write_enrichment_outputs(result, tmp_path / "generated.json", tmp_path / "report.csv")
|
|
|
|
assert rollcalc_path.read_bytes() == original_rollcalc
|
|
assert erp_path.read_bytes() == original_erp
|
|
|
|
|
|
def test_json_output_is_deterministic(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(tmp_path / "erp.csv", [{"SL_ITEM_NO": "214700"}])
|
|
result = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path)
|
|
first_json = tmp_path / "first.json"
|
|
second_json = tmp_path / "second.json"
|
|
|
|
write_enrichment_outputs(result, first_json, tmp_path / "first.csv")
|
|
write_enrichment_outputs(result, second_json, tmp_path / "second.csv")
|
|
|
|
assert first_json.read_text(encoding="utf-8") == second_json.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_csv_report_is_deterministic_and_contains_statuses(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["214700"])
|
|
erp_path = write_erp(tmp_path / "erp.csv", [{"SL_ITEM_NO": "214700"}])
|
|
result = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path)
|
|
first_report = tmp_path / "first.csv"
|
|
second_report = tmp_path / "second.csv"
|
|
|
|
write_enrichment_outputs(result, tmp_path / "first.json", first_report)
|
|
write_enrichment_outputs(result, tmp_path / "second.json", second_report)
|
|
|
|
first_content = first_report.read_text(encoding="utf-8-sig")
|
|
assert first_content == second_report.read_text(encoding="utf-8-sig")
|
|
assert "nr;name;erp_match_count;status" in first_content
|
|
assert "missing" in first_content
|
|
|
|
|
|
def test_summary_counts_articles_and_field_statuses(tmp_path: Path) -> None:
|
|
rollcalc_path = write_rollcalc(tmp_path / "article-data.json", ["A", "B", "C"])
|
|
erp_path = write_erp(
|
|
tmp_path / "erp.csv",
|
|
[
|
|
{
|
|
"SL_ITEM_NO": "A",
|
|
"ROP_PRODUCT_WIDTH": "1.0",
|
|
"ROP_RATE_OF_PRODUCTION": "2.0",
|
|
"SL_MINIMUM_PRODUCTION_QUANTITY": "3",
|
|
"WPL_WORKPLACE_TEXT": "Anlage",
|
|
},
|
|
{"SL_ITEM_NO": "B", "ROP_PRODUCT_WIDTH": "bad"},
|
|
],
|
|
)
|
|
|
|
summary = enrich_rollcalc_articles_from_erp(rollcalc_path, erp_path).summary
|
|
|
|
assert summary.rollcalc_article_count == 3
|
|
assert summary.with_erp_match_count == 2
|
|
assert summary.without_erp_match_count == 1
|
|
assert summary.complete_count == 1
|
|
assert summary.partial_count == 0
|
|
assert summary.conflict_count == 1
|
|
assert summary.field_summaries["product_width_m"].unique == 1
|
|
assert summary.field_summaries["product_width_m"].invalid == 1
|
|
assert summary.field_summaries["product_width_m"].no_erp_match == 1
|