[REF] all: pass pre-commit on the whole repo
Some checks failed
CI / lint (pull_request) Successful in 33s
CI / test (pull_request) Failing after 9m29s
CI / review (pull_request) Has been skipped

Needed for the lint job of the CI to go green. Mostly automatic hook
fixes (black, isort, prettier, pyupgrade, autoflake, whitespace). By hand:

- flake8: two unused imports, an ambiguous variable name `l`.
- pylint-odoo mandatory: manifest keys equal to their default value
  (installable, application, data, external_dependencies).
- end-of-file-fixer skips *.patch too, like trailing-whitespace: the
  upstream patches stay as git format-patch wrote them.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-10-04 14:17:09 +02:00
parent d9d9e8be2d
commit fbd43bb459
Signed by: snt
GPG key ID: A9FD34930EADBE71
45 changed files with 330 additions and 295 deletions

View file

@ -68,8 +68,9 @@ repos:
# lines of a diff are a single space, stripping it breaks them
exclude: /README\.rst$|\.pot?$|\.patch$
- id: end-of-file-fixer
# exclude autogenerated files
exclude: /README\.rst$|\.pot?$
# exclude autogenerated files, and patches: they are kept as
# git format-patch wrote them
exclude: /README\.rst$|\.pot?$|\.patch$
- id: debug-statements
- id: check-case-conflict
- id: check-docstring-first

View file

@ -1,6 +1,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo import fields
from odoo import models
class ResConfigSettings(models.TransientModel):

View file

@ -3,7 +3,8 @@
import logging
from datetime import timedelta
from odoo import fields, models
from odoo import fields
from odoo import models
_logger = logging.getLogger(__name__)

View file

@ -13,7 +13,4 @@
"membership_variable_period",
"account",
],
"data": [],
"installable": True,
"application": False,
}

View file

