[IMP] point_of_sale: Use the cash rounding in the pos

Because of the law of the 1st december 2019, we can't pay with 1 or 2 cents anymore.
We have to round the total price to the 0.05 unit.
If the customer pay without rounding values, it triggers an error.
Before the order is finalized, we add a line with a product rounding to round the total amount.

Task-id: 2035010
X-original-commit: 58477e5503a18cc98fe686f8857233063eb26f4c
This commit is contained in:
Robin Heinz
2019-12-03 14:20:31 +00:00
committed by Pierre Masereel
parent aab5cab129
commit 42425996e3
10 changed files with 249 additions and 13 deletions
@@ -214,6 +214,9 @@ class PosConfig(models.Model):
company_has_template = fields.Boolean(string="Company has chart of accounts", compute="_compute_company_has_template")
current_user_id = fields.Many2one('res.users', string='Current Session Responsible', compute='_compute_current_session_user')
other_devices = fields.Boolean(string="Other Devices", help="Connect devices to your PoS without an IoT Box.")
rounding_method = fields.Many2one('account.cash.rounding', string="Cash rounding")
cash_rounding = fields.Boolean(string="Cash Rounding")
only_round_cash_method = fields.Boolean(string="Only apply rounding on cash")
@api.depends('use_pricelist', 'available_pricelist_ids')
def _compute_allowed_pricelist_ids(self):
@@ -307,6 +310,11 @@ class PosConfig(models.Model):
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):
if self.cash_rounding and self.rounding_method.strategy != 'add_invoice_line':
raise ValidationError(_("Cash rounding strategy must be: 'Add a rounding line'"))
@api.constrains('company_id', 'journal_id')
def _check_company_journal(self):
if self.journal_id and self.journal_id.company_id.id != self.company_id.id:
+21 -4
View File
@@ -8,8 +8,8 @@ import psycopg2
import pytz
from odoo import api, fields, models, tools, _
from odoo.tools import float_is_zero
from odoo.exceptions import UserError
from odoo.tools import float_is_zero, float_round
from odoo.exceptions import ValidationError, UserError
from odoo.http import request
from odoo.osv.expression import AND
import base64
@@ -271,7 +271,6 @@ class PosOrder(models.Model):
for order in self:
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)
@api.onchange('payment_ids', 'lines')
def _onchange_amount_all(self):
for order in self:
@@ -358,8 +357,15 @@ class PosOrder(models.Model):
def action_pos_order_paid(self):
self.ensure_one()
if not float_is_zero(self.amount_total - self.amount_paid, precision_rounding=self.currency_id.rounding):
if not self.config_id.cash_rounding:
total = self.amount_total
else:
total = float_round(self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method)
if not float_is_zero(total - self.amount_paid, precision_rounding=self.currency_id.rounding):
raise UserError(_("Order %s is not fully paid.") % self.name)
self.write({'state': 'paid'})
return True
@@ -389,6 +395,7 @@ class PosOrder(models.Model):
'invoice_date': order.date_order.date(),
'fiscal_position_id': order.fiscal_position_id.id,
'invoice_line_ids': [(0, None, order._prepare_invoice_line(line)) for line in order.lines],
'invoice_cash_rounding_id': order.config_id.rounding_method.id if order.config_id.cash_rounding else False
}
new_move = moves.sudo()\
.with_company(order.company_id)\
@@ -824,3 +831,13 @@ class ReportSaleDetails(models.AbstractModel):
configs = self.env['pos.config'].browse(data['config_ids'])
data.update(self.get_sale_details(data['date_start'], data['date_stop'], configs))
return data
class AccountCashRounding(models.Model):
_inherit = 'account.cash.rounding'
@api.constrains('rounding', 'rounding_method', 'strategy')
def _check_session_state(self):
open_session = self.env['pos.session'].search([('config_id.rounding_method', '=', self.id), ('state', '!=', 'closed')])
if open_session:
raise ValidationError(
_("You are not allowed to change the cash rounding configuration while a pos session using it is already opened."))
+29 -2
View File
@@ -6,7 +6,7 @@ from datetime import timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_is_zero
from odoo.tools import float_is_zero, float_compare
class PosSession(models.Model):
@@ -375,6 +375,7 @@ class PosSession(models.Model):
taxes = defaultdict(tax_amounts)
stock_expense = defaultdict(amounts)
stock_output = defaultdict(amounts)
rounding_difference = {'amount': 0.0, 'amount_converted': 0.0}
# Track the receivable lines of the invoiced orders' account moves for reconciliation
# These receivable lines are reconciled to the corresponding invoice receivable lines
# of this session's move_id.
@@ -400,7 +401,10 @@ class PosSession(models.Model):
if order.is_invoiced:
# Combine invoice receivable lines
key = order.partner_id.property_account_receivable_id.id
invoice_receivables[key] = self._update_amounts(invoice_receivables[key], {'amount': order.amount_total}, order.date_order)
if self.config_id.cash_rounding:
invoice_receivables[key] = self._update_amounts(invoice_receivables[key], {'amount': order.amount_paid}, order.date_order)
else:
invoice_receivables[key] = self._update_amounts(invoice_receivables[key], {'amount': order.amount_total}, order.date_order)
# side loop to gather receivable lines by account for reconciliation
for move_line in order.account_move.line_ids.filtered(lambda aml: aml.account_id.internal_type == 'receivable'):
order_account_move_receivable_lines[move_line.account_id.id] |= move_line
@@ -447,6 +451,10 @@ class PosSession(models.Model):
stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date)
stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date)
if self.config_id.cash_rounding:
diff = order.amount_paid - order.amount_total
rounding_difference = self._update_amounts(rounding_difference, {'amount': diff}, order.date_order)
if self.company_id.anglo_saxon_accounting:
global_session_pickings = self.picking_ids.filtered(lambda p: not p.pos_order_id)
if global_session_pickings:
@@ -480,6 +488,10 @@ class PosSession(models.Model):
'Please set corresponding tax account in each repartition line of the following taxes: \n%s'
) % ', '.join(tax_names_no_account)
raise UserError(error_message)
rounding_vals = []
if not float_is_zero(rounding_difference['amount'], precision_rounding=self.currency_id.rounding) or not float_is_zero(rounding_difference['amount_converted'], precision_rounding=self.currency_id.rounding):
rounding_vals = [self._get_rounding_difference_vals(rounding_difference['amount'], rounding_difference['amount_converted'])]
MoveLine.create(
tax_vals
@@ -487,6 +499,7 @@ class PosSession(models.Model):
+ [self._get_stock_expense_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in stock_expense.items()]
+ [self._get_split_receivable_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in split_receivables.items()]
+ [self._get_combine_receivable_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in combine_receivables.items()]
+ rounding_vals
)
## SECTION: Create cash statement lines and cash move lines
@@ -603,6 +616,20 @@ class PosSession(models.Model):
'taxes': taxes,
}
def _get_rounding_difference_vals(self, amount, amount_converted):
if self.config_id.cash_rounding:
partial_args = {
'name': 'Rounding line',
'move_id': self.move_id.id,
}
if float_compare(0.0, amount, precision_rounding=self.currency_id.rounding) > 0: # loss
partial_args['account_id'] = self.config_id.rounding_method.loss_account_id.id
return self._debit_amounts(partial_args, -amount, -amount_converted)
if float_compare(0.0, amount, precision_rounding=self.currency_id.rounding) < 0: # profit
partial_args['account_id'] = self.config_id.rounding_method.profit_account_id.id
return self._credit_amounts(partial_args, amount, amount_converted)
def _get_split_receivable_vals(self, payment, amount, amount_converted):
partial_vals = {
'account_id': payment.payment_method_id.receivable_account_id.id,
+42 -4
View File
@@ -419,6 +419,13 @@ exports.PosModel = Backbone.Model.extend({
return new exports.Product({}, product);
}));
},
},{
model: 'account.cash.rounding',
fields: ['name', 'rounding', 'rounding_method'],
domain: function(self){return [['id', '=', self.config.rounding_method[0]]]; },
loaded: function(self, cash_rounding) {
self.cash_rounding = cash_rounding;
}
},{
model: 'pos.payment.method',
fields: ['name', 'is_cash_count', 'use_payment_terminal'],
@@ -2446,10 +2453,12 @@ exports.Order = Backbone.Model.extend({
paymentlines: paymentlines,
subtotal: this.get_subtotal(),
total_with_tax: this.get_total_with_tax(),
total_rounded: this.get_total_with_tax() + this.get_rounding_applied(),
total_without_tax: this.get_total_without_tax(),
total_tax: this.get_total_tax(),
total_paid: this.get_total_paid(),
total_discount: this.get_total_discount(),
rounding_applied: this.get_rounding_applied(),
tax_details: this.get_tax_details(),
change: this.get_change(),
name : this.get_name(),
@@ -2734,7 +2743,7 @@ exports.Order = Backbone.Model.extend({
this.assert_editable();
var newPaymentline = new exports.Paymentline({},{order: this, payment_method:payment_method, pos: this.pos});
if(!payment_method.is_cash_count || this.pos.config.iface_precompute_cash){
newPaymentline.set_amount( this.get_due() );
newPaymentline.set_amount(this.get_due() );
};
this.paymentlines.add(newPaymentline);
this.select_paymentline(newPaymentline);
@@ -2812,7 +2821,7 @@ exports.Order = Backbone.Model.extend({
}
},
/* ---- Payment Status --- */
get_subtotal : function(){
get_subtotal: function(){
return round_pr(this.orderlines.reduce((function(sum, orderLine){
return sum + orderLine.get_display_price();
}), 0), this.pos.currency.rounding);
@@ -2942,7 +2951,7 @@ exports.Order = Backbone.Model.extend({
},
get_change: function(paymentline) {
if (!paymentline) {
var change = this.get_total_paid() - this.get_total_with_tax();
var change = this.get_total_paid() - this.get_total_with_tax() - this.get_rounding_applied();
} else {
var change = -this.get_total_with_tax();
var lines = this.paymentlines.models;
@@ -2957,7 +2966,7 @@ exports.Order = Backbone.Model.extend({
},
get_due: function(paymentline) {
if (!paymentline) {
var due = this.get_total_with_tax() - this.get_total_paid();
var due = this.get_total_with_tax() - this.get_total_paid() + this.get_rounding_applied();
} else {
var due = this.get_total_with_tax();
var lines = this.paymentlines.models;
@@ -2971,6 +2980,35 @@ exports.Order = Backbone.Model.extend({
}
return round_pr(due, this.pos.currency.rounding);
},
get_rounding_applied: function() {
if(this.pos.config.cash_rounding) {
var total = round_pr(this.get_total_with_tax(), this.pos.cash_rounding[0].rounding);
var rounding_applied = total - (this.pos.config['iface_tax_included'] === "total"? this.get_subtotal(): this.get_total_with_tax());
// because floor and ceil doesn't include decimals in calculation, we reuse the value of the half-up and adapt it.
if(this.pos.cash_rounding[0].rounding_method === "UP" && rounding_applied < 0) {
rounding_applied += this.pos.cash_rounding[0].rounding;
}
else if(this.pos.cash_rounding[0].rounding_method === "DOWN" && rounding_applied > 0){
rounding_applied -= this.pos.cash_rounding[0].rounding;
}
return rounding_applied;
}
return 0;
},
has_not_valid_rounding: function() {
if(!this.pos.config.cash_rounding)
return false;
var lines = this.paymentlines.models;
for(var i = 0; i < lines.length; i++) {
var line = lines[i];
if(!utils.float_is_zero(line.amount - round_pr(line.amount, this.pos.cash_rounding[0].rounding), 6))
return line;
}
return false;
},
is_paid: function(){
return this.get_due() <= 0;
},
+10 -1
View File
@@ -2093,7 +2093,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
var extradue = this.compute_extradue(order);
this.$('.paymentlines-container').empty();
var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
widget: this,
order: order,
paymentlines: lines,
@@ -2302,6 +2302,15 @@ var PaymentScreenWidget = ScreenWidget.extend({
return false;
}
if(order.has_not_valid_rounding()) {
var line = order.has_not_valid_rounding();
this.gui.show_popup('error',{
title: _t('Incorrect rounding'),
body: _t('You have to round your payments lines.' + line.amount + ' is not rounded.'),
});
return false;
}
// The exact amount must be paid if there is no cash payment method defined.
if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) {
var cash = false;
+8 -2
View File
@@ -658,7 +658,7 @@
<t t-if="!paymentlines.length">
<div class='paymentlines-empty'>
<div class='total'>
<t t-esc="widget.format_currency(order.get_total_with_tax())"/>
<t t-esc="widget.format_currency(order.get_total_with_tax() + order.get_rounding_applied())"/>
</div>
<div class='message'>
Please select a payment method.
@@ -678,7 +678,7 @@
<div class="payment-status-total-due">
<span class="label">Total Due</span>
<span>
<t t-esc="widget.format_currency(order.get_total_with_tax())" />
<t t-esc="widget.format_currency(order.get_total_with_tax() + order.get_rounding_applied())" />
</span>
</div>
</div>
@@ -994,6 +994,12 @@
TOTAL
<span t-esc='widget.format_currency(receipt.total_with_tax)' class="pos-receipt-right-align"/>
</div>
<t t-if="receipt.total_rounded != receipt.total_with_tax">
<div class="pos-receipt-amount">
ROUNDED
<span t-esc='widget.format_currency(receipt.total_rounded)' class="pos-receipt-right-align"/>
</div>
</t>
<br/><br/>
<!-- Payment Lines -->
@@ -21,6 +21,7 @@ class TestPoSBasicConfig(TestPoSCommon):
self.product1 = self.create_product('Product 1', self.categ_basic, 10.0, 5)
self.product2 = self.create_product('Product 2', self.categ_basic, 20.0, 10)
self.product3 = self.create_product('Product 3', self.categ_basic, 30.0, 15)
self.product4 = self.create_product('Product_4', self.categ_basic, 9.96, 4.98)
self.adjust_inventory([self.product1, self.product2, self.product3], [100, 50, 50])
def test_orders_no_invoiced(self):
@@ -441,3 +442,79 @@ class TestPoSBasicConfig(TestPoSCommon):
for line in cash_receivable_lines:
self.assertTrue(line.full_reconcile_id, msg='Each cash receivable line should be fully-reconciled.')
def test_rounding_method(self):
# set the cash rounding method
self.config.cash_rounding = True
self.config.rounding_method = self.env['account.cash.rounding'].create({
'name': 'add_invoice_line',
'rounding': 0.05,
'strategy': 'add_invoice_line',
'profit_account_id': self.company['default_cash_difference_income_account_id'].copy().id,
'loss_account_id': self.company['default_cash_difference_expense_account_id'].copy().id,
'rounding_method': 'HALF-UP',
})
self.open_new_session()
""" Test for orders: one with invoice
3 orders
- order 1, paid by cash
- order 2, paid by bank
- order 3, paid by bank, invoiced
Orders
======
+---------+----------+---------------+----------+-----+-------+
| order | payments | invoiced? | product | qty | total |
+---------+----------+---------------+----------+-----+-------+
| order 1 | bank | no | product1 | 6 | 60 |
| | | | product4 | 4 | 39.84 |
+---------+----------+---------------+----------+-----+-------+
| order 2 | bank | yes | product4 | 3 | 29.88 |
| | | | product2 | 20 | 400 |
+---------+----------+---------------+----------+-----+-------+
Expected Result
===============
+---------------------+---------+
| account | balance |
+---------------------+---------+
| sale | -596,56 |
| pos receivable bank | 516,64 |
| Rounding applied | -0,01 |
+---------------------+---------+
| Total balance | 0.0 |
+---------------------+---------+
"""
# create orders
orders = []
# create orders
orders = []
orders.append(self.create_ui_order_data(
[(self.product4, 3), (self.product2, 20)],
payments=[(self.bank_pm, 429.90)]
))
orders.append(self.create_ui_order_data(
[(self.product1, 6), (self.product4, 4)],
payments=[(self.bank_pm, 99.85)]
))
# sync orders
order = self.env['pos.order'].create_from_ui(orders)
self.assertEqual(orders[0]['data']['amount_return'], 0, msg='The amount return should be 0')
self.assertEqual(orders[1]['data']['amount_return'], 0, msg='The amount return should be 0')
# close the session
self.pos_session.action_pos_session_validate()
# check values after the session is closed
session_account_move = self.pos_session.move_id
rounding_line = session_account_move.line_ids.filtered(lambda line: line.name == 'Rounding line')
self.assertAlmostEqual(rounding_line.credit, 0.03, msg='The credit should be equals to 0.03')
@@ -342,6 +342,23 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" groups="account.group_cash_rounding">
<div class="o_setting_left_pane">
<field name="cash_rounding"/>
</div>
<div class="o_setting_right_pane">
<label for="cash_rounding"/>
<div class="text-muted">
Define the smallest coinage of the currency used to pay
</div>
<div class="content-group mt16" attrs="{'invisible': [('cash_rounding', '=', False)]}">
<div class="row mt16">
<label string="Rounding Method" for="rounding_method" class="col-lg-3 o_light_label" />
<field name="rounding_method" attrs="{'required' : [('cash_rounding', '=', True)]}"/>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="cash_control"/>
@@ -84,6 +84,11 @@
nolabel="1"
class="oe_subtotal_footer_separator"
widget="monetary"/>
<field name="amount_paid"
string="Total Paid (with rounding)"
class="oe_subtotal_footer_separator"
widget="monetary"
attrs="{'invisible': [('amount_paid','=', 'amount_total')]}"/>
</group>
<div class="oe_clear"/>
</page>
@@ -347,4 +352,17 @@
</search>
</field>
</record>
<record id="pos_rounding_form_view_inherited" model="ir.ui.view">
<field name="name">pos.cash.rounding.form.inherited</field>
<field name="model">account.cash.rounding</field>
<field name="inherit_id" ref="account.rounding_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="before">
<div class="o_notification_alert alert alert-warning" role="alert">
The Point of Sale only support the "add a rounding line" rounding strategy.
</div>
</xpath>
</field>
</record>
</odoo>
@@ -83,6 +83,25 @@
</div>
</div>
</div>
<h2>Payments</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_cash_rounding"/>
</div>
<div class="o_setting_right_pane">
<label for="group_cash_rounding"/>
<div class="text-muted">
Define the smallest coinage of the currency used to pay by cash
</div>
<div class="mt8">
<button name="%(account.rounding_list_action)d" icon="fa-arrow-right"
type="action" string="Cash Roundings" class="btn-link"
attrs="{'invisible': [('group_cash_rounding', '=', False)]}"/>
</div>
</div>
</div>
</div>
<h2>Payment Terminals</h2>
<div class="row mt16 o_settings_container" id="o_settings_use_payment_terminals">
<div class="col-12 col-lg-6 o_setting_box" title="The transactions are processed by Vantiv. Set your Vantiv credentials on the related payment method.">