按自然年度统计供应商合作频次
This commit is contained in:
parent
dbf7ba2d64
commit
b7a79f1791
@ -25,7 +25,7 @@ class SupplierMetric(ApiSchema):
|
|||||||
supplier_score: float | None = None
|
supplier_score: float | None = None
|
||||||
risk_score: float | None = None
|
risk_score: float | None = None
|
||||||
main_category_count: int
|
main_category_count: int
|
||||||
cooperation_count_basis: str = "按近3年采购订单去重单据编号统计"
|
cooperation_count_basis: str = "按自然年度边界采购订单去重单据编号统计"
|
||||||
anomaly_rate_basis: str = "暂无采购订单/生产批次总量,无法计算真实异常率"
|
anomaly_rate_basis: str = "暂无采购订单/生产批次总量,无法计算真实异常率"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections.abc import AsyncGenerator, Sequence
|
from collections.abc import AsyncGenerator, Sequence
|
||||||
from datetime import UTC, date, datetime, timedelta
|
from datetime import UTC, date, datetime
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from sqlalchemy import delete, func, or_, select
|
from sqlalchemy import delete, func, or_, select
|
||||||
@ -36,14 +36,14 @@ from app.schemas.risk_prediction import (
|
|||||||
)
|
)
|
||||||
from app.services.database_risk_scoring import DatabaseRiskScoringService
|
from app.services.database_risk_scoring import DatabaseRiskScoringService
|
||||||
from app.services.llm_client import LLMClient
|
from app.services.llm_client import LLMClient
|
||||||
from app.services.purchase_orders import supplier_order_counts
|
from app.services.purchase_orders import calendar_year_window, supplier_order_counts
|
||||||
from app.services.rag import AnomalyCaseRAG
|
from app.services.rag import AnomalyCaseRAG
|
||||||
|
|
||||||
logger = logging.getLogger("dieya_risk_prediction.conversations")
|
logger = logging.getLogger("dieya_risk_prediction.conversations")
|
||||||
|
|
||||||
|
|
||||||
class ConversationService:
|
class ConversationService:
|
||||||
_supplier_history_days = 365 * 3
|
_supplier_history_years = 3
|
||||||
_engine = BaselineRiskEngine()
|
_engine = BaselineRiskEngine()
|
||||||
_risk_intents = {
|
_risk_intents = {
|
||||||
"assessment",
|
"assessment",
|
||||||
@ -582,7 +582,7 @@ class ConversationService:
|
|||||||
self.session,
|
self.session,
|
||||||
name,
|
name,
|
||||||
self._supplier_history_start(),
|
self._supplier_history_start(),
|
||||||
datetime.now(UTC).date(),
|
self._supplier_history_end(),
|
||||||
)
|
)
|
||||||
if hasattr(self, "session")
|
if hasattr(self, "session")
|
||||||
else None
|
else None
|
||||||
@ -626,9 +626,9 @@ class ConversationService:
|
|||||||
"supplierScore": supplier_dimension.score if supplier_dimension else None,
|
"supplierScore": supplier_dimension.score if supplier_dimension else None,
|
||||||
"topProblems": problems,
|
"topProblems": problems,
|
||||||
"suggestions": self._supplier_management_suggestions(tier),
|
"suggestions": self._supplier_management_suggestions(tier),
|
||||||
"frequencyBasis": "近3年采购订单去重单据数" if has_order_data else "暂无匹配采购订单",
|
"frequencyBasis": "按自然年度边界采购订单去重单据数" if has_order_data else "暂无匹配采购订单",
|
||||||
"note": (
|
"note": (
|
||||||
"合作次数按近3年采购订单去重单据编号统计。"
|
"合作次数按自然年度边界采购订单去重单据编号统计。"
|
||||||
if has_order_data
|
if has_order_data
|
||||||
else "未匹配到该供应商采购订单,不展示合作次数。"
|
else "未匹配到该供应商采购订单,不展示合作次数。"
|
||||||
),
|
),
|
||||||
@ -938,7 +938,7 @@ class ConversationService:
|
|||||||
[
|
[
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= since,
|
AnomalyCase.date >= since,
|
||||||
AnomalyCase.date <= datetime.now(UTC).date(),
|
AnomalyCase.date <= self._supplier_history_end(),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
rows = await self.session.execute(
|
rows = await self.session.execute(
|
||||||
@ -1082,7 +1082,7 @@ class ConversationService:
|
|||||||
self.session,
|
self.session,
|
||||||
supplier,
|
supplier,
|
||||||
self._supplier_history_start(),
|
self._supplier_history_start(),
|
||||||
datetime.now(UTC).date(),
|
self._supplier_history_end(),
|
||||||
)
|
)
|
||||||
if supplier
|
if supplier
|
||||||
else None
|
else None
|
||||||
@ -1175,9 +1175,9 @@ class ConversationService:
|
|||||||
),
|
),
|
||||||
tier=tier,
|
tier=tier,
|
||||||
suggestion=";".join(self._supplier_management_suggestions(tier)),
|
suggestion=";".join(self._supplier_management_suggestions(tier)),
|
||||||
frequency_basis="近3年采购订单去重单据数" if cooperation_count else "暂无匹配采购订单",
|
frequency_basis="按自然年度边界采购订单去重单据数" if cooperation_count else "暂无匹配采购订单",
|
||||||
note=(
|
note=(
|
||||||
"合作次数按采购订单去重单据编号统计;异常数仍按近3年异常案例统计。"
|
"合作次数按自然年度边界采购订单去重单据编号统计;异常数按自然年度边界异常案例统计。"
|
||||||
if cooperation_count
|
if cooperation_count
|
||||||
else "未匹配到该供应商采购订单,不展示合作次数。"
|
else "未匹配到该供应商采购订单,不展示合作次数。"
|
||||||
),
|
),
|
||||||
@ -1209,14 +1209,18 @@ class ConversationService:
|
|||||||
column.ilike(f"%{value}%"),
|
column.ilike(f"%{value}%"),
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= self._supplier_history_start(),
|
AnomalyCase.date >= self._supplier_history_start(),
|
||||||
AnomalyCase.date <= datetime.now(UTC).date(),
|
AnomalyCase.date <= self._supplier_history_end(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return int(count or 0)
|
return int(count or 0)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _supplier_history_start(cls) -> date:
|
def _supplier_history_start(cls) -> date:
|
||||||
return datetime.now(UTC).date() - timedelta(days=cls._supplier_history_days)
|
return calendar_year_window(datetime.now(UTC).date(), cls._supplier_history_years)[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _supplier_history_end(cls) -> date:
|
||||||
|
return calendar_year_window(datetime.now(UTC).date(), cls._supplier_history_years)[1]
|
||||||
|
|
||||||
async def _supplier_history_stats(
|
async def _supplier_history_stats(
|
||||||
self,
|
self,
|
||||||
@ -1234,7 +1238,7 @@ class ConversationService:
|
|||||||
column.ilike(f"%{value}%"),
|
column.ilike(f"%{value}%"),
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= self._supplier_history_start(),
|
AnomalyCase.date >= self._supplier_history_start(),
|
||||||
AnomalyCase.date <= datetime.now(UTC).date(),
|
AnomalyCase.date <= self._supplier_history_end(),
|
||||||
)
|
)
|
||||||
.group_by(AnomalyCase.defect_level)
|
.group_by(AnomalyCase.defect_level)
|
||||||
)
|
)
|
||||||
@ -1354,7 +1358,7 @@ class ConversationService:
|
|||||||
),
|
),
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= self._supplier_history_start(),
|
AnomalyCase.date >= self._supplier_history_start(),
|
||||||
AnomalyCase.date <= datetime.now(UTC).date(),
|
AnomalyCase.date <= self._supplier_history_end(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return int(count or 0)
|
return int(count or 0)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.domain.model_config import DEFAULT_FREQUENCY_CAP, DEFAULT_FREQUENCY_COEFFICIENT, DEFAULT_WEIGHTS
|
from app.domain.model_config import DEFAULT_FREQUENCY_CAP, DEFAULT_FREQUENCY_COEFFICIENT, DEFAULT_WEIGHTS
|
||||||
from app.domain.risk_engine import RiskLevel
|
from app.domain.risk_engine import RiskLevel
|
||||||
from app.models.entities import AnomalyCase, RiskModelConfig
|
from app.models.entities import AnomalyCase, RiskModelConfig
|
||||||
from app.services.purchase_orders import supplier_order_counts
|
from app.services.purchase_orders import calendar_year_window, supplier_order_counts
|
||||||
|
|
||||||
STAGES = ("需求发现", "产品立项", "产品开发", "量产上市", "产品生命管理")
|
STAGES = ("需求发现", "产品立项", "产品开发", "量产上市", "产品生命管理")
|
||||||
|
|
||||||
@ -107,8 +107,7 @@ class DatabaseRiskScoringService:
|
|||||||
return await supplier_order_counts(
|
return await supplier_order_counts(
|
||||||
self.session,
|
self.session,
|
||||||
party_name,
|
party_name,
|
||||||
self.as_of - timedelta(days=365 * 3),
|
*calendar_year_window(self.as_of, 3),
|
||||||
self.as_of,
|
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001 - legacy/test databases may not have the order ledger yet
|
except Exception: # noqa: BLE001 - legacy/test databases may not have the order ledger yet
|
||||||
return None
|
return None
|
||||||
@ -127,7 +126,7 @@ class DatabaseRiskScoringService:
|
|||||||
self.frequency_cap = float(config.frequency_cap)
|
self.frequency_cap = float(config.frequency_cap)
|
||||||
|
|
||||||
async def _load_rows(self) -> list[dict[str, Any]]:
|
async def _load_rows(self) -> list[dict[str, Any]]:
|
||||||
start = self.as_of - timedelta(days=365 * 3)
|
start, end = calendar_year_window(self.as_of, 3)
|
||||||
statement = select(
|
statement = select(
|
||||||
AnomalyCase.date,
|
AnomalyCase.date,
|
||||||
AnomalyCase.product_name,
|
AnomalyCase.product_name,
|
||||||
@ -140,7 +139,7 @@ class DatabaseRiskScoringService:
|
|||||||
).where(
|
).where(
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= start,
|
AnomalyCase.date >= start,
|
||||||
AnomalyCase.date <= self.as_of,
|
AnomalyCase.date <= end,
|
||||||
)
|
)
|
||||||
return [dict(row) for row in (await self.session.execute(statement)).mappings().all()]
|
return [dict(row) for row in (await self.session.execute(statement)).mappings().all()]
|
||||||
|
|
||||||
@ -200,9 +199,9 @@ class DatabaseRiskScoringService:
|
|||||||
weight=weight,
|
weight=weight,
|
||||||
status="database",
|
status="database",
|
||||||
reason=(
|
reason=(
|
||||||
f"近 3 年采购订单 {count} 单,异常案例 {anomaly_count} 条,基础分 {base:g},α={alpha:.2f}"
|
f"近 3 个自然年度采购订单 {count} 单,异常案例 {anomaly_count} 条,基础分 {base:g},α={alpha:.2f}"
|
||||||
if cooperation_count is not None and cooperation_count > 0
|
if cooperation_count is not None and cooperation_count > 0
|
||||||
else f"近 3 年异常 {count} 条,基础分 {base:g},α={alpha:.2f}"
|
else f"近 3 个自然年度异常 {count} 条,基础分 {base:g},α={alpha:.2f}"
|
||||||
),
|
),
|
||||||
sample_size=anomaly_count,
|
sample_size=anomaly_count,
|
||||||
raw_value=count,
|
raw_value=count,
|
||||||
|
|||||||
@ -16,6 +16,20 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.models.entities import PurchaseOrderLine
|
from app.models.entities import PurchaseOrderLine
|
||||||
|
|
||||||
|
|
||||||
|
def calendar_year_window(as_of: date, years: int = 3) -> tuple[date, date]:
|
||||||
|
"""Return the natural-year window used by historical statistics.
|
||||||
|
|
||||||
|
``years=3`` means the year three calendar years before ``as_of`` through
|
||||||
|
the end of ``as_of``'s calendar year. For example, an as-of date in 2026
|
||||||
|
uses 2023-01-01 through 2026-12-31, so a partial current year never drops
|
||||||
|
an otherwise valid record merely because of its month/day.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if years < 1:
|
||||||
|
raise ValueError("years must be positive")
|
||||||
|
return date(as_of.year - years, 1, 1), date(as_of.year, 12, 31)
|
||||||
|
|
||||||
|
|
||||||
def clean_text(value: Any) -> str | None:
|
def clean_text(value: Any) -> str | None:
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -341,7 +341,7 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes:
|
|||||||
if (supplier and supplier.supplier_name) or supplier_cards:
|
if (supplier and supplier.supplier_name) or supplier_cards:
|
||||||
supplier_rows = [["维度", "数值", "说明"]]
|
supplier_rows = [["维度", "数值", "说明"]]
|
||||||
if supplier and supplier.cooperation_count is not None:
|
if supplier and supplier.cooperation_count is not None:
|
||||||
supplier_rows.append(["合作次数", f"{supplier.cooperation_count} 次", "近三年采购订单去重单据数"])
|
supplier_rows.append(["合作次数", f"{supplier.cooperation_count} 次", "按自然年度边界采购订单去重单据数"])
|
||||||
if supplier:
|
if supplier:
|
||||||
supplier_rows.append(["异常次数", f"{supplier.anomaly_count or 0} 次", "异常案例库统计"])
|
supplier_rows.append(["异常次数", f"{supplier.anomaly_count or 0} 次", "异常案例库统计"])
|
||||||
if supplier and supplier.alpha is not None:
|
if supplier and supplier.alpha is not None:
|
||||||
@ -572,7 +572,9 @@ def build_risk_report_docx(result: RiskAssessmentResult) -> bytes:
|
|||||||
heading("三、供应商合作频次分析")
|
heading("三、供应商合作频次分析")
|
||||||
rows = [["维度", "数值", "说明"]]
|
rows = [["维度", "数值", "说明"]]
|
||||||
if result.supplier_freq and result.supplier_freq.cooperation_count is not None:
|
if result.supplier_freq and result.supplier_freq.cooperation_count is not None:
|
||||||
rows.append(["合作次数", f"{result.supplier_freq.cooperation_count} 次", "近三年采购订单去重单据数"])
|
rows.append(
|
||||||
|
["合作次数", f"{result.supplier_freq.cooperation_count} 次", "按自然年度边界采购订单去重单据数"]
|
||||||
|
)
|
||||||
if result.supplier_freq:
|
if result.supplier_freq:
|
||||||
rows.append(["异常次数", f"{result.supplier_freq.anomaly_count or 0} 次", "异常案例库统计"])
|
rows.append(["异常次数", f"{result.supplier_freq.anomaly_count or 0} 次", "异常案例库统计"])
|
||||||
if result.supplier_freq and result.supplier_freq.alpha is not None:
|
if result.supplier_freq and result.supplier_freq.alpha is not None:
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from datetime import UTC, date, datetime, timedelta
|
from datetime import UTC, date, datetime
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@ -20,11 +20,11 @@ from app.schemas.suppliers import (
|
|||||||
SupplierRecentRecord,
|
SupplierRecentRecord,
|
||||||
)
|
)
|
||||||
from app.services.database_risk_scoring import DatabaseRiskScoringService, normalize_category
|
from app.services.database_risk_scoring import DatabaseRiskScoringService, normalize_category
|
||||||
from app.services.purchase_orders import supplier_order_counts
|
from app.services.purchase_orders import calendar_year_window, supplier_order_counts
|
||||||
|
|
||||||
|
|
||||||
class SupplierProfileService:
|
class SupplierProfileService:
|
||||||
history_days = 365 * 3
|
history_years = 3
|
||||||
recent_months = 6
|
recent_months = 6
|
||||||
max_categories = 5
|
max_categories = 5
|
||||||
max_recent_records = 10
|
max_recent_records = 10
|
||||||
@ -58,14 +58,15 @@ class SupplierProfileService:
|
|||||||
all_rows = await self._load_rows()
|
all_rows = await self._load_rows()
|
||||||
rows = [row for row in all_rows if self.matches(row.get("supplier"), supplier_name)]
|
rows = [row for row in all_rows if self.matches(row.get("supplier"), supplier_name)]
|
||||||
if not rows:
|
if not rows:
|
||||||
raise AppException("供应商不存在或暂无近3年异常记录", code=404, status_code=404)
|
raise AppException("供应商不存在或暂无近3个自然年度异常记录", code=404, status_code=404)
|
||||||
|
|
||||||
anomaly_total = len(rows)
|
anomaly_total = len(rows)
|
||||||
|
history_start, history_end = calendar_year_window(self.as_of, self.history_years)
|
||||||
order_count = await supplier_order_counts(
|
order_count = await supplier_order_counts(
|
||||||
self.session,
|
self.session,
|
||||||
supplier_name,
|
supplier_name,
|
||||||
self.as_of - timedelta(days=self.history_days),
|
history_start,
|
||||||
self.as_of,
|
history_end,
|
||||||
)
|
)
|
||||||
a_count = sum(self.defect_code(row.get("defect_level")) == "A" for row in rows)
|
a_count = sum(self.defect_code(row.get("defect_level")) == "A" for row in rows)
|
||||||
b_count = sum(self.defect_code(row.get("defect_level")) == "B" for row in rows)
|
b_count = sum(self.defect_code(row.get("defect_level")) == "B" for row in rows)
|
||||||
@ -116,7 +117,7 @@ class SupplierProfileService:
|
|||||||
monthly_trend=self._monthly_trend(rows),
|
monthly_trend=self._monthly_trend(rows),
|
||||||
recent_records=self._recent_records(rows),
|
recent_records=self._recent_records(rows),
|
||||||
factory_recommendations=[],
|
factory_recommendations=[],
|
||||||
recommendation_note="合作次数按近3年采购订单去重单据编号统计;异常数和供应商层级仍按异常案例库统计。",
|
recommendation_note="合作次数按自然年度边界采购订单去重单据编号统计;异常数和供应商层级仍按异常案例库统计。",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _load_supplier_summary(
|
async def _load_supplier_summary(
|
||||||
@ -130,14 +131,14 @@ class SupplierProfileService:
|
|||||||
the remote PostgreSQL connection.
|
the remote PostgreSQL connection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
start = self.as_of - timedelta(days=self.history_days)
|
start, end = calendar_year_window(self.as_of, self.history_years)
|
||||||
statement = (
|
statement = (
|
||||||
select(AnomalyCase.supplier, AnomalyCase.defect_level, func.count())
|
select(AnomalyCase.supplier, AnomalyCase.defect_level, func.count())
|
||||||
.where(
|
.where(
|
||||||
AnomalyCase.supplier.is_not(None),
|
AnomalyCase.supplier.is_not(None),
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= start,
|
AnomalyCase.date >= start,
|
||||||
AnomalyCase.date <= self.as_of,
|
AnomalyCase.date <= end,
|
||||||
)
|
)
|
||||||
.group_by(AnomalyCase.supplier, AnomalyCase.defect_level)
|
.group_by(AnomalyCase.supplier, AnomalyCase.defect_level)
|
||||||
)
|
)
|
||||||
@ -161,7 +162,7 @@ class SupplierProfileService:
|
|||||||
return grouped
|
return grouped
|
||||||
|
|
||||||
async def _load_rows(self, *, supplier_name: str | None = None) -> list[dict]:
|
async def _load_rows(self, *, supplier_name: str | None = None) -> list[dict]:
|
||||||
start = self.as_of - timedelta(days=self.history_days)
|
start, end = calendar_year_window(self.as_of, self.history_years)
|
||||||
statement = select(
|
statement = select(
|
||||||
AnomalyCase.id,
|
AnomalyCase.id,
|
||||||
AnomalyCase.date,
|
AnomalyCase.date,
|
||||||
@ -175,7 +176,7 @@ class SupplierProfileService:
|
|||||||
).where(
|
).where(
|
||||||
AnomalyCase.date.is_not(None),
|
AnomalyCase.date.is_not(None),
|
||||||
AnomalyCase.date >= start,
|
AnomalyCase.date >= start,
|
||||||
AnomalyCase.date <= self.as_of,
|
AnomalyCase.date <= end,
|
||||||
)
|
)
|
||||||
if supplier_name:
|
if supplier_name:
|
||||||
statement = statement.where(AnomalyCase.supplier.ilike(f"%{supplier_name}%"))
|
statement = statement.where(AnomalyCase.supplier.ilike(f"%{supplier_name}%"))
|
||||||
@ -223,7 +224,7 @@ class SupplierProfileService:
|
|||||||
"A级": "异常记录 20~60 次,且 A/B 级缺陷占比 < 30%",
|
"A级": "异常记录 20~60 次,且 A/B 级缺陷占比 < 30%",
|
||||||
"B级": "异常记录 60~120 次,或 A/B 级缺陷占比 30%~50%",
|
"B级": "异常记录 60~120 次,或 A/B 级缺陷占比 30%~50%",
|
||||||
"C级": "异常记录 > 120 次,或存在 A 级缺陷,或 A/B 级缺陷占比 > 50%",
|
"C级": "异常记录 > 120 次,或存在 A 级缺陷,或 A/B 级缺陷占比 > 50%",
|
||||||
"新供应商": "近3年异常记录为 0",
|
"新供应商": "近3个自然年度异常记录为 0",
|
||||||
}[tier]
|
}[tier]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
15
tests/test_purchase_orders.py
Normal file
15
tests/test_purchase_orders.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from app.services.purchase_orders import calendar_year_window
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("as_of", "expected"),
|
||||||
|
[
|
||||||
|
(date(2026, 9, 10), (date(2023, 1, 1), date(2026, 12, 31))),
|
||||||
|
(date(2025, 1, 1), (date(2022, 1, 1), date(2025, 12, 31))),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_three_year_window_uses_calendar_years(as_of: date, expected: tuple[date, date]) -> None:
|
||||||
|
assert calendar_year_window(as_of, 3) == expected
|
||||||
Loading…
x
Reference in New Issue
Block a user