metatroncubeswdev e5d90a83d4 feat(community_membership): renewal automation - cron, templates, draft invoices (Session 1-B)
Adds the daily membership renewal cron (_cron_process_membership_renewals):
sends a "renewal upcoming" reminder at each non-final configured offset,
flips state to renewal_due and sends a "renewal due" notice at the final
(smallest) offset, and on expiry flips to 'expired', sends an expiry notice,
and creates a draft renewal invoice for the member's tier product.

Email templates (renewal_upcoming, renewal_due, membership_expired) use
Odoo 19's current mail.template syntax ({{ }} subject/QWeb t-out body) -
the plan referenced the older ${object...} syntax, which v19 no longer
uses. Org name is pulled via a new non-stored res.partner.membership_org_name
compute field backed by ir.config_parameter, never hardcoded.

Also had to adapt the ir.cron data record: Odoo 19 removed 'numbercall'
entirely (ir.cron now delegates most fields to a linked ir.actions.server
under the hood, though name/model_id/state/code are still settable
directly on the record, per the core mail module's own cron definitions).

Verified against a live Odoo 19 + Postgres 16 container: module upgrades
clean, all 8 tests pass (4 from 1-A + 4 new: non-final-offset reminder,
final-offset state flip to renewal_due, expiry creates a draft invoice +
notice, no action outside any offset window).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 20:08:26 -04:00

70 lines
3.3 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_template_membership_renewal_upcoming" model="mail.template">
<field name="name">Membership: Renewal Upcoming</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="subject">{{ object.membership_org_name or 'Your organization' }} membership renewal reminder</field>
<field name="partner_to">{{ object.id }}</field>
<field name="lang">{{ object.lang }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p>Dear <t t-out="object.name or ''">Member</t>,</p>
<p>
Your membership
<t t-if="object.membership_member_id">(<t t-out="object.membership_member_id"/>)</t>
with <t t-out="object.membership_org_name or 'us'"/> is due for renewal on
<t t-out="format_date(object.membership_expiry)"/>.
</p>
<p>No action is needed yet - this is an early reminder. You will receive another notice closer to the date.</p>
</div>
</field>
</record>
<record id="mail_template_membership_renewal_due" model="mail.template">
<field name="name">Membership: Renewal Due</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="subject">{{ object.membership_org_name or 'Your organization' }} membership renewal is due</field>
<field name="partner_to">{{ object.id }}</field>
<field name="lang">{{ object.lang }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p>Dear <t t-out="object.name or ''">Member</t>,</p>
<p>
Your membership
<t t-if="object.membership_member_id">(<t t-out="object.membership_member_id"/>)</t>
with <t t-out="object.membership_org_name or 'us'"/> expires on
<t t-out="format_date(object.membership_expiry)"/>.
</p>
<p>Please renew soon to keep your membership active and avoid a lapse in benefits.</p>
</div>
</field>
</record>
<record id="mail_template_membership_expired" model="mail.template">
<field name="name">Membership: Expired</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="subject">Your {{ object.membership_org_name or 'organization' }} membership has expired</field>
<field name="partner_to">{{ object.id }}</field>
<field name="lang">{{ object.lang }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p>Dear <t t-out="object.name or ''">Member</t>,</p>
<p>
Your membership
<t t-if="object.membership_member_id">(<t t-out="object.membership_member_id"/>)</t>
with <t t-out="object.membership_org_name or 'us'"/> expired on
<t t-out="format_date(object.membership_expiry)"/>.
</p>
<p>A renewal invoice has been prepared for you. Please get in touch to reactivate your membership.</p>
</div>
</field>
</record>
</data>
</odoo>