[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:
committed by
Pierre Masereel
parent
fbf5217cb7
commit
442f856bb8
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -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 < 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 < 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 > 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);
|
||||
|
||||
|
||||
@@ -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 && !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>
|
||||
@@ -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 & 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 & Post Entries" states="closing_control"
|
||||
class="oe_highlight"/>
|
||||
|
||||
<button id="validate_closing_control" name="action_pos_session_closing_control" type="object" string="Close Session & Post Entries" states="closing_control"
|
||||
attrs="{'invisible': [ '|', '&',('state', '!=', 'closing_control'), ('rescue', '=', False),
|
||||
'&',('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');
|
||||
|
||||
Reference in New Issue
Block a user