From 907258b4f454716d5682f65fc73887a208ca6026 Mon Sep 17 00:00:00 2001
From: pilarvargas-tecnativa
Date: Wed, 19 Feb 2025 15:37:45 +0100
Subject: [PATCH 1/2] [ADD] website_sale_slides_multi_qty: New module
TT54986
---
website_sale_slides_multi_qty/README.rst | 79 +++
website_sale_slides_multi_qty/__init__.py | 2 +
website_sale_slides_multi_qty/__manifest__.py | 27 +
.../controllers/__init__.py | 1 +
.../controllers/main.py | 478 ++++++++++++++++++
.../data/mail_template_data.xml | 66 +++
website_sale_slides_multi_qty/i18n/es.po | 267 ++++++++++
.../i18n/website_sale_slides_multi_qty.pot | 198 ++++++++
.../models/__init__.py | 2 +
.../models/slide_channel.py | 278 ++++++++++
.../models/slide_slide.py | 236 +++++++++
.../readme/CONTRIBUTORS.rst | 4 +
.../readme/DESCRIPTION.rst | 1 +
.../security/ir.model.access.csv | 2 +
.../static/description/icon.png | Bin 0 -> 9455 bytes
.../static/description/index.html | 426 ++++++++++++++++
.../static/src/js/slides_access_form.js | 17 +
.../src/js/slides_course_fullscreen_player.js | 89 ++++
.../views/slide_channel_partner_views.xml | 16 +
.../views/slide_channel_views.xml | 14 +
.../views/website_slides_templates.xml | 16 +
.../views/website_slides_templates_course.xml | 99 ++++
.../website_slides_templates_homepage.xml | 12 +
23 files changed, 2330 insertions(+)
create mode 100644 website_sale_slides_multi_qty/README.rst
create mode 100644 website_sale_slides_multi_qty/__init__.py
create mode 100644 website_sale_slides_multi_qty/__manifest__.py
create mode 100644 website_sale_slides_multi_qty/controllers/__init__.py
create mode 100644 website_sale_slides_multi_qty/controllers/main.py
create mode 100644 website_sale_slides_multi_qty/data/mail_template_data.xml
create mode 100644 website_sale_slides_multi_qty/i18n/es.po
create mode 100644 website_sale_slides_multi_qty/i18n/website_sale_slides_multi_qty.pot
create mode 100644 website_sale_slides_multi_qty/models/__init__.py
create mode 100644 website_sale_slides_multi_qty/models/slide_channel.py
create mode 100644 website_sale_slides_multi_qty/models/slide_slide.py
create mode 100644 website_sale_slides_multi_qty/readme/CONTRIBUTORS.rst
create mode 100644 website_sale_slides_multi_qty/readme/DESCRIPTION.rst
create mode 100644 website_sale_slides_multi_qty/security/ir.model.access.csv
create mode 100644 website_sale_slides_multi_qty/static/description/icon.png
create mode 100644 website_sale_slides_multi_qty/static/description/index.html
create mode 100644 website_sale_slides_multi_qty/static/src/js/slides_access_form.js
create mode 100644 website_sale_slides_multi_qty/static/src/js/slides_course_fullscreen_player.js
create mode 100644 website_sale_slides_multi_qty/views/slide_channel_partner_views.xml
create mode 100644 website_sale_slides_multi_qty/views/slide_channel_views.xml
create mode 100644 website_sale_slides_multi_qty/views/website_slides_templates.xml
create mode 100644 website_sale_slides_multi_qty/views/website_slides_templates_course.xml
create mode 100644 website_sale_slides_multi_qty/views/website_slides_templates_homepage.xml
diff --git a/website_sale_slides_multi_qty/README.rst b/website_sale_slides_multi_qty/README.rst
new file mode 100644
index 0000000..9f9808a
--- /dev/null
+++ b/website_sale_slides_multi_qty/README.rst
@@ -0,0 +1,79 @@
+=============================
+Website Sale Slides Multi Qty
+=============================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:3a794fd5342313c42c608fcbc6bb79937d6bfe85b2257ab195a03dcbfa6c5703
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--learning-lightgray.png?logo=github
+ :target: https://github.com/OCA/e-learning/tree/15.0/website_sale_slides_multi_qty
+ :alt: OCA/e-learning
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/e-learning-15-0/e-learning-15-0-website_sale_slides_multi_qty
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/e-learning&target_branch=15.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Tecnativa
+
+Contributors
+~~~~~~~~~~~~
+
+* `Tecnativa `__:
+
+ * David Vidal
+ * Pilar Vargas
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/e-learning `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/website_sale_slides_multi_qty/__init__.py b/website_sale_slides_multi_qty/__init__.py
new file mode 100644
index 0000000..91c5580
--- /dev/null
+++ b/website_sale_slides_multi_qty/__init__.py
@@ -0,0 +1,2 @@
+from . import controllers
+from . import models
diff --git a/website_sale_slides_multi_qty/__manifest__.py b/website_sale_slides_multi_qty/__manifest__.py
new file mode 100644
index 0000000..ee4036c
--- /dev/null
+++ b/website_sale_slides_multi_qty/__manifest__.py
@@ -0,0 +1,27 @@
+# Copyright 2025 Tecnativa - Pilar Vargas
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+{
+ "name": "Website Sale Slides Multi Qty",
+ "version": "15.0.1.0.0",
+ "category": "Website/eLearning",
+ "author": "Tecnativa, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/e-learning",
+ "license": "AGPL-3",
+ "summary": "",
+ "depends": ["website_sale_slides_order_line_link"],
+ "data": [
+ "data/mail_template_data.xml",
+ "security/ir.model.access.csv",
+ "views/slide_channel_partner_views.xml",
+ "views/slide_channel_views.xml",
+ "views/website_slides_templates_course.xml",
+ "views/website_slides_templates_homepage.xml",
+ "views/website_slides_templates.xml",
+ ],
+ "installable": True,
+ "assets": {
+ "web.assets_frontend": [
+ "website_sale_slides_multi_qty/static/src/js/*.js",
+ ],
+ },
+}
diff --git a/website_sale_slides_multi_qty/controllers/__init__.py b/website_sale_slides_multi_qty/controllers/__init__.py
new file mode 100644
index 0000000..12a7e52
--- /dev/null
+++ b/website_sale_slides_multi_qty/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/website_sale_slides_multi_qty/controllers/main.py b/website_sale_slides_multi_qty/controllers/main.py
new file mode 100644
index 0000000..9535a1a
--- /dev/null
+++ b/website_sale_slides_multi_qty/controllers/main.py
@@ -0,0 +1,478 @@
+# Copyright 2025 Tecnativa - Pilar Vargas
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from odoo import _, http
+from odoo.http import request
+
+from odoo.addons.website_slides.controllers.main import WebsiteSlides
+
+
+class WebsiteSaleSlides(WebsiteSlides):
+ def _set_viewed_slide(self, slide, quiz_attempts_inc=False):
+ identification_number = request.session.get("identification_number", False)
+ if (
+ request.env.user._is_public()
+ and slide.channel_id.is_member
+ and identification_number
+ ):
+ slide.action_set_viewed(quiz_attempts_inc=quiz_attempts_inc)
+ return True
+ return super()._set_viewed_slide(slide, quiz_attempts_inc=quiz_attempts_inc)
+
+ def _get_slide_detail(self, slide):
+ values = super()._get_slide_detail(slide)
+ identification_number = request.session.get("identification_number", False)
+ # Si el usuario tiene clave, no debe ser considerado público
+ if identification_number:
+ values["is_public_user"] = False
+ # Evitar que intente publicar comentarios si no tiene partner_id
+ if "message_post_pid" in values:
+ values["message_post_pid"] = False
+ return values
+
+ def _get_channel_progress(self, channel, include_quiz=False):
+ values = super()._get_channel_progress(channel, include_quiz=include_quiz)
+ identification_number = request.session.get("identification_number", False)
+ if request.website.is_public_user() and identification_number:
+ slides = (
+ request.env["slide.slide"]
+ .sudo()
+ .search([("channel_id", "=", channel.id)])
+ )
+ slide_partners = (
+ request.env["slide.slide.partner"]
+ .sudo()
+ .search(
+ [
+ ("channel_id", "=", channel.id),
+ (
+ "partner_id",
+ "=",
+ int(request.session.get("invite_partner_id")),
+ ),
+ ("identification_number", "=", identification_number),
+ ("slide_id", "in", slides.ids),
+ ]
+ )
+ )
+ for slide_partner in slide_partners:
+ values[slide_partner.slide_id.id].update(slide_partner.read()[0])
+ if slide_partner.slide_id in values:
+ values[slide_partner.slide_id].update(
+ slide_partner.read()[0]
+ ) # Copia los datos de progreso
+ # Manejo de intentos y recompensas en quiz
+ if slide_partner.slide_id.sudo().question_ids:
+ gains = [
+ slide_partner.slide_id.quiz_first_attempt_reward,
+ slide_partner.slide_id.quiz_second_attempt_reward,
+ slide_partner.slide_id.quiz_third_attempt_reward,
+ slide_partner.slide_id.quiz_fourth_attempt_reward,
+ ]
+ values[slide_partner.slide_id.id]["quiz_gain"] = (
+ gains[slide_partner.quiz_attempts_count]
+ if slide_partner.quiz_attempts_count < len(gains)
+ else gains[-1]
+ )
+ return values
+
+ def _check_identification_number(self, identification_number, partner):
+ # Validate ID depending on the country of the parent partner
+ if not identification_number or not partner or not partner.sudo().country_id:
+ return True # Allow if insufficient data
+ return request.env["res.partner"].simple_vat_check(
+ partner.country_id.code.upper(), identification_number.strip().upper()
+ )
+
+ def _session_data(self):
+ return {
+ "invite_hash": request.session.get("invite_hash", False),
+ "identification_number": request.session.get(
+ "identification_number", False
+ ),
+ "invite_partner_id": int(request.session.get("invite_partner_id", False)),
+ }
+
+ def _delete_session_data(self):
+ request.session.pop("invite_hash", None)
+ request.session.pop("identification_number", None)
+ request.session.pop("invite_partner_id", None)
+
+ def _set_session_data(self, identification_number, invite_partner_id, invite_hash):
+ request.session["identification_number"] = identification_number
+ request.session["invite_partner_id"] = invite_partner_id
+ request.session["invite_hash"] = invite_hash
+
+ @http.route("/slides/is_public_with_key", type="json", auth="public", website=True)
+ def session_data(self):
+ return self._session_data()
+
+ @http.route()
+ def channel(
+ self,
+ channel,
+ category=None,
+ tag=None,
+ page=1,
+ slide_type=None,
+ uncategorized=False,
+ sorting=None,
+ search=None,
+ **kw,
+ ):
+ session_data = self._session_data()
+ participation = False
+ if session_data["invite_hash"]:
+ participation = (
+ request.env["slide.channel.partner"]
+ .sudo()
+ .search(
+ [
+ ("invitation_hash", "=", session_data["invite_hash"]),
+ (
+ "identification_number",
+ "=",
+ session_data["identification_number"],
+ ),
+ ("channel_id", "=", channel.id),
+ ],
+ limit=1,
+ )
+ )
+ if participation and participation.channel_id != channel:
+ # If there is no valid participation, we delete the session data.
+ self._delete_session_data()
+ res = super().channel(
+ channel,
+ category=category,
+ tag=tag,
+ page=page,
+ slide_type=slide_type,
+ uncategorized=uncategorized,
+ sorting=sorting,
+ search=search,
+ **kw,
+ )
+ res.qcontext["can_enroll"] = self._can_user_register(
+ channel, request.env.user
+ ) or bool(kw.get("is_invite", False))
+ channel_error = request.session.pop("channel_error", None)
+ if channel_error:
+ res.qcontext["channel_error"] = channel_error
+ show_modal_to_join = request.session.pop("show_modal_to_join", None)
+ if show_modal_to_join:
+ res.qcontext["show_modal_to_join"] = show_modal_to_join
+ return res
+
+ @http.route(
+ "/slides/channel/join_with_id",
+ type="http",
+ auth="public",
+ methods=["POST"],
+ website=True,
+ )
+ def slide_channel_join_with_id(self, **kw):
+ identification_number = kw.get("identification_number", False)
+ channel_id = int(kw.get("channel_id"))
+ slide_channel_partner_name = kw.get("slide_channel_partner_name")
+ slide_channel_partner_email = kw.get("slide_channel_partner_email")
+ slide_channel_partner_phone = kw.get("slide_channel_partner_phone")
+ invite_partner_id = int(kw.get("invite_partner_id"))
+ invite_hash = kw.get("invite_hash")
+ channel = request.env["slide.channel"].browse(channel_id).exists()
+ slide_channel_partner = channel.sudo().channel_partner_ids.filtered(
+ lambda x: x.identification_number == identification_number
+ )
+ target_partner = request.env["res.partner"].browse(invite_partner_id)
+ parent_channel_partner = channel.sudo().channel_partner_ids.filtered(
+ lambda x: x.sale_order_line_ids and x.invitation_hash == invite_hash
+ )
+ if not slide_channel_partner:
+ redirect_url = (
+ f"/slides/{channel_id}"
+ f"/invite?invite_partner_id={invite_partner_id}"
+ f"&invite_hash={invite_hash}"
+ )
+ if (
+ not slide_channel_partner_name
+ or not slide_channel_partner_email
+ or not slide_channel_partner_phone
+ ):
+ request.session["channel_error"] = _(
+ "There is no participation for this key"
+ )
+ return request.redirect(redirect_url)
+ if not self._check_identification_number(
+ identification_number, target_partner
+ ):
+ request.session["channel_error"] = _("Invalid identification number.")
+ return request.redirect(redirect_url)
+ self._add_new_member(
+ channel,
+ target_partner,
+ parent_channel_partner,
+ slide_channel_partner_name=slide_channel_partner_name,
+ slide_channel_partner_email=slide_channel_partner_email,
+ slide_channel_partner_phone=slide_channel_partner_phone,
+ identification_number=identification_number,
+ is_public_slide_channel_partner=True,
+ )
+ self._set_session_data(identification_number, invite_partner_id, invite_hash)
+ return request.redirect(f"/slides/{channel_id}")
+
+ @http.route("/slides//join", type="http", auth="user", website=True)
+ def slide_channel_join_course(self, channel_id, **kwargs):
+ channel = request.env["slide.channel"].browse(channel_id).exists()
+ target_partner = request.env.user.partner_id
+ parent_channel_partners = channel.sudo().channel_partner_ids.filtered(
+ lambda x: x.available_registrations > 1
+ )
+ invite_partner_id = kwargs.get("invite_partner_id", False)
+ for parent_channel_partner in parent_channel_partners:
+ if parent_channel_partner.partner_id == target_partner:
+ self._add_new_member(channel, target_partner, parent_channel_partner)
+ else:
+ if (
+ parent_channel_partner.partner_id.commercial_partner_id
+ == target_partner.commercial_partner_id
+ ):
+ self._add_new_member(
+ channel, target_partner, parent_channel_partner
+ )
+ if invite_partner_id:
+ parent_channel_partner = parent_channel_partners.filtered(
+ lambda x: x.partner_id.id == int(invite_partner_id) and not x.parent_id
+ )
+ self._add_new_member(channel, target_partner, parent_channel_partner)
+ return request.redirect(f"/slides/{channel_id}")
+
+ @http.route(
+ "/slides//invite",
+ type="http",
+ auth="public",
+ website=True,
+ sitemap=False,
+ )
+ def slide_channel_invite(self, channel_id, **kwargs):
+ self._delete_session_data()
+ invite_partner_id = kwargs.get("invite_partner_id") or request.session.pop(
+ "invite_partner_id", None
+ )
+ invite_hash = kwargs.get("invite_hash") or request.session.pop(
+ "invite_hash", None
+ )
+ if not invite_partner_id or not invite_hash:
+ return request.redirect("/slides")
+ channel = request.env["slide.channel"].sudo().browse(channel_id).exists()
+ partner = request.env.user.partner_id
+ if not channel or not channel.is_published:
+ return self._redirect_to_slides_main("no_channel")
+ # A user is logged
+ if not request.website.is_public_user():
+ enroll = channel.sudo().channel_partner_ids.filtered(
+ lambda x: x.partner_id == partner
+ )
+ if not partner.id == int(invite_partner_id) and not enroll:
+ return request.redirect(
+ f"/slides/{channel_id}?is_invite=1&invite_partner_id={invite_partner_id}"
+ )
+ return request.redirect(f"/slides/{channel_id}")
+ # No user is logged.
+ if request.website.is_public_user():
+ request.session["invite_partner_id"] = int(invite_partner_id)
+ request.session["invite_hash"] = invite_hash
+ request.session["show_modal_to_join"] = True
+ return request.redirect(
+ f"/slides/{channel_id}?is_invite=1&invite_partner_id={invite_partner_id}"
+ )
+
+ @staticmethod
+ def _redirect_to_slides_main(invite_error=""):
+ # Redirige a la página de cursos y muestra un error si corresponde.
+ if invite_error:
+ request.session[
+ "error_message"
+ ] = "El curso no se encuentra disponible o no existe."
+ return request.redirect("/slides")
+
+ def _can_user_register(self, channel, user):
+ # Check if the user meets the conditions to register for the course.
+ partner = user.partner_id
+ enroll = channel.sudo().channel_partner_ids.filtered(
+ lambda x: x.partner_id == partner and not x.is_public_slide_channel_partner
+ )
+ # If the accessing user is the one who has acquired and does not have a
+ # sub-participation
+ if (
+ len(enroll) == 1
+ and enroll.available_registrations > 1
+ and enroll.available_registrations > enroll.used_registrations
+ ):
+ return True
+ # If the accessing user is a contact of the company or of the partner who has
+ # acquired the shareholding
+ enroll_comercial = channel.sudo().channel_partner_ids.filtered(
+ lambda x: x.partner_id.commercial_partner_id
+ == partner.commercial_partner_id
+ )
+ enroll_comercial_parent = enroll_comercial.filtered("child_channel_partner_ids")
+ if (
+ partner.id not in enroll_comercial.partner_id.ids
+ and enroll_comercial_parent.available_registrations
+ > enroll_comercial_parent.used_registrations
+ ):
+ return True
+ return False
+
+ def _add_new_member(
+ self, channel, target_partner, parent_channel_partner, **kwargs
+ ):
+ channel._action_add_members(
+ target_partner,
+ parent_channel_partner=parent_channel_partner,
+ identification_number=kwargs.get("identification_number", False),
+ slide_channel_partner_name=kwargs.get("slide_channel_partner_name", False),
+ slide_channel_partner_email=kwargs.get(
+ "slide_channel_partner_email", False
+ ),
+ slide_channel_partner_phone=kwargs.get(
+ "slide_channel_partner_phone", False
+ ),
+ is_public_slide_channel_partner=kwargs.get(
+ "is_public_slide_channel_partner", False
+ ),
+ )
+
+ # SLIDE.SLIDE UTILS
+
+ @http.route()
+ def slide_set_completed(self, slide_id):
+ session_data = self._session_data() or {}
+ invite_hash = session_data.get("invite_hash", False)
+ identification_number = session_data.get("identification_number", False)
+ invite_partner_id = session_data.get("invite_partner_id", False)
+ if (
+ request.website.is_public_user()
+ and identification_number
+ and invite_partner_id
+ and invite_hash
+ ):
+ fetch_res = self._fetch_slide(slide_id)
+ if fetch_res.get("error"):
+ return fetch_res
+ self._set_completed_slide(fetch_res["slide"])
+ return {"channel_completion": fetch_res["slide"].channel_id.completion}
+ return super().slide_set_completed(slide_id)
+
+ # QUIZ SECTION
+
+ @http.route()
+ def slide_quiz_submit(self, slide_id, answer_ids):
+ session_data = self._session_data() or {}
+ invite_hash = session_data.get("invite_hash", False)
+ identification_number = session_data.get("identification_number", False)
+ invite_partner_id = session_data.get("invite_partner_id", False)
+ values = super().slide_quiz_submit(slide_id, answer_ids)
+ if (
+ request.website.is_public_user()
+ and identification_number
+ and invite_partner_id
+ and invite_hash
+ ):
+ values = {}
+ fetch_res = self._fetch_slide(slide_id)
+ if fetch_res.get("error"):
+ return fetch_res
+ slide = fetch_res["slide"]
+ if slide.user_membership_id.sudo().completed:
+ self._channel_remove_session_answers(slide.channel_id, slide)
+ return {"error": "slide_quiz_done"}
+ all_questions = (
+ request.env["slide.question"]
+ .sudo()
+ .search([("slide_id", "=", slide.id)])
+ )
+ user_answers = (
+ request.env["slide.answer"].sudo().search([("id", "in", answer_ids)])
+ )
+ if user_answers.mapped("question_id") != all_questions:
+ return {"error": "slide_quiz_incomplete"}
+ user_bad_answers = user_answers.filtered(
+ lambda answer: not answer.is_correct
+ )
+ self._set_viewed_slide(slide, quiz_attempts_inc=True)
+ quiz_info = self._get_slide_quiz_partner_info(slide, quiz_done=True)
+ rank_progress = {}
+ if not user_bad_answers:
+ rank_progress["previous_rank"] = self._get_rank_values(request.env.user)
+ slide._action_set_quiz_done()
+ slide.action_set_completed()
+ rank_progress["new_rank"] = self._get_rank_values(request.env.user)
+ rank_progress.update(
+ {
+ "description": request.env.user.rank_id.description,
+ "last_rank": not request.env.user._get_next_rank(),
+ "level_up": rank_progress["previous_rank"]["lower_bound"]
+ != rank_progress["new_rank"]["lower_bound"],
+ }
+ )
+ self._channel_remove_session_answers(slide.channel_id, slide)
+ values.update(
+ {
+ "answers": {
+ answer.question_id.id: {
+ "is_correct": answer.is_correct,
+ "comment": answer.comment,
+ }
+ for answer in user_answers
+ },
+ "completed": slide.user_membership_id.sudo().completed,
+ "channel_completion": slide.channel_id.completion,
+ "quizKarmaWon": quiz_info["quiz_karma_won"],
+ "quizKarmaGain": quiz_info["quiz_karma_gain"],
+ "quizAttemptsCount": quiz_info["quiz_attempts_count"],
+ "rankProgress": rank_progress,
+ }
+ )
+ return values
+
+ # PROFILE
+
+ def _prepare_user_slides_profile(self, user):
+ invite_hash = request.session.get("invite_hash", False)
+ identification_number = request.session.get("identification_number", False)
+ invite_partner_id = request.session.get("invite_partner_id", False)
+ values = super()._prepare_user_slides_profile(user)
+ if (
+ request.website.is_public_user()
+ and identification_number
+ and invite_partner_id
+ and invite_hash
+ ):
+ courses = (
+ request.env["slide.channel.partner"]
+ .sudo()
+ .search(
+ [
+ (
+ "partner_id",
+ "=",
+ int(invite_partner_id),
+ ("identification_number", "=", identification_number),
+ )
+ ]
+ )
+ )
+ courses_completed = courses.filtered(lambda c: c.completed)
+ courses_ongoing = courses - courses_completed
+ values.update(
+ {
+ "uid": request.env.user.id,
+ "user": user,
+ "main_object": user,
+ "courses_completed": courses_completed,
+ "courses_ongoing": courses_ongoing,
+ "is_profile_page": True,
+ "badge_category": "slides",
+ }
+ )
+ return values
diff --git a/website_sale_slides_multi_qty/data/mail_template_data.xml b/website_sale_slides_multi_qty/data/mail_template_data.xml
new file mode 100644
index 0000000..e4fa405
--- /dev/null
+++ b/website_sale_slides_multi_qty/data/mail_template_data.xml
@@ -0,0 +1,66 @@
+
+
+
+
+ Channel: Confirm Access
+
+ Your access to {{ object.channel_id.name }} has been confirmed
+
+
+
+
+ Hello ,
+
+
+
+ You have successfully acquired
+ registrations for the course Course Name.
+
+
+ To allow participants to access the course, they must complete their registration
+ using the following link. They can either log in with their Odoo account or create a new participation.
+
\n"
+" You have successfully acquired \n"
+" registrations for the course Course Name.\n"
+"
\n"
+"
\n"
+" To allow participants to access the course, they "
+"must complete their registration\n"
+" using the following link. They can either log in "
+"with their Odoo account or create a new participation.\n"
+"
\n"
+" Has adquirido con éxito \n"
+" inscripciones para el curso Nombre del curso.\n"
+"
\n"
+"
\n"
+" Para que los participantes accedan al curso, deben "
+"completar su inscripción\n"
+" utilizando el siguiente enlace. Pueden iniciar sesión "
+"con su cuenta de Odoo o crear una nueva participación.\n"
+"
\n"
+" "
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__available_registrations
+msgid "Available Registrations"
+msgstr "Inscripciones disponibles"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model,name:website_sale_slides_multi_qty.model_slide_channel_partner
+msgid "Channel / Partners (Members)"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:mail.template,name:website_sale_slides_multi_qty.mail_template_slide_channel_confirm
+msgid "Channel: Confirm Access"
+msgstr "Curso: Confirmar acceso"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__child_channel_partner_ids
+msgid "Child Participation"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model,name:website_sale_slides_multi_qty.model_slide_channel
+msgid "Course"
+msgstr "Curso"
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+msgid "Enter your ID"
+msgstr "Introduce tu DNI"
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+msgid "Enter your email"
+msgstr "Introduce tu email"
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+msgid "Enter your name"
+msgstr "Introduce tu nombre"
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+msgid "Enter your phone"
+msgstr "Introduce tu teléfono"
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+msgid "I have already joined"
+msgstr "Ya me he unido"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__identification_number
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_slide_partner__identification_number
+msgid "Identification Number"
+msgstr "Número de identificación"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__invitation_hash
+msgid "Invitation Hash"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__invitation_link
+msgid "Invitation Link"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__is_public_slide_channel_partner
+msgid "Is Public Slide Channel Partner"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+msgid "Join Course"
+msgstr "Unirse al curso"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__slide_channel_partner_name
+msgid "Name"
+msgstr "Nombre"
+
+#. module: website_sale_slides_multi_qty
+#: code:addons/website_sale_slides_multi_qty/models/slide_channel.py:0
+#, python-format
+msgid "No registrations available for this course."
+msgstr "No hay inscripciones disponibles para este curso"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__parent_id
+msgid "Parent Participation"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__slide_channel_partner_email
+msgid "Participation Email"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__slide_channel_partner_phone
+msgid "Phone"
+msgstr "Teléfono"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model,name:website_sale_slides_multi_qty.model_slide_slide_partner
+msgid "Slide / Partner decorated m2m"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model,name:website_sale_slides_multi_qty.model_slide_slide
+msgid "Slides"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.constraint,message:website_sale_slides_multi_qty.constraint_slide_channel_partner_unique_channel_identification
+msgid "The identification number must be unique per course!"
+msgstr "El numero de identificación debe ser único!"
+
+#. module: website_sale_slides_multi_qty
+#: code:addons/website_sale_slides_multi_qty/controllers/main.py:0
+#, python-format
+msgid "There is no participation for this key"
+msgstr "No hay ninguna participación para esta identificación"
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__used_registrations
+msgid "Used Registrations"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.fields,help:website_sale_slides_multi_qty.field_slide_channel_partner__identification_number
+msgid "User's personal identification number"
+msgstr "Número de identificación personal del usuario"
+
+#. module: website_sale_slides_multi_qty
+#: model:mail.template,subject:website_sale_slides_multi_qty.mail_template_slide_channel_confirm
+msgid "Your access to {{ object.channel_id.name }} has been confirmed"
+msgstr "Tu acceso a {{ object.channel_id.name }} ha sido confirmado"
diff --git a/website_sale_slides_multi_qty/i18n/website_sale_slides_multi_qty.pot b/website_sale_slides_multi_qty/i18n/website_sale_slides_multi_qty.pot
new file mode 100644
index 0000000..65123f6
--- /dev/null
+++ b/website_sale_slides_multi_qty/i18n/website_sale_slides_multi_qty.pot
@@ -0,0 +1,198 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * website_sale_slides_multi_qty
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 15.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-05-06 06:24+0000\n"
+"PO-Revision-Date: 2025-05-06 06:24+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: website_sale_slides_multi_qty
+#: model:mail.template,body_html:website_sale_slides_multi_qty.mail_template_slide_channel_confirm
+msgid ""
+"
\n"
+"
\n"
+" Hello ,\n"
+"
\n"
+" \n"
+"
\n"
+" You have successfully acquired \n"
+" registrations for the course Course Name.\n"
+"
\n"
+"
\n"
+" To allow participants to access the course, they must complete their registration\n"
+" using the following link. They can either log in with their Odoo account or create a new participation.\n"
+"
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
This module is part of the OCA/e-learning project on GitHub.
\n"
-" Has adquirido con éxito \n"
+" Has adquirido con éxito \n"
" inscripciones para el curso Nombre del curso.\n"
"
\n"
"
\n"
" Para que los participantes accedan al curso, deben "
"completar su inscripción\n"
-" utilizando el siguiente enlace. Pueden iniciar sesión "
-"con su cuenta de Odoo o crear una nueva participación.\n"
+" utilizando el siguiente enlace. Pueden iniciar "
+"sesión con su cuenta de Odoo o crear una nueva participación.\n"
"
\n"
"
\n"
"
\n"
-" \n"
+" Haz clic aquí para acceder al curso\n"
" \n"
"
\n"
" \n"
@@ -111,7 +111,8 @@ msgstr ""
"channel_id.name or ''\">Nombre del curso ha sido confirmado.\n"
" \n"
"
\n"
-" Puedes comenzar a aprender haciendo clic en el siguiente enlace:\n"
+" Puedes comenzar a aprender haciendo clic en el "
+"siguiente enlace:\n"
"
\n"
"
\n"
"
\n"
"
\n"
-" Hello ,\n"
+" Hello ,\n"
"
\n"
" \n"
"
\n"
-" You have successfully acquired \n"
+" You have successfully acquired \n"
" registrations for the course Course Name.\n"
"
\n"
" \n"
@@ -79,33 +79,43 @@ msgstr ""
msgid "Child Participation"
msgstr ""
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.constraint,message:website_sale_slides_multi_qty.constraint_slide_slide_partner_slide_partner_uniq
+msgid "Constraint disabled: allowing repeated partner on the same slide."
+msgstr ""
+
#. module: website_sale_slides_multi_qty
#: model:ir.model,name:website_sale_slides_multi_qty.model_slide_channel
msgid "Course"
msgstr ""
#. module: website_sale_slides_multi_qty
-#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
msgid "Enter your ID"
msgstr ""
#. module: website_sale_slides_multi_qty
-#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
msgid "Enter your email"
msgstr ""
#. module: website_sale_slides_multi_qty
-#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
msgid "Enter your name"
msgstr ""
#. module: website_sale_slides_multi_qty
-#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
msgid "Enter your phone"
msgstr ""
#. module: website_sale_slides_multi_qty
-#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
+msgid "I accept the"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
msgid "I have already joined"
msgstr ""
@@ -115,6 +125,13 @@ msgstr ""
msgid "Identification Number"
msgstr ""
+#. module: website_sale_slides_multi_qty
+#. odoo-python
+#: code:addons/website_sale_slides_multi_qty/controllers/main.py:0
+#, python-format
+msgid "Invalid identification number."
+msgstr ""
+
#. module: website_sale_slides_multi_qty
#: model:ir.model.fields,field_description:website_sale_slides_multi_qty.field_slide_channel_partner__invitation_hash
msgid "Invitation Hash"
@@ -131,7 +148,7 @@ msgid "Is Public Slide Channel Partner"
msgstr ""
#. module: website_sale_slides_multi_qty
-#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_sidebar
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
msgid "Join Course"
msgstr ""
@@ -141,6 +158,7 @@ msgid "Name"
msgstr ""
#. module: website_sale_slides_multi_qty
+#. odoo-python
#: code:addons/website_sale_slides_multi_qty/models/slide_channel.py:0
#, python-format
msgid "No registrations available for this course."
@@ -161,6 +179,11 @@ msgstr ""
msgid "Phone"
msgstr ""
+#. module: website_sale_slides_multi_qty
+#: model:ir.model,name:website_sale_slides_multi_qty.model_sale_order
+msgid "Sales Order"
+msgstr ""
+
#. module: website_sale_slides_multi_qty
#: model:ir.model,name:website_sale_slides_multi_qty.model_slide_slide_partner
msgid "Slide / Partner decorated m2m"
@@ -171,12 +194,23 @@ msgstr ""
msgid "Slides"
msgstr ""
+#. module: website_sale_slides_multi_qty
+#: model:ir.model.constraint,message:website_sale_slides_multi_qty.constraint_slide_channel_partner_channel_partner_uniq
+msgid "Temporal constraint disabled"
+msgstr ""
+
#. module: website_sale_slides_multi_qty
#: model:ir.model.constraint,message:website_sale_slides_multi_qty.constraint_slide_channel_partner_unique_channel_identification
msgid "The identification number must be unique per course!"
msgstr ""
#. module: website_sale_slides_multi_qty
+#: model:ir.model.constraint,message:website_sale_slides_multi_qty.constraint_slide_slide_partner_unique_slide_identification
+msgid "The identification number must be unique!"
+msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#. odoo-python
#: code:addons/website_sale_slides_multi_qty/controllers/main.py:0
#, python-format
msgid "There is no participation for this key"
@@ -196,3 +230,8 @@ msgstr ""
#: model:mail.template,subject:website_sale_slides_multi_qty.mail_template_slide_channel_confirm
msgid "Your access to {{ object.channel_id.name }} has been confirmed"
msgstr ""
+
+#. module: website_sale_slides_multi_qty
+#: model_terms:ir.ui.view,arch_db:website_sale_slides_multi_qty.course_join
+msgid "terms and conditions"
+msgstr ""
diff --git a/website_sale_slides_multi_qty/models/__init__.py b/website_sale_slides_multi_qty/models/__init__.py
index 36d510f..a2c75d5 100644
--- a/website_sale_slides_multi_qty/models/__init__.py
+++ b/website_sale_slides_multi_qty/models/__init__.py
@@ -1,2 +1,3 @@
+from . import sale_order
from . import slide_channel
from . import slide_slide
diff --git a/website_sale_slides_multi_qty/models/sale_order.py b/website_sale_slides_multi_qty/models/sale_order.py
new file mode 100644
index 0000000..8169cf7
--- /dev/null
+++ b/website_sale_slides_multi_qty/models/sale_order.py
@@ -0,0 +1,17 @@
+# Copyright 2025 Tecnativa - Pilar Vargas
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from odoo import models
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+ def _verify_updated_quantity(self, order_line, product_id, new_qty, **kwargs):
+ # Allow adding more than 1 quantity of a course to the cart
+ res = super()._verify_updated_quantity(
+ order_line, product_id, new_qty, **kwargs
+ )
+ product = self.env["product.product"].browse(product_id)
+ if product.detailed_type == "course" and new_qty > 1:
+ return new_qty, ""
+ return res
diff --git a/website_sale_slides_multi_qty/models/slide_channel.py b/website_sale_slides_multi_qty/models/slide_channel.py
index 73bdb38..96b540a 100644
--- a/website_sale_slides_multi_qty/models/slide_channel.py
+++ b/website_sale_slides_multi_qty/models/slide_channel.py
@@ -1,7 +1,7 @@
# Copyright 2025 Tecnativa - Pilar Vargas
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from odoo import _, api, fields, models, tools
+from odoo import _, api, fields, models
from odoo.http import request
@@ -21,8 +21,8 @@ def _is_public_with_key(self):
)
return False
- def _compute_is_member(self):
- res = super()._compute_is_member()
+ def _compute_membership_values(self):
+ res = super()._compute_membership_values()
if self._is_public_with_key():
channel_partner = (
self.env["slide.channel.partner"]
@@ -61,10 +61,13 @@ def _compute_user_statistics(self):
]
)
)
- mapped_data = {
- info.channel_id.id: (info.completed, info.completed_slides_count)
+ mapped_data = dict(
+ (
+ info.channel_id.id,
+ (info.member_status == "completed", info.completed_slides_count),
+ )
for info in current_user_info
- }
+ )
for record in self:
completed, completed_slides_count = mapped_data.get(
record.id, (False, 0)
@@ -80,9 +83,13 @@ def _compute_user_statistics(self):
else:
return super()._compute_user_statistics()
- # def _compute_action_rights(self):
-
- def _action_add_members(self, target_partners, **member_values):
+ def _action_add_members(
+ self,
+ target_partners,
+ member_status="joined",
+ raise_on_access=False,
+ **member_values,
+ ):
# Create sub-participations
parent_channel_partner = member_values.get("parent_channel_partner", False)
to_create_values = {}
@@ -121,7 +128,11 @@ def _action_add_members(self, target_partners, **member_values):
new_target_partners = target_partners.filtered(
lambda x: x.id not in self.channel_partner_ids.partner_id.ids
)
- res = super()._action_add_members(target_partners, **member_values)
+ res = super()._action_add_members(
+ target_partners,
+ member_status=member_status,
+ raise_on_access=raise_on_access,
+ )
if new_target_partners:
for target in self.channel_partner_ids.filtered(
lambda x: x.partner_id.id in new_target_partners.ids
@@ -150,8 +161,8 @@ class SlideChannelPartner(models.Model):
used_registrations = fields.Integer(
compute="_compute_used_registrations", store=True
)
- invitation_hash = fields.Char(compute="_compute_invitation", store=True)
- invitation_link = fields.Char(compute="_compute_invitation", store=True)
+ invitation_hash = fields.Char(compute="_compute_invitation_link", store=True)
+ invitation_link = fields.Char(store=True)
slide_channel_partner_name = fields.Char(string="Name")
slide_channel_partner_email = fields.Char(string="Participation Email")
slide_channel_partner_phone = fields.Char(string="Phone")
@@ -161,11 +172,12 @@ class SlideChannelPartner(models.Model):
is_public_slide_channel_partner = fields.Boolean(default=False)
_sql_constraints = [
+ ("channel_partner_uniq", "CHECK (true)", "Temporal constraint disabled"),
(
"unique_channel_identification",
"unique(channel_id, identification_number)",
"The identification number must be unique per course!",
- )
+ ),
]
@api.depends("sale_order_line_ids.product_uom_qty")
@@ -180,18 +192,11 @@ def _compute_used_registrations(self):
record.used_registrations = len(record.child_channel_partner_ids)
@api.depends("channel_id", "partner_id")
- def _compute_invitation(self):
- # This sets the url used as hyperlink in the channel invitation email in
- # template mail_notification_channel_invite.
- # The partner_id is given in the url, as well as a hash based on the partner
- # and channel id.
+ def _compute_invitation_link(self):
+ res = super()._compute_invitation_link()
for record in self:
record.invitation_hash = record._get_invitation_hash()
- record.invitation_link = (
- f"{record.channel_id.get_base_url()}/slides/{record.channel_id.id}"
- f"/invite?invite_partner_id={record.partner_id.id}"
- f"&invite_hash={record.invitation_hash}"
- )
+ return res
@api.model_create_multi
def create(self, vals_list):
@@ -208,57 +213,52 @@ def create(self, vals_list):
return super().create(vals_list)
def _recompute_completion(self):
- if not self.channel_id._is_public_with_key():
+ slide_channel_partners = self.filtered(lambda scp: scp.identification_number)
+ if not slide_channel_partners:
return super()._recompute_completion()
- identification_number = request.session.get("identification_number", False)
- # Obtain progress only from the participation of the user with current password
read_group_res = (
self.env["slide.slide.partner"]
.sudo()
- .read_group(
+ ._read_group(
[
("channel_id", "in", self.mapped("channel_id").ids),
- ("identification_number", "=", identification_number),
+ ("identification_number", "!=", False),
("completed", "=", True),
("slide_id.is_published", "=", True),
("slide_id.active", "=", True),
],
- ["channel_id"],
- groupby=["channel_id"],
- lazy=False,
+ ["channel_id", "identification_number"],
+ aggregates=["__count"],
)
)
mapped_data = {
- item["channel_id"][0]: item["__count"] for item in read_group_res
+ (channel.id, identification_number): count
+ for channel, identification_number, count in read_group_res
}
- # Filter only shares that have an ID number
- for record in self.filtered("identification_number"):
- if record.identification_number != identification_number:
+ for record in slide_channel_partners:
+ if record.member_status in ("completed", "invited"):
continue
- completed_slides_count = mapped_data.get(record.channel_id.id, 0)
- record.completed_slides_count = completed_slides_count
- record.completion = (
+ record.completed_slides_count = mapped_data.get(
+ (record.channel_id.id, record.identification_number), 0
+ )
+ record.completion = round(
100.0
- if record.completed
- else round(
- 100.0
- * completed_slides_count
- / (record.channel_id.total_slides or 1)
- )
+ * record.completed_slides_count
+ / (record.channel_id.total_slides or 1)
)
- if (
- not record.completed
- and record.channel_id.active
- and completed_slides_count >= record.channel_id.total_slides
- ):
- record.completed = True
-
- def _get_invitation_hash(self):
- # Returns the invitation hash of the attendee, used to access courses
- # as invited / joined.
- self.ensure_one()
- token = (self.partner_id.id, self.channel_id.id)
- return tools.hmac(self.env(su=True), "website_slides-channel-invite", token)
+ if not record.channel_id.active:
+ continue
+ if record.completion == 100:
+ record.member_status = "completed"
+ elif record.completion == 0:
+ record.member_status = "joined"
+ else:
+ record.member_status = "ongoing"
+ return super(
+ SlideChannelPartner,
+ self.filtered(lambda scp: not scp.child_channel_partner_ids)
+ - slide_channel_partners,
+ )._recompute_completion()
def _send_confirm_mail(self):
self.ensure_one()
diff --git a/website_sale_slides_multi_qty/models/slide_slide.py b/website_sale_slides_multi_qty/models/slide_slide.py
index 344910b..3f18599 100644
--- a/website_sale_slides_multi_qty/models/slide_slide.py
+++ b/website_sale_slides_multi_qty/models/slide_slide.py
@@ -10,6 +10,19 @@ class SlideSlidePartner(models.Model):
identification_number = fields.Char()
+ _sql_constraints = [
+ (
+ "slide_partner_uniq",
+ "CHECK (true)",
+ "Constraint disabled: allowing repeated partner on the same slide.",
+ ),
+ (
+ "unique_slide_identification",
+ "unique(slide_id, identification_number)",
+ "The identification number must be unique!",
+ ),
+ ]
+
class SlideSlide(models.Model):
_inherit = "slide.slide"
@@ -89,16 +102,8 @@ def _action_set_viewed(self, target_partner, quiz_attempts_inc=False):
]
)
if quiz_attempts_inc and existing_sudo:
- sql.increment_field_skiplock(existing_sudo, "quiz_attempts_count")
- SlidePartnerSudo.invalidate_cache(
- fnames=["quiz_attempts_count"], ids=existing_sudo.ids
- )
- for slide in existing_sudo:
- if (
- slide.slide_id.slide_type != "quiz"
- or not slide.slide_id.question_ids
- ):
- slide.slide_id.action_set_completed()
+ sql.increment_fields_skiplock(existing_sudo, "quiz_attempts_count")
+ existing_sudo.invalidate_recordset(["quiz_attempts_count"])
new_slides = self_sudo - existing_sudo.mapped("slide_id")
return SlidePartnerSudo.create(
[
@@ -117,7 +122,7 @@ def _action_set_viewed(self, target_partner, quiz_attempts_inc=False):
target_partner, quiz_attempts_inc=quiz_attempts_inc
)
- def _action_set_completed(self, target_partner):
+ def _action_mark_completed(self):
if self._is_public_with_key():
invite_partner_id = request.session.get("invite_partner_id")
identification_number = request.session.get("identification_number")
@@ -150,11 +155,11 @@ def _action_set_completed(self, target_partner):
]
)
return True
- return super()._action_set_completed(target_partner)
+ return super()._action_mark_completed()
- def _action_set_quiz_done(self):
+ def _action_set_quiz_done(self, completed=True):
points_before = self.env.user.karma
- res = super()._action_set_quiz_done()
+ res = super()._action_set_quiz_done(completed=completed)
# If the user is public with password, resets the points to the previous value.
if self.env.user._is_public() and any(
slide.user_membership_id.identification_number for slide in self
@@ -233,4 +238,4 @@ def _apply_ir_rules(self, query, mode="read"):
def check_access_rule(self, operation):
if self._is_public_with_key():
return
- return super(SlideSlide, self).check_access_rule(operation)
+ return super().check_access_rule(operation)
diff --git a/website_sale_slides_multi_qty/pyproject.toml b/website_sale_slides_multi_qty/pyproject.toml
new file mode 100644
index 0000000..4231d0c
--- /dev/null
+++ b/website_sale_slides_multi_qty/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/website_sale_slides_multi_qty/readme/CONFIGURE.md b/website_sale_slides_multi_qty/readme/CONFIGURE.md
new file mode 100644
index 0000000..7b5d1cc
--- /dev/null
+++ b/website_sale_slides_multi_qty/readme/CONFIGURE.md
@@ -0,0 +1,7 @@
+To enable this functionality on a course:
+
+1. Create or edit a course from the backend.
+2. Set the **Enrollment Policy** to **On Payment**.
+
+This ensures that access to the course is restricted to users who have
+completed a valid purchase.
diff --git a/website_sale_slides_multi_qty/readme/CONTRIBUTORS.md b/website_sale_slides_multi_qty/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000..684a32e
--- /dev/null
+++ b/website_sale_slides_multi_qty/readme/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+- \[Tecnativa\]():
+ - David Vidal
+ - Pilar Vargas
diff --git a/website_sale_slides_multi_qty/readme/CONTRIBUTORS.rst b/website_sale_slides_multi_qty/readme/CONTRIBUTORS.rst
deleted file mode 100644
index d908072..0000000
--- a/website_sale_slides_multi_qty/readme/CONTRIBUTORS.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-* `Tecnativa `__:
-
- * David Vidal
- * Pilar Vargas
diff --git a/website_sale_slides_multi_qty/readme/DESCRIPTION.md b/website_sale_slides_multi_qty/readme/DESCRIPTION.md
new file mode 100644
index 0000000..6e3d493
--- /dev/null
+++ b/website_sale_slides_multi_qty/readme/DESCRIPTION.md
@@ -0,0 +1,4 @@
+This module allows you to order several courses at once. This generates
+an invitation link through which other users can request to participate
+in the course without having to be registered users. The number of
+participants is limited to the number of courses ordered.
diff --git a/website_sale_slides_multi_qty/readme/DESCRIPTION.rst b/website_sale_slides_multi_qty/readme/DESCRIPTION.rst
deleted file mode 100644
index 8b13789..0000000
--- a/website_sale_slides_multi_qty/readme/DESCRIPTION.rst
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/website_sale_slides_multi_qty/readme/USAGE.md b/website_sale_slides_multi_qty/readme/USAGE.md
new file mode 100644
index 0000000..98b1d7b
--- /dev/null
+++ b/website_sale_slides_multi_qty/readme/USAGE.md
@@ -0,0 +1,13 @@
+When a user orders a course with a quantity greater than 1, a
+participation link will be sent to the customer by email.
+
+There are two ways to use the participation link:
+
+- **Registered users** can access the course directly without having to
+ make a new order.
+- **Anonymous users** will be asked to enter identification details,
+ which will allow them to register and return to the course later.
+
+Access is limited to the number of places acquired. Once all places have
+been used, no additional users will be able to register via the
+participation link.
diff --git a/website_sale_slides_multi_qty/security/ir.model.access.csv b/website_sale_slides_multi_qty/security/ir.model.access.csv
deleted file mode 100644
index 01eabbf..0000000
--- a/website_sale_slides_multi_qty/security/ir.model.access.csv
+++ /dev/null
@@ -1,2 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_slide_slide_public,slide.slide public access,model_slide_slide,base.group_public,1,0,0,0
diff --git a/website_sale_slides_multi_qty/static/description/index.html b/website_sale_slides_multi_qty/static/description/index.html
index 21066e9..38bf80e 100644
--- a/website_sale_slides_multi_qty/static/description/index.html
+++ b/website_sale_slides_multi_qty/static/description/index.html
@@ -367,41 +367,72 @@
Website Sale Slides Multi Qty
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:3a794fd5342313c42c608fcbc6bb79937d6bfe85b2257ab195a03dcbfa6c5703
+!! source digest: sha256:8e2129116bdf52aaccd45cfaf84303667132f5defc24cb2c03220b6cf08ba4a7
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-
+
+
This module allows you to order several courses at once. This generates
+an invitation link through which other users can request to participate
+in the course without having to be registered users. The number of
+participants is limited to the number of courses ordered.
When a user orders a course with a quantity greater than 1, a
+participation link will be sent to the customer by email.
+
There are two ways to use the participation link:
+
+
Registered users can access the course directly without having to
+make a new order.
+
Anonymous users will be asked to enter identification details,
+which will allow them to register and return to the course later.
+
+
Access is limited to the number of places acquired. Once all places have
+been used, no additional users will be able to register via the
+participation link.
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-feedback.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
-
This module is part of the OCA/e-learning project on GitHub.
+
This module is part of the OCA/e-learning project on GitHub.