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 @@ -