[IMP] point_of_sale: opening cash control and closing session in POS UI

The opening cash control has been refactored and the closing of the session is now happening in the POS UI. This also leads to a cash control in the UI during the closing.

The advanced cash control is no longer a setting to activate in the config by the user but a computed field based on the presence of a cash payment method. We force the user to always have it now.

The opening cash control has been revamped with the addition of a new money calculator (`MoneyDetails`) allowing the user to easily compute how much money does he have based on his bills.

The closing control has been converted into a popup which allows the users to get an overview of his session details (about orders, payments, payment methods, cash moves, ...). If the `cash_control` has been set to `True`, the user still needs to count his money by introducing it or using the new calculator.

When trying to close the session, if it fails, depending on the error the user get, he can be redirected to the back end to manually close the session. At this point, the user is not able to open the POS UI.
(In order to fully bring the closing of the session in the front end, all the error handlings need to be brought to the front end as well which takes a lot more time.)

Rescue session can only be closed through the back end and the closing cash control is automatically being computed without the user's involvement. This avoid any cash profit/loss in the journals.

The session chatter is being logged with all the details regarding the opening and closing. This give the user a better view of his cash flow in a specific session.

task-2456424

closes odoo/odoo#73464

Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
This commit is contained in:
Jacky (trj)
2021-09-28 09:28:50 +00:00
committed by Pierre Masereel
parent fbf5217cb7
commit 442f856bb8
48 changed files with 1119 additions and 463 deletions
+8
View File
@@ -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',
@@ -46,6 +46,97 @@
<record id="uom.product_uom_categ_unit" model="uom.category">
<field name="is_pos_groupable">True</field>
</record>
<record model="pos.bill" id="0_01">
<field name="name">0.01</field>
<field name="value">0.01</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="0_02">
<field name="name">0.02</field>
<field name="value">0.02</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="0_05">
<field name="name">0.05</field>
<field name="value">0.05</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="0_10">
<field name="name">0.10</field>
<field name="value">0.10</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="0_20">
<field name="name">0.20</field>
<field name="value">0.20</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="0_50">
<field name="name">0.50</field>
<field name="value">0.50</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="1_00">
<field name="name">1.00</field>
<field name="value">1.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="2_00">
<field name="name">2.00</field>
<field name="value">2.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="5_00">
<field name="name">5.00</field>
<field name="value">5.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="10_00">
<field name="name">10.00</field>
<field name="value">10.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="20_00">
<field name="name">20.00</field>
<field name="value">20.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="50_00">
<field name="name">50.00</field>
<field name="value">50.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="100_00">
<field name="name">100.00</field>
<field name="value">100.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="200_00">
<field name="name">200.00</field>
<field name="value">200.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<record model="pos.bill" id="500_00">
<field name="name">500.00</field>
<field name="value">500.00</field>
<field name="pos_config_ids" eval="[(6, False, [ref('point_of_sale.pos_config_main')])]"/>
</record>
<function model="pos.config" name="post_install_pos_localisation" />
</data>
</odoo>
+1
View File
@@ -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
+16
View File
@@ -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]
+10 -77
View File
@@ -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,
+173 -52
View File
@@ -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 ''}<br/>"
if notes:
message += notes.replace('\n', '<br/>')
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='<br/>\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='<br/>\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()
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
47 access_pos_payment_user pos.payment user model_pos_payment group_pos_user 1 1 1 1
48 access_pos_payment_method_user pos.payment.method user model_pos_payment_method group_pos_user 1 0 0 0
49 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
50 access_pos_details_wizard access.pos.details.wizard model_pos_details_wizard point_of_sale.group_pos_manager 1 1 1 0
51 access_pos_make_payment access.pos.make.payment model_pos_make_payment point_of_sale.group_pos_manager 1 1 1 0
52 access_money_in_out_wizard access.money.in.out.wizard model_cash_box_out point_of_sale.group_pos_user 1 1 1 0
53 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
54 access_account_cash_rounding_pos_user account.cash.rounding pos_user account.model_account_cash_rounding group_pos_user 1 0 0 0
55 access_pos_bill pos.bill pos_user model_pos_bill group_pos_user 1 1 1 1
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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;
}
+13 -48
View File
@@ -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
*/
@@ -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';
@@ -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;
});
@@ -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() });
@@ -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 = "";
}
}
@@ -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;
});
@@ -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;
});
@@ -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);
}
@@ -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'
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CashMoveButton" owl="1">
<div class="ticket-button" t-on-click="onClick">
<div class="cash-move-button" t-on-click="onClick">
<i class="fa fa-money" aria-hidden="true"></i>
<span style="padding-left: 10px;">Cash In/Out</span>
</div>
@@ -2,9 +2,8 @@
<templates id="template" xml:space="preserve">
<t t-name="HeaderButton" owl="1">
<div class="header-button close_button" t-att-class="{ confirm: state.label === 'Confirm' }"
t-on-click="onClick">
<t t-esc="translatedLabel" />
<div class="header-button close_button" t-on-click="onClick">
Close
</div>
</t>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="CurrencyAmount" owl="1">
<label t-att-for="props.forTarget" t-att-class="{'oe_link_icon': props.forTarget}">
<t t-if="props.currency.position === 'before'"><t t-esc="props.currency.symbol"/><![CDATA[ ]]></t>
<t t-esc="props.amount"/>
<t t-if="props.currency.position === 'after'"><![CDATA[ ]]><t t-esc="props.currency.symbol"/></t>
</label>
</t>
</templates>
@@ -1,57 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="CashOpeningPopup" owl="1">
<div role="dialog" class="modal-dialog">
<div class="popup popup-confirm">
<div class="popup opening-cash-control">
<header class="title drag-handle">
Start Cash Drawer
OPENING CASH CONTROL
</header>
<main class="body">
<div class="cashbox-input">
<span class="input-title">
Starting Cash
</span>
<span class="input-amount">
<span t-if="currency.position === 'before'">
<t t-esc="currency.symbol"/><span> </span>
</span>
<t t-esc="cashBoxValue"/>
<span t-if="currency.position === 'after'">
<span> </span><t t-esc="currency.symbol"/>
</span>
</span>
</div><br/>
<textarea style="line-height: 20px;" rows="3"
placeholder="Notes" class="cashbox-input cashbox-notes"
t-model="state.notes" t-ref="input"/><br/><br/>
<div>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('1')">1</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('2')">2</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('3')">3</button>
<br/>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('4')">4</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('5')">5</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('6')">6</button>
<br />
<button class="input-button number-char cashbox-button" t-on-click="sendInput('7')">7</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('8')">8</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('9')">9</button>
<br/>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('.')">.</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('0')">0</button>
<button class="input-button number-char cashbox-button" t-on-click="sendInput('Backspace')">
<img style="pointer-events: none;" src="/point_of_sale/static/src/img/backspace.png"
width="24" height="21" alt="Backspace" />
</button>
</div><br/>
<div class="opening-cash-section">
<span class="info-title">Opening cash</span>
<div class="cash-input-sub-section" t-on-input="handleInputChange()">
<input class="pos-input" type="number" t-model.number="state.openingCash"/>
<div class="button icon" t-on-click="openDetailsPopup()">
<i class="fa fa-calculator" role="img" title="Open the money details popup"/>
</div>
</div>
</div>
<textarea placeholder="Notes" class="opening-cash-notes" t-model="state.notes"/>
</main>
<footer class="footer">
<button style="height: 50px; margin: 22px 22px 22px 0px" t-model="state.notes" class="button cashbox-button" t-on-click="startSession()">Start Drawer</button>
<div class="button" t-on-click="startSession()">Open session</div>
</footer>
</div>
<MoneyDetailsPopup t-ref="moneyDetails" t-on-money-details-validated="updateCashOpening"/>
</div>
</t>
</templates>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ClosePosPopup" owl="1">
<div role="dialog" class="modal-dialog">
<div class="popup close-pos-popup">
<header class="title">
CLOSING CONTROL
</header>
<main class="body">
<div class="session-overview">
<div class="overview-info">
<div class="info-title column-left">
<span>Total <t t-esc="ordersDetails.quantity"/> orders</span>
<span>Payments</span>
<span>Customer Account</span>
</div>
<div class="info-value column-left">
<span t-esc="env.pos.format_currency(ordersDetails.amount)"/>
<span t-esc="env.pos.format_currency(paymentsAmount)"/>
<span t-esc="env.pos.format_currency(payLaterAmount)"/>
</div>
</div>
<div t-if="openingNotes" class="opening-note">
<t t-esc="openingNotes"/>
</div>
</div>
<div class="payment-methods-overview">
<table>
<thead>
<tr>
<th>Payment Method</th>
<th>Expected</th>
<th style="width: 25%">Counted</th>
<th style="width: 20%">Difference</th>
</tr>
</thead>
<t t-if="cashControl">
<tbody>
<tr>
<td t-esc="defaultCashDetails.name"/>
<td t-esc="env.pos.format_currency(defaultCashDetails.amount)"/>
<td class="flex" t-on-input="handleInputChange()">
<input class="pos-input" type="number" t-model.number="state[defaultCashDetails.name].counted"/>
<div class="button icon" t-on-click="openDetailsPopup()">
<i class="fa fa-calculator" role="img" title="Open the money details popup"/>
</div>
</td>
<td t-esc="env.pos.format_currency(state[defaultCashDetails.name].difference)" t-att-class="{'warning': state[defaultCashDetails.name].difference}"/>
</tr>
</tbody>
<tbody class="cash-overview">
<tr>
<td>Opening</td>
<td t-esc="env.pos.format_currency(defaultCashDetails.opening)"/>
</tr>
<tr t-foreach="defaultCashDetails.moves" t-as="move" t-key="move.name">
<td>
<div class="flex">
<div class="cash-sign" t-esc="move.amount &lt; 0 ? '-' : '+'"/>
<t t-esc="move.name"/>
</div>
</td>
<td t-esc="env.pos.format_currency(Math.abs(move.amount))" />
</tr>
<tr t-if="defaultCashDetails.payment_amount">
<td>
<div class="flex">
<div class="cash-sign" t-esc="defaultCashDetails.payment_amount &lt; 0 ? '-' : '+'"/>
Payments in <t t-esc="defaultCashDetails.name"/>
</div>
</td>
<td t-esc="env.pos.format_currency(Math.abs(defaultCashDetails.payment_amount))"/>
</tr>
</tbody>
</t>
<tbody t-if="otherPaymentMethods.length &gt; 0">
<tr t-foreach="otherPaymentMethods" t-as="pm" t-key="pm.name">
<td t-esc="pm.name"/>
<td t-esc="env.pos.format_currency(pm.amount)"/>
</tr>
</tbody>
</table>
</div>
<textarea placeholder="Notes" class="closing-notes" t-model="state.notes"/>
<div t-if="cashControl" class="accept-closing" t-att-class="{invisible: !state[defaultCashDetails.name].difference}">
<input type="checkbox" id="accept" t-model="state.acceptClosing"/>
<label for="accept">Accept <t t-esc="defaultCashDetails.name"/> difference and post a profit/loss journal entry</label>
</div>
</main>
<footer class="footer">
<div class="button" t-on-click="cancel()">Continue Selling</div>
<div class="button" t-on-click="closePos()">Keep Session Open</div>
<div class="button" t-att-class="{'disabled': !canCloseSession()}" t-on-click="closeSession()">Close Session</div>
</footer>
</div>
<MoneyDetailsPopup t-ref="moneyDetails" t-on-money-details-validated="updateCountedCash"/>
</div>
</t>
</templates>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="MoneyDetailsPopup" owl="1">
<div class="popup money-details invisible">
<main class="body">
<div class="money-details-title">
Coins/Bills
</div>
<div class="money-details-info">
<div t-foreach="[firstHalfMoneyDetails, lastHalfMoneyDetails]" t-as="moneyDetailsList">
<t t-foreach="moneyDetailsList" t-as="moneyValue" t-key="moneyValue">
<div class="money-details-value" t-on-input="updateMoneyDetailsAmount()">
<input class="pos-input" t-att-id="moneyValue" type="number" t-model.number="state.moneyDetails[moneyValue]"/>
<CurrencyAmount forTarget="moneyValue" currency="currency" amount="moneyValue"/>
</div>
</t>
</div>
</div>
<div class="total-section">
<span>Total </span>
<CurrencyAmount currency="currency" amount="env.pos.format_currency_no_symbol(state.total)"/>
</div>
</main>
<footer class="footer">
<div class="button" t-on-click="discard()">Discard</div>
<div class="button" t-on-click="confirm()">Confirm</div>
</footer>
</div>
</t>
</templates>
@@ -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();
@@ -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();
@@ -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();
@@ -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 {
@@ -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,
}]);
@@ -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 */ `
<div><HeaderButton></HeaderButton></div>
`;
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);
+8 -5
View File
@@ -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
@@ -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)
@@ -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
@@ -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)
@@ -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)
@@ -11,30 +11,6 @@
</field>
</record>
<record id="view_account_bnk_stmt_cashbox" model="ir.ui.view">
<field name="name">account.bnk_stmt_cashbox.form</field>
<field name="model">account.bank.statement.cashbox</field>
<field name="inherit_id" ref="account.view_account_bnk_stmt_cashbox"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<field name="is_a_template" invisible="1"/>
</xpath>
</field>
</record>
<record id="view_account_bnk_stmt_cashbox_footer" model="ir.ui.view">
<field name="name">account.bnk_stmt_cashbox.form.close.modal</field>
<field name="model">account.bank.statement.cashbox</field>
<field name="inherit_id" ref="account.view_account_bnk_stmt_cashbox_footer"/>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='cashbox_lines_ids']" position="before">
<button name="set_default_cashbox" type="object" string="Set Default Cash Opening" />
</xpath>
</field>
</record>
<record id="view_account_journal_pos_user_form" model="ir.ui.view">
<field name="name">account.journal.pos.user.form.inherit</field>
<field name="model">account.journal</field>
@@ -94,9 +94,9 @@
<div class="col-6 o_kanban_primary_left">
<button t-if="record.current_session_state.raw_value === 'opened'" class="btn btn-primary" name="open_ui" type="object">Continue selling</button>
<button t-if="record.current_session_state.raw_value === 'new_session' || record.current_session_state.raw_value === 'opening_control'" class="btn btn-primary" name="open_existing_session_cb" type="object">Open Session</button>
<button t-if="record.current_session_state.raw_value === 'opening_control'" class="btn btn-primary" name="open_existing_session_cb" type="object">Open Session</button>
<button t-if="_.contains(['opened', 'closing_control'], record.current_session_state.raw_value)" class="btn btn-secondary" name="open_existing_session_cb" type="object">Close</button>
<button t-if="record.current_session_state.raw_value === 'closing_control'" class="btn btn-secondary" name="open_existing_session_cb" type="object">Close</button>
<button t-if="!record.current_session_id.raw_value &amp;&amp; !record.pos_session_username.value" class="btn btn-primary" name="open_session_cb" type="object">New Session</button>
@@ -114,7 +114,7 @@
<div t-if="record.last_session_closing_date.value" attrs='{"invisible": [("cash_control","=",False)]}' class="row">
<div class="col-6">
<span>Cash Balance</span>
<span>Last Closing Cash Balance</span>
</div>
<div class="col-6">
<span><field name="last_session_closing_cash" widget="monetary"/></span>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_bill_form" model="ir.ui.view">
<field name="name">pos.bill.form</field>
<field name="model">pos.bill</field>
<field name="arch" type="xml">
<form string="Bills">
<sheet>
<group>
<field name="name" />
<field name="value" />
<field name="pos_config_ids" widget="many2many_tags" />
</group>
</sheet>
</form>
</field>
</record>
<record id="view_pos_bill_tree" model="ir.ui.view">
<field name="name">pos.bill.tree</field>
<field name="model">pos.bill</field>
<field name="arch" type="xml">
<tree string="Bills" create="1" delete="1">
<field name="name" />
<field name="value" />
<field name="pos_config_ids" widget="many2many_tags" />
</tree>
</field>
</record>
<record id="action_pos_bill" model="ir.actions.act_window">
<field name="name">Coins/Bills</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.bill</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_pos_bill"
name="Coins/Bills"
parent="menu_point_config_product"
sequence="4"
action="action_pos_bill"
groups="group_pos_manager"/>
</odoo>
+18 -6
View File
@@ -43,7 +43,7 @@
</div>
<div class="o_notification_alert alert alert-warning" attrs="{'invisible':[('has_active_session','=', False)]}" role="alert">
A session is currently opened for this PoS. Some settings can only be changed after the session is closed.
<button class="btn" style="padding:0" name="open_existing_session_cb" type="object">Click here to close the session</button>
<button class="btn" style="padding:0" name="open_ui" type="object">Click here to close the session</button>
</div>
<div class="row mt16 o_settings_container">
<div id="company" class="col-12 col-lg-6 o_setting_box" groups="base.group_multi_company">
@@ -442,21 +442,33 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="col-12 col-lg-6 o_setting_box" attrs="{'invisible': [('cash_control', '=', False)]}">
<field name="cash_control" invisible="1" />
<div class="o_setting_left_pane">
<field name="cash_control" attrs="{'readonly': [('has_active_session','=', True)]}"/>
<field name="set_maximum_difference" />
</div>
<div class="o_setting_right_pane">
<label for="cash_control"/>
<label for="set_maximum_difference" />
<div class="text-muted">
Control cash box at opening and closing
Set a maximum difference allowed between the expected and counted cash during the closing of the session
</div>
<div attrs="{'invisible': [('cash_control', '=', False)]}">
<div class="content-group mt16" attrs="{'invisible': [('set_maximum_difference', '=', False)]}">
<label for="amount_authorized_diff" string="Authorized Difference" class="font-weight-normal"/>
<field name="amount_authorized_diff"/>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" attrs="{'invisible': [('cash_control', '=', False)]}">
<div class="o_setting_right_pane">
<label for="default_bill_ids" string="Coins/Bills" />
<div class="text-muted">
Set of coins/bills that will be used in opening and closing control
</div>
<div class="content-group mt16">
<field name="default_bill_ids" colspan="4" widget="many2many_tags" options="{'no_create_edit': True}" context="{'default_pos_config_ids':[(6, False, active_ids)]}"/>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
id="iface_tipproduct"
title="This product is used as reference on customer receipts.">
@@ -6,20 +6,12 @@
<field name="arch" type="xml">
<form string="Point of Sale Session" create="0" edit="0">
<header>
<button name="open_cashbox_pos" type="object" string="Open Session"
attrs="{'invisible' : ['|', ('cash_control', '=', False), ('state', '!=', 'new_session')]}" class="oe_highlight"
context="{'balance': 'start'}"/>
<button name="open_frontend_cb" type="object" string="Continue Selling"
attrs="{'invisible' : ['|', ('rescue', '=', True), ('state', 'not in', ['opening_control', 'opened'])]}"/>
<button id="end_session_opened" name="action_pos_session_closing_control" type="object" string="End of Session"
attrs="{'invisible' : ['|', ('cash_control', '=', False),('state', '!=', 'opened')]}"
class="oe_highlight"/>
<button id="validate_opened" name="action_pos_session_closing_control" type="object" string="Close Session &amp; Post Entries"
attrs="{'invisible' : ['|', ('cash_control', '=', True),('state', '!=', 'opened')]}"
class="oe_highlight"/>
<button id="validate_closing_control" name="action_pos_session_validate" type="object" string="Close Session &amp; Post Entries" states="closing_control"
class="oe_highlight"/>
<button id="validate_closing_control" name="action_pos_session_closing_control" type="object" string="Close Session &amp; Post Entries" states="closing_control"
attrs="{'invisible': [ '|', '&amp;',('state', '!=', 'closing_control'), ('rescue', '=', False),
'&amp;',('state', '=', 'closed'), ('rescue', '=', True)]}"
class="oe_highlight"/>
<field name="state" widget="statusbar" statusbar_visible="opened,closing_control,closed" nolabel="1" />
</header>
<sheet>
@@ -76,52 +68,6 @@
<field name="start_at" attrs="{'invisible' : [('state', '=', 'opening_control')]}"/>
<field name="stop_at" attrs="{'invisible' : [('state', '!=', 'closed')]}"/>
</group>
<notebook>
<page string="Cash Control" name="cash_control" attrs="{'invisible' : [('cash_control', '=', False)]}">
<group class="col-md-12" style="margin-left: 140px;">
<group class="col-md-7">
<div style="margin:0;padding:0;">
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', 'in', ['opening_control', 'closed'])]}">
<field style="text-align:right;margin:0;padding:0;" name="cash_register_total_entry_encoding" widget="monetary" options="{'currency_field': 'currency_id'}" string="+ Transactions"/>
</group>
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', '!=', 'closed')]}">
<field style="text-align:right;margin:0;padding:0;" name="cash_real_transaction" widget="monetary" options="{'currency_field': 'currency_id'}" string="+ Transactions"/>
</group>
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', 'in', ['opening_control', 'closed'])]}">
<field style="text-align:right;margin:0;padding:0;" name="cash_register_balance_end" widget="monetary" options="{'currency_field': 'currency_id'}" string="= Expected in Cash"/>
</group>
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', '!=', 'closed')]}">
<field style="text-align:right;margin:0;padding:0;" name="cash_real_expected" widget="monetary" options="{'currency_field': 'currency_id'}" string="= Expected"/>
</group>
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', 'in', ['opening_control'])]}">
<field style="text-align:right;margin:0;padding:0;" name="cash_register_balance_end_real" widget="monetary" options="{'currency_field': 'currency_id'}" string="Actual in Cash"/>
</group>
</div>
<div style="margin:0;padding:0;">
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', 'in', ['opening_control'])]}">
<button class="oe_inline oe_link" name="%(action_pos_box_out)d" type="action" attrs="{'invisible' : [('state', '=', 'closed')]}">Cash In/Out</button>
</group>
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', '=', 'opening_control')]}">
</group>
<group style="margin:0;padding:0;" attrs="{'invisible' : [('state', '=', 'opening_control')]}">
<button class="oe_inline oe_link" name="open_cashbox_pos" type="object" attrs="{'invisible': [('state', '!=', 'closing_control')]}" context="{'balance': 'close'}">Set Closing Cash</button>
</group>
</div>
</group>
</group>
<div class="col-md-4" attrs="{'invisible' : [('state', '!=', 'closing_control')]}" style="height: 30px">
<group class="oe_subtotal_footer">
<field name="cash_register_difference" widget="monetary" options="{'currency_field': 'currency_id'}" decoration-danger="cash_register_difference != 0"/>
</group>
</div>
<div class="col-md-4" attrs="{'invisible' : [('state', '!=', 'closed')]}" style="height: 30px">
<group class="oe_subtotal_footer">
<field name="cash_real_difference" widget="monetary" options="{'currency_field': 'currency_id'}" decoration-danger="cash_real_difference != 0"/>
</group>
</div>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="activity_ids"/>
@@ -233,24 +179,6 @@
<field name="delay_count">0</field>
</record>
<record model="ir.ui.view" id="closing_balance_confirm">
<field name="name">Confirming balance closing</field>
<field name="model">closing.balance.confirm.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form>
<group>
<span>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?</span>
</group>
<footer>
<button name="confirm_closing_balance" string="Confirm" type="object" class="oe_highlight, btn-primary" data-hotkey="q"/>
<button string="Discard" special="cancel" data-hotkey="z" class="oe_highlight, btn-secondary"/>
</footer>
</form>
</field>
</record>
<menuitem
id="menu_pos_session_all"
parent="menu_point_of_sale"
@@ -11,6 +11,7 @@ odoo.define('pos_coupon.tour.pos_coupon1', function (require) {
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// basic order
@@ -13,6 +13,7 @@ odoo.define('point_of_sale.tour.PosHr', function (require) {
startSteps();
ProductScreen.do.confirmOpeningPopup();
PosHr.check.loginScreenIsShown();
PosHr.do.clickLoginButton();
SelectionPopup.check.isShown();
@@ -15,6 +15,7 @@ odoo.define('pos_restaurant.tour.SplitBillScreen', function (require) {
startSteps();
FloorScreen.do.clickTable('T2');
ProductScreen.do.confirmOpeningPopup();
ProductScreen.exec.addOrderline('Water', '5', '2', '10.0');
ProductScreen.exec.addOrderline('Minute Maid', '3', '2', '6.0');
ProductScreen.exec.addOrderline('Coca-Cola', '1', '2', '2.0');
@@ -17,6 +17,7 @@ odoo.define('pos_restaurant.tour.TicketScreen', function (require) {
// Deleting the last order in the table brings back to floorscreen
FloorScreen.do.clickTable('T4');
ProductScreen.do.confirmOpeningPopup();
ProductScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, '-0001');
@@ -17,6 +17,7 @@ odoo.define('pos_restaurant.tour.TipScreen', function (require) {
// Create order that is synced when draft.
// order 1
FloorScreen.do.clickTable('T2');
ProductScreen.do.confirmOpeningPopup();
ProductScreen.exec.addOrderline('Minute Maid', '1', '2');
ProductScreen.check.totalAmountIs('2.0');
Chrome.do.backToFloor();
@@ -15,6 +15,7 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi
FloorScreen.do.clickTable('T5');
// Create first order
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickDisplayedProduct('Coca-Cola');
ProductScreen.check.selectedOrderlineHas('Coca-Cola');
ProductScreen.do.clickDisplayedProduct('Water');