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:
- Invalid token: Returns error page with "Invalid or expired link"
- Already responded: Returns status page with previous response
- 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:
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:
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:
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:
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/.