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>
80 lines
3.1 KiB
Python
80 lines
3.1 KiB
Python
from datetime import timedelta
|
|
|
|
from odoo import fields
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.tests.common import TransactionCase, tagged
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestClassifieds(TransactionCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.poster = self.env['res.partner'].create({'name': 'Classified Poster'})
|
|
|
|
def test_new_listing_is_pending_review(self):
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Old Bicycle', 'poster_partner_id': self.poster.id,
|
|
})
|
|
self.assertEqual(listing.state, 'pending_review')
|
|
self.assertTrue(listing.expiry_date)
|
|
|
|
def test_publish_makes_listing_visible(self):
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Piano Lessons', 'poster_partner_id': self.poster.id,
|
|
})
|
|
listing.action_publish()
|
|
self.assertEqual(listing.state, 'published')
|
|
|
|
visible = self.env['community.classified'].search([('state', '=', 'published')])
|
|
self.assertIn(listing, visible)
|
|
|
|
def test_reject_listing(self):
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Spam Listing', 'poster_partner_id': self.poster.id,
|
|
})
|
|
listing.action_reject()
|
|
self.assertEqual(listing.state, 'rejected')
|
|
|
|
def test_expiry_cron_archives_past_due_listings(self):
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Expiring Soon', 'poster_partner_id': self.poster.id,
|
|
})
|
|
listing.action_publish()
|
|
listing.expiry_date = fields.Date.today() - timedelta(days=1)
|
|
|
|
self.env['community.classified']._cron_expire_listings()
|
|
|
|
self.assertEqual(listing.state, 'expired')
|
|
|
|
def test_renew_resets_expiry_and_republishes(self):
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Renew Me', 'poster_partner_id': self.poster.id,
|
|
})
|
|
listing.action_publish()
|
|
listing.expiry_date = fields.Date.today() - timedelta(days=1)
|
|
self.env['community.classified']._cron_expire_listings()
|
|
self.assertEqual(listing.state, 'expired')
|
|
|
|
listing.action_renew()
|
|
self.assertEqual(listing.state, 'published')
|
|
self.assertGreater(listing.expiry_date, fields.Date.today())
|
|
|
|
def test_max_three_images(self):
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Many Photos', 'poster_partner_id': self.poster.id,
|
|
})
|
|
tiny_png = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
|
|
with self.assertRaises(ValidationError):
|
|
listing.write({
|
|
'image_ids': [(0, 0, {'image': tiny_png}) for _ in range(4)],
|
|
})
|
|
|
|
def test_configurable_expiry_days(self):
|
|
self.env['ir.config_parameter'].sudo().set_param('community_classifieds.expiry_days', '5')
|
|
listing = self.env['community.classified'].create({
|
|
'title': 'Short Lived', 'poster_partner_id': self.poster.id,
|
|
})
|
|
expected = fields.Date.today() + timedelta(days=5)
|
|
self.assertEqual(listing.expiry_date, expected)
|