A developer-facing guide for taking community_classifieds out of this monorepo into a different project: exact dependency surface (hard: website/portal; the one soft-check on community_membership and nowhere else, verified by grepping the whole module), how to move/rename it, the recent admin-access fix and how it works, config reference, data model, public routes, cron/email jobs, and known gaps (missing static/description/banner.png, empty demo/, placeholder pricing) to close before shipping it publicly.
7.6 KiB
community_classifieds — Module Handover
A technical guide for taking this module out of the CommunityOS monorepo
and installing it in a different Odoo project. For how the module works
day-to-day, see the CommunityOS build plan or README.rst inside the
module itself — this doc is specifically about the move.
What it is
A member-gated classifieds board: public listing board, a posting form
for logged-in users, an admin moderation queue (publish/reject), and
automatic expiry with warning emails. Odoo 19 Community Edition, licensed
LGPL-3.
Dependencies
Hard (declared in __manifest__.py, both stock Odoo Community — no
Enterprise, nothing else from this suite):
'depends': ['website', 'portal'],
Soft — one runtime check, nowhere else: controllers/main.py calls
_is_module_installed(env, 'community_membership') before gating who can
post. If community_membership is present and installed, posting
requires an active membership; if it's absent, that check is simply
skipped and posting only requires being logged in. This isn't in
depends, so Odoo has no idea the two modules are related — verified by
grepping the whole module for any other cross-reference to
community_membership, community_theme_base, community_portal, or
any other community_*/event_qr_ticketing module: there are none.
Every template it calls into (website.layout, portal.portal_layout)
comes from its own declared dependencies.
Net effect: copy the folder, install it in a project with just
website and portal, and it works exactly like it does here, minus
the optional membership-gating.
Moving it
- Copy
addons/community_classifieds/into the new project's addons path. That's the whole module — no other file in this repo is required by it. - No client data to strip. This is a product-layer module in the
original repo's own architecture — it's already been through that
repo's brand-leak CI check (
scripts/check_brand_leak.py), which greps everycommunity_*module for client names/emails/colours and fails the build if it finds any. There's nothing TNCSC-specific anywhere in it. - Consider renaming the technical prefix before distributing it
publicly or installing it alongside another copy of the same module —
community_classifiedscould collide with another vendor's module of the same name. If you rename the folder, note that the Python model name (community.classified) is hardcoded inmodels/community_classified.pyand referenced by XML id throughoutviews/,security/, anddata/— a folder rename alone is enough (Odoo resolves everything by folder/technical name automatically), but a model rename would need a project-wide find-replace across every file in the module. - Install normally:
-i community_classifieds(or via Apps once the addons path is registered).
Post-install: access & permissions
As of 19.0.1.0.1, every Odoo Administrator automatically gets
moderator access — nothing to configure. This wasn't always true: a
standalone install used to leave the installing admin unable to see the
Classifieds app or Moderation queue at all, because the
Classifieds Moderator group wasn't implied by anything an admin already
had. Fixed in security/classifieds_security.xml by making
base.group_system (Odoo's built-in Administrator group) imply
group_classifieds_moderator:
<record id="base.group_system" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_classifieds_moderator'))]"/>
</record>
Verified on a from-scratch database with only community_classifieds
installed (community_membership and any deployment layer both
uninstalled): admin.has_group('community_classifieds.group_classifieds_moderator')
returns True and "Classifieds" appears in the app switcher immediately.
To give a non-admin staff member moderator access: Settings → Users & Companies → Users → open their record → check Classifieds Moderator under the Classifieds privilege group.
Configuration
Settings → General Settings → Classifieds tab:
| Setting | Field / param | Default |
|---|---|---|
| Listing Duration (days) | community_classifieds.expiry_days |
30 |
One thing not exposed as a setting: the expiry-warning email fires
exactly 7 days before expiry — DEFAULT_WARNING_DAYS_BEFORE_EXPIRY
in models/community_classified.py, a hardcoded constant. Change the
constant if a different lead time is needed; it's not wired to a
res.config.settings field.
Data model
community.classified — the listing itself:
| Field | Type | Notes |
|---|---|---|
title |
Char | required |
category |
Selection | for_sale / housing / services / jobs / other |
description |
Html | |
image_ids |
One2many → community.classified.image |
max 3, enforced by a @api.constrains |
contact_method / contact_email / contact_phone |
||
poster_partner_id |
Many2one res.partner | readonly, set by the controller on create |
post_date |
Datetime | readonly, defaults to now |
expiry_date |
Date | readonly, computed on create/publish/renew |
state |
Selection | pending_review / published / expired / rejected |
admin_notes |
Text | |
view_count |
Integer | incremented via sudo() on each public detail-page view |
community.classified.image — classified_id (required, cascade
delete), sequence, image (Binary, attachment=True).
Public routes
| Route | Auth | Notes |
|---|---|---|
GET /classifieds |
public | list, ?category= filter, only state=published |
GET /classifieds/<id> |
public | detail page, 404s unless published |
GET/POST /classifieds/new |
portal login required | membership check if applicable (see Dependencies) |
GET /classifieds/my |
portal login required | poster's own listings, any state |
GET /classifieds/<id>/renew |
portal login required | only works if the requesting partner is the poster |
Login-required routes use auth='public' plus a manual redirect check
(_redirect_to_login_if_public), not auth='user' — a documented
workaround for an Odoo 19 bug where auth='user' + website=True throws
a raw 500 instead of redirecting to login in some builds. Worth knowing
before "simplifying" that pattern back to auth='user'.
Background jobs
Two daily ir.cron entries (data/ir_cron.xml):
- Expire Listings — flips
publishedpastexpiry_datetoexpired. - Send Expiry Warnings — emails the poster when
expiry_dateis exactly 7 days out.
Email templates
data/mail_templates.xml: one to moderators on every new submission
(mail_template_new_submission), one to the poster on approaching expiry
(mail_template_expiry_warning). Both are looked up via
env.ref(..., raise_if_not_found=False) — if you strip the data file,
the module still runs, it just silently skips sending.
Known gaps to fix before shipping this elsewhere
static/description/banner.pngis referenced in the manifest'simageskey but the file doesn't exist. The App Store / Apps-list listing will show a broken image until a real banner asset is added.demo/is empty (just a.gitkeep) — no demo data ships with the module.- Manifest
price/currencyare placeholder zero values (0.00 USD) — set real pricing before listing it for sale.
Tests
tests/test_classifieds.py covers: new listing starts pending review,
publish makes it visible, reject, the expiry cron, renew resets expiry,
the 3-image max constraint, and the configurable expiry-days setting.
Run with --test-enable --test-tags /community_classifieds on install.