Files
nocobase-mcp/nocobase_crm_mcp/crm_presenters.py
T

283 lines
12 KiB
Python

from __future__ import annotations
from .crm_mixin import CrmMixin
from .crm_shared import (
CUSTOMER_STATUS_LABELS,
FOLLOWUP_CONTENT_KEYS,
FOLLOWUP_DATE_KEYS,
FOLLOWUP_STATUS_LABELS,
FOLLOWUP_TYPE_LABELS,
UNKNOWN_NAME_PREFIX,
Any,
Mapping,
_beijing_datetime,
_compact_field_list,
_fields_with_projections,
_first_text,
_first_value,
_prune,
_ref_ids,
_relation_refs,
)
class CrmPresentersMixin(CrmMixin):
def _project_fields(self) -> list[str]:
project = self.config.project
scalars = [
project.id_field,
project.name_field,
project.customer_field,
project.inquiry_date_field,
project.progress_field,
project.applications_field,
project.part_numbers_field,
project.period_usage_field,
project.details_field,
project.attachment_field,
"createdAt",
"updatedAt",
]
projections = [
*self._projection(project.customer_relation_field, self._customer_spec(), None),
*self._projection(project.contact_field, self._contact_spec(), None),
]
return _fields_with_projections(scalars, projections)
def _compact_project(self, row: Mapping[str, Any], *, include_raw: bool) -> dict[str, Any]:
project = self.config.project
progress = row.get(project.progress_field)
progress_labels = {value: label for label, value in project.progress_values.items()}
compact: dict[str, Any] = {
"id": row.get(project.id_field),
"name": row.get(project.name_field),
"customer_id": row.get(project.customer_field),
"customers": _relation_refs(row, [project.customer_relation_field], self._customer_spec()),
"inquiry_date": row.get(project.inquiry_date_field),
"stage": progress_labels.get(str(progress), progress) if progress is not None else None,
"applications": row.get(project.applications_field),
"part_numbers": row.get(project.part_numbers_field),
"period_usage": row.get(project.period_usage_field),
"details": row.get(project.details_field),
"contacts": _relation_refs(row, [project.contact_field], self._contact_spec()),
"createdAt": _beijing_datetime(row.get("createdAt"), self.config.timezone),
"updatedAt": _beijing_datetime(row.get("updatedAt"), self.config.timezone),
}
compact = _prune(compact)
if include_raw:
compact["raw"] = dict(row)
return compact
def _compact_project_update(self, row: Mapping[str, Any]) -> dict[str, Any]:
creators = _relation_refs(row, ["createdBy"], self._user_spec())
return _prune(
{
"id": row.get("id"),
"updated_at": _beijing_datetime(
row.get(self.config.project.update_date_field) or row.get("createdAt"),
self.config.timezone,
),
"content": row.get(self.config.project.update_content_field),
"creator": creators[0] if creators else None,
}
)
def _compact_customers(self, rows: list[dict[str, Any]], *, include_raw: bool = False) -> list[dict[str, Any]]:
return [self._compact_customer(row, include_raw=include_raw) for row in rows]
def _customer_name_candidates(self) -> list[str]:
return [*(self.config.customer.name_fields or []), "name", "title"]
def _customer_compact_fields(self, *, detail: bool = False) -> list[str]:
customer = self.config.customer
scalars = _compact_field_list(
customer.fields,
[
customer.id_field,
*self._customer_name_candidates(),
customer.english_name_field,
customer.territorial_name_field,
customer.status_field,
customer.source_field,
"createdAt",
"updatedAt",
*(customer.detail_fields.values() if detail else []),
],
)
projections = [
*self._projection(customer.owner_field, self._user_spec(), customer.fields),
*self._projection(customer.manager_field, self._user_spec(), customer.fields),
]
return _fields_with_projections(scalars, projections)
def _compact_customer(
self,
row: Mapping[str, Any],
*,
detail: bool = False,
include_raw: bool = False,
) -> dict[str, Any]:
customer = self.config.customer
identifier = row.get(customer.id_field)
name = _first_text(row, self._customer_name_candidates())
if name is None and identifier is not None:
name = f"{UNKNOWN_NAME_PREFIX}{identifier}"
english_name = _first_text(row, [customer.english_name_field, "english_name"])
territorial_name = _first_text(row, [customer.territorial_name_field, "territorial_name"])
status = _first_value(row, [customer.status_field, "status"])
compact: dict[str, Any] = {
"id": identifier,
"name": name,
# Only worth repeating when they carry information the name does not.
"english_name": english_name if english_name != name else None,
"territorial_name": territorial_name if territorial_name != name else None,
"status": CUSTOMER_STATUS_LABELS.get(status, str(status) if status is not None else None),
"source": _first_value(row, [customer.source_field, "source"]),
"owners": _relation_refs(row, [customer.owner_field], self._user_spec()),
"managers": _relation_refs(row, [customer.manager_field], self._user_spec()),
"createdAt": _beijing_datetime(row.get("createdAt"), self.config.timezone),
"updatedAt": _beijing_datetime(row.get("updatedAt"), self.config.timezone),
}
if detail:
for output_key, source_field in customer.detail_fields.items():
if output_key not in compact:
compact[output_key] = row.get(source_field)
compact = _prune(compact)
if include_raw:
compact["raw"] = dict(row)
return compact
def _compact_contacts(
self,
rows: list[dict[str, Any]],
*,
include_details: bool = True,
include_raw: bool = False,
) -> list[dict[str, Any]]:
return [self._compact_contact(row, include_details=include_details, include_raw=include_raw) for row in rows]
def _contact_name_candidates(self) -> list[str]:
return [*(self.config.contact.name_fields or []), "name", "title"]
def _contact_compact_fields(self, *, include_details: bool = True) -> list[str]:
contact = self.config.contact
scalars = _compact_field_list(
contact.fields,
[
contact.id_field,
*self._contact_name_candidates(),
contact.title_field,
"position",
*([contact.phone_field, "phone", contact.email_field, "email"] if include_details else []),
"createdAt",
"updatedAt",
],
)
projections = self._projection(contact.customer_field, self._customer_spec(), contact.fields)
return _fields_with_projections(scalars, projections)
def _compact_contact(
self,
row: Mapping[str, Any],
*,
include_details: bool = True,
include_raw: bool = False,
) -> dict[str, Any]:
contact = self.config.contact
compact: dict[str, Any] = {
"id": row.get(contact.id_field),
"name": _first_text(row, self._contact_name_candidates()),
"title": _first_text(row, [contact.title_field, "title", "position"]),
"customers": _relation_refs(row, [contact.customer_field], self._customer_spec()),
"createdAt": _beijing_datetime(row.get("createdAt"), self.config.timezone),
"updatedAt": _beijing_datetime(row.get("updatedAt"), self.config.timezone),
}
if include_details:
compact["phone"] = _first_text(row, [contact.phone_field, "phone"])
compact["email"] = _first_text(row, [contact.email_field, "email"])
compact = _prune(compact)
if include_raw:
compact["raw"] = dict(row)
return compact
def _compact_followups(self, rows: list[dict[str, Any]], *, include_raw: bool = False) -> list[dict[str, Any]]:
return [self._compact_followup(row, include_raw=include_raw) for row in rows]
def _followup_content_fields(self) -> list[str]:
return [
self.config.followup.content_field,
*(self.config.followup.search_fields or []),
*FOLLOWUP_CONTENT_KEYS,
]
def _followup_compact_fields(self) -> list[str]:
followup = self.config.followup
scalars = _compact_field_list(
followup.fields,
[
followup.id_field,
followup.date_field,
*FOLLOWUP_DATE_KEYS,
*self._followup_content_fields(),
followup.type_field,
followup.status_field,
followup.location_field,
],
)
projections = [
*self._projection(followup.customer_field, self._customer_spec(), followup.fields),
*self._projection(followup.contact_field, self._contact_spec(), followup.fields),
*self._projection(followup.project_field, self._project_spec(), followup.fields),
*self._projection(followup.owner_field, self._user_spec(), followup.fields),
*self._projection("createdBy", self._user_spec(), followup.fields),
]
return _fields_with_projections(scalars, projections)
def _compact_followup(self, row: Mapping[str, Any], *, include_raw: bool = False) -> dict[str, Any]:
followup = self.config.followup
summary = self._followup_summary(row)
participants = summary["participants"]
followup_type = row.get(followup.type_field)
record_status = row.get(followup.status_field)
compact: dict[str, Any] = {
"id": row.get(followup.id_field),
"followedAt": _beijing_datetime(summary["followedAt"], self.config.timezone),
"customers": summary["customers"],
"contacts": summary["contacts"],
"participants": participants,
"projects": summary["projects"],
"type": FOLLOWUP_TYPE_LABELS.get(str(followup_type), followup_type) if followup_type is not None else None,
"status": (
FOLLOWUP_STATUS_LABELS.get(str(record_status), record_status) if record_status is not None else None
),
"location": _first_text(row, [followup.location_field]),
"content": summary["content"],
}
# A record is usually submitted by one of its own participants; naming the
# submitter again in that case is pure repetition, so it only shows up when
# it actually tells the reader something new.
creator = summary["creator"]
if creator and creator.get("id") not in _ref_ids(participants):
compact["creator"] = creator
compact = _prune(compact)
if include_raw:
compact["raw"] = dict(row)
return compact
def _followup_summary(self, row: Mapping[str, Any]) -> dict[str, Any]:
followup = self.config.followup
date_fields = [followup.date_field, *FOLLOWUP_DATE_KEYS]
# owner_fields drives *filtering* and may include createdBy; participants
# are only ever the follow-up person relation (the contribution basis).
participant_fields = [followup.owner_field]
creators = _relation_refs(row, ["createdBy"], self._user_spec())
return {
"content": _first_text(row, self._followup_content_fields()),
"followedAt": _first_text(row, date_fields),
"customers": _relation_refs(row, [followup.customer_field], self._customer_spec()),
"contacts": _relation_refs(row, [followup.contact_field], self._contact_spec()),
"participants": _relation_refs(row, participant_fields, self._user_spec()),
"creator": creators[0] if creators else None,
"projects": _relation_refs(row, [followup.project_field], self._project_spec()),
}