283 lines
12 KiB
Python
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()),
|
|
}
|