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

85 lines
3.8 KiB
Python

import base64
from odoo import http
from odoo.http import request
MAX_IMAGES = 3
def _is_module_installed(env, module_name):
return bool(env['ir.module.module'].sudo().search_count(
[('name', '=', module_name), ('state', '=', 'installed')]
))
def _has_active_membership(partner):
"""Soft-check: only meaningful if community_membership is installed."""
if 'membership_state' not in partner._fields:
return True
return partner.membership_state in ('active', 'renewal_due')
class ClassifiedsController(http.Controller):
@http.route(['/classifieds'], type='http', auth='public', website=True, sitemap=True)
def classifieds_list(self, category=None, **kwargs):
domain = [('state', '=', 'published')]
if category:
domain.append(('category', '=', category))
listings = request.env['community.classified'].sudo().search(domain)
return request.render('community_classifieds.classifieds_list_page', {
'listings': listings,
'category': category,
})
@http.route(['/classifieds/<int:classified_id>'], type='http', auth='public', website=True, sitemap=False)
def classifieds_detail(self, classified_id, **kwargs):
listing = request.env['community.classified'].sudo().browse(classified_id)
if not listing.exists() or listing.state != 'published':
return request.not_found()
listing._increment_view_count()
return request.render('community_classifieds.classifieds_detail_page', {'listing': listing})
@http.route(['/classifieds/new'], type='http', auth='user', website=True)
def classifieds_new(self, **kwargs):
partner = request.env.user.partner_id
if _is_module_installed(request.env, 'community_membership') and not _has_active_membership(partner):
return request.render('community_classifieds.classifieds_membership_required', {})
if request.httprequest.method == 'POST':
image_ids = []
for field_name in ('image1', 'image2', 'image3'):
upload = kwargs.get(field_name)
if upload and getattr(upload, 'filename', None):
image_ids.append((0, 0, {'image': base64.b64encode(upload.read())}))
listing = request.env['community.classified'].sudo().create({
'title': kwargs.get('title', '').strip(),
'category': kwargs.get('category', 'other'),
'description': kwargs.get('description', ''),
'contact_method': kwargs.get('contact_method', 'email'),
'contact_email': kwargs.get('contact_email', '').strip(),
'contact_phone': kwargs.get('contact_phone', '').strip(),
'poster_partner_id': partner.id,
'image_ids': image_ids[:MAX_IMAGES],
})
return request.redirect(f'/classifieds/my?posted={listing.id}')
return request.render('community_classifieds.classifieds_new_page', {})
@http.route(['/classifieds/my'], type='http', auth='user', website=True)
def classifieds_my(self, **kwargs):
partner = request.env.user.partner_id
listings = request.env['community.classified'].sudo().search([('poster_partner_id', '=', partner.id)])
return request.render('community_classifieds.classifieds_my_page', {'listings': listings})
@http.route(['/classifieds/<int:classified_id>/renew'], type='http', auth='user', website=True)
def classifieds_renew(self, classified_id, **kwargs):
partner = request.env.user.partner_id
listing = request.env['community.classified'].sudo().search([
('id', '=', classified_id), ('poster_partner_id', '=', partner.id),
], limit=1)
if listing:
listing.action_renew()
return request.redirect('/classifieds/my')