19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:29:53 +01:00
parent 6e54c1af6c
commit 3ca647e428
1087 changed files with 132065 additions and 108499 deletions

View file

@ -3,6 +3,9 @@
from . import pos_config
from . import pos_order
from . import hr_employee
from . import hr_employee_public
from . import pos_session
from . import res_config_settings
from . import product_product
from . import pos_payment
from . import account_bank_statement
from . import single_employee_sales_report

View file

@ -0,0 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
employee_id = fields.Many2one('hr.employee', string="Employee", help="The employee who made the cash move.")

View file

@ -6,9 +6,52 @@ import hashlib
from odoo import api, models, _
from odoo.exceptions import UserError
class HrEmployee(models.Model):
_inherit = 'hr.employee'
class HrEmployee(models.Model):
_name = 'hr.employee'
_inherit = ['hr.employee', 'pos.load.mixin']
@api.model
def _load_pos_data_domain(self, data, config):
return config._employee_domain(config.current_user_id.id)
@api.model
def _load_pos_data_fields(self, config):
return ['name', 'user_id', 'work_contact_id']
def _server_date_to_domain(self, domain):
return domain
@api.model
def _load_pos_data_read(self, records, config):
# NOTE:
# hr.employee have a public fallback mechanism
# where users without read access may still receive records from
# the corresponding public model (hr.employee.public) so thats why we are bypassing
# the access right.
fields = self._load_pos_data_fields(config)
read_records = records.read(fields, load=False)
manager_ids = records.filtered(lambda emp: config.group_pos_manager_id.id in emp.user_id.all_group_ids.ids).ids
employees_barcode_pin = records.get_barcodes_and_pin_hashed()
bp_per_employee_id = {bp_e['id']: bp_e for bp_e in employees_barcode_pin}
for employee in read_records:
if employee['id'] in manager_ids:
role = 'manager'
employee['_user_role'] = 'admin'
elif employee['id'] in config.advanced_employee_ids.ids:
role = 'manager'
elif employee['id'] in config.minimal_employee_ids.ids:
role = 'minimal'
else:
role = 'cashier'
employee['_role'] = role
employee['_barcode'] = bp_per_employee_id[employee['id']]['barcode']
employee['_pin'] = bp_per_employee_id[employee['id']]['pin']
return read_records
def get_barcodes_and_pin_hashed(self):
if not self.env.user.has_group('point_of_sale.group_pos_user'):
@ -24,14 +67,14 @@ class HrEmployee(models.Model):
@api.ondelete(at_uninstall=False)
def _unlink_except_active_pos_session(self):
configs_with_employees = self.env['pos.config'].sudo().search([('module_pos_hr', '=', 'True')]).filtered(lambda c: c.current_session_id)
configs_with_all_employees = configs_with_employees.filtered(lambda c: not c.employee_ids)
configs_with_specific_employees = configs_with_employees.filtered(lambda c: c.employee_ids & self)
configs_with_employees = self.env['pos.config'].sudo().search([('module_pos_hr', '=', True)]).filtered(lambda c: c.current_session_id)
configs_with_all_employees = configs_with_employees.filtered(lambda c: not c.basic_employee_ids and not c.advanced_employee_ids and not c.minimal_employee_ids)
configs_with_specific_employees = configs_with_employees.filtered(lambda c: (c.basic_employee_ids or c.advanced_employee_ids or c.minimal_employee_ids) & self)
if configs_with_all_employees or configs_with_specific_employees:
error_msg = _("You cannot delete an employee that may be used in an active PoS session, close the session(s) first: \n")
for employee in self:
config_ids = configs_with_all_employees | configs_with_specific_employees.filtered(lambda c: employee in c.employee_ids)
config_ids = configs_with_all_employees | configs_with_specific_employees.filtered(lambda c: employee in c.basic_employee_ids)
if config_ids:
error_msg += _("Employee: %s - PoS Config(s): %s \n") % (employee.name, ', '.join(config.name for config in config_ids))
error_msg += _("Employee: %(employee)s - PoS Config(s): %(config_list)s \n", employee=employee.name, config_list=config_ids.mapped("name"))
raise UserError(error_msg)

View file

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class HrEmployeePublic(models.Model):
_inherit = "hr.employee.public"
def read(self, fields=None, load='_classic_read'):
# as `pos_blackbox_be` is a certified module, it's hard to make fixes in it
# so this is a workaround to remove `insz_or_bis_number` field from the fields list
# as the parent hr.employee model will attempt to read it from hr.employee.public
# where it doesn't exist
if fields and 'insz_or_bis_number' in fields:
pos_blackbox_be_installed = self.env['ir.module.module'].sudo().search_count([('name', '=', 'pos_blackbox_be'), ('state', '=', 'installed')])
has_hr_user_group = self.env.user.has_group('hr.group_hr_user')
if pos_blackbox_be_installed and not has_hr_user_group:
fields.remove('insz_or_bis_number')
return super().read(fields=fields, load=load)

