feat(1c): decouple company intelligence from 1c names
This commit is contained in:
@@ -115,6 +115,7 @@
|
|||||||
- check_ingest_freshness.sh
|
- check_ingest_freshness.sh
|
||||||
- run_company_intelligence_api.sh
|
- run_company_intelligence_api.sh
|
||||||
- run_company_intelligence_refresh.sh
|
- run_company_intelligence_refresh.sh
|
||||||
|
- run_company_registry_bindings_refresh.sh
|
||||||
- run_manager_brief.sh
|
- run_manager_brief.sh
|
||||||
- run_ingest_cycle.sh
|
- run_ingest_cycle.sh
|
||||||
|
|
||||||
|
|||||||
@@ -463,6 +463,35 @@ def grafana_company_dashboard_url() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_company_portfolio_card(company_ref: str, infobase: str | None = None) -> dict[str, Any]:
|
||||||
|
client = ch_client()
|
||||||
|
filters = [
|
||||||
|
"("
|
||||||
|
+ " OR ".join(
|
||||||
|
[
|
||||||
|
f"company_entity_key = {q(company_ref)}",
|
||||||
|
f"counterparty = {q(company_ref)}",
|
||||||
|
f"company_name = {q(company_ref)}",
|
||||||
|
f"source_counterparty = {q(company_ref)}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ ")"
|
||||||
|
]
|
||||||
|
if infobase:
|
||||||
|
filters.append(f"infobase = {q(infobase)}")
|
||||||
|
sql = f"""
|
||||||
|
SELECT *
|
||||||
|
FROM analytics_1c.v_company_portfolio_overview
|
||||||
|
WHERE {' AND '.join(filters)}
|
||||||
|
ORDER BY last_company_snapshot_at DESC, amount_30d DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
rows = rows_to_dict(client.query(sql))
|
||||||
|
if not rows:
|
||||||
|
raise HTTPException(status_code=404, detail="company not found in analytics_1c.v_company_portfolio_overview")
|
||||||
|
return rows[0]
|
||||||
|
|
||||||
|
|
||||||
def fmt_number(value: Any) -> str:
|
def fmt_number(value: Any) -> str:
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
return "-"
|
return "-"
|
||||||
@@ -484,8 +513,8 @@ def severity_badge(severity: str) -> str:
|
|||||||
return f'<span class="badge badge-{tone}">{html.escape(severity or "none")}</span>'
|
return f'<span class="badge badge-{tone}">{html.escape(severity or "none")}</span>'
|
||||||
|
|
||||||
|
|
||||||
def company_detail_url(counterparty: str, infobase: str | None = None) -> str:
|
def company_detail_url(company_ref: str, infobase: str | None = None) -> str:
|
||||||
base = f"/manager/company/{quote(counterparty)}"
|
base = f"/manager/company/{quote(company_ref)}"
|
||||||
if infobase:
|
if infobase:
|
||||||
return f"{base}?infobase={quote(infobase)}"
|
return f"{base}?infobase={quote(infobase)}"
|
||||||
return base
|
return base
|
||||||
@@ -538,7 +567,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
|
|||||||
f"<div class=\"stack-card-head\"><h3>{html.escape(item.get('company', '-'))}</h3>{severity_badge(item.get('severity', ''))}</div>"
|
f"<div class=\"stack-card-head\"><h3>{html.escape(item.get('company', '-'))}</h3>{severity_badge(item.get('severity', ''))}</div>"
|
||||||
f"<p class=\"stack-card-body\">{html.escape(item.get('reason', '-'))}</p>"
|
f"<p class=\"stack-card-body\">{html.escape(item.get('reason', '-'))}</p>"
|
||||||
f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(item.get('recommended_action', '-'))}</p>"
|
f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(item.get('recommended_action', '-'))}</p>"
|
||||||
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(item.get('company', '-'))}\">Открыть карточку компании</a></p>"
|
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('company') or '-'), str(item.get('infobase') or '') or None)}\">Открыть карточку компании</a></p>"
|
||||||
"</article>"
|
"</article>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -550,7 +579,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
|
|||||||
f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3></div>"
|
f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3></div>"
|
||||||
f"<p class=\"metric-line\"><strong>Прогноз 30д:</strong> {html.escape(item.get('forecast_30d', '-'))}</p>"
|
f"<p class=\"metric-line\"><strong>Прогноз 30д:</strong> {html.escape(item.get('forecast_30d', '-'))}</p>"
|
||||||
f"<p class=\"stack-card-body\">{html.escape(item.get('interpretation', '-'))}</p>"
|
f"<p class=\"stack-card-body\">{html.escape(item.get('interpretation', '-'))}</p>"
|
||||||
f"<a class=\"inline-link\" href=\"{company_detail_url(company)}\">Карточка компании</a>"
|
f"<a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or company), str(item.get('infobase') or '') or None)}\">Карточка компании</a>"
|
||||||
"</article>"
|
"</article>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -955,7 +984,7 @@ def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]
|
|||||||
WITH recent AS (
|
WITH recent AS (
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
counterparty,
|
counterparty AS company_entity_key,
|
||||||
max(generated_at) AS latest_signal_at,
|
max(generated_at) AS latest_signal_at,
|
||||||
max(score) AS max_score,
|
max(score) AS max_score,
|
||||||
sum(score) AS total_score,
|
sum(score) AS total_score,
|
||||||
@@ -971,7 +1000,9 @@ def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
p.infobase AS infobase,
|
p.infobase AS infobase,
|
||||||
|
p.company_entity_key AS company_entity_key,
|
||||||
p.counterparty AS counterparty,
|
p.counterparty AS counterparty,
|
||||||
|
p.source_counterparty,
|
||||||
p.company_name,
|
p.company_name,
|
||||||
p.normalized_counterparty,
|
p.normalized_counterparty,
|
||||||
p.registry_match_mode,
|
p.registry_match_mode,
|
||||||
@@ -997,7 +1028,7 @@ def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]
|
|||||||
FROM recent AS r
|
FROM recent AS r
|
||||||
INNER JOIN analytics_1c.v_company_portfolio_overview AS p
|
INNER JOIN analytics_1c.v_company_portfolio_overview AS p
|
||||||
ON p.infobase = r.infobase
|
ON p.infobase = r.infobase
|
||||||
AND p.counterparty = r.counterparty
|
AND p.company_entity_key = r.company_entity_key
|
||||||
ORDER BY r.max_score DESC, r.signals_total DESC, p.amount_30d DESC, p.counterparty
|
ORDER BY r.max_score DESC, r.signals_total DESC, p.amount_30d DESC, p.counterparty
|
||||||
LIMIT {int(limit)}
|
LIMIT {int(limit)}
|
||||||
"""
|
"""
|
||||||
@@ -1089,7 +1120,7 @@ def render_problematic_companies_html(items: list[dict[str, Any]], days: int) ->
|
|||||||
for item in items:
|
for item in items:
|
||||||
rows.append(
|
rows.append(
|
||||||
"<tr>"
|
"<tr>"
|
||||||
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('counterparty', '-')), str(item.get('infobase', '')) if item.get('infobase') else None)}\">{html.escape(str(item.get('counterparty', '-')))}</a></td>"
|
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('counterparty') or '-'), str(item.get('infobase', '')) if item.get('infobase') else None)}\">{html.escape(str(item.get('counterparty', '-')))}</a></td>"
|
||||||
f"<td>{html.escape(str(item.get('normalized_counterparty') or '-'))}</td>"
|
f"<td>{html.escape(str(item.get('normalized_counterparty') or '-'))}</td>"
|
||||||
f"<td>{severity_badge(str(item.get('top_severity') or item.get('signal_severity') or 'none'))}</td>"
|
f"<td>{severity_badge(str(item.get('top_severity') or item.get('signal_severity') or 'none'))}</td>"
|
||||||
f"<td>{fmt_number(item.get('max_score'))}</td>"
|
f"<td>{fmt_number(item.get('max_score'))}</td>"
|
||||||
@@ -1231,7 +1262,7 @@ def render_brief_delta_html(payload: dict[str, Any]) -> str:
|
|||||||
priority_tier = str(item.get("priority_tier") or "low")
|
priority_tier = str(item.get("priority_tier") or "low")
|
||||||
rows.append(
|
rows.append(
|
||||||
"<tr>"
|
"<tr>"
|
||||||
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(counterparty), str(infobase) if infobase else None)}\">{html.escape(str(counterparty or '-'))}</a></td>"
|
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or counterparty), str(infobase) if infobase else None)}\">{html.escape(str(counterparty or '-'))}</a></td>"
|
||||||
f"<td>{html.escape(tier_labels.get(priority_tier, priority_tier))}</td>"
|
f"<td>{html.escape(tier_labels.get(priority_tier, priority_tier))}</td>"
|
||||||
f"<td>{delta_value(item.get('priority_score', 0))}</td>"
|
f"<td>{delta_value(item.get('priority_score', 0))}</td>"
|
||||||
f"<td>{html.escape(str(item.get('change_type') or '-'))}</td>"
|
f"<td>{html.escape(str(item.get('change_type') or '-'))}</td>"
|
||||||
@@ -1538,7 +1569,7 @@ def render_weekly_digest_html(payload: dict[str, Any]) -> str:
|
|||||||
f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3>{severity_badge(str(item.get('priority') or 'low'))}</div>"
|
f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3>{severity_badge(str(item.get('priority') or 'low'))}</div>"
|
||||||
f"<p class=\"stack-card-body\">{html.escape(str(item.get('reason') or '-'))}</p>"
|
f"<p class=\"stack-card-body\">{html.escape(str(item.get('reason') or '-'))}</p>"
|
||||||
f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(str(item.get('recommended_action') or '-'))}</p>"
|
f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(str(item.get('recommended_action') or '-'))}</p>"
|
||||||
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(company)}\">Карточка компании</a></p>"
|
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or company), str(item.get('infobase') or '') or None)}\">Карточка компании</a></p>"
|
||||||
"</article>"
|
"</article>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1792,11 +1823,16 @@ def companies_overview(
|
|||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
|
company_entity_key,
|
||||||
organization,
|
organization,
|
||||||
counterparty,
|
counterparty,
|
||||||
|
source_counterparty,
|
||||||
company_name,
|
company_name,
|
||||||
normalized_counterparty,
|
normalized_counterparty,
|
||||||
registry_match_mode,
|
registry_match_mode,
|
||||||
|
registry_company_key,
|
||||||
|
registry_binding_source,
|
||||||
|
registry_binding_note,
|
||||||
registry_assignee_name,
|
registry_assignee_name,
|
||||||
registry_status,
|
registry_status,
|
||||||
registry_share_text,
|
registry_share_text,
|
||||||
@@ -1833,39 +1869,27 @@ def companies_overview(
|
|||||||
@app.get("/api/1/analytics-1c/companies/{counterparty}/summary")
|
@app.get("/api/1/analytics-1c/companies/{counterparty}/summary")
|
||||||
def company_summary(counterparty: str, infobase: str | None = None) -> dict[str, Any]:
|
def company_summary(counterparty: str, infobase: str | None = None) -> dict[str, Any]:
|
||||||
client = ch_client()
|
client = ch_client()
|
||||||
filters = [f"counterparty = {q(counterparty)}"]
|
card = resolve_company_portfolio_card(counterparty, infobase)
|
||||||
if infobase:
|
entity_key = str(card.get("company_entity_key") or "")
|
||||||
filters.append(f"infobase = {q(infobase)}")
|
|
||||||
sql = f"""
|
|
||||||
SELECT *
|
|
||||||
FROM analytics_1c.v_company_portfolio_overview
|
|
||||||
WHERE {' AND '.join(filters)}
|
|
||||||
ORDER BY last_company_snapshot_at DESC, amount_30d DESC
|
|
||||||
LIMIT 1
|
|
||||||
"""
|
|
||||||
rows = rows_to_dict(client.query(sql))
|
|
||||||
if not rows:
|
|
||||||
raise HTTPException(status_code=404, detail="counterparty not found in analytics_1c.v_company_portfolio_overview")
|
|
||||||
card = rows[0]
|
|
||||||
forecast_sql = f"""
|
forecast_sql = f"""
|
||||||
SELECT metric, horizon_days, baseline_daily, trend_slope, predicted_daily, predicted_total, confidence, note
|
SELECT metric, horizon_days, baseline_daily, trend_slope, predicted_daily, predicted_total, confidence, note
|
||||||
FROM analytics_1c.v_company_forecasts_current
|
FROM analytics_1c.v_company_forecasts_current
|
||||||
WHERE counterparty = {q(counterparty)}
|
WHERE counterparty = {q(entity_key)}
|
||||||
{"AND infobase = " + q(infobase) if infobase else ""}
|
{"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
|
||||||
ORDER BY metric, horizon_days
|
ORDER BY metric, horizon_days
|
||||||
"""
|
"""
|
||||||
signals_sql = f"""
|
signals_sql = f"""
|
||||||
SELECT generated_at, severity, score, signal_type, summary
|
SELECT generated_at, severity, score, signal_type, summary
|
||||||
FROM analytics_1c.v_company_health_current
|
FROM analytics_1c.v_company_health_current
|
||||||
WHERE counterparty = {q(counterparty)}
|
WHERE counterparty = {q(entity_key)}
|
||||||
{"AND infobase = " + q(infobase) if infobase else ""}
|
{"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
|
||||||
ORDER BY score DESC, generated_at DESC
|
ORDER BY score DESC, generated_at DESC
|
||||||
"""
|
"""
|
||||||
timeline_sql = f"""
|
timeline_sql = f"""
|
||||||
SELECT last_company_snapshot_at AS ts, infobase, company_name, owner_user, current_status, db_size_bytes, reglog_size_bytes, active_locks, current_activity_score
|
SELECT last_company_snapshot_at AS ts, infobase, company_name, owner_user, current_status, db_size_bytes, reglog_size_bytes, active_locks, current_activity_score
|
||||||
FROM analytics_1c.v_company_portfolio_overview
|
FROM analytics_1c.v_company_portfolio_overview
|
||||||
WHERE counterparty = {q(counterparty)}
|
WHERE company_entity_key = {q(entity_key)}
|
||||||
{"AND infobase = " + q(infobase) if infobase else ""}
|
{"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
|
||||||
ORDER BY ts DESC
|
ORDER BY ts DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
@@ -1875,14 +1899,14 @@ def company_summary(counterparty: str, infobase: str | None = None) -> dict[str,
|
|||||||
timeline_sql = f"""
|
timeline_sql = f"""
|
||||||
SELECT ts, infobase, doc_type, operation_type, amount, status, author
|
SELECT ts, infobase, doc_type, operation_type, amount, status, author
|
||||||
FROM analytics_1c.documents
|
FROM analytics_1c.documents
|
||||||
WHERE counterparty = {q(counterparty)}
|
WHERE infobase = {q(str(card.get('infobase') or infobase or ''))}
|
||||||
{"AND infobase = " + q(infobase) if infobase else ""}
|
AND counterparty != ''
|
||||||
ORDER BY ts DESC
|
ORDER BY ts DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
"""
|
"""
|
||||||
timeline = rows_to_dict(client.query(timeline_sql))
|
timeline = rows_to_dict(client.query(timeline_sql))
|
||||||
essence = (
|
essence = (
|
||||||
f"Компания {counterparty}: за 30 дней событий {card['docs_30d']}, суммарная активность {card['amount_30d']}, "
|
f"Компания {card['counterparty']}: за 30 дней событий {card['docs_30d']}, суммарная активность {card['amount_30d']}, "
|
||||||
f"прогноз активности на 30 дней {card['amount_forecast_30d']}, риск {card['signal_severity']}."
|
f"прогноз активности на 30 дней {card['amount_forecast_30d']}, риск {card['signal_severity']}."
|
||||||
)
|
)
|
||||||
payload = {
|
payload = {
|
||||||
@@ -1905,9 +1929,10 @@ def company_forecast(
|
|||||||
horizon_days: int | None = Query(default=None, ge=1, le=365),
|
horizon_days: int | None = Query(default=None, ge=1, le=365),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
client = ch_client()
|
client = ch_client()
|
||||||
filters = [f"counterparty = {q(counterparty)}"]
|
card = resolve_company_portfolio_card(counterparty, infobase)
|
||||||
if infobase:
|
filters = [f"counterparty = {q(str(card.get('company_entity_key') or counterparty))}"]
|
||||||
filters.append(f"infobase = {q(infobase)}")
|
if card.get("infobase") or infobase:
|
||||||
|
filters.append(f"infobase = {q(str(card.get('infobase') or infobase))}")
|
||||||
if horizon_days is not None:
|
if horizon_days is not None:
|
||||||
filters.append(f"horizon_days = {int(horizon_days)}")
|
filters.append(f"horizon_days = {int(horizon_days)}")
|
||||||
sql = f"""
|
sql = f"""
|
||||||
@@ -1927,9 +1952,8 @@ def company_timeline(
|
|||||||
limit: int = Query(default=100, ge=1, le=500),
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
client = ch_client()
|
client = ch_client()
|
||||||
filters = [f"counterparty = {q(counterparty)}"]
|
card = resolve_company_portfolio_card(counterparty, infobase)
|
||||||
if infobase:
|
filters = [f"infobase = {q(str(card.get('infobase') or infobase or ''))}", "counterparty != ''"]
|
||||||
filters.append(f"infobase = {q(infobase)}")
|
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT ts, infobase, organization, doc_type, doc_number, author, operation_type, amount, status, posted
|
SELECT ts, infobase, organization, doc_type, doc_number, author, operation_type, amount, status, posted
|
||||||
FROM analytics_1c.documents
|
FROM analytics_1c.documents
|
||||||
@@ -2076,13 +2100,14 @@ def render_company_detail_html(summary_payload: dict[str, Any], infobase: str |
|
|||||||
title = card.get("counterparty", "Карточка компании")
|
title = card.get("counterparty", "Карточка компании")
|
||||||
subtitle = summary_payload.get("essence", "")
|
subtitle = summary_payload.get("essence", "")
|
||||||
grafana_url = grafana_company_dashboard_url()
|
grafana_url = grafana_company_dashboard_url()
|
||||||
summary_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/summary"
|
company_ref = str(card.get("company_entity_key") or card.get("counterparty") or "")
|
||||||
|
summary_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/summary"
|
||||||
if infobase:
|
if infobase:
|
||||||
summary_url += f"?infobase={quote(infobase)}"
|
summary_url += f"?infobase={quote(infobase)}"
|
||||||
timeline_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/timeline"
|
timeline_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/timeline"
|
||||||
if infobase:
|
if infobase:
|
||||||
timeline_url += f"?infobase={quote(infobase)}"
|
timeline_url += f"?infobase={quote(infobase)}"
|
||||||
forecast_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/forecast"
|
forecast_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/forecast"
|
||||||
if infobase:
|
if infobase:
|
||||||
forecast_url += f"?infobase={quote(infobase)}"
|
forecast_url += f"?infobase={quote(infobase)}"
|
||||||
|
|
||||||
|
|||||||
@@ -185,14 +185,14 @@ def snapshot_from_context(context: dict[str, Any]) -> dict[tuple[Any, Any], dict
|
|||||||
snapshot_items = context.get("portfolio_snapshot") or []
|
snapshot_items = context.get("portfolio_snapshot") or []
|
||||||
if snapshot_items:
|
if snapshot_items:
|
||||||
return {
|
return {
|
||||||
(item.get("infobase"), item.get("counterparty")): item
|
(item.get("infobase"), item.get("company_entity_key") or item.get("counterparty")): item
|
||||||
for item in snapshot_items
|
for item in snapshot_items
|
||||||
}
|
}
|
||||||
|
|
||||||
merged: dict[tuple[Any, Any], dict[str, Any]] = {}
|
merged: dict[tuple[Any, Any], dict[str, Any]] = {}
|
||||||
for source_name in ("top_risks", "top_forecasts", "watchlist", "busy_bases"):
|
for source_name in ("top_risks", "top_forecasts", "watchlist", "busy_bases"):
|
||||||
for item in context.get(source_name, []):
|
for item in context.get(source_name, []):
|
||||||
key = (item.get("infobase"), item.get("counterparty"))
|
key = (item.get("infobase"), item.get("company_entity_key") or item.get("counterparty"))
|
||||||
if key not in merged:
|
if key not in merged:
|
||||||
merged[key] = dict(item)
|
merged[key] = dict(item)
|
||||||
else:
|
else:
|
||||||
@@ -268,6 +268,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
|
company_entity_key,
|
||||||
counterparty,
|
counterparty,
|
||||||
normalized_counterparty,
|
normalized_counterparty,
|
||||||
registry_match_mode,
|
registry_match_mode,
|
||||||
@@ -294,6 +295,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
|
company_entity_key,
|
||||||
counterparty,
|
counterparty,
|
||||||
normalized_counterparty,
|
normalized_counterparty,
|
||||||
registry_match_mode,
|
registry_match_mode,
|
||||||
@@ -316,6 +318,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
s.infobase,
|
s.infobase,
|
||||||
|
p.company_entity_key,
|
||||||
s.counterparty,
|
s.counterparty,
|
||||||
p.normalized_counterparty,
|
p.normalized_counterparty,
|
||||||
p.registry_match_mode,
|
p.registry_match_mode,
|
||||||
@@ -328,7 +331,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
round(p.amount_forecast_30d, 2) AS amount_forecast_30d
|
round(p.amount_forecast_30d, 2) AS amount_forecast_30d
|
||||||
FROM analytics_1c.v_company_health_current AS s
|
FROM analytics_1c.v_company_health_current AS s
|
||||||
LEFT JOIN analytics_1c.v_company_portfolio_overview AS p
|
LEFT JOIN analytics_1c.v_company_portfolio_overview AS p
|
||||||
ON p.infobase = s.infobase AND p.counterparty = s.counterparty
|
ON p.infobase = s.infobase AND p.company_entity_key = s.counterparty
|
||||||
WHERE s.signal_type IN ('inactive_company', 'amount_drop', 'docs_stopped')
|
WHERE s.signal_type IN ('inactive_company', 'amount_drop', 'docs_stopped')
|
||||||
ORDER BY s.score DESC, p.days_since_last_activity DESC, p.amount_30d DESC
|
ORDER BY s.score DESC, p.days_since_last_activity DESC, p.amount_30d DESC
|
||||||
LIMIT {int(top_limit)}
|
LIMIT {int(top_limit)}
|
||||||
@@ -341,6 +344,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
|
company_entity_key,
|
||||||
counterparty,
|
counterparty,
|
||||||
normalized_counterparty,
|
normalized_counterparty,
|
||||||
current_status,
|
current_status,
|
||||||
@@ -364,11 +368,14 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
SELECT
|
SELECT
|
||||||
c.opened_at,
|
c.opened_at,
|
||||||
c.infobase,
|
c.infobase,
|
||||||
|
p.company_entity_key,
|
||||||
c.entity_id AS counterparty,
|
c.entity_id AS counterparty,
|
||||||
c.title,
|
c.title,
|
||||||
c.severity,
|
c.severity,
|
||||||
c.status
|
c.status
|
||||||
FROM analytics_1c.cases AS c
|
FROM analytics_1c.cases AS c
|
||||||
|
LEFT JOIN analytics_1c.v_company_portfolio_overview AS p
|
||||||
|
ON p.infobase = c.infobase AND p.company_entity_key = c.entity_id
|
||||||
WHERE c.entity_type = 'counterparty' AND c.status != 'closed'
|
WHERE c.entity_type = 'counterparty' AND c.status != 'closed'
|
||||||
ORDER BY c.opened_at DESC
|
ORDER BY c.opened_at DESC
|
||||||
LIMIT {int(top_limit)}
|
LIMIT {int(top_limit)}
|
||||||
@@ -381,6 +388,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
|
|||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
|
company_entity_key,
|
||||||
counterparty,
|
counterparty,
|
||||||
normalized_counterparty,
|
normalized_counterparty,
|
||||||
registry_match_mode,
|
registry_match_mode,
|
||||||
@@ -424,8 +432,8 @@ def compute_delta_context(current: dict[str, Any], previous_artifact: dict[str,
|
|||||||
previous = previous_artifact.get("context", {})
|
previous = previous_artifact.get("context", {})
|
||||||
current_summary = current.get("portfolio_summary", {})
|
current_summary = current.get("portfolio_summary", {})
|
||||||
previous_summary = previous.get("portfolio_summary", {})
|
previous_summary = previous.get("portfolio_summary", {})
|
||||||
current_watchlist = {(item.get("infobase"), item.get("counterparty")) for item in current.get("watchlist", [])}
|
current_watchlist = {(item.get("infobase"), item.get("company_entity_key") or item.get("counterparty")) for item in current.get("watchlist", [])}
|
||||||
previous_watchlist = {(item.get("infobase"), item.get("counterparty")) for item in previous.get("watchlist", [])}
|
previous_watchlist = {(item.get("infobase"), item.get("company_entity_key") or item.get("counterparty")) for item in previous.get("watchlist", [])}
|
||||||
current_snapshot = snapshot_from_context(current)
|
current_snapshot = snapshot_from_context(current)
|
||||||
previous_snapshot = snapshot_from_context(previous)
|
previous_snapshot = snapshot_from_context(previous)
|
||||||
|
|
||||||
@@ -530,6 +538,7 @@ def compute_delta_context(current: dict[str, Any], previous_artifact: dict[str,
|
|||||||
rank_top_change(
|
rank_top_change(
|
||||||
{
|
{
|
||||||
"infobase": current_item.get("infobase"),
|
"infobase": current_item.get("infobase"),
|
||||||
|
"company_entity_key": current_item.get("company_entity_key"),
|
||||||
"company": current_item.get("counterparty"),
|
"company": current_item.get("counterparty"),
|
||||||
"normalized_counterparty": current_item.get("normalized_counterparty"),
|
"normalized_counterparty": current_item.get("normalized_counterparty"),
|
||||||
"registry_match_mode": current_item.get("registry_match_mode"),
|
"registry_match_mode": current_item.get("registry_match_mode"),
|
||||||
|
|||||||
@@ -146,7 +146,9 @@ def build_context(client, args: argparse.Namespace) -> dict[str, Any]:
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
p.infobase AS infobase,
|
p.infobase AS infobase,
|
||||||
|
p.company_entity_key AS company_entity_key,
|
||||||
p.counterparty AS counterparty,
|
p.counterparty AS counterparty,
|
||||||
|
p.source_counterparty,
|
||||||
p.company_name,
|
p.company_name,
|
||||||
p.normalized_counterparty,
|
p.normalized_counterparty,
|
||||||
p.registry_match_mode,
|
p.registry_match_mode,
|
||||||
@@ -220,7 +222,7 @@ def render_deterministic_recovery(context: dict[str, Any]) -> dict[str, Any]:
|
|||||||
]
|
]
|
||||||
top_incidents = []
|
top_incidents = []
|
||||||
for item in problematic[:6]:
|
for item in problematic[:6]:
|
||||||
company = str(item.get("counterparty") or "-")
|
company = str(item.get("counterparty") or item.get("company_name") or "-")
|
||||||
actions = [
|
actions = [
|
||||||
"Проверить владельца и состав открытых кейсов по компании.",
|
"Проверить владельца и состав открытых кейсов по компании.",
|
||||||
"Подтвердить, что по компании есть план снижения хвоста в ближайшие 24 часа.",
|
"Подтвердить, что по компании есть план снижения хвоста в ближайшие 24 часа.",
|
||||||
@@ -231,6 +233,7 @@ def render_deterministic_recovery(context: dict[str, Any]) -> dict[str, Any]:
|
|||||||
actions.append("Сначала подтвердить корректность manual-сопоставления.")
|
actions.append("Сначала подтвердить корректность manual-сопоставления.")
|
||||||
top_incidents.append(
|
top_incidents.append(
|
||||||
{
|
{
|
||||||
|
"company_entity_key": str(item.get("company_entity_key") or ""),
|
||||||
"company": company,
|
"company": company,
|
||||||
"severity": str(item.get("signal_severity") or item.get("top_severity") or "critical"),
|
"severity": str(item.get("signal_severity") or item.get("top_severity") or "critical"),
|
||||||
"diagnosis": (
|
"diagnosis": (
|
||||||
|
|||||||
@@ -130,18 +130,18 @@ def main() -> int:
|
|||||||
daily_rows = query_rows(
|
daily_rows = query_rows(
|
||||||
client,
|
client,
|
||||||
"""
|
"""
|
||||||
SELECT infobase, organization, counterparty, d, docs_total, amount_total
|
SELECT infobase, organization, company_entity_key, source_counterparty, d, docs_total, amount_total
|
||||||
FROM analytics_1c.v_counterparty_daily
|
FROM analytics_1c.v_company_activity_daily
|
||||||
ORDER BY infobase, counterparty, d
|
ORDER BY infobase, company_entity_key, d
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
if not daily_rows:
|
if not daily_rows:
|
||||||
print("no counterparty rows in analytics_1c.v_counterparty_daily; nothing to refresh")
|
print("no company activity rows in analytics_1c.v_company_activity_daily; nothing to refresh")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
grouped: dict[tuple[str, str, str], list[DailyPoint]] = defaultdict(list)
|
grouped: dict[tuple[str, str, str, str], list[DailyPoint]] = defaultdict(list)
|
||||||
for row in daily_rows:
|
for row in daily_rows:
|
||||||
key = (row["infobase"], row["organization"], row["counterparty"])
|
key = (row["infobase"], row["organization"], row["company_entity_key"], row.get("source_counterparty") or row["company_entity_key"])
|
||||||
grouped[key].append(
|
grouped[key].append(
|
||||||
DailyPoint(
|
DailyPoint(
|
||||||
d=row["d"],
|
d=row["d"],
|
||||||
@@ -151,26 +151,26 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
cases_map = {
|
cases_map = {
|
||||||
(row["infobase"], row["counterparty"]): int(row["open_cases_total"] or 0)
|
(row["infobase"], row["company_entity_key"]): int(row["open_cases_total"] or 0)
|
||||||
for row in query_rows(
|
for row in query_rows(
|
||||||
client,
|
client,
|
||||||
"""
|
"""
|
||||||
SELECT infobase, entity_id AS counterparty, countIf(status != 'closed') AS open_cases_total
|
SELECT infobase, entity_id AS company_entity_key, countIf(status != 'closed') AS open_cases_total
|
||||||
FROM analytics_1c.cases
|
FROM analytics_1c.cases
|
||||||
WHERE entity_type = 'counterparty'
|
WHERE entity_type = 'counterparty'
|
||||||
GROUP BY infobase, counterparty
|
GROUP BY infobase, company_entity_key
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
detections_map = {
|
detections_map = {
|
||||||
(row["infobase"], row["counterparty"]): int(row["detections_total"] or 0)
|
(row["infobase"], row["company_entity_key"]): int(row["detections_total"] or 0)
|
||||||
for row in query_rows(
|
for row in query_rows(
|
||||||
client,
|
client,
|
||||||
"""
|
"""
|
||||||
SELECT infobase, entity_id AS counterparty, count() AS detections_total
|
SELECT infobase, entity_id AS company_entity_key, count() AS detections_total
|
||||||
FROM analytics_1c.detections
|
FROM analytics_1c.detections
|
||||||
WHERE entity_type = 'counterparty' AND status != 'closed'
|
WHERE entity_type = 'counterparty' AND status != 'closed'
|
||||||
GROUP BY infobase, counterparty
|
GROUP BY infobase, company_entity_key
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -205,8 +205,8 @@ def main() -> int:
|
|||||||
forecast_rows: list[list[Any]] = []
|
forecast_rows: list[list[Any]] = []
|
||||||
signal_rows: list[list[Any]] = []
|
signal_rows: list[list[Any]] = []
|
||||||
|
|
||||||
for (infobase, _organization, counterparty), points in grouped.items():
|
for (infobase, _organization, company_entity_key, source_counterparty), points in grouped.items():
|
||||||
if normalize_company_key(counterparty) in excluded_company_keys:
|
if normalize_company_key(source_counterparty) in excluded_company_keys:
|
||||||
continue
|
continue
|
||||||
points.sort(key=lambda p: p.d)
|
points.sort(key=lambda p: p.d)
|
||||||
filled = fill_daily_series(points)
|
filled = fill_daily_series(points)
|
||||||
@@ -223,8 +223,8 @@ def main() -> int:
|
|||||||
amount_7d = float(sum(p.amount_total for p in last_7))
|
amount_7d = float(sum(p.amount_total for p in last_7))
|
||||||
amount_prev_7d = float(sum(p.amount_total for p in prev_7))
|
amount_prev_7d = float(sum(p.amount_total for p in prev_7))
|
||||||
days_since_last_activity = (date.today() - latest_day).days
|
days_since_last_activity = (date.today() - latest_day).days
|
||||||
open_cases_total = cases_map.get((infobase, counterparty), 0)
|
open_cases_total = cases_map.get((infobase, company_entity_key), 0)
|
||||||
detections_total = detections_map.get((infobase, counterparty), 0)
|
detections_total = detections_map.get((infobase, company_entity_key), 0)
|
||||||
company_state = company_state_map.get(infobase, {})
|
company_state = company_state_map.get(infobase, {})
|
||||||
current_status = str(company_state.get("current_status") or "")
|
current_status = str(company_state.get("current_status") or "")
|
||||||
active_locks = int(company_state.get("active_locks") or 0)
|
active_locks = int(company_state.get("active_locks") or 0)
|
||||||
@@ -245,7 +245,7 @@ def main() -> int:
|
|||||||
generated_at,
|
generated_at,
|
||||||
latest_day,
|
latest_day,
|
||||||
infobase,
|
infobase,
|
||||||
counterparty,
|
company_entity_key,
|
||||||
int(horizon),
|
int(horizon),
|
||||||
metric,
|
metric,
|
||||||
float(baseline),
|
float(baseline),
|
||||||
@@ -261,29 +261,29 @@ def main() -> int:
|
|||||||
|
|
||||||
signals: list[tuple[str, int, str, str]] = []
|
signals: list[tuple[str, int, str, str]] = []
|
||||||
if days_since_last_activity >= 14 and (docs_prev_7d > 0 or amount_prev_7d > 0):
|
if days_since_last_activity >= 14 and (docs_prev_7d > 0 or amount_prev_7d > 0):
|
||||||
signals.append(("inactive_company", 85, "high", f"Нет активности по компании {counterparty} уже {days_since_last_activity} дн."))
|
signals.append(("inactive_company", 85, "high", f"Нет активности по компании {source_counterparty} уже {days_since_last_activity} дн."))
|
||||||
if amount_prev_7d > 0 and amount_7d < amount_prev_7d * 0.5:
|
if amount_prev_7d > 0 and amount_7d < amount_prev_7d * 0.5:
|
||||||
signals.append(("amount_drop", 70, "high", f"Активность по компании {counterparty} упала более чем на 50% неделя к неделе."))
|
signals.append(("amount_drop", 70, "high", f"Активность по компании {source_counterparty} упала более чем на 50% неделя к неделе."))
|
||||||
if docs_prev_7d > 0 and docs_7d == 0:
|
if docs_prev_7d > 0 and docs_7d == 0:
|
||||||
signals.append(("docs_stopped", 55, "medium", f"По компании {counterparty} прекратился поток документов за последние 7 дней."))
|
signals.append(("docs_stopped", 55, "medium", f"По компании {source_counterparty} прекратился поток документов за последние 7 дней."))
|
||||||
if current_status == "busy" or active_locks > 0 or temp_db_present > 0:
|
if current_status == "busy" or active_locks > 0 or temp_db_present > 0:
|
||||||
score = min(85, 45 + active_locks * 5 + temp_db_present * 10)
|
score = min(85, 45 + active_locks * 5 + temp_db_present * 10)
|
||||||
signals.append(("base_busy", score, severity_score_to_label(score), f"Файловая база компании {counterparty} занята: status={current_status}, locks={active_locks}, tempDb={temp_db_present}."))
|
signals.append(("base_busy", score, severity_score_to_label(score), f"Файловая база компании {source_counterparty} занята: status={current_status}, locks={active_locks}, tempDb={temp_db_present}."))
|
||||||
if scheduler_touched > 0 and current_activity_score >= 15:
|
if scheduler_touched > 0 and current_activity_score >= 15:
|
||||||
signals.append(("scheduler_activity", 35, "medium", f"По компании {counterparty} есть активность scheduler и повышенный activity score {current_activity_score}."))
|
signals.append(("scheduler_activity", 35, "medium", f"По компании {source_counterparty} есть активность scheduler и повышенный activity score {current_activity_score}."))
|
||||||
if open_cases_total > 0:
|
if open_cases_total > 0:
|
||||||
signals.append(("open_cases", min(95, 40 + open_cases_total * 10), severity_score_to_label(min(95, 40 + open_cases_total * 10)), f"По компании {counterparty} есть открытые кейсы: {open_cases_total}."))
|
signals.append(("open_cases", min(95, 40 + open_cases_total * 10), severity_score_to_label(min(95, 40 + open_cases_total * 10)), f"По компании {source_counterparty} есть открытые кейсы: {open_cases_total}."))
|
||||||
if detections_total > 0:
|
if detections_total > 0:
|
||||||
signals.append(("open_detections", min(90, 35 + detections_total * 5), severity_score_to_label(min(90, 35 + detections_total * 5)), f"По компании {counterparty} есть активные detections: {detections_total}."))
|
signals.append(("open_detections", min(90, 35 + detections_total * 5), severity_score_to_label(min(90, 35 + detections_total * 5)), f"По компании {source_counterparty} есть активные detections: {detections_total}."))
|
||||||
|
|
||||||
for signal_type, score, severity, summary in signals:
|
for signal_type, score, severity, summary in signals:
|
||||||
signal_rows.append(
|
signal_rows.append(
|
||||||
[
|
[
|
||||||
generated_at,
|
generated_at,
|
||||||
infobase,
|
infobase,
|
||||||
counterparty,
|
company_entity_key,
|
||||||
f"{signal_type}:{infobase}:{counterparty}",
|
f"{signal_type}:{infobase}:{company_entity_key}",
|
||||||
severity,
|
severity,
|
||||||
int(score),
|
int(score),
|
||||||
signal_type,
|
signal_type,
|
||||||
summary,
|
summary,
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import clickhouse_connect
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(description="Refresh technical company->registry bindings for analytics_1c")
|
||||||
|
p.add_argument("--host", default=os.getenv("CLICKHOUSE_HOST", "localhost"))
|
||||||
|
p.add_argument("--port", type=int, default=int(os.getenv("CLICKHOUSE_PORT", "8123")))
|
||||||
|
p.add_argument("--user", default=os.getenv("CLICKHOUSE_USER", "default"))
|
||||||
|
p.add_argument("--password", default=os.getenv("CLICKHOUSE_PASSWORD", ""))
|
||||||
|
p.add_argument("--database", default=os.getenv("CLICKHOUSE_DB", "analytics_1c"))
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def ch_client(args: argparse.Namespace):
|
||||||
|
return clickhouse_connect.get_client(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
username=args.user,
|
||||||
|
password=args.password,
|
||||||
|
database=args.database,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def query_rows(client, sql: str) -> list[dict[str, Any]]:
|
||||||
|
result = client.query(sql)
|
||||||
|
return [dict(zip(result.column_names, row)) for row in result.result_rows]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
client = ch_client(args)
|
||||||
|
generated_at = datetime.now(UTC).replace(tzinfo=None, microsecond=0)
|
||||||
|
|
||||||
|
current_bindings = {
|
||||||
|
str(row["company_entity_key"]): row
|
||||||
|
for row in query_rows(
|
||||||
|
client,
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
company_entity_key,
|
||||||
|
registry_company_key,
|
||||||
|
registry_company_name,
|
||||||
|
binding_source
|
||||||
|
FROM analytics_1c.v_company_registry_bindings_current
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = query_rows(
|
||||||
|
client,
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
company_entity_key,
|
||||||
|
infobase,
|
||||||
|
base_id,
|
||||||
|
base_path,
|
||||||
|
ifNull(base_path_key, '') AS base_path_key,
|
||||||
|
registry_company_key,
|
||||||
|
company_name,
|
||||||
|
registry_match_mode
|
||||||
|
FROM analytics_1c.v_company_portfolio_overview
|
||||||
|
WHERE registry_match_mode IN ('direct', 'alias', 'manual')
|
||||||
|
AND registry_company_key != ''
|
||||||
|
AND company_entity_key != ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
inserts: list[list[Any]] = []
|
||||||
|
for row in candidates:
|
||||||
|
entity_key = str(row["company_entity_key"])
|
||||||
|
registry_key = str(row["registry_company_key"])
|
||||||
|
current = current_bindings.get(entity_key)
|
||||||
|
if current and str(current.get("registry_company_key") or "") == registry_key:
|
||||||
|
continue
|
||||||
|
inserts.append(
|
||||||
|
[
|
||||||
|
generated_at,
|
||||||
|
str(row["infobase"] or ""),
|
||||||
|
entity_key,
|
||||||
|
str(row["base_id"] or ""),
|
||||||
|
str(row["base_path"] or ""),
|
||||||
|
str(row["base_path_key"] or ""),
|
||||||
|
registry_key,
|
||||||
|
str(row["company_name"] or ""),
|
||||||
|
f"bootstrap_{row['registry_match_mode']}",
|
||||||
|
"autobound_from_portfolio",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if inserts:
|
||||||
|
client.insert(
|
||||||
|
"analytics_1c.company_registry_bindings",
|
||||||
|
inserts,
|
||||||
|
column_names=[
|
||||||
|
"ts",
|
||||||
|
"infobase",
|
||||||
|
"company_entity_key",
|
||||||
|
"base_id",
|
||||||
|
"base_path",
|
||||||
|
"base_path_key",
|
||||||
|
"registry_company_key",
|
||||||
|
"registry_company_name",
|
||||||
|
"binding_source",
|
||||||
|
"note",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"company registry bindings refreshed: inserted={len(inserts)} generated_at={generated_at.isoformat()}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -39,38 +39,101 @@ CREATE TABLE IF NOT EXISTS analytics_1c.company_health_signals
|
|||||||
ENGINE = MergeTree
|
ENGINE = MergeTree
|
||||||
ORDER BY (generated_at, severity, infobase, counterparty, signal_id);
|
ORDER BY (generated_at, severity, infobase, counterparty, signal_id);
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW analytics_1c.v_counterparty_daily AS
|
CREATE TABLE IF NOT EXISTS analytics_1c.company_registry_bindings
|
||||||
SELECT
|
(
|
||||||
toDate(ts) AS d,
|
ts DateTime,
|
||||||
infobase,
|
infobase LowCardinality(String),
|
||||||
organization,
|
company_entity_key String,
|
||||||
counterparty,
|
base_id String,
|
||||||
count() AS docs_total,
|
base_path String,
|
||||||
sum(amount) AS amount_total,
|
base_path_key String,
|
||||||
countIf(posted = 1) AS posted_docs_total,
|
registry_company_key String,
|
||||||
countIf(posted = 0) AS unposted_docs_total,
|
registry_company_name String,
|
||||||
countIf(status = 'busy') AS busy_docs_total,
|
binding_source LowCardinality(String),
|
||||||
countIf(status = 'online') AS online_docs_total,
|
note String
|
||||||
uniqExact(doc_type) AS doc_types_total
|
)
|
||||||
FROM analytics_1c.documents
|
ENGINE = MergeTree
|
||||||
WHERE counterparty != ''
|
ORDER BY (company_entity_key, ts);
|
||||||
GROUP BY d, infobase, organization, counterparty;
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW analytics_1c.v_counterparty_latest_activity AS
|
CREATE OR REPLACE VIEW analytics_1c.v_companies_current AS
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
|
company_name,
|
||||||
organization,
|
organization,
|
||||||
counterparty,
|
owner_user,
|
||||||
max(ts) AS last_seen_at,
|
base_id,
|
||||||
argMax(doc_type, ts) AS last_doc_type,
|
base_path,
|
||||||
argMax(operation_type, ts) AS last_operation_type,
|
trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(base_path), '[\\\\/]+', '/'), '[^0-9A-ZА-ЯЁ:/._ -]+', ' '), '\\s+', ' ')) AS base_path_key,
|
||||||
argMax(status, ts) AS last_status,
|
multiIf(
|
||||||
argMax(amount, ts) AS last_amount,
|
base_id != '', concat('baseid:', base_id),
|
||||||
|
base_path != '', concat('basepath:', trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(base_path), '[\\\\/]+', '/'), '[^0-9A-ZА-ЯЁ:/._ -]+', ' '), '\\s+', ' '))),
|
||||||
|
concat('infobase:', trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(infobase), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' ')))
|
||||||
|
) AS company_entity_key,
|
||||||
|
current_status,
|
||||||
|
db_size_bytes,
|
||||||
|
reglog_size_bytes,
|
||||||
|
active_locks,
|
||||||
|
temp_db_present,
|
||||||
|
scheduler_touched,
|
||||||
|
current_activity_score,
|
||||||
|
last_company_snapshot_at
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
infobase,
|
||||||
|
argMax(company_name, ts) AS company_name,
|
||||||
|
argMax(organization, ts) AS organization,
|
||||||
|
argMax(owner_user, ts) AS owner_user,
|
||||||
|
argMax(base_id, ts) AS base_id,
|
||||||
|
argMax(base_path, ts) AS base_path,
|
||||||
|
argMax(status, ts) AS current_status,
|
||||||
|
argMax(db_size_bytes, ts) AS db_size_bytes,
|
||||||
|
argMax(reglog_size_bytes, ts) AS reglog_size_bytes,
|
||||||
|
argMax(active_locks, ts) AS active_locks,
|
||||||
|
argMax(temp_db_present, ts) AS temp_db_present,
|
||||||
|
argMax(scheduler_touched, ts) AS scheduler_touched,
|
||||||
|
argMax(activity_score, ts) AS current_activity_score,
|
||||||
|
max(ts) AS last_company_snapshot_at
|
||||||
|
FROM analytics_1c.companies
|
||||||
|
GROUP BY infobase
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW analytics_1c.v_company_activity_daily AS
|
||||||
|
SELECT
|
||||||
|
toDate(documents.ts) AS d,
|
||||||
|
documents.infobase AS infobase,
|
||||||
|
ifNull(companies.organization, documents.organization) AS organization,
|
||||||
|
ifNull(companies.company_entity_key, concat('infobase:', trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(documents.infobase), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' ')))) AS company_entity_key,
|
||||||
|
argMax(documents.counterparty, documents.ts) AS source_counterparty,
|
||||||
|
count() AS docs_total,
|
||||||
|
sum(documents.amount) AS amount_total,
|
||||||
|
countIf(documents.posted = 1) AS posted_docs_total,
|
||||||
|
countIf(documents.posted = 0) AS unposted_docs_total,
|
||||||
|
countIf(documents.status = 'busy') AS busy_docs_total,
|
||||||
|
countIf(documents.status = 'online') AS online_docs_total,
|
||||||
|
uniqExact(documents.doc_type) AS doc_types_total
|
||||||
|
FROM analytics_1c.documents AS documents
|
||||||
|
LEFT JOIN analytics_1c.v_companies_current AS companies ON companies.infobase = documents.infobase
|
||||||
|
WHERE documents.counterparty != ''
|
||||||
|
GROUP BY d, documents.infobase, ifNull(companies.organization, documents.organization), ifNull(companies.company_entity_key, concat('infobase:', trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(documents.infobase), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' '))));
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW analytics_1c.v_company_activity_latest AS
|
||||||
|
SELECT
|
||||||
|
documents.infobase AS infobase,
|
||||||
|
ifNull(companies.organization, documents.organization) AS organization,
|
||||||
|
ifNull(companies.company_entity_key, concat('infobase:', trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(documents.infobase), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' ')))) AS company_entity_key,
|
||||||
|
argMax(documents.counterparty, documents.ts) AS source_counterparty,
|
||||||
|
max(documents.ts) AS last_seen_at,
|
||||||
|
argMax(documents.doc_type, documents.ts) AS last_doc_type,
|
||||||
|
argMax(documents.operation_type, documents.ts) AS last_operation_type,
|
||||||
|
argMax(documents.status, documents.ts) AS last_status,
|
||||||
|
argMax(documents.amount, documents.ts) AS last_amount,
|
||||||
count() AS docs_lifetime,
|
count() AS docs_lifetime,
|
||||||
sum(amount) AS amount_lifetime
|
sum(documents.amount) AS amount_lifetime
|
||||||
FROM analytics_1c.documents
|
FROM analytics_1c.documents AS documents
|
||||||
WHERE counterparty != ''
|
LEFT JOIN analytics_1c.v_companies_current AS companies ON companies.infobase = documents.infobase
|
||||||
GROUP BY infobase, organization, counterparty;
|
WHERE documents.counterparty != ''
|
||||||
|
GROUP BY documents.infobase, ifNull(companies.organization, documents.organization), ifNull(companies.company_entity_key, concat('infobase:', trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(documents.infobase), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' '))));
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW analytics_1c.v_company_forecasts_current AS
|
CREATE OR REPLACE VIEW analytics_1c.v_company_forecasts_current AS
|
||||||
SELECT *
|
SELECT *
|
||||||
@@ -82,24 +145,35 @@ SELECT *
|
|||||||
FROM analytics_1c.company_health_signals
|
FROM analytics_1c.company_health_signals
|
||||||
WHERE generated_at = (SELECT max(generated_at) FROM analytics_1c.company_health_signals);
|
WHERE generated_at = (SELECT max(generated_at) FROM analytics_1c.company_health_signals);
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW analytics_1c.v_companies_current AS
|
CREATE OR REPLACE VIEW analytics_1c.v_counterparty_daily AS
|
||||||
|
SELECT
|
||||||
|
d,
|
||||||
|
infobase,
|
||||||
|
organization,
|
||||||
|
company_entity_key AS counterparty,
|
||||||
|
docs_total,
|
||||||
|
amount_total,
|
||||||
|
posted_docs_total,
|
||||||
|
unposted_docs_total,
|
||||||
|
busy_docs_total,
|
||||||
|
online_docs_total,
|
||||||
|
doc_types_total
|
||||||
|
FROM analytics_1c.v_company_activity_daily;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW analytics_1c.v_counterparty_latest_activity AS
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
argMax(company_name, ts) AS company_name,
|
organization,
|
||||||
argMax(organization, ts) AS organization,
|
company_entity_key AS counterparty,
|
||||||
argMax(owner_user, ts) AS owner_user,
|
source_counterparty,
|
||||||
argMax(base_id, ts) AS base_id,
|
last_seen_at,
|
||||||
argMax(base_path, ts) AS base_path,
|
last_doc_type,
|
||||||
argMax(status, ts) AS current_status,
|
last_operation_type,
|
||||||
argMax(db_size_bytes, ts) AS db_size_bytes,
|
last_status,
|
||||||
argMax(reglog_size_bytes, ts) AS reglog_size_bytes,
|
last_amount,
|
||||||
argMax(active_locks, ts) AS active_locks,
|
docs_lifetime,
|
||||||
argMax(temp_db_present, ts) AS temp_db_present,
|
amount_lifetime
|
||||||
argMax(scheduler_touched, ts) AS scheduler_touched,
|
FROM analytics_1c.v_company_activity_latest;
|
||||||
argMax(activity_score, ts) AS current_activity_score,
|
|
||||||
max(ts) AS last_company_snapshot_at
|
|
||||||
FROM analytics_1c.companies
|
|
||||||
GROUP BY infobase;
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW analytics_1c.v_company_registry_current AS
|
CREATE OR REPLACE VIEW analytics_1c.v_company_registry_current AS
|
||||||
SELECT
|
SELECT
|
||||||
@@ -115,6 +189,21 @@ SELECT
|
|||||||
FROM analytics_1c.company_registry
|
FROM analytics_1c.company_registry
|
||||||
GROUP BY company_key;
|
GROUP BY company_key;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW analytics_1c.v_company_registry_bindings_current AS
|
||||||
|
SELECT
|
||||||
|
company_entity_key,
|
||||||
|
argMax(infobase, ts) AS infobase,
|
||||||
|
argMax(base_id, ts) AS base_id,
|
||||||
|
argMax(base_path, ts) AS base_path,
|
||||||
|
argMax(base_path_key, ts) AS base_path_key,
|
||||||
|
argMax(registry_company_key, ts) AS registry_company_key,
|
||||||
|
argMax(registry_company_name, ts) AS registry_company_name,
|
||||||
|
argMax(binding_source, ts) AS binding_source,
|
||||||
|
argMax(note, ts) AS note,
|
||||||
|
max(ts) AS last_binding_at
|
||||||
|
FROM analytics_1c.company_registry_bindings
|
||||||
|
GROUP BY company_entity_key;
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW analytics_1c.v_company_registry_alias_map AS
|
CREATE OR REPLACE VIEW analytics_1c.v_company_registry_alias_map AS
|
||||||
SELECT
|
SELECT
|
||||||
source_company_key,
|
source_company_key,
|
||||||
@@ -220,7 +309,7 @@ base AS
|
|||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
*,
|
*,
|
||||||
trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(counterparty), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' ')) AS counterparty_key
|
trimBoth(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(upperUTF8(source_counterparty), '(^|\\s)20[0-9]{2}($|\\s)', ' '), '[^0-9A-ZА-ЯЁ]+', ' '), '\\s+', ' ')) AS source_counterparty_key
|
||||||
FROM analytics_1c.v_counterparty_latest_activity
|
FROM analytics_1c.v_counterparty_latest_activity
|
||||||
),
|
),
|
||||||
d7 AS
|
d7 AS
|
||||||
@@ -252,6 +341,11 @@ company_state AS
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM analytics_1c.v_companies_current
|
FROM analytics_1c.v_companies_current
|
||||||
),
|
),
|
||||||
|
binding_state AS
|
||||||
|
(
|
||||||
|
SELECT *
|
||||||
|
FROM analytics_1c.v_company_registry_bindings_current
|
||||||
|
),
|
||||||
registry_state AS
|
registry_state AS
|
||||||
(
|
(
|
||||||
SELECT *
|
SELECT *
|
||||||
@@ -271,18 +365,18 @@ signals AS
|
|||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
counterparty,
|
counterparty AS company_entity_key,
|
||||||
max(score) AS signal_score,
|
max(score) AS signal_score,
|
||||||
argMax(severity, score) AS signal_severity,
|
argMax(severity, score) AS signal_severity,
|
||||||
argMax(summary, score) AS top_signal
|
argMax(summary, score) AS top_signal
|
||||||
FROM analytics_1c.v_company_health_current
|
FROM analytics_1c.v_company_health_current
|
||||||
GROUP BY infobase, counterparty
|
GROUP BY infobase, company_entity_key
|
||||||
),
|
),
|
||||||
amount_forecast AS
|
amount_forecast AS
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
counterparty,
|
counterparty AS company_entity_key,
|
||||||
predicted_total AS amount_forecast_30d,
|
predicted_total AS amount_forecast_30d,
|
||||||
confidence AS amount_forecast_confidence
|
confidence AS amount_forecast_confidence
|
||||||
FROM analytics_1c.v_company_forecasts_current
|
FROM analytics_1c.v_company_forecasts_current
|
||||||
@@ -293,7 +387,7 @@ docs_forecast AS
|
|||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
counterparty,
|
counterparty AS company_entity_key,
|
||||||
predicted_total AS docs_forecast_30d,
|
predicted_total AS docs_forecast_30d,
|
||||||
confidence AS docs_forecast_confidence
|
confidence AS docs_forecast_confidence
|
||||||
FROM analytics_1c.v_company_forecasts_current
|
FROM analytics_1c.v_company_forecasts_current
|
||||||
@@ -304,30 +398,35 @@ cases_current AS
|
|||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
entity_id AS counterparty,
|
entity_id AS company_entity_key,
|
||||||
countIf(status != 'closed') AS open_cases_total
|
countIf(status != 'closed') AS open_cases_total
|
||||||
FROM analytics_1c.cases
|
FROM analytics_1c.cases
|
||||||
WHERE entity_type = 'counterparty'
|
WHERE entity_type = 'counterparty'
|
||||||
GROUP BY infobase, counterparty
|
GROUP BY infobase, company_entity_key
|
||||||
),
|
),
|
||||||
detections_current AS
|
detections_current AS
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
infobase,
|
infobase,
|
||||||
entity_id AS counterparty,
|
entity_id AS company_entity_key,
|
||||||
count() AS detections_total
|
count() AS detections_total
|
||||||
FROM analytics_1c.detections
|
FROM analytics_1c.detections
|
||||||
WHERE entity_type = 'counterparty'
|
WHERE entity_type = 'counterparty'
|
||||||
AND status != 'closed'
|
AND status != 'closed'
|
||||||
GROUP BY infobase, counterparty
|
GROUP BY infobase, company_entity_key
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
base.infobase AS infobase,
|
base.infobase AS infobase,
|
||||||
|
base.counterparty AS company_entity_key,
|
||||||
if(company_state.organization != '', company_state.organization, base.organization) AS organization,
|
if(company_state.organization != '', company_state.organization, base.organization) AS organization,
|
||||||
base.counterparty AS counterparty,
|
base.source_counterparty AS source_counterparty,
|
||||||
if(alias_state.target_company_name != '', alias_state.target_company_name, if(manual_state.company_name != '', manual_state.company_name, if(company_state.company_name != '', company_state.company_name, base.counterparty))) AS company_name,
|
if(binding_state.registry_company_name != '', binding_state.registry_company_name, if(alias_state.target_company_name != '', alias_state.target_company_name, if(manual_state.company_name != '', manual_state.company_name, if(company_state.company_name != '', company_state.company_name, base.source_counterparty)))) AS counterparty,
|
||||||
if(alias_state.target_company_name != '', alias_state.target_company_name, if(manual_state.company_name != '', manual_state.company_name, base.counterparty)) AS normalized_counterparty,
|
if(binding_state.registry_company_name != '', binding_state.registry_company_name, if(alias_state.target_company_name != '', alias_state.target_company_name, if(manual_state.company_name != '', manual_state.company_name, if(company_state.company_name != '', company_state.company_name, base.source_counterparty)))) AS company_name,
|
||||||
multiIf(ifNull(alias_state.exclude_from_portfolio, 0) = 1, 'excluded', alias_state.target_company_key != '' AND registry_state.company_key != '', 'alias', registry_state.company_key != '', 'direct', manual_state.company_key != '', 'manual', 'none') AS registry_match_mode,
|
if(binding_state.registry_company_name != '', binding_state.registry_company_name, if(alias_state.target_company_name != '', alias_state.target_company_name, if(manual_state.company_name != '', manual_state.company_name, base.source_counterparty))) AS normalized_counterparty,
|
||||||
|
multiIf(binding_state.registry_company_key != '', 'technical', ifNull(alias_state.exclude_from_portfolio, 0) = 1, 'excluded', alias_state.target_company_key != '' AND registry_state.company_key != '', 'alias', registry_state.company_key != '', 'direct', manual_state.company_key != '', 'manual', 'none') AS registry_match_mode,
|
||||||
|
if(binding_state.registry_company_key != '', binding_state.registry_company_key, if(registry_state.company_key != '', registry_state.company_key, ifNull(manual_state.company_key, ''))) AS registry_company_key,
|
||||||
|
ifNull(binding_state.binding_source, '') AS registry_binding_source,
|
||||||
|
ifNull(binding_state.note, '') AS registry_binding_note,
|
||||||
if(registry_state.assignee_name != '', registry_state.assignee_name, ifNull(manual_state.assignee_name, '')) AS registry_assignee_name,
|
if(registry_state.assignee_name != '', registry_state.assignee_name, ifNull(manual_state.assignee_name, '')) AS registry_assignee_name,
|
||||||
if(registry_state.registry_status != '', registry_state.registry_status, ifNull(manual_state.registry_status, '')) AS registry_status,
|
if(registry_state.registry_status != '', registry_state.registry_status, ifNull(manual_state.registry_status, '')) AS registry_status,
|
||||||
if(registry_state.share_text != '', registry_state.share_text, ifNull(manual_state.share_text, '')) AS registry_share_text,
|
if(registry_state.share_text != '', registry_state.share_text, ifNull(manual_state.share_text, '')) AS registry_share_text,
|
||||||
@@ -337,6 +436,7 @@ SELECT
|
|||||||
ifNull(company_state.owner_user, '') AS owner_user,
|
ifNull(company_state.owner_user, '') AS owner_user,
|
||||||
ifNull(company_state.base_id, '') AS base_id,
|
ifNull(company_state.base_id, '') AS base_id,
|
||||||
ifNull(company_state.base_path, '') AS base_path,
|
ifNull(company_state.base_path, '') AS base_path,
|
||||||
|
ifNull(company_state.base_path_key, '') AS base_path_key,
|
||||||
base.last_seen_at,
|
base.last_seen_at,
|
||||||
company_state.last_company_snapshot_at,
|
company_state.last_company_snapshot_at,
|
||||||
base.last_doc_type,
|
base.last_doc_type,
|
||||||
@@ -367,14 +467,15 @@ SELECT
|
|||||||
ifNull(signals.top_signal, '') AS top_signal
|
ifNull(signals.top_signal, '') AS top_signal
|
||||||
FROM base
|
FROM base
|
||||||
LEFT JOIN company_state ON company_state.infobase = base.infobase
|
LEFT JOIN company_state ON company_state.infobase = base.infobase
|
||||||
LEFT JOIN alias_state ON alias_state.source_company_key = base.counterparty_key
|
LEFT JOIN binding_state ON binding_state.company_entity_key = base.counterparty
|
||||||
LEFT JOIN registry_state ON registry_state.company_key = if(alias_state.target_company_key != '', alias_state.target_company_key, base.counterparty_key)
|
LEFT JOIN alias_state ON alias_state.source_company_key = base.source_counterparty_key
|
||||||
LEFT JOIN manual_state ON manual_state.company_key = if(alias_state.target_company_key != '', alias_state.target_company_key, base.counterparty_key)
|
LEFT JOIN registry_state ON registry_state.company_key = if(binding_state.registry_company_key != '', binding_state.registry_company_key, if(alias_state.target_company_key != '', alias_state.target_company_key, base.source_counterparty_key))
|
||||||
|
LEFT JOIN manual_state ON manual_state.company_key = if(binding_state.registry_company_key != '', binding_state.registry_company_key, if(alias_state.target_company_key != '', alias_state.target_company_key, base.source_counterparty_key))
|
||||||
LEFT JOIN d7 ON d7.infobase = base.infobase AND d7.counterparty = base.counterparty
|
LEFT JOIN d7 ON d7.infobase = base.infobase AND d7.counterparty = base.counterparty
|
||||||
LEFT JOIN d30 ON d30.infobase = base.infobase AND d30.counterparty = base.counterparty
|
LEFT JOIN d30 ON d30.infobase = base.infobase AND d30.counterparty = base.counterparty
|
||||||
LEFT JOIN signals ON signals.infobase = base.infobase AND signals.counterparty = base.counterparty
|
LEFT JOIN signals ON signals.infobase = base.infobase AND signals.company_entity_key = base.counterparty
|
||||||
LEFT JOIN amount_forecast ON amount_forecast.infobase = base.infobase AND amount_forecast.counterparty = base.counterparty
|
LEFT JOIN amount_forecast ON amount_forecast.infobase = base.infobase AND amount_forecast.company_entity_key = base.counterparty
|
||||||
LEFT JOIN docs_forecast ON docs_forecast.infobase = base.infobase AND docs_forecast.counterparty = base.counterparty
|
LEFT JOIN docs_forecast ON docs_forecast.infobase = base.infobase AND docs_forecast.company_entity_key = base.counterparty
|
||||||
LEFT JOIN cases_current ON cases_current.infobase = base.infobase AND cases_current.counterparty = base.counterparty
|
LEFT JOIN cases_current ON cases_current.infobase = base.infobase AND cases_current.company_entity_key = base.counterparty
|
||||||
LEFT JOIN detections_current ON detections_current.infobase = base.infobase AND detections_current.counterparty = base.counterparty
|
LEFT JOIN detections_current ON detections_current.infobase = base.infobase AND detections_current.company_entity_key = base.counterparty
|
||||||
WHERE ifNull(alias_state.exclude_from_portfolio, 0) = 0;
|
WHERE ifNull(alias_state.exclude_from_portfolio, 0) = 0;
|
||||||
|
|||||||
@@ -21,19 +21,22 @@ INSERT INTO analytics_1c.entity_timeline
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
ts,
|
documents.ts AS ts,
|
||||||
'counterparty' AS entity_type,
|
'counterparty' AS entity_type,
|
||||||
counterparty AS entity_id,
|
ifNull(portfolio.company_entity_key, documents.counterparty) AS entity_id,
|
||||||
infobase,
|
documents.infobase AS infobase,
|
||||||
author AS actor,
|
documents.author AS actor,
|
||||||
'documents' AS source,
|
'documents' AS source,
|
||||||
concat('counterparty:', operation_type) AS event_type,
|
concat('counterparty:', documents.operation_type) AS event_type,
|
||||||
if(status = 'busy', 'medium', 'low') AS severity,
|
if(documents.status = 'busy', 'medium', 'low') AS severity,
|
||||||
greatest(10, toUInt32(round(amount))) AS score,
|
greatest(10, toUInt32(round(documents.amount))) AS score,
|
||||||
concat('counterparty:', counterparty, ':', doc_id, ':', toString(toUnixTimestamp(ts))) AS ref_id,
|
concat('counterparty:', ifNull(portfolio.company_entity_key, documents.counterparty), ':', documents.doc_id, ':', toString(toUnixTimestamp(documents.ts))) AS ref_id,
|
||||||
concat('Активность компании ', counterparty, ': ', doc_type, ' score=', toString(amount), ' status=', status) AS summary
|
concat('Активность компании ', ifNull(portfolio.company_name, documents.counterparty), ': ', documents.doc_type, ' score=', toString(documents.amount), ' status=', documents.status) AS summary
|
||||||
FROM analytics_1c.documents
|
FROM analytics_1c.documents AS documents
|
||||||
WHERE counterparty != ''
|
LEFT JOIN analytics_1c.v_company_portfolio_overview AS portfolio
|
||||||
|
ON portfolio.infobase = documents.infobase
|
||||||
|
AND portfolio.source_counterparty = documents.counterparty
|
||||||
|
WHERE documents.counterparty != ''
|
||||||
) AS src
|
) AS src
|
||||||
WHERE src.ref_id NOT IN (SELECT ref_id FROM analytics_1c.entity_timeline);
|
WHERE src.ref_id NOT IN (SELECT ref_id FROM analytics_1c.entity_timeline);
|
||||||
|
|
||||||
@@ -41,18 +44,18 @@ INSERT INTO analytics_1c.entity_timeline
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
ts,
|
last_company_snapshot_at AS ts,
|
||||||
'counterparty' AS entity_type,
|
'counterparty' AS entity_type,
|
||||||
company_name AS entity_id,
|
company_entity_key AS entity_id,
|
||||||
infobase,
|
infobase,
|
||||||
owner_user AS actor,
|
owner_user AS actor,
|
||||||
'companies' AS source,
|
'companies' AS source,
|
||||||
'company_snapshot' AS event_type,
|
'company_snapshot' AS event_type,
|
||||||
if(status = 'busy' OR active_locks > 0 OR temp_db_present = 1, 'medium', 'low') AS severity,
|
if(current_status = 'busy' OR active_locks > 0 OR temp_db_present = 1, 'medium', 'low') AS severity,
|
||||||
greatest(10, toUInt32(round(activity_score))) AS score,
|
greatest(10, toUInt32(round(current_activity_score))) AS score,
|
||||||
concat('company:', infobase, ':', toString(toUnixTimestamp(ts))) AS ref_id,
|
concat('company:', company_entity_key, ':', toString(toUnixTimestamp(last_company_snapshot_at))) AS ref_id,
|
||||||
concat('Company snapshot ', company_name, ': status=', status, ' locks=', toString(active_locks), ' score=', toString(activity_score)) AS summary
|
concat('Company snapshot ', company_name, ': status=', current_status, ' locks=', toString(active_locks), ' score=', toString(current_activity_score)) AS summary
|
||||||
FROM analytics_1c.companies
|
FROM analytics_1c.v_companies_current
|
||||||
) AS src
|
) AS src
|
||||||
WHERE src.ref_id NOT IN (SELECT ref_id FROM analytics_1c.entity_timeline);
|
WHERE src.ref_id NOT IN (SELECT ref_id FROM analytics_1c.entity_timeline);
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ docker exec -i "${CH_CONTAINER}" clickhouse-client \
|
|||||||
--database "${CLICKHOUSE_DB}" \
|
--database "${CLICKHOUSE_DB}" \
|
||||||
< "${ROOT}/clickhouse/init/04_company_intelligence.sql"
|
< "${ROOT}/clickhouse/init/04_company_intelligence.sql"
|
||||||
|
|
||||||
|
"${ROOT}/ops/run_company_registry_bindings_refresh.sh"
|
||||||
|
|
||||||
"${VENV}/bin/python" "${ROOT}/ai/refresh_company_intelligence.py" \
|
"${VENV}/bin/python" "${ROOT}/ai/refresh_company_intelligence.py" \
|
||||||
--host "${CH_RUNTIME_HOST}" \
|
--host "${CH_RUNTIME_HOST}" \
|
||||||
--port "${CLICKHOUSE_PORT}" \
|
--port "${CLICKHOUSE_PORT}" \
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="${AW_1C_ROOT:-/opt/activitywatch/clickhouse-1c}"
|
||||||
|
ENV_FILE="${ROOT}/.env"
|
||||||
|
VENV="${ROOT}/.venv"
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "missing env file: ${ENV_FILE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${VENV}/bin/python" ]]; then
|
||||||
|
echo "missing venv python: ${VENV}/bin/python" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${ENV_FILE}"
|
||||||
|
|
||||||
|
CH_RUNTIME_HOST="${AW_1C_CLICKHOUSE_RUNTIME_HOST:-${CLICKHOUSE_HOST}}"
|
||||||
|
if [[ "${CH_RUNTIME_HOST}" == "clickhouse" ]]; then
|
||||||
|
CH_RUNTIME_HOST="127.0.0.1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${VENV}/bin/python" "${ROOT}/ai/refresh_company_registry_bindings.py" \
|
||||||
|
--host "${CH_RUNTIME_HOST}" \
|
||||||
|
--port "${CLICKHOUSE_PORT}" \
|
||||||
|
--user "${CLICKHOUSE_USER}" \
|
||||||
|
--password "${CLICKHOUSE_PASSWORD}" \
|
||||||
|
--database "${CLICKHOUSE_DB}"
|
||||||
@@ -55,6 +55,7 @@ docker exec -i "${CH_CONTAINER}" clickhouse-client \
|
|||||||
--database "${CLICKHOUSE_DB}" \
|
--database "${CLICKHOUSE_DB}" \
|
||||||
< "${ROOT}/clickhouse/init/04_company_intelligence.sql"
|
< "${ROOT}/clickhouse/init/04_company_intelligence.sql"
|
||||||
|
|
||||||
|
"${ROOT}/ops/run_company_registry_bindings_refresh.sh"
|
||||||
"${ROOT}/ops/run_company_intelligence_refresh.sh"
|
"${ROOT}/ops/run_company_intelligence_refresh.sh"
|
||||||
|
|
||||||
docker exec -i "${CH_CONTAINER}" clickhouse-client \
|
docker exec -i "${CH_CONTAINER}" clickhouse-client \
|
||||||
|
|||||||
Reference in New Issue
Block a user