@ -4,7 +4,8 @@
import math
from datetime import timedelta
from odoo import fields, models
from odoo import fields
from odoo import models
class AccountMove(models.Model):
@ -74,7 +75,11 @@ class AccountMove(models.Model):
return fields.Date.today()
def _compute_membership_dates(
self, partner, product, payment_date, quantity=1.0,
self,
partner,
product,
payment_date,
quantity=1.0,
exclude_line_ids=None,
):
"""Calculate date_from and date_to for a membership line.

View file

@ -4,11 +4,10 @@
from datetime import date
from freezegun import freeze_time
from psycopg2 import ProgrammingError
from odoo import fields
from odoo.tests import Form, common
from odoo.tests import Form
from odoo.tests import common
@freeze_time("2026-01-15")
@ -123,9 +122,7 @@ class TestMembershipPaymentDate(common.TransactionCase):
def _create_invoice(self, product, invoice_date, quantity=1.0):
"""Create and return a draft out_invoice with the given product."""
invoice_form = Form(
self.env["account.move"].with_context(
default_move_type="out_invoice"
)
self.env["account.move"].with_context(default_move_type="out_invoice")
)
invoice_form.invoice_date = invoice_date
invoice_form.partner_id = self.partner

View file

@ -1,7 +1,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, http
from odoo.exceptions import AccessError, MissingError
from odoo import _
from odoo import http
from odoo.exceptions import AccessError
from odoo.exceptions import MissingError
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal

View file

@ -2,33 +2,32 @@ import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_sc
import { patch } from "@web/core/utils/patch";
patch(TicketScreen.prototype, {
/**
* Mark every refundable line of the selected order for a full refund and
* trigger the standard refund flow.
*
* `onDoRefund` is what the "Refund" action of the ticket screen calls: it
* builds the negative lines, links the combo children, carries over the
* fiscal position and the partner, and switches to the destination order.
*/
async onDoFullRefund() {
const order = this.getSelectedOrder();
if (!order) {
return;
}
for (const line of order.lines) {
const toRefundDetails = line
.getAllLinesInCombo()
.map((comboLine) => this.getToRefundDetail(comboLine));
for (const toRefundDetail of toRefundDetails) {
// Per line, and net of what was already refunded: the same
// guard the core applies in `_onUpdateSelectedOrderline`.
const refundableQty =
toRefundDetail.line.qty - toRefundDetail.line.refunded_qty;
if (refundableQty > 0) {
toRefundDetail.qty = refundableQty;
/**
* Mark every refundable line of the selected order for a full refund and
* trigger the standard refund flow.
*
* `onDoRefund` is what the "Refund" action of the ticket screen calls: it
* builds the negative lines, links the combo children, carries over the
* fiscal position and the partner, and switches to the destination order.
*/
async onDoFullRefund() {
const order = this.getSelectedOrder();
if (!order) {
return;
}
}
}
await this.onDoRefund();
},
for (const line of order.lines) {
const toRefundDetails = line
.getAllLinesInCombo()
.map((comboLine) => this.getToRefundDetail(comboLine));
for (const toRefundDetail of toRefundDetails) {
// Per line, and net of what was already refunded: the same
// guard the core applies in `_onUpdateSelectedOrderline`.
const refundableQty = toRefundDetail.line.qty - toRefundDetail.line.refunded_qty;
if (refundableQty > 0) {
toRefundDetail.qty = refundableQty;
}
}
}
await this.onDoRefund();
},
});

View file

@ -14,27 +14,24 @@ import { registry } from "@web/core/registry";
* negated at its full quantity, without touching the numpad.
*/
registry.category("web_tour.tours").add("pos_full_refund_tour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "2", "3"),
ProductScreen.addOrderline("Letter Tray", "3", "2"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "2", "3"),
ProductScreen.addOrderline("Letter Tray", "3", "2"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("-0001"),
TicketScreen.clickControlButton("Do Full Refund"),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("-0001"),
TicketScreen.clickControlButton("Do Full Refund"),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.isShown(),
inLeftSide([
...Order.hasLine("Desk Pad", "-2"),
...Order.hasLine("Letter Tray", "-3"),
]),
].flat(),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.isShown(),
inLeftSide([...Order.hasLine("Desk Pad", "-2"), ...Order.hasLine("Letter Tray", "-3")]),
].flat(),
});

View file

@ -4,64 +4,64 @@ import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/store/pos_hook";
export class ReorderButton extends Component {
static template = "ReorderButton";
static template = "ReorderButton";
setup() {
this.pos = usePos();
}
get isEmptyOrder() {
if (!this.props.order) return true;
return this.props.order.is_empty();
}
_reOrder() {
if (this.isEmptyOrder) {
return;
setup() {
this.pos = usePos();
}
const order = this.props.order;
const pos = this.pos;
const partner = order.get_partner();
const newOrder = pos.add_new_order();
if (partner) {
newOrder.set_partner(partner);
get isEmptyOrder() {
if (!this.props.order) return true;
return this.props.order.is_empty();
}
if (order.fiscal_position) {
newOrder.fiscal_position = order.fiscal_position;
_reOrder() {
if (this.isEmptyOrder) {
return;
}
const order = this.props.order;
const pos = this.pos;
const partner = order.get_partner();
const newOrder = pos.add_new_order();
if (partner) {
newOrder.set_partner(partner);
}
if (order.fiscal_position) {
newOrder.fiscal_position = order.fiscal_position;
}
if (order.pricelist) {
newOrder.set_pricelist(order.pricelist);
}
const lines = order.get_orderlines();
for (var i = 0; i < lines.length; i++) {
const line = lines[i];
const new_line = this.props.order.models["pos.order.line"].create(
this._prepareReorderLineVals(newOrder, line)
);
if (line.pack_lot_lines) {
new_line.setPackLotLines({
modifiedPackLotLines: [],
newPackLotLines: (line.lot_names || []).map((name) => ({
lot_name: name,
})),
});
}
new_line.set_unit_price(line.get_unit_price());
new_line.set_quantity(line.get_quantity());
new_line.set_discount(line.get_discount());
}
this.pos.closeScreen();
}
if (order.pricelist) {
newOrder.set_pricelist(order.pricelist);
_prepareReorderLineVals(order, line) {
return {
order_id: order,
product_id: line.product_id,
description: line.name,
price_unit: line.price_unit,
tax_ids: line.tax_ids.map((tax) => ["link", tax]),
price_manually_set: true,
customer_note: line.customer_note,
};
}
const lines = order.get_orderlines();
for (var i = 0; i < lines.length; i++) {
const line = lines[i];
const new_line = this.props.order.models["pos.order.line"].create(
this._prepareReorderLineVals(newOrder, line)
);
if (line.pack_lot_lines) {
new_line.setPackLotLines({
modifiedPackLotLines: [],
newPackLotLines: (line.lot_names || []).map((name) => ({
lot_name: name,
})),
});
}
new_line.set_unit_price(line.get_unit_price());
new_line.set_quantity(line.get_quantity());
new_line.set_discount(line.get_discount());
_onClick() {
this._reOrder();
}
this.pos.closeScreen();
}
_prepareReorderLineVals(order, line) {
return {
order_id: order,
product_id: line.product_id,
description: line.name,
price_unit: line.price_unit,
tax_ids: line.tax_ids.map((tax) => ["link", tax]),
price_manually_set: true,
customer_note: line.customer_note,
};
}
_onClick() {
this._reOrder();
}
}

View file

@ -3,5 +3,5 @@ import { patch } from "@web/core/utils/patch";
import { ReorderButton } from "@pos_order_reorder/js/Screens/TicketScreen/ControlButtons/ReorderButton.esm";
patch(TicketScreen, {
components: { ...TicketScreen.components, ReorderButton },
components: { ...TicketScreen.components, ReorderButton },
});

View file

@ -19,5 +19,4 @@
"pos_payment_method_cashdro_fix/static/src/js/models_fix.esm.js",
],
},
"installable": True,
}

View file

@ -17,6 +17,4 @@
"views/res_config_settings_views.xml",
"views/account_move_views.xml",
],
"installable": True,
"application": False,
}

View file

@ -1,6 +1,8 @@
# Copyright 2024 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo import api
from odoo import fields
from odoo import models
class AccountMove(models.Model):

View file

@ -1,6 +1,7 @@
# Copyright 2024 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo import fields
from odoo import models
class ResCompany(models.Model):

View file

@ -1,6 +1,7 @@
# Copyright 2024 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo import fields
from odoo import models
class ResConfigSettings(models.TransientModel):

View file

@ -1,6 +1,7 @@
# Copyright 2024 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo import fields
from odoo import models
class ResPartner(models.Model):

View file

@ -4,6 +4,7 @@
from odoo import fields
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingCommon

View file

@ -2,7 +2,10 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict
from odoo import _, api, fields, models
from odoo import _
from odoo import api
from odoo import fields
from odoo import models
from odoo.exceptions import UserError
@ -52,8 +55,7 @@ class AccountMoveRappelWizard(models.TransientModel):
# Filter only posted vendor bills and refunds
valid_moves = moves.filtered(
lambda m: m.move_type in ("in_invoice", "in_refund")
and m.state == "posted"
lambda m: m.move_type in ("in_invoice", "in_refund") and m.state == "posted"
)
if not valid_moves:
raise UserError(

View file

@ -13,5 +13,4 @@
"membership",
"website_membership",
],
"data": [],
}

View file

@ -1,7 +1,8 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, http
from odoo import fields
from odoo import http
from odoo.http import request
from odoo.tools.translate import _
@ -28,9 +29,7 @@ class WebsiteMembership(BaseWebsiteMembership):
self._add_associate_members(response, membership_id, country_id, page, post)
return response
def _add_associate_members(
self, response, membership_id, country_id, page, post
):
def _add_associate_members(self, response, membership_id, country_id, page, post):
values = response.qcontext
memberships_partner_ids = values.get("memberships_partner_ids", {})
if not memberships_partner_ids:
@ -67,25 +66,19 @@ class WebsiteMembership(BaseWebsiteMembership):
if associate.id not in new_memberships_partner_ids[key]:
new_memberships_partner_ids[key].append(associate.id)
values["memberships_partner_ids"] = {
key: ids
for key, ids in new_memberships_partner_ids.items()
if ids
key: ids for key, ids in new_memberships_partner_ids.items() if ids
}
displayed_ids = set(
pid
for ids in values["memberships_partner_ids"].values()
for pid in ids
)
displayed_ids = {
pid for ids in values["memberships_partner_ids"].values() for pid in ids
}
values["partners"] = {
p.id: p
for p in request.env["res.partner"]
.sudo()
.browse(list(displayed_ids))
for p in request.env["res.partner"].sudo().browse(list(displayed_ids))
}
all_host_ids, all_free_host_ids, all_associates = (
self._get_all_visible_ids(membership_id, country_id, post_name)
all_host_ids, all_free_host_ids, all_associates = self._get_all_visible_ids(
membership_id, country_id, post_name
)
self._recompute_countries(
values, all_host_ids, all_free_host_ids, all_associates, country_id
@ -134,9 +127,7 @@ class WebsiteMembership(BaseWebsiteMembership):
if membership_id == "free":
all_host_ids = set()
else:
all_host_ids = self._get_all_host_ids(
membership_id, country_id, post_name
)
all_host_ids = self._get_all_host_ids(membership_id, country_id, post_name)
all_free_host_ids = (
self._get_all_free_host_ids(country_id, post_name)
if free_selected
@ -214,8 +205,7 @@ class WebsiteMembership(BaseWebsiteMembership):
if country_id:
current_country = Country.browse(country_id).read(["id", "name"])[0]
if not any(
x["country_id"] and x["country_id"][0] == country_id
for x in countries
x["country_id"] and x["country_id"][0] == country_id for x in countries
):
countries.append(
{
@ -243,16 +233,12 @@ class WebsiteMembership(BaseWebsiteMembership):
return
partners = request.env["res.partner"].sudo().browse(map_ids)
if country_id:
partners = partners.filtered(
lambda p: p.country_id.id == country_id
)
partners = partners.filtered(lambda p: p.country_id.id == country_id)
if post_name:
partners = partners.filtered(
lambda p: self._partner_matches_search(p, post_name)
)
values["google_map_partner_ids"] = ",".join(
str(pid) for pid in partners.ids
)
values["google_map_partner_ids"] = ",".join(str(pid) for pid in partners.ids)
def _update_pager(
self,
@ -268,9 +254,7 @@ class WebsiteMembership(BaseWebsiteMembership):
if membership_id == "free":
displayed_host_ids = set()
else:
host_records = request.env["res.partner"].sudo().browse(
list(all_host_ids)
)
host_records = request.env["res.partner"].sudo().browse(list(all_host_ids))
displayed_host_ids = {
host.id
for host in host_records
@ -278,7 +262,7 @@ class WebsiteMembership(BaseWebsiteMembership):
}
count_members = len(displayed_host_ids) + len(all_free_host_ids)
limit = self._references_per_page
base_url = "/members%s%s" % (
base_url = "/members{}{}".format(
"/association/%s" % membership_id if membership_id else "",
"/country/%s" % country_id if country_id else "",
)

View file

@ -1,7 +1,8 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, models
from odoo import api
from odoo import models
from odoo.osv import expression
@ -41,10 +42,7 @@ class MembershipLine(models.Model):
("website_description", "ilike", post_name),
]
host_ids = (
self.env["res.partner"]
.sudo()
.search(associate_domain)
.associate_member.ids
self.env["res.partner"].sudo().search(associate_domain).associate_member.ids
)
if not host_ids:
return domain
@ -84,8 +82,7 @@ class MembershipLine(models.Model):
AND p.is_company
AND m.id IN %s
ORDER BY p.id
"""
+ limit_clause,
""" + limit_clause,
(tuple(self.ids),),
)
return [row[0] for row in self.env.cr.fetchall()]

View file

@ -2,7 +2,8 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields
from odoo.tests import HttpCase, tagged
from odoo.tests import HttpCase
from odoo.tests import tagged
from odoo.addons.membership.tests.common import TestMembershipCommon
@ -125,9 +126,7 @@ class TestWebsiteMembershipAssociatePagination(HttpCase, TestMembershipCommon):
for i in range(20)
]
cls.paginated_hosts = cls.env["res.partner"].create(host_vals)
invoices = cls.paginated_hosts.create_membership_invoice(
cls.membership_1, 75.0
)
invoices = cls.paginated_hosts.create_membership_invoice(cls.membership_1, 75.0)
invoices.action_post()
cls.env["account.payment.register"].with_context(
active_model="account.move", active_ids=invoices.ids

View file

@ -6,7 +6,7 @@
"version": "18.0.1.0.1",
"category": "Website/Sale",
"summary": "Force a clean /web/signup step before adding membership "
"products to cart on a b2b website (EMES-style flow).",
"products to cart on a b2b website (EMES-style flow).",
"author": "Criptomart",
"website": "https://git.criptomart.net/criptomart/addons-cm",
"license": "AGPL-3",
@ -25,7 +25,4 @@
"website_membership_signup_required/static/src/js/website_membership_signup_required.js",
],
},
"external_dependencies": {},
"installable": True,
"application": False,
}

View file

@ -1,11 +1,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, http
from odoo.addons.auth_signup.controllers.main import AuthSignupHome
from odoo.addons.web.controllers.home import SIGN_UP_REQUEST_PARAMS
from odoo import _
from odoo import http
from odoo.exceptions import UserError
from odoo.http import request
from odoo.addons.auth_signup.controllers.main import AuthSignupHome
from odoo.addons.web.controllers.home import SIGN_UP_REQUEST_PARAMS
# partner_firstname exposes `firstname` / `lastname` on res.users and
# res.partner; we need them as accepted qcontext params so the signup form
# roundtrips them transparently through `get_auth_signup_qcontext`.
@ -18,7 +20,10 @@ SIGN_UP_REQUEST_PARAMS.add("lastname")
SIGN_UP_REQUEST_PARAMS.add("accepted_terms")
# Session keys used by the membership signup flow.
MEMBERSHIP_SIGNUP_SESSION_KEYS = ("membership_signup_active", "membership_signup_product_id")
MEMBERSHIP_SIGNUP_SESSION_KEYS = (
"membership_signup_active",
"membership_signup_product_id",
)
class AuthSignupHomeMembership(AuthSignupHome):
@ -71,9 +76,7 @@ class AuthSignupHomeMembership(AuthSignupHome):
values["firstname"] = firstname
values["lastname"] = lastname
partner_model = request.env["res.partner"]
values["name"] = partner_model._get_computed_name(
lastname, firstname
)
values["name"] = partner_model._get_computed_name(lastname, firstname)
return values
@http.route()

View file

@ -3,9 +3,10 @@
from urllib.parse import urlencode
from odoo import http
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.http import request
from odoo.addons.website_sale.controllers.main import WebsiteSale
SIGNUP_FLAG = "membership_signup=1"
@ -36,24 +37,37 @@ class WebsiteSaleMembershipSignup(WebsiteSale):
@http.route()
def cart_update(
self, product_id, add_qty=1, set_qty=0,
self,
product_id,
add_qty=1,
set_qty=0,
product_custom_attribute_values=None,
no_variant_attribute_value_ids=None, **kwargs
no_variant_attribute_value_ids=None,
**kwargs,
):
if self._membership_signup_is_required(product_id):
self._membership_signup_mark_session(product_id)
return request.redirect(self._membership_signup_redirect_url(product_id))
return super().cart_update(
product_id=product_id, add_qty=add_qty, set_qty=set_qty,
product_id=product_id,
add_qty=add_qty,
set_qty=set_qty,
product_custom_attribute_values=product_custom_attribute_values,
no_variant_attribute_value_ids=no_variant_attribute_value_ids,
**kwargs
**kwargs,
)
@http.route()
def cart_update_json(
self, product_id, line_id=None, add_qty=None, set_qty=None, display=True,
product_custom_attribute_values=None, no_variant_attribute_value_ids=None, **kwargs
self,
product_id,
line_id=None,
add_qty=None,
set_qty=None,
display=True,
product_custom_attribute_values=None,
no_variant_attribute_value_ids=None,
**kwargs,
):
if self._membership_signup_is_required(product_id):
self._membership_signup_mark_session(product_id)
@ -90,11 +104,14 @@ class WebsiteSaleMembershipSignup(WebsiteSale):
"notification_info": {"lines": [], "warning": ""},
}
return super().cart_update_json(
product_id=product_id, line_id=line_id, add_qty=add_qty,
set_qty=set_qty, display=display,
product_id=product_id,
line_id=line_id,
add_qty=add_qty,
set_qty=set_qty,
display=display,
product_custom_attribute_values=product_custom_attribute_values,
no_variant_attribute_value_ids=no_variant_attribute_value_ids,
**kwargs
**kwargs,
)
def _get_mandatory_address_fields(self, country_sudo):

View file

@ -1,6 +1,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo import fields
from odoo import models
class ResConfigSettings(models.TransientModel):

View file

@ -1,6 +1,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
from odoo import api
from odoo import models
class ResPartner(models.Model):

View file

@ -1,6 +1,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
from odoo import api
from odoo import models
from odoo.http import request

View file

@ -1,6 +1,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo import fields
from odoo import models
class Website(models.Model):
@ -9,8 +10,8 @@ class Website(models.Model):
membership_signup_required = fields.Boolean(
string="Force signup for membership products",
help="When enabled, anonymous users trying to add a membership product "
"to cart are redirected to /web/signup before the product is "
"added. Only affects the e-commerce flow.",
"to cart are redirected to /web/signup before the product is "
"added. Only affects the e-commerce flow.",
default=True,
)

View file

@ -64,8 +64,9 @@ registry.category("web_tour.tours").add("website_membership_signup_required_tour
run: "click",
},
{
content: "Submit the signup form -> redirect straight to cart "
+ "with the product already added (post-login auto-add)",
content:
"Submit the signup form -> redirect straight to cart " +
"with the product already added (post-login auto-add)",
trigger: ".oe_signup_form button[type='submit']",
run: "click",
expectUnloadPage: true,

View file

@ -4,15 +4,16 @@ import werkzeug.datastructures
from odoo.exceptions import UserError
from odoo.fields import Date
from odoo.tests import TransactionCase
from odoo.tests import tagged
from odoo.addons.website.tools import MockRequest
from odoo.addons.website_membership_signup_required.controllers.main import (
AuthSignupHomeMembership as AuthSignupHome,
)
from odoo.addons.website_membership_signup_required.controllers.website_sale import (
WebsiteSaleMembershipSignup,
)
from odoo.addons.website.tools import MockRequest
from odoo.tests import TransactionCase, tagged
@tagged("post_install", "-at_install")
@ -24,22 +25,28 @@ class TestMembershipSignup(TransactionCase):
cls.website = cls.env.ref("website.default_website")
cls.website.auth_signup_uninvited = "b2b"
today = Date.to_date(Date.today())
cls.membership_product = cls.env["product.product"].create({
"name": "Membership Card",
"membership": True,
"list_price": 50.0,
"membership_date_from": today.replace(month=1, day=1),
"membership_date_to": today.replace(month=12, day=31),
})
cls.regular_product = cls.env["product.product"].create({
"name": "Regular Mug",
"membership": False,
"list_price": 10.0,
})
cls.no_user_partner = cls.env["res.partner"].create({
"firstname": "Backend",
"lastname": "Partner",
})
cls.membership_product = cls.env["product.product"].create(
{
"name": "Membership Card",
"membership": True,
"list_price": 50.0,
"membership_date_from": today.replace(month=1, day=1),
"membership_date_to": today.replace(month=12, day=31),
}
)
cls.regular_product = cls.env["product.product"].create(
{
"name": "Regular Mug",
"membership": False,
"list_price": 10.0,
}
)
cls.no_user_partner = cls.env["res.partner"].create(
{
"firstname": "Backend",
"lastname": "Partner",
}
)
# -- RF-07 / RF-default ------------------------------------------------
@ -198,13 +205,21 @@ class TestMembershipSignup(TransactionCase):
# -- RF-08 -------------------------------------------------------------
def test_08_backend_sale_order_with_membership_product_without_user(self):
order = self.env["sale.order"].create({
"partner_id": self.no_user_partner.id,
"order_line": [(0, 0, {
"product_id": self.membership_product.id,
"product_uom_qty": 1.0,
})],
})
order = self.env["sale.order"].create(
{
"partner_id": self.no_user_partner.id,
"order_line": [
(
0,
0,
{
"product_id": self.membership_product.id,
"product_uom_qty": 1.0,
},
)
],
}
)
self.assertTrue(order)
self.assertEqual(order.order_line.product_id, self.membership_product)
# Confirming the SO drives membership creation (core `membership`).
@ -276,12 +291,14 @@ class TestMembershipSignup(TransactionCase):
The invited partner must NOT have a user yet, otherwise the token
payload check (`user_ids`) won't match.
"""
partner = self.env["res.partner"].create({
"firstname": "Invited",
"lastname": "Researcher",
"email": "invited.researcher@example.com",
"signup_type": "signup",
})
partner = self.env["res.partner"].create(
{
"firstname": "Invited",
"lastname": "Researcher",
"email": "invited.researcher@example.com",
"signup_type": "signup",
}
)
self.assertFalse(partner.user_ids, "test precondition: no user yet")
token = partner._generate_signup_token()
info = self.env["res.partner"]._signup_retrieve_info(token)
@ -297,11 +314,13 @@ class TestMembershipSignup(TransactionCase):
still works — the form will show empty readonly inputs for the split
fields, which is the best we can do without re-splitting.
"""
partner = self.env["res.partner"].create({
"name": "Legacy Researcher",
"email": "legacy@example.com",
"signup_type": "signup",
})
partner = self.env["res.partner"].create(
{
"name": "Legacy Researcher",
"email": "legacy@example.com",
"signup_type": "signup",
}
)
# partner_firstname's post_create hook may or may not have split
# `name` automatically; we just check the override does not crash
# and returns the contract keys.

View file

@ -7,8 +7,8 @@ import time
from urllib.parse import unquote
from odoo.fields import Date
from odoo.tests import HttpCase, tagged
from odoo.tests import HttpCase
from odoo.tests import tagged
@tagged("post_install", "-at_install")
@ -21,28 +21,34 @@ class TestMembershipSignupTour(HttpCase):
cls.website.auth_signup_uninvited = "b2b"
cls.website.membership_signup_required = True
today = Date.to_date(Date.today())
cls.membership_product = cls.env["product.template"].create({
"name": "Membership Test Product",
"membership": True,
"list_price": 60.0,
"is_published": True,
"sale_ok": True,
"website_id": cls.website.id,
"membership_date_from": today.replace(month=1, day=1),
"membership_date_to": today.replace(month=12, day=31),
})
cls.regular_product = cls.env["product.template"].create({
"name": "Regular Test Product",
"membership": False,
"list_price": 5.0,
"is_published": True,
"sale_ok": True,
"website_id": cls.website.id,
})
cls.membership_product = cls.env["product.template"].create(
{
"name": "Membership Test Product",
"membership": True,
"list_price": 60.0,
"is_published": True,
"sale_ok": True,
"website_id": cls.website.id,
"membership_date_from": today.replace(month=1, day=1),
"membership_date_to": today.replace(month=12, day=31),
}
)
cls.regular_product = cls.env["product.template"].create(
{
"name": "Regular Test Product",
"membership": False,
"list_price": 5.0,
"is_published": True,
"sale_ok": True,
"website_id": cls.website.id,
}
)
# Enable the custom/transfer payment provider if it is installed so the
# cart page renders fully (no checkout/payment is exercised by the
# tours, but it avoids warnings during cart rendering).
provider = cls.env.ref("payment.payment_provider_transfer", raise_if_not_found=False)
provider = cls.env.ref(
"payment.payment_provider_transfer", raise_if_not_found=False
)
if provider and provider.state != "enabled":
provider.write({"state": "enabled", "is_published": True})
@ -54,20 +60,24 @@ class TestMembershipSignupTour(HttpCase):
)
# The tour submitted the signup form, so a new portal user must exist
# and the cart of that user must contain the membership product.
users = self.env["res.users"].search([
("login", "like", "membership.tour.%@test.example.com"),
])
users = self.env["res.users"].search(
[
("login", "like", "membership.tour.%@test.example.com"),
]
)
self.assertTrue(users, "Tour should have created a user via /web/signup")
first = users.sorted("create_date")[-1]
self.assertTrue(first.partner_id.firstname)
self.assertTrue(first.partner_id.lastname)
carts = self.env["sale.order"].search([
("partner_id", "=", first.partner_id.id),
("state", "=", "draft"),
])
carts = self.env["sale.order"].search(
[
("partner_id", "=", first.partner_id.id),
("state", "=", "draft"),
]
)
self.assertTrue(carts)
order_lines = carts.mapped("order_line").filtered(
lambda l: l.product_id.product_tmpl_id == self.membership_product
lambda line: line.product_id.product_tmpl_id == self.membership_product
)
self.assertTrue(order_lines, "Cart must contain the membership product")
@ -93,7 +103,7 @@ class TestMembershipSignupTour(HttpCase):
return None
secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
max_ts = int(time.time() + 31536000) # 1y, matches Odoo's default.
msg = f"{sid}{max_ts}".encode("utf-8")
msg = f"{sid}{max_ts}".encode()
hm = hmac.new(secret.encode("ascii"), msg, hashlib.sha1).hexdigest()
return f"{hm}o{max_ts}"
@ -106,8 +116,10 @@ class TestMembershipSignupTour(HttpCase):
product_url = "/shop/%s" % self.env["ir.http"]._slug(product)
page = self.url_open(product_url, allow_redirects=True)
self.assertEqual(
page.status_code, 200,
"Product page must render (got %s for %s)" % (page.status_code, product_url),
page.status_code,
200,
"Product page must render (got %s for %s)"
% (page.status_code, product_url),
)
match = self._CSRF_RE.search(page.text)
csrf_token = match.group(1) if match else self._compute_csrf_token()

View file

@ -18,5 +18,4 @@
"views/res_config_settings_views.xml",
"views/website_sale_disable_cart_templates.xml",
],
"installable": True,
}

View file

@ -24,5 +24,4 @@
"website_sale_lazy_loading/static/src/js/website_sale_lazy_loading.js",
],
},
"installable": True,
}

View file

@ -76,7 +76,7 @@ class WebsiteSaleLazyLoading(WebsiteSale):
query = urlencode(params, doseq=True)
if not query:
return LAZY_LOADING_ROUTE
return "%s?%s" % (LAZY_LOADING_ROUTE, query)
return f"{LAZY_LOADING_ROUTE}?{query}"
# ------------------------------------------------------------------
# AJAX endpoint

View file

@ -47,10 +47,10 @@ class TestLazyLoading(HttpCase):
def _get_lazy_page(self, page, **params):
"""Call the AJAX endpoint and return its decoded payload."""
query = "&".join("%s=%s" % (key, value) for key, value in params.items())
query = "&".join(f"{key}={value}" for key, value in params.items())
url = "/shop/lazy_products?page=%s" % page
if query:
url = "%s&%s" % (url, query)
url = f"{url}&{query}"
response = self.url_open(url)
self.assertEqual(response.status_code, 200)
return response.json()