View file

@ -1,13 +1,75 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from functools import partial
from odoo import models, fields
from odoo import models, fields, api
from odoo.fields import Domain
class PosConfig(models.Model):
_inherit = 'pos.config'
_name = 'pos.config'
_inherit = ['hr.mixin', 'pos.config']
employee_ids = fields.Many2many(
'hr.employee', string="Employees with access",
help='If left empty, all employees can log in to the PoS session')
minimal_employee_ids = fields.Many2many(
'hr.employee', 'pos_hr_minimal_employee_hr_employee', string="Employees with minimal access",
help='If left empty, all employees can log in to PoS')
basic_employee_ids = fields.Many2many(
'hr.employee', 'pos_hr_basic_employee_hr_employee', string="Employees with basic access",
help='If left empty, all employees can log in to PoS')
advanced_employee_ids = fields.Many2many(
'hr.employee', 'pos_hr_advanced_employee_hr_employee', string="Employees with manager access",
help='Employees linked to users with the PoS Manager role are automatically added to this list')
def write(self, vals):
if 'advanced_employee_ids' not in vals:
vals['advanced_employee_ids'] = []
vals['advanced_employee_ids'] += [(4, emp_id) for emp_id in self._get_group_pos_manager().user_ids.employee_id.ids]
# write employees in sudo, because we have no access to these corecords
sudo_vals = {
field_name: vals.pop(field_name)
for field_name in ('minimal_employee_ids', 'basic_employee_ids', 'advanced_employee_ids')
if not self.env.su
if isinstance(vals.get(field_name), list)
if all(isinstance(cmd, (list, tuple)) for cmd in vals[field_name])
}
res = super().write(vals)
if sudo_vals:
super(PosConfig, self.sudo()).write(sudo_vals)
return res
@api.onchange('minimal_employee_ids')
def _onchange_minimal_employee_ids(self):
for employee in self.minimal_employee_ids:
if employee.user_id._has_group('point_of_sale.group_pos_manager'):
self.minimal_employee_ids -= employee
elif employee in self.basic_employee_ids:
self.basic_employee_ids -= employee
elif employee in self.advanced_employee_ids:
self.advanced_employee_ids -= employee
@api.onchange('basic_employee_ids')
def _onchange_basic_employee_ids(self):
for employee in self.basic_employee_ids:
if employee.user_id._has_group('point_of_sale.group_pos_manager'):
self.basic_employee_ids -= employee
elif employee in self.advanced_employee_ids:
self.advanced_employee_ids -= employee
elif employee in self.minimal_employee_ids:
self.minimal_employee_ids -= employee
@api.onchange('advanced_employee_ids')
def _onchange_advanced_employee_ids(self):
for employee in self.advanced_employee_ids:
if employee in self.basic_employee_ids:
self.basic_employee_ids -= employee
if employee in self.minimal_employee_ids:
self.minimal_employee_ids -= employee
def _employee_domain(self, user_id):
domain = self._check_company_domain(self.company_id)
if len(self.basic_employee_ids) > 0:
domain = Domain.AND([
domain,
['|', ('user_id', '=', user_id), ('id', 'in', self.basic_employee_ids.ids + self.advanced_employee_ids.ids + self.minimal_employee_ids.ids)]
])
return domain

View file

@ -1,18 +1,13 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo import models, fields, api, _
from markupsafe import Markup
class PosOrder(models.Model):
_inherit = "pos.order"
employee_id = fields.Many2one('hr.employee', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]})
cashier = fields.Char(string="Cashier", compute="_compute_cashier", store=True)
@api.model
def _order_fields(self, ui_order):
order_fields = super(PosOrder, self)._order_fields(ui_order)
order_fields['employee_id'] = ui_order.get('employee_id')
return order_fields
employee_id = fields.Many2one('hr.employee', string="Cashier", help="The employee who uses the cash register.")
cashier = fields.Char(string="Cashier name", compute="_compute_cashier", store=True)
@api.depends('employee_id', 'user_id')
def _compute_cashier(self):
@ -22,9 +17,5 @@ class PosOrder(models.Model):
else:
order.cashier = order.user_id.name
def _export_for_ui(self, order):
result = super(PosOrder, self)._export_for_ui(order)
result.update({
'employee_id': order.employee_id.id,
})
return result
def _prepare_pos_log(self, body):
return super()._prepare_pos_log(body) + Markup("<br/>") + _("Cashier %s", self.cashier)

View file

@ -0,0 +1,15 @@
from odoo import models, fields, api
class PosPayment(models.Model):
_inherit = "pos.payment"
employee_id = fields.Many2one('hr.employee', string='Cashier', related='pos_order_id.employee_id', store=True, index=True)
@api.depends('employee_id', 'user_id')
def _compute_cashier(self):
for order in self:
if order.employee_id:
order.cashier = order.employee_id.name
else:
order.cashier = order.user_id.name

