Files
leocrm/app/plugins/builtins/contacts/plugin.py
T

220 lines
11 KiB
Python
Raw Normal View History

"""Contacts plugin — Core CRM entity lifecycle management.
Registers Contact entity models, permissions, restore config, and history hooks
via the same plugin lifecycle as all other business plugins. No Core special case.
"""
from __future__ import annotations
import logging
from app.plugins.base import BasePlugin
from app.plugins.manifest import (
FieldDefinition,
FrontendDashboardWidget,
FrontendMenuItem,
FrontendPageRoute,
MiniAppContribution,
PluginManifest,
PluginRouteDef,
)
logger = logging.getLogger(__name__)
class ContactsPlugin(BasePlugin):
"""Contacts plugin — owns the full Contact domain (Block B1).
Routes (contacts, companies, contact folders, folder permissions) live in
this plugin and are mounted via manifest.routes with
require_active_plugin("contacts") protection. Entity lifecycle
(permissions, entity models, restore, history) is managed through
on_activate/on_deactivate like every other business plugin.
"""
manifest = PluginManifest(
name="contacts",
version="1.1.0",
display_name="Contacts",
description="Core CRM contacts — persons and companies.",
dependencies=[],
routes=[
PluginRouteDef(
path="/api/v1/contacts",
module="app.plugins.builtins.contacts.routes",
router_attr="router",
),
PluginRouteDef(
path="/api/v1/companies",
module="app.plugins.builtins.contacts.company_routes",
router_attr="router",
),
PluginRouteDef(
path="/api/v1/contact-folders",
module="app.plugins.builtins.contacts.folder_routes",
router_attr="router",
),
PluginRouteDef(
path="/api/v1/contact-folders",
module="app.plugins.builtins.contacts.folder_permission_routes",
router_attr="router",
),
],
events=[],
migrations=[],
miniapps=[
MiniAppContribution(
app_id="contacts_stats",
name="Kontakt-Zähler",
icon="Building2",
description="Firmen- und Kontakt-Zähler (persönliche StatCards).",
permission="contacts:read",
settings_schema={
"fields": [
{"name": "show_companies", "label": "Firmen anzeigen", "type": "boolean", "default": True},
{"name": "show_persons", "label": "Personen anzeigen", "type": "boolean", "default": True},
]
},
col_span=2,
row_span=1,
hosts=["chat", "dashboard", "window"],
component="@/components/dashboard/ContactsStatsWidget",
order=5,
),
],
dashboard_widgets=[
FrontendDashboardWidget(
id="recent_contacts",
label_key="dashboard.recentContacts",
label="Recent Contacts",
component="@/components/dashboard/RecentContactsWidget",
icon="Users",
order=10,
col_span=2,
permission="contacts:read",
),
],
menu_items=[
FrontendMenuItem(label_key='nav.contacts', label='Kontakte', path='/contacts', icon='Users', order=10, permission='contacts:read'),
],
page_routes=[
FrontendPageRoute(path='/contacts', component='@/pages/ContactsList', protected=True, permission='contacts:read'),
FrontendPageRoute(path='/contacts/:id', component='@/pages/ContactDetailPage', protected=True, permission='contacts:read'),
FrontendPageRoute(path='/contacts/dedup', component='@/pages/DedupMerge', protected=True, permission='contacts:read'),
],
permissions=[
"contacts:read",
"contacts:write",
"contacts:delete",
],
# Audit P1/P2: contact field definitions are plugin-owned (moved
# from CORE_FIELD_DEFINITIONS) — registered at activation time via
# register_field_definitions() and removed on deactivation.
field_definitions=[
FieldDefinition(module="contacts", field="firstname", label="First Name", sensitivity="normal"),
FieldDefinition(module="contacts", field="surname", label="Last Name", sensitivity="normal"),
FieldDefinition(module="contacts", field="displayname", label="Display Name", sensitivity="normal"),
FieldDefinition(module="contacts", field="name", label="Name", sensitivity="normal"),
FieldDefinition(module="contacts", field="email_1", label="Email 1", sensitivity="normal"),
FieldDefinition(module="contacts", field="email_2", label="Email 2", sensitivity="normal"),
FieldDefinition(module="contacts", field="phone_1", label="Phone 1", sensitivity="normal"),
FieldDefinition(module="contacts", field="phone_2", label="Phone 2", sensitivity="normal"),
FieldDefinition(module="contacts", field="mobilephone", label="Mobile", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="function", label="Position", sensitivity="normal"),
FieldDefinition(module="contacts", field="website", label="Website", sensitivity="normal"),
FieldDefinition(module="contacts", field="status", label="Status", sensitivity="normal"),
FieldDefinition(module="contacts", field="type", label="Type", sensitivity="normal"),
FieldDefinition(module="contacts", field="gender", label="Gender", sensitivity="normal"),
FieldDefinition(module="contacts", field="suffix", label="Suffix", sensitivity="normal"),
FieldDefinition(module="contacts", field="ext_name_line", label="Extra Name Line", sensitivity="normal"),
FieldDefinition(module="contacts", field="country", label="Country", sensitivity="normal"),
FieldDefinition(module="contacts", field="code", label="Code", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="accounting_code", label="Accounting Code", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="vendor_accounting_code", label="Vendor Accounting Code", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="vat_code", label="VAT Code", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="fiscal_code", label="Fiscal Code", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="commerce_code", label="Commerce Code", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="purchase_number", label="Purchase Number", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="bic", label="BIC", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="mailing_street", label="Mailing Street", sensitivity="normal"),
FieldDefinition(module="contacts", field="mailing_city", label="Mailing City", sensitivity="normal"),
FieldDefinition(module="contacts", field="mailing_postalcode", label="Mailing Postal Code", sensitivity="normal"),
FieldDefinition(module="contacts", field="mailing_country", label="Mailing Country", sensitivity="normal"),
FieldDefinition(module="contacts", field="visit_street", label="Visit Street", sensitivity="normal"),
FieldDefinition(module="contacts", field="visit_city", label="Visit City", sensitivity="normal"),
FieldDefinition(module="contacts", field="visit_postalcode", label="Visit Postal Code", sensitivity="normal"),
FieldDefinition(module="contacts", field="visit_country", label="Visit Country", sensitivity="normal"),
FieldDefinition(module="contacts", field="invoice_street", label="Invoice Street", sensitivity="normal"),
FieldDefinition(module="contacts", field="invoice_city", label="Invoice City", sensitivity="normal"),
FieldDefinition(module="contacts", field="invoice_postalcode", label="Invoice Postal Code", sensitivity="normal"),
FieldDefinition(module="contacts", field="invoice_country", label="Invoice Country", sensitivity="normal"),
FieldDefinition(module="contacts", field="notes", label="Notes", sensitivity="sensitive"),
FieldDefinition(module="contacts", field="tags", label="Tags", sensitivity="sensitive"),
],
is_core=True,
author="LeoCRM Team",
min_app_version="1.0.0",
contract_version="1.0.0",
)
def get_job_modules(self) -> list[str]:
"""Worker discovers the contacts trash-cleanup job here (audit P2)."""
return ["app.plugins.builtins.contacts.jobs"]
def get_entity_models(self) -> dict[str, type]:
from app.models.contact import Contact
from app.models.contact_folder import ContactFolder
return {
"contact": Contact,
"contacts": Contact,
"company": Contact,
# Audit P2: contact_folder is contacts-plugin-owned domain data
# (moved from the static core ENTITY_MODELS map).
"contact_folder": ContactFolder,
}
async def on_activate(self, db, service_container, event_bus) -> None:
"""Activate: register restore config + history hooks for Contact."""
await super().on_activate(db, service_container, event_bus)
# Register restore config for Contact entities
from app.core.restore_registry import RestoreConfig, get_restore_registry
from app.models.contact import Contact
get_restore_registry().register(RestoreConfig(
entity_type="contact",
model_class=Contact,
restore_permission="contacts:write",
excluded_fields=frozenset({
"search_tsv", "embedding", "default_person_id", "admin_contactperson_id",
}),
))
# Register history hooks for Contact entities
from app.core.history_hooks import register_history_hooks
from app.core.hooks import get_hook_registry
register_history_hooks(
get_hook_registry(), "contact",
"contact.after_create", "contact.after_update", "contact.after_delete",
owner_tag="contacts",
)
logger.info("Contacts plugin activated: restore + history registered")
async def on_deactivate(self, db, service_container, event_bus) -> None:
"""Deactivate: unregister restore config + history hooks for Contact."""
from app.core.restore_registry import get_restore_registry
get_restore_registry().unregister("contact")
# Unregister history hooks — use owner_tag to remove only this plugin's hooks
from app.core.hooks import get_hook_registry
hook_reg = get_hook_registry()
hook_reg.unregister_actions_by_owner("contact.after_create", "contacts")
hook_reg.unregister_actions_by_owner("contact.after_update", "contacts")
hook_reg.unregister_actions_by_owner("contact.after_delete", "contacts")
from app.plugins.builtins.contracts import get_contract_registry
get_contract_registry().unregister(self.manifest.name)
await super().on_deactivate(db, service_container, event_bus)
logger.info("Contacts plugin deactivated: restore + history unregistered")