diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index 33af46e6b72..2d39e2d11be 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -31,6 +31,7 @@ 'views/pos_payment_method_views.xml', 'views/pos_payment_views.xml', 'views/pos_config_view.xml', + 'views/pos_bill_view.xml', 'views/pos_session_view.xml', 'views/point_of_sale_sequence.xml', 'data/point_of_sale_data.xml', @@ -60,6 +61,10 @@ 'point_of_sale/static/src/css/keyboard.css', 'point_of_sale/static/src/css/pos_receipts.css', 'point_of_sale/static/src/css/popups/product_info_popup.css', + 'point_of_sale/static/src/css/popups/common.css', + 'point_of_sale/static/src/css/popups/cash_opening_popup.css', + 'point_of_sale/static/src/css/popups/closing_pos_popup.css', + 'point_of_sale/static/src/css/popups/money_details_popup.css', 'web/static/src/legacy/scss/fontawesome_overridden.scss', 'point_of_sale/static/lib/html2canvas.js', 'point_of_sale/static/lib/backbone/backbone.js', @@ -124,6 +129,7 @@ 'point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js', 'point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js', 'point_of_sale/static/src/js/ChromeWidgets/TicketButton.js', + 'point_of_sale/static/src/js/Misc/CurrencyAmount.js', 'point_of_sale/static/src/js/Misc/Draggable.js', 'point_of_sale/static/src/js/Misc/NotificationSound.js', 'point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js', @@ -148,6 +154,8 @@ 'point_of_sale/static/src/js/Popups/CashOpeningPopup.js', 'point_of_sale/static/src/js/Popups/ProductInfoPopup.js', 'point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js', + 'point_of_sale/static/src/js/Popups/ClosePosPopup.js', + 'point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js', 'point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js', 'point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js', 'point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js', diff --git a/addons/point_of_sale/data/point_of_sale_data.xml b/addons/point_of_sale/data/point_of_sale_data.xml index ea61776fb73..7e3b34d16b3 100644 --- a/addons/point_of_sale/data/point_of_sale_data.xml +++ b/addons/point_of_sale/data/point_of_sale_data.xml @@ -46,6 +46,97 @@ True + + + 0.01 + 0.01 + + + + + 0.02 + 0.02 + + + + + 0.05 + 0.05 + + + + + 0.10 + 0.10 + + + + + 0.20 + 0.20 + + + + + 0.50 + 0.50 + + + + + 1.00 + 1.00 + + + + + 2.00 + 2.00 + + + + + 5.00 + 5.00 + + + + + 10.00 + 10.00 + + + + + 20.00 + 20.00 + + + + + 50.00 + 50.00 + + + + + 100.00 + 100.00 + + + + + 200.00 + 200.00 + + + + + 500.00 + 500.00 + + + diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index 83aaa1cdc15..2123edf1ca5 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -21,3 +21,4 @@ from . import stock_picking from . import stock_warehouse from . import pos_payment from . import pos_payment_method +from . import pos_bill diff --git a/addons/point_of_sale/models/pos_bill.py b/addons/point_of_sale/models/pos_bill.py new file mode 100644 index 00000000000..0374a1c6dcd --- /dev/null +++ b/addons/point_of_sale/models/pos_bill.py @@ -0,0 +1,16 @@ +from odoo import api, fields, models + + +class Bill(models.Model): + _name = "pos.bill" + _order = "value" + _description = "Coins/Bills" + + name = fields.Char("Name") + value = fields.Float("Coin/Bill Value", required=True, digits=0) + pos_config_ids = fields.Many2many("pos.config") + + @api.model + def name_create(self, name): + result = super().create({"name": name, "value": float(name)}) + return result.name_get()[0] diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 464063ed22b..e26e3e43789 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -9,69 +9,6 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError, UserError -class AccountBankStmtCashWizard(models.Model): - _inherit = 'account.bank.statement.cashbox' - - @api.depends('pos_config_ids') - @api.depends_context('current_currency_id') - def _compute_currency(self): - super(AccountBankStmtCashWizard, self)._compute_currency() - for cashbox in self: - if cashbox.pos_config_ids: - cashbox.currency_id = cashbox.pos_config_ids[0].currency_id.id - elif self.env.context.get('current_currency_id'): - cashbox.currency_id = self.env.context.get('current_currency_id') - - pos_config_ids = fields.One2many('pos.config', 'default_cashbox_id') - is_a_template = fields.Boolean(default=False) - - @api.model - def default_get(self, fields): - vals = super(AccountBankStmtCashWizard, self).default_get(fields) - if 'cashbox_lines_ids' not in fields: - return vals - config_id = self.env.context.get('default_pos_id') - if config_id: - config = self.env['pos.config'].browse(config_id) - if config.last_session_closing_cashbox.cashbox_lines_ids: - lines = config.last_session_closing_cashbox.cashbox_lines_ids - else: - lines = config.default_cashbox_id.cashbox_lines_ids - if self.env.context.get('balance', False) == 'start': - vals['cashbox_lines_ids'] = [[0, 0, {'coin_value': line.coin_value, 'number': line.number, 'subtotal': line.subtotal}] for line in lines] - else: - vals['cashbox_lines_ids'] = [[0, 0, {'coin_value': line.coin_value, 'number': 0, 'subtotal': 0.0}] for line in lines] - return vals - - def _validate_cashbox(self): - super(AccountBankStmtCashWizard, self)._validate_cashbox() - session_id = self.env.context.get('pos_session_id') - if session_id: - current_session = self.env['pos.session'].browse(session_id) - if current_session.state == 'new_session': - current_session.write({'state': 'opening_control'}) - - def set_default_cashbox(self): - self.ensure_one() - current_session = self.env['pos.session'].browse(self.env.context['pos_session_id']) - lines = current_session.config_id.default_cashbox_id.cashbox_lines_ids - context = dict(self._context) - self.cashbox_lines_ids.unlink() - self.cashbox_lines_ids = [[0, 0, {'coin_value': line.coin_value, 'number': line.number, 'subtotal': line.subtotal}] for line in lines] - - return { - 'name': _('Cash Control'), - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'account.bank.statement.cashbox', - 'view_id': self.env.ref('point_of_sale.view_account_bnk_stmt_cashbox_footer').id, - 'type': 'ir.actions.act_window', - 'context': context, - 'target': 'new', - 'res_id': self.id, - } - - class PosConfig(models.Model): _name = 'pos.config' _description = 'Point of Sale Configuration' @@ -145,7 +82,8 @@ class PosConfig(models.Model): help="The product categories will be displayed with pictures.") restrict_price_control = fields.Boolean(string='Restrict Price Modifications to Managers', help="Only users with Manager access rights for PoS app can modify the product prices on orders.") - cash_control = fields.Boolean(string='Advanced Cash Control', help="Check the amount of the cashbox at opening and closing.") + cash_control = fields.Boolean(string='Advanced Cash Control', compute='_compute_cash_control', help="Check the amount of the cashbox at opening and closing.") + set_maximum_difference = fields.Boolean('Set Maximum Difference', help="Set a maximum difference allowed between the expected and counted cash during the closing of the session.") receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt.") receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt.") proxy_ip = fields.Char(string='IP Address', size=45, @@ -165,7 +103,6 @@ class PosConfig(models.Model): number_of_opened_session = fields.Integer(string="Number of Opened Session", compute='_compute_current_session') last_session_closing_cash = fields.Float(compute='_compute_last_session') last_session_closing_date = fields.Date(compute='_compute_last_session') - last_session_closing_cashbox = fields.Many2one('account.bank.statement.cashbox', compute='_compute_last_session') pos_session_username = fields.Char(compute='_compute_current_session_user') pos_session_state = fields.Char(compute='_compute_current_session_user') pos_session_duration = fields.Char(compute='_compute_current_session_user') @@ -192,7 +129,7 @@ class PosConfig(models.Model): help="This product is used as reference on customer receipts.") fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.') default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position') - default_cashbox_id = fields.Many2one('account.bank.statement.cashbox', string='Default Balance') + default_bill_ids = fields.Many2many('pos.bill', string="Coins/Bills") use_pricelist = fields.Boolean("Use a pricelist.") tax_regime = fields.Boolean("Tax Regime") tax_regime_selection = fields.Boolean("Tax Regime Selection value") @@ -242,6 +179,11 @@ class PosConfig(models.Model): limited_partners_amount = fields.Integer(default=100) partner_load_background = fields.Boolean() + @api.depends('payment_method_ids') + def _compute_cash_control(self): + for config in self: + config.cash_control = bool(config.payment_method_ids.filtered('is_cash_count')) + @api.depends('use_pricelist', 'available_pricelist_ids') def _compute_allowed_pricelist_ids(self): for config in self: @@ -297,14 +239,11 @@ class PosConfig(models.Model): pos_config.last_session_closing_date = session[0]['stop_at'].astimezone(timezone).date() if session[0]['cash_register_id']: pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real'] - pos_config.last_session_closing_cashbox = self.env['account.bank.statement'].browse(session[0]['cash_register_id'][0]).cashbox_end_id else: pos_config.last_session_closing_cash = 0 - pos_config.last_session_closing_cashbox = False else: pos_config.last_session_closing_cash = 0 pos_config.last_session_closing_date = False - pos_config.last_session_closing_cashbox = False @api.depends('session_ids') def _compute_current_session_user(self): @@ -336,12 +275,6 @@ class PosConfig(models.Model): for config in self: config.iface_customer_facing_display = config.iface_customer_facing_display_via_proxy or config.iface_customer_facing_display_local - @api.constrains('cash_control') - def _check_session_state(self): - open_session = self.env['pos.session'].search([('config_id', 'in', self.ids), ('state', '!=', 'closed')], limit=1) - if open_session: - raise ValidationError(_("You are not allowed to change the cash control status while a session is already opened.")) - @api.constrains('rounding_method') def _check_rounding_method_strategy(self): for config in self: @@ -536,7 +469,7 @@ class PosConfig(models.Model): return result def _get_forbidden_change_fields(self): - forbidden_keys = ['module_pos_hr', 'cash_control', 'module_pos_restaurant', 'available_pricelist_ids', + forbidden_keys = ['module_pos_hr', 'module_pos_restaurant', 'available_pricelist_ids', 'limit_categories', 'iface_available_categ_ids', 'use_pricelist', 'module_pos_discount', 'payment_method_ids', 'iface_tipproduc'] return forbidden_keys @@ -609,7 +542,7 @@ class PosConfig(models.Model): """ self.ensure_one() # check all constraints, raises if any is not met - self._validate_fields(set(self._fields) - {"cash_control"}) + self._validate_fields(self._fields) return { 'type': 'ir.actions.act_url', 'url': self._get_pos_base_url() + '?config_id=%d' % self.id, diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index dfc30556bc3..faaf4bf3b25 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -49,6 +49,7 @@ class PosSession(models.Model): sequence_number = fields.Integer(string='Order Sequence Number', help='A sequence number that is incremented with each order', default=1) login_number = fields.Integer(string='Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session', default=0) + opening_notes = fields.Text(string="Opening Notes") cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control', compute_sudo=True) cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_all', string='Cash Journal', store=True) cash_register_id = fields.Many2one('account.bank.statement', compute='_compute_cash_all', string='Cash Register', store=True) @@ -253,7 +254,7 @@ class PosSession(models.Model): def action_pos_session_open(self): # second browse because we need to refetch the data from the DB for cash_register_id # we only open sessions that haven't already been opened - for session in self.filtered(lambda session: session.state in ('new_session', 'opening_control')): + for session in self.filtered(lambda session: session.state == 'opening_control'): values = {} if not session.start_at: values['start_at'] = fields.Datetime.now() @@ -277,6 +278,18 @@ class PosSession(models.Model): session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()}) if not session.config_id.cash_control: return session.action_pos_session_close(balancing_account, amount_to_balance) + # If the session is in rescue, we only compute the payments in the cash register + # It is not yet possible to close a rescue session through the front end, see `close_session_from_ui` + if session.rescue and session.config_id.cash_control: + default_cash_payment_method_id = self.payment_method_ids.filtered(lambda pm: pm.type == 'cash')[0] + orders = self.order_ids.filtered(lambda o: o.state == 'paid' or o.state == 'invoiced') + total_cash = sum( + orders.payment_ids.filtered(lambda p: p.payment_method_id == default_cash_payment_method_id).mapped('amount') + ) + self.cash_register_balance_start + + session.cash_register_id.balance_end_real = total_cash + + return session.action_pos_session_validate(balancing_account, amount_to_balance) def _check_pos_session_balance(self): for session in self: @@ -284,29 +297,16 @@ class PosSession(models.Model): if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real): statement.write({'balance_end_real': statement.balance_end}) - def action_pos_session_validate(self): + def action_pos_session_validate(self, balancing_account=False, amount_to_balance=0): self._check_pos_session_balance() - return self.action_pos_session_close() + return self.action_pos_session_close(balancing_account, amount_to_balance) def action_pos_session_close(self, balancing_account=False, amount_to_balance=0): # Session without cash payment method will not have a cash register. # However, there could be other payment methods, thus, session still # needs to be validated. self._check_bank_statement_state() - if not self.cash_register_id: - return self._validate_session(balancing_account, amount_to_balance) - - if self.cash_control and abs(self.cash_register_difference) > self.config_id.amount_authorized_diff: - # Only pos manager can close statements with cash_register_difference greater than amount_authorized_diff. - if not self.user_has_groups("point_of_sale.group_pos_manager"): - raise UserError(_( - "Your ending balance is too different from the theoretical cash closing (%.2f), " - "the maximum allowed is: %.2f. You can contact your manager to force it." - ) % (self.cash_register_difference, self.config_id.amount_authorized_diff)) - else: - return self._warning_balance_closing() - else: - return self._validate_session(balancing_account, amount_to_balance) + return self._validate_session(balancing_account, amount_to_balance) def _validate_session(self, balancing_account=False, amount_to_balance=0): self.ensure_one() @@ -378,6 +378,142 @@ class PosSession(models.Model): 'context': {**self.env.context, 'active_ids': self.ids, 'active_model': 'pos.session'}, } + def close_session_from_ui(self): + """Calling this method will try to close the session. + + If successful, it returns {'successful': True} + Otherwise, it returns {'successful': False, 'message': str, 'redirect': bool}. + 'redirect' is a boolean used to know whether we redirect the user to the back end or not. + When necessary, error (i.e. UserError, AccessError) is raised which should redirect the user to the back end. + """ + self.ensure_one() + # Even if this is called in `post_closing_cash_details`, we need to call this here too for case + # where cash_control = False + check_closing_session = self._cannot_close_session() + if check_closing_session: + return check_closing_session + + # For now we won't simply do + # self._check_pos_session_balance() + # self._check_bank_statement_state() + # validate_result = self._validate_session() + # because some functions are being used and overridden in other modules... + # so we'll try to use the original flow as of now for the moment + validate_result = self.action_pos_session_closing_control() + + # If an error is raised, the user will still be redirected to the back end to manually close the session. + # If the return result is a dict, this means that normally we have a redirection or a wizard => we redirect the user + if isinstance(validate_result, dict): + # imbalance accounting entry + return { + 'successful': False, + 'message': validate_result.get('name'), + 'redirect': True + } + + self.message_post(body='Point of Sale Session ended') + + return {'successful': True} + + def update_closing_control_state_session(self, notes): + # Prevent the session to be opened again. + self.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()}) + self._post_cash_details_message('Closing', self.cash_register_difference, notes) + + def post_closing_cash_details(self, counted_cash): + """ + Calling this method will try store the cash details during the session closing. + + :param counted_cash: float, the total cash the user counted from its cash register + If successful, it returns {'successful': True} + Otherwise, it returns {'successful': False, 'message': str, 'redirect': bool}. + 'redirect' is a boolean used to know whether we redirect the user to the back end or not. + When necessary, error (i.e. UserError, AccessError) is raised which should redirect the user to the back end. + """ + self.ensure_one() + check_closing_session = self._cannot_close_session() + if check_closing_session: + return check_closing_session + + if not self.cash_register_id: + # The user is blocked anyway, this user error is mostly for developers that try to call this function + raise UserError(_("There is no cash register in this session.")) + + self.cash_register_id.balance_end_real = counted_cash + + # No need to check cash_control because it should be True at this point + if self.config_id.set_maximum_difference and abs(self.cash_register_difference) > self.config_id.amount_authorized_diff: + if not self.user_has_groups("point_of_sale.group_pos_manager"): + # We are not raising this as an error because we want the details to persist. + # It will become the starting point on next attempt to close the session. + return { + 'successful': False, + 'message': _( + "Your ending balance is too different from the theoretical cash closing (%.2f), " + "the maximum allowed is: %.2f.\n You can contact your manager to force it." + ) % (self.cash_register_difference, self.config_id.amount_authorized_diff), + 'redirect': False + } + + return {'successful': True} + + def _cannot_close_session(self): + """ + Add check in this method if you want to return or raise an error when trying to either post cash details + or close the session. Raising an error will always redirect the user to the back end. + It should return {'successful': False, 'message': str, 'redirect': bool} if we can't close the session + """ + if any(order.state == 'draft' for order in self.order_ids): + return {'successful': False, 'message': _("You cannot close the POS when orders are still in draft"), 'redirect': False} + if self.state == 'closed': + return {'successful': False, 'message': _("This session is already closed."), 'redirect': True} + + def get_closing_control_data(self): + self.ensure_one() + orders = self.order_ids.filtered(lambda o: o.state == 'paid' or o.state == 'invoiced') + payments = orders.payment_ids.filtered(lambda p: p.payment_method_id.type != "pay_later") + pay_later_payments = orders.payment_ids - payments + 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 + total_default_cash_payment_amount = sum(payments.filtered(lambda p: p.payment_method_id == default_cash_payment_method_id).mapped('amount')) if default_cash_payment_method_id else 0 + other_payment_method_ids = self.payment_method_ids - default_cash_payment_method_id if default_cash_payment_method_id else self.payment_method_ids + cash_in_count = 0 + cash_out_count = 0 + cash_in_out_list = [] + for cash_move in self.cash_register_id.line_ids.sorted('create_date'): + if cash_move.amount > 0: + cash_in_count += 1 + name = f'Cash in {cash_in_count}' + else: + cash_out_count += 1 + name = f'Cash out {cash_out_count}' + cash_in_out_list.append({ + 'name': cash_move.payment_ref if cash_move.payment_ref else name, + 'amount': cash_move.amount + }) + + return { + 'orders_details': { + 'quantity': len(orders), + 'amount': sum(orders.mapped('amount_total')) + }, + 'payments_amount': sum(payments.mapped('amount')), + 'pay_later_amount': sum(pay_later_payments.mapped('amount')), + 'opening_notes': self.opening_notes, + 'default_cash_details': { + 'name': default_cash_payment_method_id.name, + 'amount': self.cash_register_id.balance_start + total_default_cash_payment_amount + + sum(self.cash_register_id.line_ids.mapped('amount')), + 'opening': self.cash_register_id.balance_start, + 'payment_amount': total_default_cash_payment_amount, + 'moves': cash_in_out_list + } if default_cash_payment_method_id else None, + 'other_payment_methods': [{ + 'name': pm.name, + 'amount': sum(orders.payment_ids.filtered(lambda p: p.payment_method_id == pm).mapped('amount')) + } for pm in other_payment_method_ids] + } + def _create_picking_at_end_of_session(self): self.ensure_one() lines_grouped_by_dest_location = {} @@ -1230,23 +1366,29 @@ class PosSession(models.Model): 'url': self.config_id._get_pos_base_url() + '?config_id=%d' % self.config_id.id, } - def open_cashbox_pos(self): - self.ensure_one() - action = self.cash_register_id.open_cashbox_id() - action['view_id'] = self.env.ref('point_of_sale.view_account_bnk_stmt_cashbox_footer').id - action['context']['pos_session_id'] = self.id - action['context']['default_pos_id'] = self.config_id.id - return action - def set_cashbox_pos(self, cashbox_value, notes): self.state = 'opened' + self.opening_notes = notes + difference = cashbox_value - self.cash_register_id.balance_start self.cash_register_id.balance_start = cashbox_value + self._post_cash_details_message('Opening', difference, notes) + + def _post_cash_details_message(self, state, difference, notes): + message = "" + if difference: + message = f"{state} difference: " \ + f"{self.currency_id.symbol + ' ' if self.currency_id.position == 'before' else ''}" \ + f"{self.currency_id.round(difference)} " \ + f"{self.currency_id.symbol if self.currency_id.position == 'after' else ''}" if notes: + message += notes.replace('\n', '') + if message: self.env['mail.message'].create({ - 'body': notes, + 'body': message, 'model': 'account.bank.statement', 'res_id': self.cash_register_id.id, }) + self.message_post(body=message) def action_view_order(self): return { @@ -1277,23 +1419,6 @@ class PosSession(models.Model): ) ) - def _warning_balance_closing(self): - self.ensure_one() - - context = dict(self._context) - context['session_id'] = self.id - - return { - 'name': _('Balance control'), - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'closing.balance.confirm.wizard', - 'views': [(False, 'form')], - 'type': 'ir.actions.act_window', - 'context': context, - 'target': 'new' - } - def _check_if_no_draft_orders(self): draft_orders = self.order_ids.filtered(lambda order: order.state == 'draft') if draft_orders: @@ -1310,7 +1435,11 @@ class PosSession(models.Model): .with_context({'active_model': 'pos.session', 'active_ids': self.ids})\ .create({'amount': sign * amount, 'name': reason})\ .run() - self.message_post(body='\n'.join([f"Cash {extras['translatedType']}", f'- Amount: {extras["formattedAmount"]}', f'- Reason: {reason}'])) + message_content = [f"Cash {extras['translatedType']}", f'- Amount: {extras["formattedAmount"]}'] + if reason: + message_content.append(f'- Reason: {reason}') + self.message_post(body='\n'.join(message_content)) + class ProcurementGroup(models.Model): _inherit = 'procurement.group' @@ -1321,11 +1450,3 @@ class ProcurementGroup(models.Model): self.env['pos.session']._alert_old_session() if use_new_cursor: self.env.cr.commit() - -class ClosingBalanceConfirm(models.TransientModel): - _name = 'closing.balance.confirm.wizard' - _description = 'This wizard is used to display a warning message if the manager wants to close a session with a too high difference between real and expected closing balance' - - def confirm_closing_balance(self): - current_session = self.env['pos.session'].browse(self._context['session_id']) - return current_session._validate_session() diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index fce7173b119..0197886321c 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -47,9 +47,9 @@ access_decimal_precision_user,decimal.precision,base.model_decimal_precision,gro access_pos_payment_user,pos.payment user,model_pos_payment,group_pos_user,1,1,1,1 access_pos_payment_method_user,pos.payment.method user,model_pos_payment_method,group_pos_user,1,0,0,0 access_pos_payment_method_manager,pos.payment.method manager,model_pos_payment_method,group_pos_manager,1,1,1,1 -access_closing_balance_confirm_wizard,access.closing.balance.confirm.wizard,model_closing_balance_confirm_wizard,point_of_sale.group_pos_manager,1,1,1,0 access_pos_details_wizard,access.pos.details.wizard,model_pos_details_wizard,point_of_sale.group_pos_manager,1,1,1,0 access_pos_make_payment,access.pos.make.payment,model_pos_make_payment,point_of_sale.group_pos_manager,1,1,1,0 access_money_in_out_wizard,access.money.in.out.wizard,model_cash_box_out,point_of_sale.group_pos_user,1,1,1,0 access_pos_close_session_wizard,access.pos.close.session.wizard,model_pos_close_session_wizard,point_of_sale.group_pos_user,1,1,1,0 access_account_cash_rounding_pos_user,account.cash.rounding pos_user,account.model_account_cash_rounding,group_pos_user,1,0,0,0 +access_pos_bill,pos.bill pos_user,model_pos_bill,group_pos_user,1,1,1,1 diff --git a/addons/point_of_sale/static/src/css/popups/cash_opening_popup.css b/addons/point_of_sale/static/src/css/popups/cash_opening_popup.css new file mode 100644 index 00000000000..aa1a4b97e7b --- /dev/null +++ b/addons/point_of_sale/static/src/css/popups/cash_opening_popup.css @@ -0,0 +1,36 @@ +.pos .opening-cash-control .body{ + margin: 40px; +} + +.pos .opening-cash-control .opening-cash-section { + display: flex; + justify-content: space-between; + margin: 40px 0; +} + +.pos .opening-cash-section .info-title { + font-size: 20px; + font-weight: bold; +} + +.pos .opening-cash-section .cash-input-sub-section { + display: flex; +} + +.pos .cash-input-sub-section .pos-input { + width: 85px; +} + +.pos .cash-input-sub-section .button.icon { + margin: 0; + float: unset; +} + +.pos .opening-cash-control .opening-cash-notes { + font-style: italic; + font-weight: 350; + width: calc(100% - 20px); /* textarea has a padding of 10px */ + line-height: 20px; + resize: none; + height: 150px; +} diff --git a/addons/point_of_sale/static/src/css/popups/closing_pos_popup.css b/addons/point_of_sale/static/src/css/popups/closing_pos_popup.css new file mode 100644 index 00000000000..0d5c3591bcb --- /dev/null +++ b/addons/point_of_sale/static/src/css/popups/closing_pos_popup.css @@ -0,0 +1,133 @@ +.pos .close-pos-popup { + max-width: 800px !important; + max-height: 800px; +} + +.pos .close-pos-popup .body{ + margin: 0; + padding: 15px 50px; +} + +.pos .close-pos-popup .session-overview { + display: flex; + margin-bottom: 30px; +} + +.pos .close-pos-popup .overview-info { + width: 50%; + display: flex; +} + +.pos .close-pos-popup .overview-info .info-value { + margin-left: 50px; +} + +.pos .close-pos-popup .overview-info span { + margin-bottom: 10px; +} + +.pos .close-pos-popup .column-left { + display: flex; + flex-direction: column; + text-align: left; +} + +.pos .close-pos-popup .session-overview .opening-note { + margin-left: 70px; + color: darkgrey; + text-align: left; + font-size: 16px; + width: 250px; + max-height: 90px; + padding-left: 10px; + border-left: solid 3px darkgrey; + overflow-y: auto; + word-break: break-word; +} + +.pos .close-pos-popup .payment-methods-overview { + max-height: 370px; + overflow: auto; +} + +.pos .close-pos-popup .payment-methods-overview table { + width: 100%; + text-align: left; +} + +.pos .close-pos-popup .payment-methods-overview table tr { + height: 35px +} + +.pos .close-pos-popup .flex { + display: flex; +} + +.pos .close-pos-popup .payment-methods-overview table .pos-input { + width: 85px; +} + +.pos .close-pos-popup .payment-methods-overview table .warning { + color: red; + font-weight: bold; +} + +.pos .close-pos-popup .body .button.icon { + margin: 0; + float: unset; +} + +.pos .close-pos-popup .payment-methods-overview table .cash-sign { + width: 10px; + margin-right: 5px; +} + +.pos .close-pos-popup .payment-methods-overview .cash-overview { + border-left: solid 2px #555555; +} + +.pos .close-pos-popup .payment-methods-overview .cash-overview tr td:first-child { + padding-left: 10px; +} + +.pos .close-pos-popup .closing-notes { + font-style: italic; + font-weight: 350; + width: calc(100% - 20px); /* textarea has a padding of 10px */ + line-height: 20px; + resize: none; + height: 70px; + margin: 20px 0; +} + +.pos .close-pos-popup .accept-closing { + text-align: left; +} + +.pos .close-pos-popup .accept-closing.invisible { + visibility: hidden; +} + +.pos .close-pos-popup .accept-closing label, #accept { + cursor: pointer; +} + +.pos .close-pos-popup #accept { + all: revert; + margin-right: 10px; +} + +.pos .close-pos-popup .footer .button { + width: 150px; +} + +.pos .close-pos-popup .footer .button.disabled { + cursor: default; + background: bottom; + color: unset; + border: solid 1px rgba(60, 60, 60, 0.1); +} + +.pos .close-pos-popup .footer .button.disabled:active { + border: solid 1px rgba(60, 60, 60, 0.1); +} diff --git a/addons/point_of_sale/static/src/css/popups/common.css b/addons/point_of_sale/static/src/css/popups/common.css new file mode 100644 index 00000000000..9d206dd5ba6 --- /dev/null +++ b/addons/point_of_sale/static/src/css/popups/common.css @@ -0,0 +1,21 @@ +/* Input style used in cash control popups */ +.pos .popup .pos-input { + text-align: center; + font-size: 18px; + color: #555555; + background: none; + min-height: 0; + border-radius: unset; + box-shadow: none; + padding: 0; + cursor: pointer; + width: 50px; + margin-right: 15px; + border-bottom: solid 2px; +} + +.pos .popup .pos-input:focus { + box-shadow: none ; + border-color: blue; + font-weight: bold; +} diff --git a/addons/point_of_sale/static/src/css/popups/money_details_popup.css b/addons/point_of_sale/static/src/css/popups/money_details_popup.css new file mode 100644 index 00000000000..5f23d374ac0 --- /dev/null +++ b/addons/point_of_sale/static/src/css/popups/money_details_popup.css @@ -0,0 +1,36 @@ +.pos .popup.money-details { + width: 350px; + max-height: 400px; +} + +.pos .money-details.invisible { + display: none; +} + +.pos .money-details .body { + max-height: 306px; +} + +.pos .money-details .money-details-title { + text-align: left; + margin-bottom: 15px; + font-weight: bold; +} + +.pos .money-details .money-details-info { + margin-bottom: 20px; + display: flex; + justify-content: space-between; + overflow: auto; + max-height: 235px; + padding: 0 15px; +} + +.pos .money-details .money-details-value { + display: flex; + margin: 10px 0; +} + +.pos .money-details .total-section { + font-weight: bold; +} diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index f53c6bd20d3..66b81720ceb 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -497,11 +497,6 @@ td { background: rgba(0,0,0,0.2); color:#EEE; } -.pos .pos-rightheader .header-button.confirm { - background: #359766; - color: white; - font-weight: bold; -} /* c) The notifications indicator */ @@ -3423,6 +3418,19 @@ td { display: none; } +.cash-move-button { + display: flex; + align-items: center; + padding: 0 15px; + font-size: medium; + color: white; +} + +.cash-move-button:hover { + background: rgb(104,69,95); + cursor: pointer; +} + /* Product Screen Search Bar */ .search-bar-portal { @@ -3600,49 +3608,6 @@ td { } } -.pos .cashbox-button { - height: 54px; - width: 54px; - font-weight: bold; - vertical-align: middle; - color: #555555; - border-radius: 0; - border: none; - border-right: 1px solid; - border-bottom: 1px solid; - border-color: #bfbfbf; - transition: all 150ms linear; -} - -.pos .cashbox-input { - line-height: 74px; - border: solid 1px rgb(202, 202, 202); - border-top-width: 0px; - cursor: pointer; - background: white; - display: flex; - justify-content: space-between; -} - -.cashbox-input .input-title { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - font-size: 19px; -} - -.cashbox-input .input-amount { - margin-left: 10px; - margin-right: 10px; - padding-right: 8px; -} - -.cashbox-notes { - font-style: italic; - font-weight: 350; - width: 95%; -} - /** * Notification */ diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js index 84036ecbc75..b394f967b5a 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js @@ -1,31 +1,14 @@ odoo.define('point_of_sale.HeaderButton', function(require) { 'use strict'; - const { useState } = owl; const PosComponent = require('point_of_sale.PosComponent'); const Registries = require('point_of_sale.Registries'); // Previously HeaderButtonWidget // This is the close session button class HeaderButton extends PosComponent { - constructor() { - super(...arguments); - this.state = useState({ label: 'Close' }); - this.confirmed = null; - } - get translatedLabel() { - return this.env._t(this.state.label); - } onClick() { - if (!this.confirmed) { - this.state.label = 'Confirm'; - this.confirmed = setTimeout(() => { - this.state.label = 'Close'; - this.confirmed = null; - }, 2000); - } else { - this.trigger('close-pos'); - } + this.showPopup('ClosePosPopup'); } } HeaderButton.template = 'HeaderButton'; diff --git a/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js new file mode 100644 index 00000000000..e7ced094b69 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js @@ -0,0 +1,13 @@ +odoo.define('point_of_sale.CurrencyAmount', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class CurrencyAmount extends PosComponent {} + CurrencyAmount.template = 'CurrencyAmount'; + + Registries.Component.add(CurrencyAmount); + + return CurrencyAmount; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js index 6cdd6a04bab..fbbcaa10d71 100644 --- a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js @@ -32,7 +32,9 @@ odoo.define('point_of_sale.AbstractAwaitablePopup', function (require) { class AbstractAwaitablePopup extends PosComponent { constructor() { super(...arguments); - useExternalListener(window, 'keyup', this._cancelAtEscape); + if (!this.props.notEscapable) { + useExternalListener(window, 'keyup', this._cancelAtEscape); + } } async confirm() { this.props.resolve({ confirmed: true, payload: await this.getPayload() }); diff --git a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js index 53578f4689a..0c5f4e82be1 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js @@ -1,10 +1,7 @@ odoo.define('point_of_sale.CashOpeningPopup', function(require) { 'use strict'; - const { useState, useRef} = owl.hooks; - const { useListener } = require('web.custom_hooks'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const PosComponent = require('point_of_sale.PosComponent'); + const { useState, useRef } = owl.hooks; const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); const Registries = require('point_of_sale.Registries'); @@ -12,52 +9,44 @@ odoo.define('point_of_sale.CashOpeningPopup', function(require) { class CashOpeningPopup extends AbstractAwaitablePopup { constructor() { super(...arguments); - this.cashBoxValue = this.env.pos.bank_statement.balance_start || 0;; - this.currency = this.env.pos.currency; + this.manualInputCashCount = null; this.state = useState({ notes: "", + openingCash: this.env.pos.bank_statement.balance_start || 0, }); - useListener('numpad-click-input', this._updateCashAmount); - useListener('update-cash', this._updateCashAmount); - this.inputRef = useRef('input'); - NumberBuffer.use({ - nonKeyboardInputEvent: 'numpad-click-input', - triggerAtInput: 'update-cash', - useWithBarcode: false, - }); + this.moneyDetailsRef = useRef('moneyDetails'); + } + openDetailsPopup() { + if (this.moneyDetailsRef.comp.isClosed()){ + this.moneyDetailsRef.comp.openPopup(); + this.state.openingCash = 0; + this.state.notes = ""; + if (this.manualInputCashCount) { + this.moneyDetailsRef.comp.reset(); + } + } } - startSession() { - this.env.pos.bank_statement.balance_start = parseFloat(this.cashBoxValue); + this.env.pos.bank_statement.balance_start = this.state.openingCash; this.env.pos.pos_session.state = 'opened'; this.rpc({ model: 'pos.session', method: 'set_cashbox_pos', - args: [this.env.pos.pos_session.id, parseFloat(this.cashBoxValue), this.state.notes], + args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes], }); - this.trigger('close-popup'); + this.cancel(); // close popup } - - sendInput(value) { - this.trigger('numpad-click-input', { value }); - } - - async _updateCashAmount(event) { - let value = event.detail.value ? event.detail.value : event.detail.key - if(value !== "Backspace") { - if(this.cashBoxValue === 0) { - this.cashBoxValue = value !== "."? value: this.cashBoxValue + value; - } else { - this.cashBoxValue += value; - } - } else { - if(this.cashBoxValue.length > 1) { - this.cashBoxValue = this.cashBoxValue.substring(0, this.cashBoxValue.length -1) - } else { - this.cashBoxValue = 0; - } + updateCashOpening(event) { + const { total, moneyDetailsNotes } = event.detail; + this.state.openingCash = total; + if (moneyDetailsNotes) { + this.state.notes = moneyDetailsNotes; } - this.render(); + this.manualInputCashCount = false; + } + handleInputChange() { + this.manualInputCashCount = true; + this.state.notes = ""; } } diff --git a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js new file mode 100644 index 00000000000..41d8880f68a --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js @@ -0,0 +1,165 @@ +odoo.define('point_of_sale.ClosePosPopup', function(require) { + 'use strict'; + + const { useState, useRef } = owl.hooks; + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + const { identifyError } = require('point_of_sale.utils'); + const { ConnectionLostError, ConnectionAbortedError} = require('@web/core/network/rpc_service') + + /** + * This popup needs to be self-dependent because it needs to be called from different place. + */ + class ClosePosPopup extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + this.manualInputCashCount = true; + this.cashControl = this.env.pos.config.cash_control; + this.moneyDetailsRef = useRef('moneyDetails'); + this.closeSessionClicked = false; + this.moneyDetails = null; + this.state = useState({}); + } + async willStart() { + try { + const closingData = await this.rpc({ + model: 'pos.session', + method: 'get_closing_control_data', + args: [[this.env.pos.pos_session.id]] + }); + this.ordersDetails = closingData.orders_details; + this.paymentsAmount = closingData.payments_amount; + this.payLaterAmount = closingData.pay_later_amount; + this.openingNotes = closingData.opening_notes; + this.defaultCashDetails = closingData.default_cash_details; + this.otherPaymentMethods = closingData.other_payment_methods; + + // component state and refs definition + const state = {notes: '', acceptClosing: false}; + if (this.cashControl) { + state[this.defaultCashDetails.name] = {counted: 0, difference: -this.defaultCashDetails.amount}; + } + if (this.otherPaymentMethods.length > 0) { + this.otherPaymentMethods.forEach(pm => { + state[pm.name] = {counted: this.env.pos.round_decimals_currency(pm.amount), difference: 0} + }) + } + Object.assign(this.state, state); + } catch (error) { + this.error = error; + } + } + /* + * Since this popup need to be self dependent, in case of an error, the popup need to be closed on its own. + */ + mounted() { + if (this.error) { + this.cancel(); + if (identifyError(this.error) instanceof ConnectionLostError) { + this.showPopup('ErrorPopup', { + title: this.env._t('Network Error'), + body: this.env._t('Please check your internet connection and try again.'), + }); + } else { + throw this.error; + } + } + } + openDetailsPopup() { + if (this.moneyDetailsRef.comp.isClosed()){ + this.moneyDetailsRef.comp.openPopup(); + this.state[this.defaultCashDetails.name].counted = 0; + this.state.notes = ''; + if (this.manualInputCashCount) { + this.moneyDetailsRef.comp.reset(); + } + } + } + handleInputChange() { + this.manualInputCashCount = true; + this.state.notes = ''; + this.state[this.defaultCashDetails.name].difference = + this.env.pos.round_decimals_currency(this.state[this.defaultCashDetails.name].counted - this.defaultCashDetails.amount); + } + updateCountedCash(event) { + const { total, moneyDetailsNotes, moneyDetails } = event.detail; + this.state[this.defaultCashDetails.name].counted = total; + this.state[this.defaultCashDetails.name].difference = + this.env.pos.round_decimals_currency(this.state[[this.defaultCashDetails.name]].counted - this.defaultCashDetails.amount); + if (moneyDetailsNotes) { + this.state.notes = moneyDetailsNotes; + } + this.manualInputCashCount = false; + this.moneyDetails = moneyDetails; + } + canCloseSession() { + return !this.cashControl || !this.state[this.defaultCashDetails.name].difference || this.state.acceptClosing; + } + closePos() { + this.trigger('close-pos'); + } + async closeSession() { + if (this.canCloseSession() && !this.closeSessionClicked) { + this.closeSessionClicked = true; + let response; + if (this.cashControl) { + response = await this.rpc({ + model: 'pos.session', + method: 'post_closing_cash_details', + args: [this.env.pos.pos_session.id], + kwargs: { + counted_cash: this.state[this.defaultCashDetails.name].counted, + } + }) + if (!response.successful) { + return this.handleClosingError(response); + } + } + await this.rpc({ + model: 'pos.session', + method: 'update_closing_control_state_session', + args: [this.env.pos.pos_session.id, this.state.notes] + }) + try { + response = await this.rpc({ + model: 'pos.session', + method: 'close_session_from_ui', + args: [this.env.pos.pos_session.id], + }); + if (!response.successful) { + return this.handleClosingError(response); + } + window.location = '/web#action=point_of_sale.action_client_pos_menu'; + } catch (error) { + const iError = identifyError(error); + if (iError instanceof ConnectionLostError || iError instanceof ConnectionAbortedError) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Network Error'), + body: this.env._t('Cannot close the session when offline.'), + }); + } else { + await this.showPopup('ErrorPopup', { + title: this.env._t('Closing session error'), + body: this.env._t( + 'An error has occurred when trying to close the session.\n' + + 'You will be redirected to the back-end to manually close the session.') + }) + window.location = '/web#action=point_of_sale.action_client_pos_menu'; + } + } + this.closeSessionClicked = false; + } + } + async handleClosingError(response) { + await this.showPopup('ErrorPopup', {title: 'Error', body: response.message}); + if (response.redirect) { + window.location = '/web#action=point_of_sale.action_client_pos_menu'; + } + } + } + + ClosePosPopup.template = 'ClosePosPopup'; + Registries.Component.add(ClosePosPopup); + + return ClosePosPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js new file mode 100644 index 00000000000..c2f71652936 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js @@ -0,0 +1,69 @@ +odoo.define('point_of_sale.MoneyDetailsPopup', function(require) { + 'use strict'; + + const { useState } = owl.hooks; + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + /** + * Even if this component has a "confirm and cancel"-like buttons, this should not be an AbstractAwaitablePopup. + * We currently cannot show two popups at the same time, what we do is mount this component with its parent + * and hide it with some css. The confirm button will just trigger an event to the parent. + */ + class MoneyDetailsPopup extends PosComponent { + constructor() { + super(...arguments); + this.currency = this.env.pos.currency; + this.state = useState({ + moneyDetails: Object.fromEntries(this.env.pos.bills.map(bill => ([bill.value, 0]))), + total: 0, + }); + } + get firstHalfMoneyDetails() { + const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b); + return moneyDetailsKeys.slice(0, moneyDetailsKeys.length/2); + } + get lastHalfMoneyDetails() { + const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b); + return moneyDetailsKeys.slice(moneyDetailsKeys.length/2, moneyDetailsKeys.length); + } + isClosed() { + return this.el.classList.contains('invisible') + } + openPopup() { + this.el.classList.remove('invisible'); + } + updateMoneyDetailsAmount() { + let total = Object.entries(this.state.moneyDetails).reduce((total, money) => total + money[0] * money[1], 0); + this.state.total = this.env.pos.round_decimals_currency(total); + } + _closePopup() { + this.el.classList.add('invisible'); + } + confirm() { + let moneyDetailsNotes = this.state.total ? 'Money details: \n' : null; + this.env.pos.bills.forEach(bill => { + if (this.state.moneyDetails[bill.value]) { + moneyDetailsNotes += ` - ${this.state.moneyDetails[bill.value]} x ${this.env.pos.format_currency(bill.value)}\n`; + } + }) + const payload = { total: this.state.total, moneyDetailsNotes, moneyDetails: { ...this.state.moneyDetails }}; + this.trigger('money-details-validated', payload); + this._closePopup(); + } + reset() { + for (let key in this.state.moneyDetails) { this.state.moneyDetails[key] = 0 } + this.state.total = 0; + } + discard() { + this.reset(); + this._closePopup(); + } + } + + MoneyDetailsPopup.template = 'MoneyDetailsPopup'; + Registries.Component.add(MoneyDetailsPopup); + + return MoneyDetailsPopup; + +}); diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js index c1522915bfe..d8f6de0f94d 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -44,7 +44,7 @@ odoo.define('point_of_sale.ProductScreen', function(require) { } mounted() { if(this.env.pos.config.cash_control && this.env.pos.pos_session.state == 'opening_control') { - Gui.showPopup('CashOpeningPopup'); + Gui.showPopup('CashOpeningPopup', {notEscapable: true}); } this.env.pos.on('change:selectedClient', this.render, this); } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 3bd7f3e17db..364d3e3cb2f 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -350,6 +350,15 @@ exports.PosModel = Backbone.Model.extend({ } }, },{ + model: 'pos.bill', + fields: ['name', 'value'], + domain: function (self) { + return [['id', 'in', self.config.default_bill_ids]]; + }, + loaded: function (self, bills) { + self.bills = bills; + }, + }, { model: 'res.partner', label: 'load_partners', fields: ['name','street','city','state_id','country_id','vat','lang', @@ -1518,6 +1527,11 @@ exports.PosModel = Backbone.Model.extend({ return value.toFixed(decimals); }, + round_decimals_currency(value) { + const decimals = this.currency.decimals; + return parseFloat(round_di(value, decimals).toFixed(decimals)); + }, + /** * (value = 1.0000, decimals = 2) => '1' * (value = 1.1234, decimals = 2) => '1.12' diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml index 595599c111c..3dfb83bb67d 100644 --- a/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml +++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml @@ -2,7 +2,7 @@ - + Cash In/Out diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/HeaderButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/HeaderButton.xml index 19d9c7c841c..db30ad680d6 100644 --- a/addons/point_of_sale/static/src/xml/ChromeWidgets/HeaderButton.xml +++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/HeaderButton.xml @@ -2,9 +2,8 @@ - - + + Close diff --git a/addons/point_of_sale/static/src/xml/Misc/CurrencyAmount.xml b/addons/point_of_sale/static/src/xml/Misc/CurrencyAmount.xml new file mode 100644 index 00000000000..2ec97ff57f8 --- /dev/null +++ b/addons/point_of_sale/static/src/xml/Misc/CurrencyAmount.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/point_of_sale/static/src/xml/Popups/CashOpeningPopup.xml b/addons/point_of_sale/static/src/xml/Popups/CashOpeningPopup.xml index 61b6e5bdfb0..2f0e6857cf6 100644 --- a/addons/point_of_sale/static/src/xml/Popups/CashOpeningPopup.xml +++ b/addons/point_of_sale/static/src/xml/Popups/CashOpeningPopup.xml @@ -1,57 +1,28 @@ - - + - Start Cash Drawer + OPENING CASH CONTROL - - - Starting Cash - - - - - - - - - - - - - - - 1 - 2 - 3 - - 4 - 5 - 6 - - 7 - 8 - 9 - - . - 0 - - - - + + Opening cash + + + + + + + + + - diff --git a/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml b/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml new file mode 100644 index 00000000000..1ca0ddaa42d --- /dev/null +++ b/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml @@ -0,0 +1,101 @@ + + + + + + + + CLOSING CONTROL + + + + + + Total orders + Payments + Customer Account + + + + + + + + + + + + + + + + Payment Method + Expected + Counted + Difference + + + + + + + + + + + + + + + + + + + Opening + + + + + + + + + + + + + + + + Payments in + + + + + + + + + + + + + + + + + + Accept difference and post a profit/loss journal entry + + + + + + + + + diff --git a/addons/point_of_sale/static/src/xml/Popups/MoneyDetailsPopup.xml b/addons/point_of_sale/static/src/xml/Popups/MoneyDetailsPopup.xml new file mode 100644 index 00000000000..42ca5daa1f8 --- /dev/null +++ b/addons/point_of_sale/static/src/xml/Popups/MoneyDetailsPopup.xml @@ -0,0 +1,30 @@ + + + + + + + Coins/Bills + + + + + + + + + + + + + Total + + + + + + + diff --git a/addons/point_of_sale/static/tests/tours/Chrome.tour.js b/addons/point_of_sale/static/tests/tours/Chrome.tour.js index c27221771d6..122f8ec9ef9 100644 --- a/addons/point_of_sale/static/tests/tours/Chrome.tour.js +++ b/addons/point_of_sale/static/tests/tours/Chrome.tour.js @@ -12,6 +12,7 @@ odoo.define('point_of_sale.tour.Chrome', function (require) { startSteps(); // Order 1 is at Product Screen + ProductScreen.do.confirmOpeningPopup(); ProductScreen.do.clickHomeCategory(); ProductScreen.exec.addOrderline('Desk Pad', '1', '2', '2.0'); Chrome.do.clickTicketButton(); diff --git a/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js index d3acf388142..d092e7a11f7 100644 --- a/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js +++ b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js @@ -10,6 +10,7 @@ odoo.define('point_of_sale.tour.ProductConfigurator', function (require) { // when finished, steps can be taken from getSteps startSteps(); + ProductScreen.do.confirmOpeningPopup(); // Go by default to home category ProductScreen.do.clickHomeCategory(); diff --git a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js index f75dffeed6a..784e95e3d39 100644 --- a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js @@ -13,6 +13,7 @@ odoo.define('point_of_sale.tour.TicketScreen', function (require) { startSteps(); + ProductScreen.do.confirmOpeningPopup(); ProductScreen.do.clickHomeCategory(); ProductScreen.exec.addOrderline('Desk Pad', '1', '2'); ProductScreen.do.clickCustomerButton(); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js index 386606e88ce..0a6291d756c 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -140,6 +140,9 @@ odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) { }, ]; } + confirmOpeningPopup() { + return [{ trigger: '.opening-cash-control .button:contains("Open session")' }]; + } } class Check { diff --git a/addons/point_of_sale/static/tests/tours/point_of_sale.js b/addons/point_of_sale/static/tests/tours/point_of_sale.js index 744a5fa56a6..f86d2d41c78 100644 --- a/addons/point_of_sale/static/tests/tours/point_of_sale.js +++ b/addons/point_of_sale/static/tests/tours/point_of_sale.js @@ -85,6 +85,8 @@ odoo.define('point_of_sale.tour.pricelist', function (require) { $('.pos').addClass('done-testing'); }); }, + }, { + trigger: '.opening-cash-control .button:contains("Open session")', }]; steps = steps.concat([{ @@ -424,11 +426,11 @@ odoo.define('point_of_sale.tour.acceptance', function (require) { steps = steps.concat(verify_order_total('5.52')); steps = steps.concat([{ - content: "close the Point of Sale frontend", + content: "open closing the Point of Sale frontend popup", trigger: ".header-button", }, { - content: "confirm closing the frontend", - trigger: ".header-button.confirm", + content: "close the Point of Sale frontend", + trigger: ".close-pos-popup .button:contains('Continue Selling')", run: function() {}, //it's a check, }]); diff --git a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js index a0df97fdbd4..dbfa6dde611 100644 --- a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js +++ b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js @@ -28,27 +28,6 @@ odoo.define('point_of_sale.tests.ChromeWidgets', function (require) { parent.destroy(); }); - QUnit.test('HeaderButton', async function (assert) { - assert.expect(1); - - class Parent extends PosComponent {} - Parent.env = makePosTestEnv(); - Parent.template = xml/* html */ ` - - `; - - const parent = new Parent(); - await parent.mount(testUtils.prepareTarget()); - - const headerButton = parent.el.querySelector('.header-button'); - await testUtils.dom.click(headerButton); - await testUtils.nextTick(); - assert.ok(headerButton.classList.contains('confirm')); - - parent.unmount(); - parent.destroy(); - }); - QUnit.test('SyncNotification', async function (assert) { assert.expect(5); diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index 0c79b0b3538..c5f69be2494 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -502,7 +502,7 @@ class TestPoSCommon(ValuationReconciliationTestCommon): 'location_id': cls.stock_location_components.id, }).action_apply_inventory() - def open_new_session(self): + def open_new_session(self, opening_cash=0): """ Used to open new pos session in each configuration. - The idea is to properly set values that are constant @@ -522,10 +522,11 @@ class TestPoSCommon(ValuationReconciliationTestCommon): self.pos_session = self.config.current_session_id self.currency = self.pos_session.currency_id self.pricelist = self.pos_session.config_id.pricelist_id + self.pos_session.set_cashbox_pos(opening_cash, None) return self.pos_session def _run_test(self, args): - pos_session = self._start_pos_session(args['payment_methods']) + pos_session = self._start_pos_session(args['payment_methods'], args.get('opening_cash', 0)) _logger.info('DONE: Start session.') orders_map = self._create_orders(args['orders']) _logger.info('DONE: Orders created.') @@ -535,7 +536,9 @@ class TestPoSCommon(ValuationReconciliationTestCommon): _logger.info('DONE: Call of before_closing_cb.') self._check_invoice_journal_entries(pos_session, orders_map, expected_values=args['journal_entries_before_closing']) _logger.info('DONE: Checks for journal entries before closing the session.') - pos_session.action_pos_session_validate() + total_cash_payment = sum(pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) + pos_session.post_closing_cash_details(total_cash_payment) + pos_session.close_session_from_ui() after_closing_cb = args.get('after_closing_cb') if after_closing_cb: after_closing_cb() @@ -543,9 +546,9 @@ class TestPoSCommon(ValuationReconciliationTestCommon): self._check_session_journal_entries(pos_session, expected_values=args['journal_entries_after_closing']) _logger.info('DONE: Checks for journal entries after closing the session.') - def _start_pos_session(self, payment_methods): + def _start_pos_session(self, payment_methods, opening_cash): self.config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]}) - pos_session = self.open_new_session() + pos_session = self.open_new_session(opening_cash) self.assertEqual(self.config.payment_method_ids.ids, pos_session.payment_method_ids.ids, msg='Payment methods in the config should be the same as the session.') return pos_session diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py index 0a374c6a9ec..0c605b4bb25 100644 --- a/addons/point_of_sale/tests/test_anglo_saxon.py +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -68,6 +68,7 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon): self.pos_config.open_session_cb(check_coa=False) current_session = self.pos_config.current_session_id self.cash_journal.loss_account_id = self.account + current_session.set_cashbox_pos(0, None) # I create a PoS order with 1 unit of New product at 450 EUR self.pos_order_pos0 = self.PosOrder.create({ @@ -108,7 +109,8 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon): # I close the current session to generate the journal entries current_session_id = self.pos_config.current_session_id current_session_id._check_pos_session_balance() - current_session_id.action_pos_session_close() + current_session_id.post_closing_cash_details(450.0) + current_session_id.close_session_from_ui() self.assertEqual(current_session_id.state, 'closed', 'Check that session is closed') # Check if there is account_move in the order. @@ -147,6 +149,8 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon): self.pos_config.module_account = True self.pos_config.open_session_cb(check_coa=False) + pos_session = self.pos_config.current_session_id + pos_session.set_cashbox_pos(0, None) pos_order_values = { 'company_id': self.company.id, @@ -186,7 +190,8 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon): # validate the session current_session_id = self.pos_config.current_session_id - current_session_id.action_pos_session_validate() + current_session_id.post_closing_cash_details(7 * 450.0) + current_session_id.close_session_from_ui() # check the anglo saxon move lines # with uninvoiced orders, the account_move field of pos.order is empty. @@ -213,10 +218,6 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon): # Create the customer invoice pos_order_pos0.action_pos_order_invoice() - # validate the session - current_session_id = self.pos_config.current_session_id - current_session_id.action_pos_session_validate() - # check the anglo saxon move lines line = pos_order_pos0.account_move.line_ids.filtered(lambda l: l.debit and l.account_id == self.category.property_account_expense_categ_id) self.assertEqual(pos_order_pos0.account_move.journal_id, self.pos_config.invoice_journal_id) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index 1e584f991f5..ac2682deee4 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -82,7 +82,9 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): self.assertEqual(refund.state, 'paid', "The refund is not marked as paid") self.assertTrue(refund.payment_ids.payment_method_id.is_cash_count, msg='There should only be one payment and paid in cash.') - current_session.action_pos_session_closing_control() + total_cash_payment = sum(current_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) + current_session.post_closing_cash_details(total_cash_payment) + current_session.close_session_from_ui() self.assertEqual(current_session.state, 'closed', msg='State of current session should be closed.') def test_order_refund_lots(self): @@ -601,6 +603,8 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): current_session = self.pos_config.current_session_id num_starting_orders = len(current_session.order_ids) + current_session.set_cashbox_pos(0, None) + untax, atax = self.compute_tax(self.led_lamp, 0.9) carrot_order = {'data': {'amount_paid': untax + atax, @@ -708,7 +712,9 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded") # I close the session - current_session.action_pos_session_closing_control() + total_cash_payment = sum(current_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) + current_session.post_closing_cash_details(total_cash_payment) + current_session.close_session_from_ui() self.assertEqual(current_session.state, 'closed', "Session was not properly closed") self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed") @@ -725,7 +731,9 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders") # I close the rescue session - rescue_session.action_pos_session_closing_control() + total_cash_payment = sum(rescue_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) + rescue_session.post_closing_cash_details(total_cash_payment) + rescue_session.close_session_from_ui() self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed") def test_order_to_payment_currency(self): @@ -989,6 +997,9 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # delete tax dummy_50_perc_tax.unlink() + total_cash_payment = sum(pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) + pos_session.post_closing_cash_details(total_cash_payment) + # close session (should not fail here) # We don't call `action_pos_session_closing_control` to force the failed # closing which will return the action because the internal rollback call messes diff --git a/addons/point_of_sale/tests/test_pos_basic_config.py b/addons/point_of_sale/tests/test_pos_basic_config.py index 43bb67a89f6..310942a643e 100644 --- a/addons/point_of_sale/tests/test_pos_basic_config.py +++ b/addons/point_of_sale/tests/test_pos_basic_config.py @@ -757,43 +757,28 @@ class TestPoSBasicConfig(TestPoSCommon): def test_cash_register_if_no_order(self): # Process one order with product3 - self.open_new_session() + self.open_new_session(0) session = self.pos_session order_data = self.create_ui_order_data([(self.product3, 1)]) amount_paid = order_data['data']['amount_paid'] self.env['pos.order'].create_from_ui([order_data]) - session.action_pos_session_closing_control() + session.post_closing_cash_details(amount_paid) + session.close_session_from_ui() cash_register = session.cash_register_id self.assertEqual(cash_register.balance_start, 0) self.assertEqual(cash_register.balance_end_real, amount_paid) - # Open/Close session without any order - self.open_new_session() + # Open/Close session without any order in cash control + self.open_new_session(amount_paid) session = self.pos_session - session.action_pos_session_closing_control() + session.post_closing_cash_details(amount_paid) + session.close_session_from_ui() cash_register = session.cash_register_id self.assertEqual(cash_register.balance_start, amount_paid) self.assertEqual(cash_register.balance_end_real, amount_paid) self.assertEqual(self.config.last_session_closing_cash, amount_paid) - # Open/Close session with cash control and without any order - self.config.cash_control = True - self.open_new_session() - session = self.pos_session - session.set_cashbox_pos(amount_paid, False) - session.action_pos_session_closing_control() - self.env['account.bank.statement.cashbox'].create([{ - 'start_bank_stmt_ids': [], - 'end_bank_stmt_ids': [(4, session.cash_register_id.id,)], - 'cashbox_lines_ids': [(0, 0, {'number': 1, 'coin_value': amount_paid})], - 'is_a_template': False - }]) - session.action_pos_session_validate() - self.assertEqual(cash_register.balance_start, amount_paid) - self.assertEqual(cash_register.balance_end_real, amount_paid) - self.assertEqual(self.config.last_session_closing_cash, amount_paid) - def test_start_balance_with_two_pos(self): """ When having several POS with cash control, this tests ensures that each POS has its correct opening amount """ @@ -801,10 +786,9 @@ class TestPoSBasicConfig(TestPoSCommon): self.config = pos_data['config'] self.open_new_session() session = self.pos_session - self.assertEqual(session.cash_register_id.balance_start, pos_data['amount_paid']) session.set_cashbox_pos(pos_data['amount_paid'], False) + self.assertEqual(session.cash_register_id.balance_start, pos_data['amount_paid']) - self.config.cash_control = True pos01_config = self.config pos02_config = pos01_config.copy() pos01_data = {'config': pos01_config, 'p_qty': 1, 'amount_paid': 0} @@ -818,14 +802,8 @@ class TestPoSBasicConfig(TestPoSCommon): pos_data['amount_paid'] += order_data['data']['amount_paid'] self.env['pos.order'].create_from_ui([order_data]) - session.action_pos_session_closing_control() - self.env['account.bank.statement.cashbox'].create([{ - 'start_bank_stmt_ids': [], - 'end_bank_stmt_ids': [(4, session.cash_register_id.id,)], - 'cashbox_lines_ids': [(0, 0, {'number': 1, 'coin_value': pos_data['amount_paid']})], - 'is_a_template': False - }]) - session.action_pos_session_validate() + session.post_closing_cash_details(pos_data['amount_paid']) + session.close_session_from_ui() open_and_check(pos01_data) open_and_check(pos02_data) diff --git a/addons/point_of_sale/tests/test_pos_margin.py b/addons/point_of_sale/tests/test_pos_margin.py index db838600695..bb5ec0eb344 100644 --- a/addons/point_of_sale/tests/test_pos_margin.py +++ b/addons/point_of_sale/tests/test_pos_margin.py @@ -385,7 +385,9 @@ class TestPosMargin(TestPoSCommon): self.assertEqual(self.pos_session.order_ids[1].margin_percent, 0) # close session - self.pos_session.action_pos_session_validate() + total_cash_payment = sum(self.pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) + self.pos_session.post_closing_cash_details(total_cash_payment) + self.pos_session.close_session_from_ui() # check margins self.assertEqual(self.pos_session.order_ids[0].margin, 26) diff --git a/addons/point_of_sale/views/account_statement_view.xml b/addons/point_of_sale/views/account_statement_view.xml index 781921d5008..5a94e5e4fd1 100644 --- a/addons/point_of_sale/views/account_statement_view.xml +++ b/addons/point_of_sale/views/account_statement_view.xml @@ -11,30 +11,6 @@ - - account.bnk_stmt_cashbox.form - account.bank.statement.cashbox - - - - - - - - - - account.bnk_stmt_cashbox.form.close.modal - account.bank.statement.cashbox - - primary - 1000 - - - - - - - account.journal.pos.user.form.inherit account.journal diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml index a25867a8ea5..f797fbedef8 100644 --- a/addons/point_of_sale/views/point_of_sale_dashboard.xml +++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml @@ -94,9 +94,9 @@ Continue selling - Open Session + Open Session - Close + Close New Session @@ -114,7 +114,7 @@ - Cash Balance + Last Closing Cash Balance diff --git a/addons/point_of_sale/views/pos_bill_view.xml b/addons/point_of_sale/views/pos_bill_view.xml new file mode 100644 index 00000000000..3a5a0a9d4d3 --- /dev/null +++ b/addons/point_of_sale/views/pos_bill_view.xml @@ -0,0 +1,45 @@ + + + + pos.bill.form + pos.bill + + + + + + + + + + + + + + + pos.bill.tree + pos.bill + + + + + + + + + + + Coins/Bills + ir.actions.act_window + pos.bill + tree,form + + + + diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 7b7ab39da82..2e06d77ea45 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -43,7 +43,7 @@ A session is currently opened for this PoS. Some settings can only be changed after the session is closed. - Click here to close the session + Click here to close the session @@ -442,21 +442,33 @@ - + + - + - + - Control cash box at opening and closing + Set a maximum difference allowed between the expected and counted cash during the closing of the session - + + + + + + Set of coins/bills that will be used in opening and closing control + + + + + + diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml index e12b467ded8..bd03a8e38e2 100644 --- a/addons/point_of_sale/views/pos_session_view.xml +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -6,20 +6,12 @@ - - - - - + @@ -76,52 +68,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - Cash In/Out - - - - - Set Closing Cash - - - - - - - - - - - - - - - - - @@ -233,24 +179,6 @@ 0 - - - Confirming balance closing - closing.balance.confirm.wizard - form - - - - There is a difference between the expected and actual closing in cash. Are you sure you want to close the session and post the accounting entries? - - - - - -