View file

@ -1,43 +1,102 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import fields, models, api, _
from odoo.tools import plaintext2html
class PosSession(models.Model):
_inherit = 'pos.session'
employee_id = fields.Many2one(
"hr.employee",
string="Cashier",
help="The employee who currently uses the cash register",
tracking=True,
)
def _pos_data_process(self, loaded_data):
super()._pos_data_process(loaded_data)
if self.config_id.module_pos_hr:
loaded_data['employee_by_id'] = {employee['id']: employee for employee in loaded_data['hr.employee']}
@api.model
def _load_pos_data_models(self, config):
data = super()._load_pos_data_models(config)
if config.module_pos_hr:
data += ['hr.employee']
return data
def _pos_ui_models_to_load(self):
result = super()._pos_ui_models_to_load()
if self.config_id.module_pos_hr:
new_model = 'hr.employee'
if new_model not in result:
result.append(new_model)
return result
def _set_opening_control_data(self, cashbox_value: int, notes: str):
super()._set_opening_control_data(cashbox_value, notes)
if author_id := self._get_message_author():
self.message_post(body=plaintext2html(_('Opened register')), author_id=author_id.id)
def _loader_params_hr_employee(self):
if len(self.config_id.employee_ids) > 0:
domain = ['&', ('company_id', '=', self.config_id.company_id.id), '|', ('user_id', '=', self.user_id.id), ('id', 'in', self.config_id.employee_ids.ids)]
def post_close_register_message(self):
if author_id := self._get_message_author():
self.message_post(body=plaintext2html(_('Closed Register')), author_id=author_id.id)
else:
domain = [('company_id', '=', self.config_id.company_id.id)]
return {'search_params': {'domain': domain, 'fields': ['name', 'id', 'user_id'], 'load': False}}
return super().post_close_register_message()
def _get_pos_ui_hr_employee(self, params):
employees = self.env['hr.employee'].search_read(**params['search_params'])
employee_ids = [employee['id'] for employee in employees]
user_ids = [employee['user_id'] for employee in employees if employee['user_id']]
manager_ids = self.env['res.users'].browse(user_ids).filtered(lambda user: self.config_id.group_pos_manager_id in user.groups_id).mapped('id')
def _get_message_author(self):
if not self.employee_id:
return None
if related_partners := self.employee_id._get_related_partners():
return related_partners[0]
return self.user_id.partner_id
employees_barcode_pin = self.env['hr.employee'].browse(employee_ids).get_barcodes_and_pin_hashed()
bp_per_employee_id = {bp_e['id']: bp_e for bp_e in employees_barcode_pin}
for employee in employees:
employee['role'] = 'manager' if employee['user_id'] and employee['user_id'] in manager_ids else 'cashier'
employee['barcode'] = bp_per_employee_id[employee['id']]['barcode']
employee['pin'] = bp_per_employee_id[employee['id']]['pin']
def _aggregate_payments_amounts_by_employee(self, all_payments):
payments_by_employee = []
return employees
for employee, payments_group in all_payments.grouped('employee_id').items():
payments_by_employee.append({
'id': employee.id if employee else 'others',
'name': employee.name if employee else _('Others'),
'amount': sum(payments_group.mapped('amount')),
})
# Sort such that "Others" is always the last item
return sorted(
payments_by_employee,
key=lambda p: (p['id'] == 'others', p['name'])
)
def _aggregate_moves_by_employee(self):
moves_per_employee = {}
for employee, moves in self.sudo().statement_line_ids.grouped('employee_id').items():
moves_per_employee[employee.id] = {
'id': employee.id,
'name': employee.name,
'amount': sum(moves.mapped('amount')),
}
return sorted(moves_per_employee.values(), key=lambda p: -p['amount'])
def get_closing_control_data(self):
data = super().get_closing_control_data()
orders = self._get_closed_orders()
payments = orders.payment_ids.filtered(lambda p: p.payment_method_id.type != "pay_later")
cash_payment_method_ids = self.payment_method_ids.filtered(lambda pm: pm.type == 'cash')
default_cash_payment_method_id = cash_payment_method_ids[0] if cash_payment_method_ids else None
default_cash_payments = payments.filtered(lambda p: p.payment_method_id == default_cash_payment_method_id) if default_cash_payment_method_id else self.env['pos.payment']
non_cash_payment_method_ids = self.payment_method_ids - default_cash_payment_method_id if default_cash_payment_method_id else self.payment_method_ids
non_cash_payments_grouped_by_method_id = {pm.id: orders.payment_ids.filtered(lambda p: p.payment_method_id == pm) for pm in non_cash_payment_method_ids}
data['default_cash_details']['amount_per_employee'] = self._aggregate_payments_amounts_by_employee(default_cash_payments)
for payment_method in data['non_cash_payment_methods']:
payment_method['amount_per_employee'] = self._aggregate_payments_amounts_by_employee(non_cash_payments_grouped_by_method_id[payment_method['id']])
data['default_cash_details']['moves_per_employee'] = self._aggregate_moves_by_employee()
return data
def _prepare_account_bank_statement_line_vals(self, session, sign, amount, reason, partner_id, extras):
vals = super()._prepare_account_bank_statement_line_vals(session, sign, amount, reason, partner_id, extras)
if extras.get('employee_id'):
vals['employee_id'] = extras['employee_id']
return vals
def get_cash_in_out_list(self):
cash_in_out_list = super().get_cash_in_out_list()
if self.config_id.module_pos_hr:
for cash_in_out in cash_in_out_list:
cash_move = self.env['account.bank.statement.line'].browse(cash_in_out['id'])
if cash_move.employee_id:
cash_in_out['cashier_name'] = cash_move.partner_id.name
return cash_in_out_list

