Initial commit: Pos packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 95dfb9edb0
1301 changed files with 264148 additions and 0 deletions

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import barcode_rule
from . import loyalty_card
from . import loyalty_mail
from . import loyalty_program
from . import loyalty_reward
from . import loyalty_rule
from . import pos_config
from . import pos_order_line
from . import pos_order
from . import pos_session
from . import res_config_settings

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class BarcodeRule(models.Model):
_inherit = 'barcode.rule'
type = fields.Selection(selection_add=[('coupon', 'Coupon')], ondelete={'coupon': 'set default'})

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class LoyaltyCard(models.Model):
_inherit = 'loyalty.card'
source_pos_order_id = fields.Many2one('pos.order', "PoS Order Reference",
help="PoS order where this coupon was generated.")
def _has_source_order(self):
return super()._has_source_order() or bool(self.source_pos_order_id)
def _get_default_template(self):
self.ensure_one()
if self.source_pos_order_id:
return self.env.ref('pos_loyalty.mail_coupon_template', False)
return super()._get_default_template()
def _get_mail_partner(self):
return super()._get_mail_partner() or self.sudo().source_pos_order_id.partner_id
def _get_signature(self):
return self.source_pos_order_id.user_id.signature or super()._get_signature()
def _compute_use_count(self):
super()._compute_use_count()
read_group_res = self.env['pos.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}
for card in self:
card.use_count += count_per_coupon.get(card.id, 0)

View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class LoyaltyMail(models.Model):
_inherit = 'loyalty.mail'
pos_report_print_id = fields.Many2one('ir.actions.report', string="Print Report", domain=[('model', '=', 'loyalty.card')],
help="The report action to be executed when creating a coupon/gift card/loyalty card in the PoS.",
)

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.tools import unique
from odoo.exceptions import UserError
class LoyaltyProgram(models.Model):
_inherit = 'loyalty.program'
# NOTE: `pos_config_ids` satisfies an excpeptional use case: when no PoS is specified, the loyalty program is
# applied to every PoS. You can access the loyalty programs of a PoS using _get_program_ids() of pos.config
pos_config_ids = fields.Many2many('pos.config', compute="_compute_pos_config_ids", store=True, readonly=False, string="Point of Sales", help="Restrict publishing to those shops.")
pos_order_count = fields.Integer("PoS Order Count", compute='_compute_pos_order_count')
pos_ok = fields.Boolean("Point of Sale", default=True)
pos_report_print_id = fields.Many2one('ir.actions.report', string="Print Report", domain=[('model', '=', 'loyalty.card')], compute='_compute_pos_report_print_id', inverse='_inverse_pos_report_print_id', readonly=False,
help="This is used to print the generated gift cards from PoS.")
@api.depends("communication_plan_ids.pos_report_print_id")
def _compute_pos_report_print_id(self):
for program in self:
program.pos_report_print_id = program.communication_plan_ids.pos_report_print_id[:1]
def _inverse_pos_report_print_id(self):
for program in self:
if program.program_type not in ("gift_card", "ewallet"):
continue
if program.pos_report_print_id:
if not program.mail_template_id:
mail_template_label = program._fields.get('mail_template_id').get_description(self.env)['string']
pos_report_print_label = program._fields.get('pos_report_print_id').get_description(self.env)['string']
raise UserError(_("You must set '%s' before setting '%s'.", mail_template_label, pos_report_print_label))
else:
if not program.communication_plan_ids:
program.communication_plan_ids = self.env['loyalty.mail'].create({
'program_id': program.id,
'trigger': 'create',
'mail_template_id': program.mail_template_id.id,
'pos_report_print_id': program.pos_report_print_id.id,
})
else:
program.communication_plan_ids.write({
'trigger': 'create',
'pos_report_print_id': program.pos_report_print_id.id,
})
@api.depends('pos_ok')
def _compute_pos_config_ids(self):
for program in self:
if not program.pos_ok:
program.pos_config_ids = False
def _compute_pos_order_count(self):
query = """
WITH reward_to_orders_count AS (
SELECT reward.id AS lr_id,
COUNT(DISTINCT pos_order.id) AS orders_count
FROM pos_order_line line
JOIN pos_order ON line.order_id = pos_order.id
JOIN loyalty_reward reward ON line.reward_id = reward.id
GROUP BY lr_id
),
program_to_reward AS (
SELECT reward.id AS reward_id,
program.id AS program_id
FROM loyalty_program program
JOIN loyalty_reward reward ON reward.program_id = program.id
WHERE program.id = ANY (%s)
)
SELECT program_to_reward.program_id,
SUM(reward_to_orders_count.orders_count)
FROM program_to_reward
LEFT JOIN reward_to_orders_count ON reward_to_orders_count.lr_id = program_to_reward.reward_id
GROUP BY program_to_reward.program_id
"""
self._cr.execute(query, (self.ids,))
res = self._cr.dictfetchall()
res = {k['program_id']: k['sum'] for k in res}
for rec in self:
rec.pos_order_count = res.get(rec.id) or 0
def _compute_total_order_count(self):
super()._compute_total_order_count()
for program in self:
program.total_order_count += program.pos_order_count
def action_view_pos_orders(self):
self.ensure_one()
pos_order_ids = list(unique(r['order_id'] for r in\
self.env['pos.order.line'].search_read([('reward_id', 'in', self.reward_ids.ids)], fields=['order_id'])))
return {
'name': _("PoS Orders"),
'view_mode': 'tree,form',
'res_model': 'pos.order',
'type': 'ir.actions.act_window',
'domain': [('id', 'in', pos_order_ids)],
'context': dict(self._context, create=False),
}

View file

@ -0,0 +1,18 @@
# -*- 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'
def _get_discount_product_values(self):
res = super()._get_discount_product_values()
for vals in res:
vals.update({'taxes_id': False})
return res
def unlink(self):
if len(self) == 1 and self.env['pos.order.line'].sudo().search_count([('reward_id', 'in', self.ids)], limit=1):
return self.action_archive()
return super().unlink()

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.osv import expression
from odoo.tools import ustr
class LoyaltyRule(models.Model):
_inherit = 'loyalty.rule'
valid_product_ids = fields.Many2many(
'product.product', "Valid Products", compute='_compute_valid_product_ids',
help="These are the products that are valid for this rule.")
any_product = fields.Boolean(
compute='_compute_valid_product_ids', help="Technical field, whether all product match")
promo_barcode = fields.Char("Barcode", compute='_compute_promo_barcode', store=True, readonly=False,
help="A technical field used as an alternative to the promo code. "
"This is automatically generated when the promo code is changed."
)
@api.depends('product_ids', 'product_category_id', 'product_tag_id') #TODO later: product tags
def _compute_valid_product_ids(self):
domain_products = {}
for rule in self:
if rule.product_ids or\
rule.product_category_id or\
rule.product_tag_id or\
rule.product_domain not in ('[]', "[['sale_ok', '=', True]]"):
domain = rule._get_valid_product_domain()
domain = expression.AND([[('available_in_pos', '=', True)], domain])
product_ids = domain_products.get(ustr(domain))
if product_ids is None:
product_ids = self.env['product.product'].search(domain, order="id")
domain_products[ustr(domain)] = product_ids
rule.valid_product_ids = product_ids
rule.any_product = False
else:
rule.any_product = True
rule.valid_product_ids = self.env['product.product']
@api.depends('code')
def _compute_promo_barcode(self):
for rule in self:
rule.promo_barcode = self.env['loyalty.card']._generate_code()

View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, models
from odoo.exceptions import UserError
class PosConfig(models.Model):
_inherit = 'pos.config'
gift_card_settings = fields.Selection(
[
("create_set", "Generate PDF cards"),
("scan_use", "Scan existing cards"),
],
string="Gift Cards settings",
default="create_set",
help="Defines the way you want to set your gift cards.",
)
# NOTE: this funtions acts as a m2m field with loyalty.program model. We do this to handle an excpetional use case:
# When no PoS is specified at a loyalty program form, this program is applied to every PoS (instead of none)
def _get_program_ids(self):
return self.env['loyalty.program'].search(['&', ('pos_ok', '=', True), '|', ('pos_config_ids', '=', self.id), ('pos_config_ids', '=', False)])
def _check_before_creating_new_session(self):
self.ensure_one()
# Check validity of programs before opening a new session
invalid_reward_products_msg = ''
for reward in self._get_program_ids().reward_ids:
if reward.reward_type == 'product':
for product in reward.reward_product_ids:
if product.available_in_pos:
continue
invalid_reward_products_msg += "\n\t"
invalid_reward_products_msg += _(
"Program: %(name)s, Reward Product: `%(reward_product)s`",
name=reward.program_id.name,
reward_product=product.name,
)
gift_card_programs = self._get_program_ids().filtered(lambda p: p.program_type == 'gift_card')
for product in gift_card_programs.mapped('rule_ids.valid_product_ids'):
if product.available_in_pos:
continue
invalid_reward_products_msg += "\n\t"
invalid_reward_products_msg += _(
"Program: %(name)s, Rule Product: `%(rule_product)s`",
name=reward.program_id.name,
rule_product=product.name,
)
if invalid_reward_products_msg:
prefix_error_msg = _("To continue, make the following reward products available in Point of Sale.")
raise UserError(f"{prefix_error_msg}\n{invalid_reward_products_msg}")
if gift_card_programs:
for gc_program in gift_card_programs:
# Do not allow a gift card program with more than one rule or reward, and check that they make sense
if len(gc_program.reward_ids) > 1:
raise UserError(_('Invalid gift card program. More than one reward.'))
elif len(gc_program.rule_ids) > 1:
raise UserError(_('Invalid gift card program. More than one rule.'))
rule = gc_program.rule_ids
if rule.reward_point_amount != 1 or rule.reward_point_mode != 'money':
raise UserError(_('Invalid gift card program rule. Use 1 point per currency spent.'))
reward = gc_program.reward_ids
if reward.reward_type != 'discount' or reward.discount_mode != 'per_point' or reward.discount != 1:
raise UserError(_('Invalid gift card program reward. Use 1 currency per point discount.'))
if self.gift_card_settings == "create_set":
if not gc_program.mail_template_id:
raise UserError(_('There is no email template on the gift card program and your pos is set to print them.'))
if not gc_program.pos_report_print_id:
raise UserError(_('There is no print report on the gift card program and your pos is set to print them.'))
return super()._check_before_creating_new_session()
def use_coupon_code(self, code, creation_date, partner_id):
self.ensure_one()
# Points desc so that in coupon mode one could use a coupon multiple times
coupon = self.env['loyalty.card'].search(
[('program_id', 'in', self._get_program_ids().ids),
'|', ('partner_id', 'in', (False, partner_id)), ('program_type', '=', 'gift_card'),
('code', '=', code)],
order='points desc', limit=1)
if not coupon or not coupon.program_id.active:
return {
'successful': False,
'payload': {
'error_message': _('This coupon is invalid (%s).', code),
},
}
check_date = fields.Date.from_string(creation_date[:11])
if (coupon.expiration_date and coupon.expiration_date < check_date) or\
(coupon.program_id.date_to and coupon.program_id.date_to < fields.Date.context_today(self)) or\
(coupon.program_id.limit_usage and coupon.program_id.total_order_count >= coupon.program_id.max_usage):
return {
'successful': False,
'payload': {
'error_message': _('This coupon is expired (%s).', code),
},
}
if not coupon.program_id.reward_ids or not any(reward.required_points <= coupon.points for reward in coupon.program_id.reward_ids):
return {
'successful': False,
'payload': {
'error_message': _('No reward can be claimed with this coupon.'),
},
}
return {
'successful': True,
'payload': {
'program_id': coupon.program_id.id,
'coupon_id': coupon.id,
'coupon_partner_id': coupon.partner_id.id,
'points': coupon.points,
'has_source_order': coupon._has_source_order(),
},
}

View file

@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _, models
from odoo.tools import float_compare
import base64
class PosOrder(models.Model):
_inherit = 'pos.order'
def validate_coupon_programs(self, point_changes, new_codes):
"""
This is called upon validating the order in the pos.
This will check the balance for any pre-existing coupon to make sure that the rewards are in fact all claimable.
This will also check that any set code for coupons do not exist in the database.
"""
point_changes = {int(k): v for k, v in point_changes.items()}
coupon_ids_from_pos = set(point_changes.keys())
coupons = self.env['loyalty.card'].browse(coupon_ids_from_pos).exists().filtered('program_id.active')
coupon_difference = set(coupons.ids) ^ coupon_ids_from_pos
if coupon_difference:
return {
'successful': False,
'payload': {
'message': _('Some coupons are invalid. The applied coupons have been updated. Please check the order.'),
'removed_coupons': list(coupon_difference),
}
}
for coupon in coupons:
if float_compare(coupon.points, -point_changes[coupon.id], 2) == -1:
return {
'successful': False,
'payload': {
'message': _('There are not enough points for the coupon: %s.', coupon.code),
'updated_points': {c.id: c.points for c in coupons}
}
}
# Check existing coupons
coupons = self.env['loyalty.card'].search([('code', 'in', new_codes)])
if coupons:
return {
'successful': False,
'payload': {
'message': _('The following codes already exist in the database, perhaps they were already sold?\n%s',
', '.join(coupons.mapped('code'))),
}
}
return {
'successful': True,
'payload': {},
}
def confirm_coupon_programs(self, coupon_data):
"""
This is called after the order is created.
This will create all necessary coupons and link them to their line orders etc..
It will also return the points of all concerned coupons to be updated in the cache.
"""
# Keys are stringified when using rpc
coupon_data = {int(k): v for k, v in coupon_data.items()}
self._check_existing_loyalty_cards(coupon_data)
# Map negative id to newly created ids.
coupon_new_id_map = {k: k for k in coupon_data.keys() if k > 0}
# Create the coupons that were awarded by the order.
coupons_to_create = {k: v for k, v in coupon_data.items() if k < 0 and not v.get('giftCardId')}
coupon_create_vals = [{
'program_id': p['program_id'],
'partner_id': p.get('partner_id', False),
'code': p.get('barcode') or self.env['loyalty.card']._generate_code(),
'points': 0,
'expiration_date': p.get('date_to', False),
'source_pos_order_id': self.id,
} for p in coupons_to_create.values()]
# Pos users don't have the create permission
new_coupons = self.env['loyalty.card'].with_context(action_no_send_mail=True).sudo().create(coupon_create_vals)
# We update the gift card that we sold when the gift_card_settings = 'scan_use'.
gift_cards_to_update = [v for v in coupon_data.values() if v.get('giftCardId')]
updated_gift_cards = self.env['loyalty.card']
for coupon_vals in gift_cards_to_update:
gift_card = self.env['loyalty.card'].browse(coupon_vals.get('giftCardId'))
gift_card.write({
'points': coupon_vals['points'],
'source_pos_order_id': self.id,
'partner_id': coupon_vals.get('partner_id', False),
})
updated_gift_cards |= gift_card
# Map the newly created coupons
for old_id, new_id in zip(coupons_to_create.keys(), new_coupons):
coupon_new_id_map[new_id.id] = old_id
all_coupons = self.env['loyalty.card'].browse(coupon_new_id_map.keys()).exists()
lines_per_reward_code = defaultdict(lambda: self.env['pos.order.line'])
for line in self.lines:
if not line.reward_identifier_code:
continue
lines_per_reward_code[line.reward_identifier_code] |= line
for coupon in all_coupons:
if coupon.id in coupon_new_id_map:
# Coupon existed previously, update amount of points.
coupon.points += coupon_data[coupon_new_id_map[coupon.id]]['points']
for reward_code in coupon_data[coupon_new_id_map[coupon.id]].get('line_codes', []):
lines_per_reward_code[reward_code].coupon_id = coupon
# Send creation email
new_coupons.with_context(action_no_send_mail=False)._send_creation_communication()
# Reports per program
report_per_program = {}
coupon_per_report = defaultdict(list)
# Important to include the updated gift cards so that it can be printed. Check coupon_report.
for coupon in new_coupons | updated_gift_cards:
if coupon.program_id not in report_per_program:
report_per_program[coupon.program_id] = coupon.program_id.communication_plan_ids.\
filtered(lambda c: c.trigger == 'create').pos_report_print_id
for report in report_per_program[coupon.program_id]:
coupon_per_report[report.id].append(coupon.id)
return {
'coupon_updates': [{
'old_id': coupon_new_id_map[coupon.id],
'id': coupon.id,
'points': coupon.points,
'code': coupon.code,
'program_id': coupon.program_id.id,
'partner_id': coupon.partner_id.id,
} for coupon in all_coupons if coupon.program_id.is_nominative],
'program_updates': [{
'program_id': program.id,
'usages': program.total_order_count,
} for program in all_coupons.program_id],
'new_coupon_info': [{
'program_name': coupon.program_id.name,
'expiration_date': coupon.expiration_date,
'code': coupon.code,
} for coupon in new_coupons if (
coupon.program_id.applies_on == 'future'
# Don't send the coupon code for the gift card and ewallet programs.
# It should not be printed in the ticket.
and coupon.program_id.program_type not in ['gift_card', 'ewallet']
)],
'coupon_report': coupon_per_report,
}
def _check_existing_loyalty_cards(self, coupon_data):
coupon_key_to_modify = []
for coupon_id, coupon_vals in coupon_data.items():
partner_id = coupon_vals.get('partner_id', False)
if partner_id:
partner_coupons = self.env['loyalty.card'].search(
[('partner_id', '=', partner_id), ('program_type', '=', 'loyalty')])
existing_coupon_for_program = partner_coupons.filtered(lambda c: c.program_id.id == coupon_vals['program_id'])
if existing_coupon_for_program:
coupon_vals['coupon_id'] = existing_coupon_for_program[0].id
coupon_key_to_modify.append([coupon_id, existing_coupon_for_program[0].id])
for old_key, new_key in coupon_key_to_modify:
coupon_data[new_key] = coupon_data.pop(old_key)
def _get_fields_for_order_line(self):
fields = super(PosOrder, self)._get_fields_for_order_line()
fields.extend(['is_reward_line', 'reward_id', 'coupon_id', 'reward_identifier_code', 'points_cost'])
return fields
def _prepare_order_line(self, order_line):
order_line = super()._prepare_order_line(order_line)
for f in ['reward_id', 'coupon_id']:
if order_line.get(f):
order_line[f] = order_line[f][0]
return order_line
def _add_activated_coupon_to_draft_orders(self, table_orders):
table_orders = super()._add_activated_coupon_to_draft_orders(table_orders)
for order in table_orders:
activated_coupon = []
rewards_list = [{
'reward_id': orderline[2]['reward_id'],
'coupon_id': orderline[2]['coupon_id']
} for orderline in order['lines'] if orderline[2]['is_reward_line'] and orderline[2]['reward_id']
]
order_reward_ids = self.env['loyalty.reward'].browse(set([reward_id['reward_id'] for reward_id in rewards_list]))
for reward in rewards_list:
order_reward_id = order_reward_ids.filtered(lambda order_reward: order_reward.id == reward['reward_id'])
if order_reward_id:
if order_reward_id.program_type in ['gift_card', 'ewallet']:
coupon_id = self.env['loyalty.card'].search([('id', '=', reward['coupon_id'])])
activated_coupon.append({
'balance': coupon_id.points,
'code': coupon_id.code,
'id': coupon_id.id,
'program_id': coupon_id.program_id.id,
})
order['codeActivatedCoupons'] = activated_coupon
return table_orders
def _add_mail_attachment(self, name, ticket):
attachment = super()._add_mail_attachment(name, ticket)
gift_card_programs = self.config_id._get_program_ids().filtered(lambda p: p.program_type == 'gift_card' and
p.pos_report_print_id)
if gift_card_programs:
gift_cards = self.env['loyalty.card'].search([('source_pos_order_id', '=', self.id),
('program_id', 'in', gift_card_programs.mapped('id'))])
if gift_cards:
for program in gift_card_programs:
filtered_gift_cards = gift_cards.filtered(lambda gc: gc.program_id == program)
if filtered_gift_cards:
action_report = program.pos_report_print_id
report = action_report._render_qweb_pdf(action_report.report_name, filtered_gift_cards.mapped('id'))
filename = name + '.pdf'
gift_card_pdf = self.env['ir.attachment'].create({
'name': filename,
'type': 'binary',
'datas': base64.b64encode(report[0]),
'store_fname': filename,
'res_model': 'pos.order',
'res_id': self.ids[0],
'mimetype': 'application/x-pdf'
})
attachment += [(4, gift_card_pdf.id)]
return attachment

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class PosOrderLine(models.Model):
_inherit = 'pos.order.line'
is_reward_line = fields.Boolean(
help="Whether this line is part of a reward or not.")
reward_id = fields.Many2one(
'loyalty.reward', "Reward", ondelete='restrict',
help="The reward associated with this line.")
coupon_id = fields.Many2one(
'loyalty.card', "Coupon", ondelete='restrict',
help="The coupon used to claim that reward.")
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 many point this reward cost on the coupon.")
def _order_line_fields(self, line, session_id=None):
res = super()._order_line_fields(line, session_id)
# coupon_id may be negative in case of new coupons, they will be added after validating the order.
if 'coupon_id' in res[2] and res[2]['coupon_id'] < 1:
res[2].pop('coupon_id')
return res
def _is_not_sellable_line(self):
return super().is_not_sellable_line() or self.reward_id
def _export_for_ui(self, orderline):
result = super()._export_for_ui(orderline)
result['is_reward_line'] = orderline.is_reward_line
result['reward_id'] = orderline.reward_id.id
return result

View file

@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.osv.expression import AND
import ast
import json
class PosSession(models.Model):
_inherit = 'pos.session'
def _pos_ui_models_to_load(self):
result = super()._pos_ui_models_to_load()
if self.config_id._get_program_ids():
result += [
'loyalty.program',
'loyalty.rule',
'loyalty.reward',
]
return result
def _loader_params_loyalty_program(self):
return {
'search_params': {
'domain': [('id', 'in', self.config_id._get_program_ids().ids)],
'fields': ['name', 'trigger', 'applies_on', 'program_type', 'date_to', 'total_order_count',
'limit_usage', 'max_usage', 'is_nominative', 'portal_visible', 'portal_point_name', 'trigger_product_ids'],
},
}
def _loader_params_loyalty_rule(self):
return {
'search_params': {
'domain': [('program_id', 'in', self.config_id._get_program_ids().ids)],
'fields': ['program_id', 'valid_product_ids', 'any_product', 'currency_id',
'reward_point_amount', 'reward_point_split', 'reward_point_mode',
'minimum_qty', 'minimum_amount', 'minimum_amount_tax_mode', 'mode', 'code'],
}
}
def _loader_params_loyalty_reward(self):
domain_products = self.env['loyalty.reward']._get_active_products_domain()
return {
'search_params': {
'domain': AND([[('program_id', 'in', self.config_id._get_program_ids().ids)], domain_products]),
'fields': ['description', 'program_id', 'reward_type', 'required_points', 'clear_wallet', 'currency_id',
'discount', 'discount_mode', 'discount_applicability', 'all_discount_product_ids', 'is_global_discount',
'discount_max_amount', 'discount_line_product_id',
'multi_product', 'reward_product_ids', 'reward_product_qty', 'reward_product_uom_id', 'reward_product_domain'],
}
}
def _get_pos_ui_loyalty_program(self, params):
return self.env['loyalty.program'].search_read(**params['search_params'])
def _get_pos_ui_loyalty_rule(self, params):
return self.env['loyalty.rule'].search_read(**params['search_params'])
def _get_pos_ui_loyalty_reward(self, params):
rewards = self.env['loyalty.reward'].search_read(**params['search_params'])
for reward in rewards:
reward['reward_product_domain'] = self._replace_ilike_with_in(reward['reward_product_domain'])
return rewards
def _replace_ilike_with_in(self, domain_str):
if domain_str == "null":
return domain_str
domain = ast.literal_eval(domain_str)
for index, condition in enumerate(domain):
if isinstance(condition, (list, tuple)) and len(condition) == 3:
field_name, operator, value = condition
field = self.env['product.product']._fields.get(field_name)
if field and field.type == 'many2one' and operator in ('ilike', 'not ilike'):
comodel = self.env[field.comodel_name]
matching_ids = list(comodel._name_search(value, [], operator, limit=None))
new_operator = 'in' if operator == 'ilike' else 'not in'
domain[index] = [field_name, new_operator, matching_ids]
return json.dumps(domain)
def _get_pos_ui_product_product(self, params):
result = super()._get_pos_ui_product_product(params)
self = self.with_context(**params['context'])
rewards = self.config_id._get_program_ids().reward_ids
products = rewards.discount_line_product_id | rewards.reward_product_ids
products |= self.config_id._get_program_ids().filtered(lambda p: p.program_type == 'ewallet').trigger_product_ids
# Only load products that are not already in the result
products = list(set(products.ids) - set(product['id'] for product in result))
products = self.env['product.product'].search_read([('id', 'in', products)], fields=params['search_params']['fields'])
self._process_pos_ui_product_product(products)
result.extend(products)
return result
def _get_pos_ui_res_partner(self, params):
partners = super()._get_pos_ui_res_partner(params)
self._set_loyalty_cards(partners)
return partners
def get_pos_ui_res_partner_by_params(self, custom_search_params):
partners = super().get_pos_ui_res_partner_by_params(custom_search_params)
self._set_loyalty_cards(partners)
return partners
def _set_loyalty_cards(self, partners):
# Map partner_id to its loyalty cards from all loyalty programs.
loyalty_programs = self.config_id._get_program_ids().filtered(lambda p: p.program_type == 'loyalty')
loyalty_card_fields = ['points', 'code', 'program_id']
partner_id_to_loyalty_card = {}
for group in self.env['loyalty.card'].read_group(
domain=[('partner_id', 'in', [p['id'] for p in partners]), ('program_id', 'in', loyalty_programs.ids)],
fields=[f"{field_name}:array_agg" for field_name in loyalty_card_fields] + ["ids:array_agg(id)"],
groupby=['partner_id']
):
loyalty_cards = {}
for i in range(group['partner_id_count']):
loyalty_cards[group['ids'][i]] = {field_name: group[field_name][i] for field_name in loyalty_card_fields}
partner_id_to_loyalty_card[group['partner_id'][0]] = loyalty_cards
# Assign loyalty cards to each partner to load.
for partner in partners:
partner['loyalty_cards'] = partner_id_to_loyalty_card.get(partner['id'], {})
return partners
def _pos_data_process(self, loaded_data):
super()._pos_data_process(loaded_data)
# Additional post processing to link gift card and ewallet programs
# to their rules' products.
# Important because points from their products are only counted once.
product_id_to_program_ids = {}
for program in self.config_id._get_program_ids():
if program.program_type in ['gift_card', 'ewallet']:
for product in program.trigger_product_ids:
product_id_to_program_ids.setdefault(product['id'], [])
product_id_to_program_ids[product['id']].append(program['id'])
loaded_data['product_id_to_program_ids'] = product_id_to_program_ids
product_product_fields = self.env['product.product'].fields_get(self._loader_params_product_product()['search_params']['fields'])
loaded_data['field_types'] = {
'product.product': {f:v['type'] for f, v in product_product_fields.items()}
}
def _loader_params_product_product(self):
params = super()._loader_params_product_product()
# this is usefull to evaluate reward domain in frontend
params['search_params']['fields'].append('all_product_tag_ids')
return params

View file

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
pos_gift_card_settings = fields.Selection(related='pos_config_id.gift_card_settings', readonly=False)