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

67 lines
2.8 KiB
Python

from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestBenefits(TransactionCase):
def setUp(self):
super().setUp()
self.tier = self.env['community.membership.tier'].create({
'name': 'Individual', 'code': 'BEN-IND', 'price': 50.0, 'period': 'annual',
})
self.other_tier = self.env['community.membership.tier'].create({
'name': 'Student', 'code': 'BEN-STU', 'price': 20.0, 'period': 'annual',
})
self.member = self.env['res.partner'].create({
'name': 'Benefit Member', 'membership_tier_id': self.tier.id,
})
self.member.action_activate_membership()
vendor_partner = self.env['res.partner'].create({'name': 'Local Cafe'})
self.centre = self.env['community.benefit.partner'].create({
'partner_id': vendor_partner.id, 'category': 'food',
})
self.benefit = self.env['community.benefit'].create({
'name': '10% off coffee',
'benefit_partner_id': self.centre.id,
'tier_ids': [(6, 0, [self.tier.id])],
'discount_type': 'percent',
'value': 10.0,
})
def test_entitlement_resolves_by_tier(self):
self.assertTrue(self.benefit.is_entitled(self.member))
other_member = self.env['res.partner'].create({
'name': 'Other Member', 'membership_tier_id': self.other_tier.id,
})
other_member.action_activate_membership()
self.assertFalse(self.benefit.is_entitled(other_member), "Wrong tier should not be entitled")
def test_redemption_logs_for_active_member(self):
redemption = self.env['community.benefit.redemption'].create({
'member_id': self.member.id, 'benefit_id': self.benefit.id,
})
self.assertTrue(redemption)
def test_redemption_blocked_for_inactive_member(self):
inactive_member = self.env['res.partner'].create({
'name': 'Inactive Member', 'membership_tier_id': self.tier.id,
})
# Never activated - membership_state stays 'none'.
with self.assertRaises(ValidationError):
self.env['community.benefit.redemption'].create({
'member_id': inactive_member.id, 'benefit_id': self.benefit.id,
})
def test_redemption_blocked_for_wrong_tier(self):
other_member = self.env['res.partner'].create({
'name': 'Wrong Tier Member', 'membership_tier_id': self.other_tier.id,
})
other_member.action_activate_membership()
with self.assertRaises(ValidationError):
self.env['community.benefit.redemption'].create({
'member_id': other_member.id, 'benefit_id': self.benefit.id,
})