Skip to content

Technical Reference

Overview

Technical documentation for developers maintaining and extending the Email Pickup Response System.

Last Updated: February 2026 Duration: 30 minutes Audience: Developers, IT Administrators


Architecture

System Flow

┌─────────────────────────────────────────────────────────────────────┐
│                        EMAIL BLAST FLOW                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   Mass Email Wizard                                                  │
│   (sr_mass_email_from_filter)                                       │
│          │                                                           │
│          ▼                                                           │
│   generate_token_for_partner()                                       │
│          │                                                           │
│          ▼                                                           │
│   email.pickup.response record created                               │
│   (token = UUID, status = pending)                                   │
│          │                                                           │
│          ▼                                                           │
│   mailing.mailing created with                                       │
│   {{ object.get_pickup_token() }} in body                           │
│          │                                                           │
│          ▼                                                           │
│   Email sent to customers                                            │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                      RESPONSE FLOW                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   Customer clicks link:                                              │
│   /pickup/response/<token>/need  OR  /pickup/response/<token>/no    │
│          │                                                           │
│          ▼                                                           │
│   PickupResponseController (public route, no auth)                   │
│          │                                                           │
│          ▼                                                           │
│   email.pickup.response.get_response_by_token()                      │
│          │                                                           │
│          ▼                                                           │
│   process_need_pickup() OR process_no_pickup()                       │
│          │                                                           │
│          ├──► Create/update CRM lead (need_pickup only)              │
│          │                                                           │
│          ├──► Create note.note with contact_result_id=11             │
│          │    (triggers last_decision_maker_date update)             │
│          │                                                           │
│          └──► Update response record (type, date, lead_id, note_id)  │
│                                                                      │
│          ▼                                                           │
│   Return HTML confirmation page                                      │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Module Structure

Files in sr_management

File Purpose
models/email_pickup_response.py Main model for tracking responses
models/res_partner.py Added get_pickup_token() method
models/crm_lead.py Added skip_call_activity context flag
controllers/pickup_response.py HTTP routes for public access
views/email_pickup_response_views.xml Admin interface views
views/pickup_response_templates.xml QWeb templates (unused)
data/pickup_blast_template.xml Email template reference
security/ir.model.access.csv Access control rules

Files in sr_mass_email_from_filter

File Purpose
models/mass_email_wizard.py Added pickup link generation methods
views/mass_email_wizard_views.xml Added checkbox and conditional buttons

Model: email.pickup.response

Fields

class EmailPickupResponse(models.Model):
    _name = 'email.pickup.response'
    _description = 'Email Pickup Response'
    _order = 'create_date desc'

    partner_id = fields.Many2one('res.partner', required=True, index=True)
    partner_name = fields.Char(related='partner_id.name', store=True)
    token = fields.Char(required=True, index=True, readonly=True)
    response_type = fields.Selection([
        ('pending', 'Pending'),
        ('need_pickup', 'Need Pickup'),
        ('no_pickup', 'No Pickup Needed')
    ], default='pending')
    response_date = fields.Datetime()
    mailing_id = fields.Many2one('mailing.mailing')
    lead_id = fields.Many2one('crm.lead')
    note_id = fields.Many2one('note.note')

Key Methods

generate_token_for_partner(partner_id, mailing_id=False)

@api.model
def generate_token_for_partner(self, partner_id, mailing_id=False):
    """Generate a unique token for a partner."""
    token = str(uuid.uuid4())
    return self.sudo().create({
        'partner_id': partner_id,
        'token': token,
        'mailing_id': mailing_id,
    })

process_need_pickup()

def process_need_pickup(self):
    """Process a 'need pickup' response."""
    self.ensure_one()
    if self.response_type != 'pending':
        return False  # Already processed

    # Create or update lead
    lead = self._get_or_create_lead()

    # Create note with "Receive Email" contact result
    note = self._create_response_note(need_pickup=True)

    # Update response record
    self.write({
        'response_type': 'need_pickup',
        'response_date': fields.Datetime.now(),
        'lead_id': lead.id,
        'note_id': note.id,
    })
    return True

