metatroncubeswdev 91e91fe1b8 feat(community_classifieds, community_benefits): Phase 4
community_classifieds: a member-gated classifieds board. community.classified
(title/category/description/up-to-3-images via a child .image model/contact
info/state) with a public board at /classifieds, detail pages that track
view_count, portal posting at /classifieds/new (soft-detects
community_membership - blocks non-active-members only if that module is
installed, otherwise anyone logged in can post), a "my listings" portal
page with self-service renew, and an admin moderation queue
(publish/reject). Daily crons expire past-due listings and send a 7-day
expiry warning; a new-submission notice goes out to the Classifieds
Moderator group. Listing duration is configurable (Settings), never
hardcoded.

community_benefits: community.benefit.partner (benefit centres) offer
community.benefit entitlements scoped to specific community_membership
tiers via tier_ids. community.benefit.redemption logs a redemption but
its create() is guarded by a constraint that re-checks the same
active-membership condition the membership QR verification page uses
(membership_state in active/renewal_due) plus tier entitlement, refusing
the redemption otherwise. Public directory at /benefits, portal page at
/my/benefits listing only benefits the member's tier actually entitles
them to.

Two more real Odoo 19 API changes hit here: search-view <group> elements
for "Group By" sections no longer accept a `string` attribute (must use
`name` only - same fix as community_membership's partner search view,
applied here to a fresh module), and res.groups.users was renamed to
user_ids.

Verified against a live Odoo 19 + Postgres 16 container: 7+4 automated
tests pass, plus a full manual live run covering the Phase 4 gate exactly -
confirmed a non-member is blocked from /classifieds/new, activated a real
membership, posted a classified (pending_review), published it via the
moderator action, confirmed it appears on the public board and detail
page with view_count incrementing, entitled a tier to a benefit, logged a
redemption for the active member, confirmed it shows on /my/benefits, and
confirmed redemption creation is refused for a non-member.

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

46 lines
1.6 KiB
Python

from odoo import fields, models
class CommunityBenefit(models.Model):
_name = 'community.benefit'
_description = 'Membership Benefit'
_order = 'name'
name = fields.Char(required=True)
benefit_partner_id = fields.Many2one('community.benefit.partner', string='Benefit Centre', required=True)
description = fields.Html()
tier_ids = fields.Many2many('community.membership.tier', string='Eligible Tiers')
discount_type = fields.Selection(
[('percent', 'Percentage'), ('amount', 'Fixed Amount'), ('perk', 'Perk (non-monetary)')],
default='percent', required=True,
)
value = fields.Float(help="Percentage or fixed amount, depending on Discount Type. Ignored for 'Perk'.")
valid_from = fields.Date()
valid_to = fields.Date()
active = fields.Boolean(default=True)
def _is_valid_today(self):
self.ensure_one()
today = fields.Date.context_today(self)
if self.valid_from and today < self.valid_from:
return False
if self.valid_to and today > self.valid_to:
return False
return True
def is_entitled(self, partner):
"""Whether the given res.partner is entitled to this benefit right now."""
self.ensure_one()
if not self.active or not self._is_valid_today():
return False
if partner.membership_state not in ('active', 'renewal_due'):
return False
return partner.membership_tier_id in self.tier_ids
@staticmethod
def _entitled_domain_for_partner(partner):
return [
('tier_ids', 'in', [partner.membership_tier_id.id]),
('active', '=', True),
]