19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import loyalty_card
from . import loyalty_history
from . import loyalty_program
from . import loyalty_reward
from . import sale_order
from . import sale_order_coupon_points
from . import sale_order_line
from . import sale_order

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
@ -7,8 +6,15 @@ from odoo import fields, models
class LoyaltyCard(models.Model):
_inherit = 'loyalty.card'
order_id = fields.Many2one('sale.order', 'Order Reference', readonly=True,
help="The sales order from which coupon is generated")
order_id = fields.Many2one(
string="Order Reference",
help="The sales order from which coupon is generated",
comodel_name='sale.order',
readonly=True,
)
order_id_partner_id = fields.Many2one(
string="Sale Order Customer", comodel_name='res.partner', related='order_id.partner_id'
)
def _get_default_template(self):
default_template = super()._get_default_template()
@ -16,11 +22,11 @@ class LoyaltyCard(models.Model):
default_template = self.env.ref('loyalty.mail_template_loyalty_card', raise_if_not_found=False)
return default_template
def _get_mail_partner(self):
return super()._get_mail_partner() or self.order_id.partner_id
def _mail_get_partner_fields(self, introspect_fields=False):
return super()._mail_get_partner_fields(introspect_fields=introspect_fields) + ['order_id_partner_id']
def _get_mail_author(self):
"""Default author is the order's salesperson if available, else the order's company."""
# Default author is the order's salesperson if available, else the order's company.
if not self.order_id or self.order_id.sudo().company_id not in self.env.companies:
return super()._get_mail_author()
self.ensure_one()
@ -32,10 +38,17 @@ class LoyaltyCard(models.Model):
def _compute_use_count(self):
super()._compute_use_count()
read_group_res = self.env['sale.order.line']._read_group(
[('coupon_id', 'in', self.ids)], ['id'], ['coupon_id'])
count_per_coupon = {r['coupon_id'][0]: r['coupon_id_count'] for r in read_group_res}
[('coupon_id', 'in', self.ids)], ['coupon_id'], ['__count'])
count_per_coupon = {coupon.id: count for coupon, count in read_group_res}
for card in self:
card.use_count += count_per_coupon.get(card.id, 0)
def _has_source_order(self):
return super()._has_source_order() or bool(self.order_id)
def action_archive(self):
self.env['sale.order.coupon.points'].search([
('coupon_id', 'in', self.ids),
('order_id.state', '=', 'draft'),
]).unlink()
return super().action_archive()

View file

@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class LoyaltyHistory(models.Model):
_inherit = 'loyalty.history'
def _get_order_portal_url(self):
if self.order_id and self.order_model == 'sale.order':
return self.env['sale.order'].browse(self.order_id).get_portal_url()
return super()._get_order_portal_url()

View file

@ -1,21 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class LoyaltyProgram(models.Model):
_inherit = 'loyalty.program'
order_count = fields.Integer(compute='_compute_order_count')
sale_ok = fields.Boolean("Sales", default=True)
sale_ok = fields.Boolean(string="Sales", default=True)
def _compute_order_count(self):
# An order should count only once PER program but may appear in multiple programs
read_group_res = self.env['sale.order.line'].sudo()._read_group(
[('reward_id', 'in', self.reward_ids.ids)], ['reward_id:array_agg'], ['order_id'])
read_group_res = self.env['sale.order.line']._read_group(
[('reward_id', 'in', self.reward_ids.ids)], ['order_id'], ['reward_id:array_agg'])
for program in self:
program_reward_ids = program.reward_ids.ids
program.order_count = sum(1 if any(id in group['reward_id'] for id in program_reward_ids) else 0 for group in read_group_res)
program.order_count = sum(
any(id_ in reward_ids for id_ in program_reward_ids)
for __, reward_ids in read_group_res
)
def _compute_total_order_count(self):
super()._compute_total_order_count()

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class LoyaltyReward(models.Model):
_inherit = 'loyalty.reward'

View file

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class SaleOrderCouponPoints(models.Model):
_name = 'sale.order.coupon.points'
_description = 'Sale Order Coupon Points - Keeps track of how a sale order impacts a coupon'
_description = "Sale Order Coupon Points - Keeps track of how a sale order impacts a coupon"
order_id = fields.Many2one('sale.order', required=True, ondelete='cascade')
coupon_id = fields.Many2one('loyalty.card', required=True, ondelete='cascade')
order_id = fields.Many2one(comodel_name='sale.order', ondelete='cascade', required=True, index=True)
coupon_id = fields.Many2one(comodel_name='loyalty.card', ondelete='cascade', required=True)
points = fields.Float(required=True)
_sql_constraints = [
('order_coupon_unique', 'UNIQUE (order_id, coupon_id)',
'The coupon points entry already exists.')
]
_order_coupon_unique = models.Constraint(
'UNIQUE (order_id, coupon_id)',
"The coupon points entry already exists.",
)

View file