View file

@ -0,0 +1,11 @@
from odoo import api, models
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.model
def _load_pos_data_fields(self, config):
result = super()._load_pos_data_fields(config)
result.append('all_product_tag_ids')
return result

View file

@ -1,10 +1,51 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
from odoo import fields, models, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
# pos.config fields
pos_employee_ids = fields.Many2many(related='pos_config_id.employee_ids', readonly=False)
pos_basic_employee_ids = fields.Many2many(related='pos_config_id.basic_employee_ids', readonly=False,
help='If left empty, all employees can log in to PoS')
pos_advanced_employee_ids = fields.Many2many(related='pos_config_id.advanced_employee_ids', readonly=False,
help='Employees linked to users with the PoS Manager role are automatically added to this list')
pos_minimal_employee_ids = fields.Many2many(related='pos_config_id.minimal_employee_ids', readonly=False,
help='If left empty, all employees can log in to PoS')
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
pos_config_id = vals.get('pos_config_id')
if pos_config_id:
vals['pos_advanced_employee_ids'] = vals.get('pos_advanced_employee_ids', []) + [[4, emp_id] for emp_id in self.env['pos.config'].browse(pos_config_id)._get_group_pos_manager().user_ids.employee_id.ids]
return super().create(vals_list)
@api.onchange('pos_minimal_employee_ids')
def _onchange_minimal_employee_ids(self):
for employee in self.pos_minimal_employee_ids:
if employee.user_id._has_group('point_of_sale.group_pos_manager'):
self.pos_minimal_employee_ids -= employee
elif employee in self.pos_basic_employee_ids:
self.pos_basic_employee_ids -= employee
elif employee in self.pos_advanced_employee_ids:
self.pos_advanced_employee_ids -= employee
@api.onchange('pos_basic_employee_ids')
def _onchange_basic_employee_ids(self):
for employee in self.pos_basic_employee_ids:
if employee.user_id._has_group('point_of_sale.group_pos_manager'):
self.pos_basic_employee_ids -= employee
elif employee in self.pos_advanced_employee_ids:
self.pos_advanced_employee_ids -= employee
elif employee in self.pos_minimal_employee_ids:
self.pos_minimal_employee_ids -= employee
@api.onchange('pos_advanced_employee_ids')
def _onchange_advanced_employee_ids(self):
for employee in self.pos_advanced_employee_ids:
if employee in self.pos_basic_employee_ids:
self.pos_basic_employee_ids -= employee
if employee in self.pos_minimal_employee_ids:
self.pos_minimal_employee_ids -= employee

View file

@ -0,0 +1,32 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.fields import Domain
class ReportPos_HrSingle_Employee_Sales_Report(models.AbstractModel):
_name = 'report.pos_hr.single_employee_sales_report'
_inherit = ['report.point_of_sale.report_saledetails']
_description = 'Session sales details for a single employee'
def _get_domain(self, date_start=False, date_stop=False, config_ids=False, session_ids=False, employee_id=False):
domain = super()._get_domain(config_ids=config_ids, session_ids=session_ids)
if (employee_id):
domain = Domain.AND([domain, [('employee_id', '=', employee_id)]])
return domain
def _prepare_get_sale_details_args_kwargs(self, data):
args, kwargs = super()._prepare_get_sale_details_args_kwargs(data)
kwargs['employee_id'] = data.get('employee_id')
return args, kwargs
@api.model
def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, session_ids=False, employee_id=False):
data = super().get_sale_details(config_ids=config_ids, session_ids=session_ids, employee_id=employee_id)
if (employee_id):
employee = self.env['hr.employee'].search([('id', '=', employee_id)])
data['employee_name'] = employee.name
return data