Split FSM into separate modules and switch customer to address book

This commit is contained in:
Ruslan Bakiev
2026-02-13 15:27:48 +07:00
parent 98a92286ce
commit dc58e1ffe4
46 changed files with 3125 additions and 152 deletions

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_change_employee_wizard_form" model="ir.ui.view">
<field name="name">change.employee.wizard.form</field>
<field name="model">change.employee.wizard</field>
<field name="arch" type="xml">
<form string="Change Employee">
<group>
<field name="model_name" invisible="1"/>
<field name="field_name" invisible="1"/>
<field name="user_id" options="{'no_create': True, 'no_create_edit': True}"/>
</group>
<footer>
<button string="Apply" name="action_change_employee" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<delete id="view_contact_context_tree" model="ir.ui.view"/>
<delete id="view_contact_context_form" model="ir.ui.view"/>
<delete id="view_contact_context_search" model="ir.ui.view"/>
<delete id="action_contact_context" model="ir.actions.act_window"/>
<delete id="menu_contact_context" model="ir.ui.menu"/>
<delete id="access_contact_context_user" model="ir.model.access"/>
<delete id="access_contact_context_admin" model="ir.model.access"/>
</odoo>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_contact_event_form" model="ir.ui.view">
<field name="name">contact.event.form</field>
<field name="model">contact.event</field>
<field name="arch" type="xml">
<form string="Contact Event">
<sheet>
<group>
<group>
<field name="contact_id"/>
<field name="date_start"/>
</group>
<group>
<field name="duration" widget="float_time"/>
<field name="user_id"/>
<field name="calendar_event_id" readonly="1"/>
</group>
</group>
<group string="Notes">
<field name="notes" nolabel="1" colspan="2"/>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_contact_event_tree" model="ir.ui.view">
<field name="name">contact.event.tree</field>
<field name="model">contact.event</field>
<field name="arch" type="xml">
<list string="Contact Events">
<field name="contact_id"/>
<field name="notes"/>
<field name="date_start"/>
<field name="duration" widget="float_time"/>
<field name="user_id" optional="show"/>
</list>
</field>
</record>
<!-- Calendar View -->
<record id="view_contact_event_calendar" model="ir.ui.view">
<field name="name">contact.event.calendar</field>
<field name="model">contact.event</field>
<field name="arch" type="xml">
<calendar string="Contact Events" date_start="date_start"
color="user_id" mode="month">
<field name="notes"/>
<field name="contact_id"/>
<field name="user_id"/>
</calendar>
</field>
</record>
<!-- Search View -->
<record id="view_contact_event_search" model="ir.ui.view">
<field name="name">contact.event.search</field>
<field name="model">contact.event</field>
<field name="arch" type="xml">
<search string="Search Contact Events">
<field name="notes"/>
<field name="contact_id"/>
<field name="user_id"/>
<separator/>
<filter string="Today" name="today"
domain="[('date_start', '&gt;=', datetime.datetime.now().replace(hour=0, minute=0, second=0)),
('date_start', '&lt;', datetime.datetime.now().replace(hour=23, minute=59, second=59))]"/>
<filter string="This Week" name="this_week"
domain="[('date_start', '&gt;=', (datetime.datetime.now() - datetime.timedelta(days=datetime.datetime.now().weekday())).replace(hour=0, minute=0, second=0)),
('date_start', '&lt;', (datetime.datetime.now() + datetime.timedelta(days=6-datetime.datetime.now().weekday())).replace(hour=23, minute=59, second=59))]"/>
<separator/>
<filter string="Contact" name="group_contact" domain="[]" context="{'group_by': 'contact_id'}"/>
<filter string="User" name="group_user" domain="[]" context="{'group_by': 'user_id'}"/>
<filter string="Date" name="group_date" domain="[]" context="{'group_by': 'date_start:day'}"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_contact_event" model="ir.actions.act_window">
<field name="name">Contact Events</field>
<field name="res_model">contact.event</field>
<field name="view_mode">calendar,list,form</field>
<field name="search_view_id" ref="view_contact_event_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first contact event
</p>
<p>
Track meetings, calls and other events with your contacts.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Contact Source Form View -->
<record id="view_contact_source_form" model="ir.ui.view">
<field name="name">Contact Source Form</field>
<field name="model">contact.source</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_activate" type="object" string="Activate"
class="btn-primary" invisible="state == 'active'"/>
<button name="action_draft" type="object" string="Reset to Draft"
invisible="state == 'draft'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,active"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="parent_id"/>
</group>
<group>
<field name="color" widget="color"/>
</group>
</group>
<group>
<field name="description"/>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Contact Source List View -->
<record id="view_contact_source_list" model="ir.ui.view">
<field name="name">Contact Source List</field>
<field name="model">contact.source</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="parent_id" optional="show"/>
<field name="state" widget="badge" optional="show"/>
<field name="color" widget="color" optional="hide"/>
<field name="create_date" optional="hide"/>
<field name="write_uid" optional="hide"/>
</list>
</field>
</record>
<!-- Contact Source Search View -->
<record id="view_contact_source_search" model="ir.ui.view">
<field name="name">Contact Source Search</field>
<field name="model">contact.source</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="parent_id"/>
<field name="description"/>
<!-- Filters -->
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Active" name="active_state" domain="[('state', '=', 'active')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<!-- Group By -->
<separator/>
<filter string="Status" name="group_by_state" context="{'group_by': 'state'}"/>
<filter string="Parent Source" name="group_by_parent" context="{'group_by': 'parent_id'}"/>
</search>
</field>
</record>
<!-- Contact Source Action -->
<record id="action_contact_source" model="ir.actions.act_window">
<field name="name">Contact Sources</field>
<field name="res_model">contact.source</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_contact_source_search"/>
<field name="context">{'search_default_active_state': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first contact source!
</p>
<p>
Contact sources help you track where your contacts come from.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="view_dsrpt_communication_type_list" model="ir.ui.view">
<field name="name">dsrpt.communication.type.list</field>
<field name="model">dsrpt.communication.type</field>
<field name="arch" type="xml">
<list string="Communication Types">
<field name="name"/>
<field name="code"/>
<field name="state" widget="badge" decoration-success="state == 'active'" decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_dsrpt_communication_type_form" model="ir.ui.view">
<field name="name">dsrpt.communication.type.form</field>
<field name="model">dsrpt.communication.type</field>
<field name="arch" type="xml">
<form string="Communication Type">
<header>
<button name="action_activate" type="object" string="Activate" class="oe_highlight" invisible="state == 'active'"/>
<button name="action_draft" type="object" string="Set to Draft" invisible="state == 'draft'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,active"/>
</header>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" invisible="active"/>
<group>
<field name="name"/>
<field name="code"/>
<field name="active" invisible="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_dsrpt_communication_type_search" model="ir.ui.view">
<field name="name">dsrpt.communication.type.search</field>
<field name="model">dsrpt.communication.type</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="code"/>
<separator/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Active" name="active_state" domain="[('state', '=', 'active')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_dsrpt_communication_type" model="ir.actions.act_window">
<field name="name">Communication Types</field>
<field name="res_model">dsrpt.communication.type</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_dsrpt_communication_type_search"/>
<field name="context">{'search_default_active_state': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first communication type!
</p>
<p>
Communication types define the ways to contact people (Phone, Email, Telegram, etc.)
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="view_dsrpt_contact_list" model="ir.ui.view">
<field name="name">dsrpt.contact.list</field>
<field name="model">dsrpt.contact</field>
<field name="arch" type="xml">
<list string="Contacts">
<field name="name"/>
<field name="user_id" optional="show"/>
<field name="next_contact" widget="date" optional="show"/>
<field name="communication_ids" widget="many2many_tags" optional="show"
context="{'tree_view_ref': 'dsrpt_address_book.view_dsrpt_contact_communication_tree_simple'}"/>
<field name="create_date" optional="hide"/>
<field name="write_date" optional="hide"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_dsrpt_contact_form" model="ir.ui.view">
<field name="name">dsrpt.contact.form</field>
<field name="model">dsrpt.contact</field>
<field name="arch" type="xml">
<form string="Contact">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" invisible="active"/>
<div class="oe_button_box" name="button_box">
<button name="action_view_events" type="object" class="oe_stat_button" icon="fa-calendar">
<field name="event_count" widget="statinfo" string="Events"/>
</button>
<!-- Calls button moved to dsrpt_calls module -->
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Contact Name..."/>
</h1>
</div>
<!-- Top row: Basic info (left) and Notes (right) -->
<group col="2">
<group>
<field name="user_id"/>
<field name="source_id"/>
<field name="next_contact" readonly="1"/>
</group>
<group string="Notes">
<field name="note" nolabel="1" placeholder="Add notes about this contact..."/>
</group>
</group>
<field name="active" invisible="1"/>
<!-- Second row: Events (left) and Requests (right) -->
<group col="2" name="middle_section">
<group string="Events">
<field name="event_ids" nolabel="1" context="{'default_contact_id': id}">
<kanban>
<field name="date_start"/>
<field name="notes"/>
<field name="duration"/>
<field name="user_id"/>
<templates>
<t t-name="card">
<div class="oe_kanban_card oe_kanban_global_click" style="margin-bottom: 8px;">
<div class="o_kanban_record_body">
<!-- Date and time in bold -->
<div class="o_kanban_record_title mb-2">
<strong>
<t t-esc="luxon.DateTime.fromISO(record.date_start.raw_value).setLocale('en').toFormat('d MMMM yyyy')"/>
<t t-esc="luxon.DateTime.fromISO(record.date_start.raw_value).toFormat('HH:mm')"/>
</strong>
</div>
<!-- Description full width -->
<div class="mb-2">
<field name="notes"/>
</div>
<!-- User avatar -->
<div t-if="record.user_id.raw_value" class="d-flex align-items-center">
<img t-attf-src="/web/image/res.users/{{record.user_id.raw_value}}/avatar_128"
class="o_kanban_image_fill_left"
width="24" height="24"
style="border-radius: 50%;"/>
<span class="ml-1 text-muted">
<field name="user_id"/>
</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
<!-- Requests group will be added by realty_sales module -->
<group name="requests_placeholder"/>
</group>
<!-- Third row: Communications (full width) -->
<group col="1" string="Communications">
<field name="communication_ids" nolabel="1" context="{'default_contact_id': id}">
<list editable="bottom">
<field name="communication_type_id" options="{'no_create': True}"/>
<field name="value"/>
<field name="is_preferred" widget="boolean_toggle"/>
</list>
</field>
</group>
<!-- Fourth row: Calls (full width) - will be added by dsrpt_calls module -->
<group col="1" name="calls_section"/>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_dsrpt_contact_search" model="ir.ui.view">
<field name="name">dsrpt.contact.search</field>
<field name="model">dsrpt.contact</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="phone_numbers" string="Phone"
filter_domain="[('phone_numbers', 'ilike', self)]"/>
<field name="communication_ids" string="Communication"
filter_domain="[('communication_ids.value', 'ilike', self)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<separator/>
<filter string="Create Date" name="group_create_date" context="{'group_by': 'create_date'}"/>
</search>
</field>
</record>
<!-- Simple tree for Many2many tags widget -->
<record id="view_dsrpt_contact_communication_tree_simple" model="ir.ui.view">
<field name="name">dsrpt.contact.communication.tree.simple</field>
<field name="model">dsrpt.contact.communication</field>
<field name="arch" type="xml">
<list>
<field name="communication_type_id"/>
<field name="value"/>
<field name="is_preferred"/>
</list>
</field>
</record>
<!-- Action -->
<record id="action_dsrpt_contact" model="ir.actions.act_window">
<field name="name">Contacts</field>
<field name="res_model">dsrpt.contact</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_dsrpt_contact_search"/>
<field name="context">{}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first contact!
</p>
<p>
Manage your contacts and their communication methods in one place.
</p>
</field>
</record>
<!-- Server Actions -->
<!-- Server Action: Send Need Qualification Event -->
<record id="action_server_send_transcription_added" model="ir.actions.server">
<field name="name">Send Need Qualification Event</field>
<field name="model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_view_types">list,form</field>
<field name="state">code</field>
<field name="code">
# Send transcription added event for selected contacts
if records:
for record in records:
# Trigger event - listeners will decide what to do
# Send need events based on contact status
if record.status == 'awaiting_qualification':
record._event('on_need_qualification').notify(record)
else:
record._event('on_need_request_summary_update').notify(record)
record._event('on_need_calendar_event').notify(record)
# Show notification
# Count events sent
qualification_count = len([r for r in records if r.status == 'awaiting_qualification'])
other_count = len(records) - qualification_count
message_parts = []
if qualification_count:
message_parts.append(f"{qualification_count} qualification events")
if other_count:
message_parts.append(f"{other_count} summary/calendar events")
message = f"Need events sent: {', '.join(message_parts) if message_parts else 'none'}"
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Event Sent',
'message': message,
'type': 'success',
'sticky': False,
}
}
</field>
</record>
<!-- Server Action: Send Need Summary/Calendar Events -->
<record id="action_server_send_context_update" model="ir.actions.server">
<field name="name">Send Need Summary/Calendar Events</field>
<field name="model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_view_types">list,form</field>
<field name="state">code</field>
<field name="code">
# Send context update event for selected contacts
if records:
for record in records:
# Trigger event - ContextUpdatedListener will handle it
# Send need events for non-qualifying contacts
if record.status != 'awaiting_qualification':
record._event('on_need_request_summary_update').notify(record)
record._event('on_need_calendar_event').notify(record)
# Show notification
qualified_count = len([r for r in records if r.status == 'awaiting_qualification'])
processed_count = len(records) - qualified_count
message = f"Need events sent for {processed_count} contacts ({qualified_count} skipped - awaiting qualification)"
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Event Sent',
'message': message,
'type': 'success',
'sticky': False,
}
}
</field>
</record>
<!-- Server Action: Change Employee for Contacts -->
<record id="action_server_change_employee_contacts" model="ir.actions.server">
<field name="name">Change Employee</field>
<field name="model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
if records:
action = {
'name': 'Change Employee',
'type': 'ir.actions.act_window',
'res_model': 'change.employee.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_model': 'dsrpt.contact',
'active_ids': records.ids,
'default_model_name': 'dsrpt.contact',
'default_field_name': 'user_id'
}
}
action = action
</field>
</record>
<!-- Server Action: Recompute Next Contact -->
<record id="action_server_recompute_next_contact" model="ir.actions.server">
<field name="name">Recompute "Next Contact"</field>
<field name="model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_model_id" ref="dsrpt_address_book.model_dsrpt_contact"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
if records:
action = records[0].recompute_next_contact_all()
</field>
</record>
</odoo>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Main Menu -->
<menuitem id="menu_dsrpt_address_book_root"
name="Address Book"
sequence="50"
web_icon="dsrpt_address_book,static/description/icon.png"/>
<!-- Contacts Menu -->
<menuitem id="menu_dsrpt_contacts"
name="Contacts"
parent="menu_dsrpt_address_book_root"
action="action_dsrpt_contact"
sequence="20"/>
<!-- Contact Events Calendar -->
<menuitem id="menu_dsrpt_contact_events"
name="Calendar"
parent="menu_dsrpt_address_book_root"
action="action_contact_event"
sequence="10"/>
<!-- Configuration Menu -->
<menuitem id="menu_dsrpt_address_book_config"
name="Configuration"
parent="menu_dsrpt_address_book_root"
sequence="100"/>
<!-- Communication Types Menu -->
<menuitem id="menu_dsrpt_communication_types"
name="Communication Types"
parent="menu_dsrpt_address_book_config"
action="action_dsrpt_communication_type"
sequence="10"/>
<!-- Contact Sources Menu -->
<menuitem id="menu_contact_sources"
name="Contact Sources"
parent="menu_dsrpt_address_book_config"
action="action_contact_source"
sequence="20"/>
</odoo>