process_no_pickup()

def process_no_pickup(self):
    """Process a 'no pickup needed' response."""
    self.ensure_one()
    if self.response_type != 'pending':
        return False

    # Create note only (no lead)
    note = self._create_response_note(need_pickup=False)

    self.write({
        'response_type': 'no_pickup',
        'response_date': fields.Datetime.now(),
        'note_id': note.id,
    })
    return True

Controller: PickupResponseController

Route Configuration

class PickupResponseController(http.Controller):

    @http.route(
        '/pickup/response/<string:token>/need',
        type='http',
        auth='public',
        csrf=False,
        sitemap=False
    )
    def pickup_need(self, token, **kwargs):
        """Handle 'Need Pickup' response."""
        # ... implementation

Key Points

Attribute Value Reason
auth='public' No login required Customers click from email
csrf=False No CSRF token GET request from email link
sitemap=False Not indexed Internal functional URLs

Error Handling

The controller handles these cases:

  1. Invalid token: Returns error page with "Invalid or expired link"
  2. Already responded: Returns status page with previous response
  3. Processing error: Logs error and returns generic error page

Integration Points

CRM Lead Creation

When creating leads, the system uses context flags to bypass permission checks:

lead = self.env['crm.lead'].sudo().with_context(
    skip_call_activity=True,  # Prevents permission error
    mail_create_nosubscribe=True,
).create({
    'name': f'Pickup Request - {partner.name}',
    'partner_id': partner.id,
    'stage_id': 7,  # Needs Pickup stage
    'type': 'opportunity',
})

Stage ID Hardcoding

The "Needs Pickup" stage ID (7) is hardcoded. If stages are modified, update _get_needs_pickup_stage_id() method.

Note Creation

Notes are created with the "Receive Email" contact result to trigger last_decision_maker_date update:

note = self.env['note.note'].sudo().create({
    'customer_id': partner.id,
    'comment': 'Customer requested pickup via email link',
    'contact_result_id': 11,  # "Receive Email"
})

Contact Result ID Hardcoding

The "Receive Email" contact result ID (11) is hardcoded. Verify this ID in production.


Database Schema

Table: email_pickup_response

CREATE TABLE email_pickup_response (
    id SERIAL PRIMARY KEY,
    partner_id INTEGER NOT NULL REFERENCES res_partner(id),
    partner_name VARCHAR,
    token VARCHAR NOT NULL,
    response_type VARCHAR DEFAULT 'pending',
    response_date TIMESTAMP,
    mailing_id INTEGER REFERENCES mailing_mailing(id),
    lead_id INTEGER REFERENCES crm_lead(id),
    note_id INTEGER REFERENCES note_note(id),
    create_uid INTEGER REFERENCES res_users(id),
    write_uid INTEGER REFERENCES res_users(id),
    create_date TIMESTAMP,
    write_date TIMESTAMP
);

CREATE INDEX idx_pickup_response_token ON email_pickup_response(token);
CREATE INDEX idx_pickup_response_partner ON email_pickup_response(partner_id);

Key IDs Reference

Entity ID Name
CRM Stage 7 Needs Pickup
Contact Result 11 Receive Email

Security Model

Access Control

Defined in security/ir.model.access.csv:

Group Read Write Create Delete
Internal User Yes Yes Yes No
CSR Yes Yes Yes No
Dispatcher Yes Yes Yes No
Manager Yes Yes Yes Yes
System Admin Yes Yes Yes Yes

Public Access

The controller uses sudo() to bypass access control for public routes:

response = request.env['email.pickup.response'].sudo().search([
    ('token', '=', token)
], limit=1)

Token Security

  • Tokens are UUID v4 (122 bits of randomness)
  • Tokens are single-use (status changes after first click)
  • No expiration currently implemented

Configuration

Required System Parameters

Parameter Value Purpose
web.base.url https://app2.scottrecycling.com Base URL for email links

Database Filter

The /etc/odoo17.conf must include:

dbfilter = ^database_name$

Without this, public routes return 404 due to database selection issues.


Email Template

Jinja Syntax