@ -1,32 +1,40 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
is_reward_line = fields.Boolean('Is a program reward line', compute='_compute_is_reward_line')
reward_id = fields.Many2one('loyalty.reward', ondelete='restrict', readonly=True)
coupon_id = fields.Many2one('loyalty.card', ondelete='restrict', readonly=True)
reward_identifier_code = fields.Char(help="""
Technical field used to link multiple reward lines from the same reward together.
""")
points_cost = fields.Float(help='How much point this reward cost on the loyalty card.')
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
is_reward_line = fields.Boolean(
string="Is a program reward line", compute='_compute_is_reward_line'
)
reward_id = fields.Many2one(
comodel_name='loyalty.reward', ondelete='restrict', readonly=True
)
coupon_id = fields.Many2one(comodel_name='loyalty.card', ondelete='restrict', readonly=True)
reward_identifier_code = fields.Char(
help="Technical field used to link multiple reward lines from the same reward together."
)
points_cost = fields.Float(help="How much point this reward costs on the loyalty card.")
def _compute_name(self):
# Avoid computing the name for reward lines
reward = self.filtered('reward_id')
super(SaleOrderLine, self - reward)._compute_name()
def _compute_discount(self):
rewards = self.filtered('reward_id')
return super(SaleOrderLine, self - rewards)._compute_discount()
@api.depends('reward_id')
def _compute_is_reward_line(self):
for line in self:
line.is_reward_line = bool(line.reward_id)
def _compute_tax_id(self):
def _compute_tax_ids(self):
reward_lines = self.filtered('is_reward_line')
super(SaleOrderLine, self - reward_lines)._compute_tax_id()
super(SaleOrderLine, self - reward_lines)._compute_tax_ids()
# Discount reward line is split per tax, the discount is set on the line but not on the product
# as the product is the generic discount line.
# In case of a free product, retrieving the tax on the line instead of the product won't affect the behavior.
@ -34,8 +42,8 @@ class SaleOrderLine(models.Model):
line = line.with_company(line.company_id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id)
# If company_id is set, always filter taxes by the company
taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.tax_id = fpos.map_tax(taxes)
taxes = line.tax_ids.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.tax_ids = fpos.map_tax(taxes)
def _get_display_price(self):
# A product created from a promotion does not have a list_price.
@ -44,8 +52,11 @@ class SaleOrderLine(models.Model):
return self.price_unit
return super()._get_display_price()
def _is_not_sellable_line(self):
return self.is_reward_line or super()._is_not_sellable_line()
def _can_be_invoiced_alone(self):
return super()._can_be_invoiced_alone() and not self.is_reward_line
def _is_discount_line(self):
return super()._is_discount_line() or self.reward_id.reward_type == 'discount'
def _reset_loyalty(self, complete=False):
"""
@ -58,6 +69,7 @@ class SaleOrderLine(models.Model):
vals = {
'points_cost': 0,
'price_unit': 0,
'technical_price_unit': 0,
}
if complete:
vals.update({
@ -72,20 +84,24 @@ class SaleOrderLine(models.Model):
res = super().create(vals_list)
# Update our coupon points if the order is in a confirmed state
for line in res:
if line.coupon_id and line.points_cost and line.order_id.state in ('sale', 'done'):
if line.coupon_id and line.points_cost and line.state == 'sale':
line.coupon_id.points -= line.points_cost
line.order_id._update_loyalty_history(line.coupon_id, line.points_cost)
return res
def write(self, vals):
cost_in_vals = 'points_cost' in vals
if cost_in_vals:
previous_cost = {l: l.points_cost for l in self}
previous_vals = {line: (line.points_cost, line.coupon_id) for line in self}
res = super().write(vals)
if cost_in_vals:
# Update our coupon points if the order is in a confirmed state
for line in self:
if previous_cost[line] != line.points_cost and line.order_id.state in ('sale', 'done'):
line.coupon_id.points += (previous_cost[line] - line.points_cost)
for line, (previous_cost, previous_coupon) in previous_vals.items():
if line.state != 'sale':
continue
if line.points_cost != previous_cost or line.coupon_id != previous_coupon:
previous_coupon.points += previous_cost
line.coupon_id.points -= line.points_cost
return res
def unlink(self):
@ -109,7 +125,7 @@ class SaleOrderLine(models.Model):
line.order_id.code_enabled_rule_ids = line.order_id.code_enabled_rule_ids.filtered(lambda r: r.program_id != line.coupon_id.program_id)
# Give back the points if the order is confirmed, points are given back if the order is cancelled but in this case we need to do it directly
for line in related_lines:
if line.order_id.state in ('sale', 'done'):
if line.state == 'sale':
line.coupon_id.points += line.points_cost
res = super(SaleOrderLine, self | related_lines).unlink()
coupons_to_unlink.sudo().unlink()
@ -117,3 +133,8 @@ class SaleOrderLine(models.Model):
def _sellable_lines_domain(self):
return super()._sellable_lines_domain() + [('reward_id', '=', False)]
# === TOOLING ===#
def _can_be_edited_on_portal(self):
return super()._can_be_edited_on_portal() and not self.is_reward_line