The email body uses Odoo's Jinja templating:

<a href="{{ base_url }}/pickup/response/{{ object.get_pickup_token() }}/need">
    Yes, I Need a Pickup
</a>

The get_pickup_token() method on res.partner:

def get_pickup_token(self):
    """Get or create pickup response token for this partner."""
    self.ensure_one()
    response = self.env['email.pickup.response'].sudo().search([
        ('partner_id', '=', self.id)
    ], limit=1, order='id desc')

    if response:
        return response.token

    # Create new token if none exists
    response = self.env['email.pickup.response'].sudo().generate_token_for_partner(
        partner_id=self.id
    )
    return response.token

Troubleshooting

404 on Public Routes

Symptoms: Clicking email link returns 404

Causes: 1. dbfilter not set in odoo17.conf 2. Odoo not restarted after config change 3. Wrong URL in web.base.url

Solution:

# Check config
grep dbfilter /etc/odoo17.conf

# Should show:
dbfilter = ^database_name$

# Restart if changed
sudo systemctl restart odoo17

Permission Errors on Response

Symptoms: Error page when customer clicks link

Check logs:

sudo tail -100 /var/log/odoo17/odoo17.log | grep -i error

Common causes: 1. skip_call_activity context not set 2. sudo() missing on model operations 3. Missing access control entries

Tokens Not Generating

Symptoms: Wizard completes but no tokens created

Debug:

SELECT count(*) FROM email_pickup_response
WHERE create_date > NOW() - INTERVAL '1 hour';

Check: 1. Wizard include_pickup_links is True 2. Partners have valid IDs 3. No database transaction rollback


Extending the Module

Adding Token Expiration

To add expiration, modify email_pickup_response.py:

expires_at = fields.Datetime()

def generate_token_for_partner(self, partner_id, mailing_id=False, expires_days=30):
    expires_at = fields.Datetime.now() + timedelta(days=expires_days)
    return self.sudo().create({
        'partner_id': partner_id,
        'token': str(uuid.uuid4()),
        'mailing_id': mailing_id,
        'expires_at': expires_at,
    })

def is_valid(self):
    if self.expires_at and fields.Datetime.now() > self.expires_at:
        return False
    return self.response_type == 'pending'

Adding SMS Notifications

To notify managers when customers respond:

def process_need_pickup(self):
    result = super().process_need_pickup()
    if result:
        self._send_manager_notification()
    return result

def _send_manager_notification(self):
    # Use existing SMS infrastructure
    self.env['sms.sms'].sudo().create({
        'number': '+1234567890',
        'body': f'Pickup requested: {self.partner_id.name}',
    }).send()

Custom Response Pages

To customize confirmation pages, modify the HTML in pickup_response.py:

def _get_success_html(self, title, message, icon):
    return f'''
    <!DOCTYPE html>
    <html>
    <!-- Custom HTML here -->
    </html>
    '''

Version History

Version Date Changes
17.0.1.0.0 Dec 2025 Initial release

Files Reference

sr_management Module

sr_management/
├── __manifest__.py          # Updated: added data files
├── models/
│   ├── __init__.py          # Updated: import email_pickup_response
│   ├── email_pickup_response.py   # NEW
│   ├── res_partner.py       # Updated: get_pickup_token()
│   └── crm_lead.py          # Updated: skip_call_activity context
├── controllers/
│   ├── __init__.py          # Updated: import pickup_response
│   └── pickup_response.py   # NEW
├── views/
│   ├── email_pickup_response_views.xml  # NEW
│   └── pickup_response_templates.xml    # NEW (unused)
├── data/
│   └── pickup_blast_template.xml        # NEW
└── security/
    └── ir.model.access.csv  # Updated: 5 new lines

sr_mass_email_from_filter Module

sr_mass_email_from_filter/
├── __manifest__.py          # Updated: added sr_management dependency
├── models/
│   └── mass_email_wizard.py # Updated: pickup methods
└── views/
    └── mass_email_wizard_views.xml  # Updated: checkbox and buttons

Questions? Contact IT support or review the source code in /home/dan/SR-Odoo/.