[FIX] point_of_sale: post diff on bank payment methods when closing the session

1. When the diff amount is for "combine" bank payment method, the
account.move record used to represent the combine amount is modified
to contain the diff amount. The journal entry is modified:

FROM:
```
                Debit                           Credit
Outstanding     combine_amount
Receivable                                      combine_amount
```

TO:
```
		Debit				Credit
Outstanding 	combine_amount - diff_amount
Receivable					combine_amount
Diff account	diff_amount
```

2. When the diff amount is for "split" bank payment method, an
account.move moving the diff amount from Outstanding to Diff account
is created. The journal entry is like:

```
                Debit                           Credit
Outstanding     diff_amount
Diff account    				diff_amount
```

closes odoo/odoo#77859

Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
This commit is contained in:
Joseph Caburnay
2021-10-08 11:46:44 +00:00
parent e98ddc8ce6
commit 7eb742f6b8
5 changed files with 139 additions and 29 deletions
+2 -2
View File
@@ -629,9 +629,9 @@ class AccountJournal(models.Model):
default_account_code = self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix)
default_account_vals = self._prepare_liquidity_account_vals(company, default_account_code, vals)
vals['default_account_id'] = self.env['account.account'].create(default_account_vals).id
if journal_type == 'cash' and not has_profit_account:
if journal_type in ('cash', 'bank') and not has_profit_account:
vals['profit_account_id'] = company.default_cash_difference_income_account_id.id
if journal_type == 'cash' and not has_loss_account:
if journal_type in ('cash', 'bank') and not has_loss_account:
vals['loss_account_id'] = company.default_cash_difference_expense_account_id.id
# === Fill missing refund_sequence ===
@@ -65,8 +65,8 @@
<field name="suspense_account_id"
attrs="{'required': [('type', 'in', ('bank', 'cash'))], 'invisible': [('type', 'not in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
<field name="profit_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
<field name="loss_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
<field name="profit_account_id" attrs="{'invisible': ['!', ('type', 'in', ('cash', 'bank'))]}"/>
<field name="loss_account_id" attrs="{'invisible': ['!', ('type', 'in', ('cash', 'bank'))]}"/>
<!-- Sales -->
<field name="default_account_id" string="Default Income Account"
attrs="{'required': [('type', '=', 'sale')], 'invisible': [('type', '!=', 'sale')]}"
+123 -20
View File
@@ -4,7 +4,7 @@
from collections import defaultdict
from datetime import timedelta
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_is_zero, float_compare
@@ -268,7 +268,8 @@ class PosSession(models.Model):
session.write(values)
return True
def action_pos_session_closing_control(self, balancing_account=False, amount_to_balance=0):
def action_pos_session_closing_control(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
bank_payment_method_diffs = bank_payment_method_diffs or {}
self._check_pos_session_balance()
for session in self:
if any(order.state == 'draft' for order in session.order_ids):
@@ -277,7 +278,7 @@ class PosSession(models.Model):
raise UserError(_('This session is already closed.'))
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)
return session.action_pos_session_close(balancing_account, amount_to_balance, bank_payment_method_diffs)
# 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:
@@ -289,7 +290,7 @@ class PosSession(models.Model):
session.cash_register_id.balance_end_real = total_cash
return session.action_pos_session_validate(balancing_account, amount_to_balance)
return session.action_pos_session_validate(balancing_account, amount_to_balance, bank_payment_method_diffs)
def _check_pos_session_balance(self):
for session in self:
@@ -297,18 +298,21 @@ 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, balancing_account=False, amount_to_balance=0):
def action_pos_session_validate(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
bank_payment_method_diffs = bank_payment_method_diffs or {}
self._check_pos_session_balance()
return self.action_pos_session_close(balancing_account, amount_to_balance)
return self.action_pos_session_close(balancing_account, amount_to_balance, bank_payment_method_diffs)
def action_pos_session_close(self, balancing_account=False, amount_to_balance=0):
def action_pos_session_close(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
bank_payment_method_diffs = bank_payment_method_diffs or {}
# 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()
return self._validate_session(balancing_account, amount_to_balance)
return self._validate_session(balancing_account, amount_to_balance, bank_payment_method_diffs)
def _validate_session(self, balancing_account=False, amount_to_balance=0):
def _validate_session(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
bank_payment_method_diffs = bank_payment_method_diffs or {}
self.ensure_one()
sudo = self.user_has_groups('point_of_sale.group_pos_user')
if self.order_ids or self.statement_ids.line_ids:
@@ -322,10 +326,10 @@ class PosSession(models.Model):
self._create_picking_at_end_of_session()
self.order_ids.filtered(lambda o: not o.is_total_cost_computed)._compute_total_cost_at_session_closing(self.picking_ids.move_lines)
try:
data = self.with_company(self.company_id)._create_account_move(balancing_account, amount_to_balance)
data = self.with_company(self.company_id)._create_account_move(balancing_account, amount_to_balance, bank_payment_method_diffs)
except AccessError as e:
if sudo:
data = self.sudo().with_company(self.company_id)._create_account_move(balancing_account, amount_to_balance)
data = self.sudo().with_company(self.company_id)._create_account_move(balancing_account, amount_to_balance, bank_payment_method_diffs)
else:
raise e
@@ -360,6 +364,7 @@ class PosSession(models.Model):
return True
def _close_session_action(self, amount_to_balance):
# NOTE This can't handle `bank_payment_method_diffs` because there is no field in the wizard that can carry it.
default_account = self._get_balancing_account()
wizard = self.env['pos.close.session.wizard'].create({
'amount_to_balance': amount_to_balance,
@@ -378,18 +383,23 @@ class PosSession(models.Model):
'context': {**self.env.context, 'active_ids': self.ids, 'active_model': 'pos.session'},
}
def close_session_from_ui(self):
def close_session_from_ui(self, bank_payment_method_diff_pairs=None):
"""Calling this method will try to close the session.
param bank_payment_method_diff_pairs: list[(int, float)]
Pairs of payment_method_id and diff_amount which will be used to post
loss/profit when closing 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.
"""
bank_payment_method_diffs = dict(bank_payment_method_diff_pairs or [])
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()
check_closing_session = self._cannot_close_session(bank_payment_method_diffs)
if check_closing_session:
return check_closing_session
@@ -399,7 +409,7 @@ class PosSession(models.Model):
# 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()
validate_result = self.action_pos_session_closing_control(bank_payment_method_diffs=bank_payment_method_diffs)
# 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
@@ -443,16 +453,72 @@ class PosSession(models.Model):
return {'successful': True}
def _cannot_close_session(self):
def _create_diff_account_move_for_split_payment_method(self, payment_method, diff_amount):
self.ensure_one()
get_diff_vals_result = self._get_diff_vals(payment_method.id, diff_amount)
if not get_diff_vals_result:
return
source_vals, dest_vals = get_diff_vals_result
diff_move = self.env['account.move'].create({
'journal_id': payment_method.journal_id.id,
'date': fields.Date.context_today(self),
'ref': self._get_diff_account_move_ref(payment_method),
'line_ids': [Command.create(source_vals), Command.create(dest_vals)]
})
diff_move._post()
def _get_diff_account_move_ref(self, payment_method):
return _('Closing difference in %s (%s)', payment_method.name, self.name)
def _get_diff_vals(self, payment_method_id, diff_amount):
payment_method = self.env['pos.payment.method'].browse(payment_method_id)
diff_compare_to_zero = self.currency_id.compare_amounts(diff_amount, 0)
source_account = payment_method.outstanding_account_id or self.company_id.account_journal_payment_debit_account_id
destination_account = self.env['account.account']
if (diff_compare_to_zero > 0):
destination_account = payment_method.journal_id.profit_account_id
elif (diff_compare_to_zero < 0):
destination_account = payment_method.journal_id.loss_account_id
if (diff_compare_to_zero == 0 or not source_account):
return False
amounts = self._update_amounts({'amount': 0, 'amount_converted': 0}, {'amount': diff_amount}, self.stop_at)
source_vals = self._debit_amounts({'account_id': source_account.id}, amounts['amount'], amounts['amount_converted'])
dest_vals = self._credit_amounts({'account_id': destination_account.id}, amounts['amount'], amounts['amount_converted'])
return [source_vals, dest_vals]
def _cannot_close_session(self, bank_payment_method_diffs=None):
"""
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
"""
bank_payment_method_diffs = bank_payment_method_diffs or {}
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}
if bank_payment_method_diffs:
no_loss_account = self.env['account.journal']
no_profit_account = self.env['account.journal']
for payment_method in self.env['pos.payment.method'].browse(bank_payment_method_diffs.keys()):
journal = payment_method.journal_id
compare_to_zero = self.currency_id.compare_amounts(bank_payment_method_diffs.get(payment_method.id), 0)
if compare_to_zero == -1 and not journal.loss_account_id:
no_loss_account |= journal
elif compare_to_zero == 1 and not journal.profit_account_id:
no_profit_account |= journal
message = ''
if no_loss_account:
message += _("Need loss account for the following journals to post the lost amount: %s\n", ', '.join(no_loss_account.mapped('name')))
if no_profit_account:
message += _("Need profit account for the following journals to post the gained amount: %s", ', '.join(no_profit_account.mapped('name')))
if message:
return {'successful': False, 'message': message, 'redirect': False}
def get_closing_control_data(self):
self.ensure_one()
@@ -498,6 +564,7 @@ class PosSession(models.Model):
'other_payment_methods': [{
'name': pm.name,
'amount': sum(orders.payment_ids.filtered(lambda p: p.payment_method_id == pm).mapped('amount')),
'number': len(orders.payment_ids.filtered(lambda p: p.payment_method_id == pm)),
'id': pm.id,
'type': pm.type,
} for pm in other_payment_method_ids],
@@ -554,7 +621,7 @@ class PosSession(models.Model):
propoerty_account = self.env['ir.property']._get('property_account_receivable_id', 'res.partner')
return self.company_id.account_default_pos_receivable_account_id or propoerty_account or self.env['account.account']
def _create_account_move(self, balancing_account=False, amount_to_balance=0):
def _create_account_move(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
""" Create account.move and account.move.line records for this session.
Side-effects include:
@@ -572,7 +639,7 @@ class PosSession(models.Model):
})
self.write({'move_id': account_move.id})
data = {}
data = {'bank_payment_method_diffs': bank_payment_method_diffs or {}}
data = self._accumulate_amounts(data)
data = self._create_non_reconciliable_move_lines(data)
data = self._create_bank_payment_moves(data)
@@ -792,12 +859,13 @@ class PosSession(models.Model):
def _create_bank_payment_moves(self, data):
combine_receivables_bank = data.get('combine_receivables_bank')
split_receivables_bank = data.get('split_receivables_bank')
bank_payment_method_diffs = data.get('bank_payment_method_diffs')
MoveLine = data.get('MoveLine')
payment_method_to_receivable_lines = {}
payment_to_receivable_lines = {}
for payment_method, amounts in combine_receivables_bank.items():
combine_receivable_line = MoveLine.create(self._get_combine_receivable_vals(payment_method, amounts['amount'], amounts['amount_converted']))
payment_receivable_line = self._create_combine_account_payment(payment_method, amounts)
payment_receivable_line = self._create_combine_account_payment(payment_method, amounts, diff_amount=bank_payment_method_diffs.get(payment_method.id) or 0)
payment_method_to_receivable_lines[payment_method] = combine_receivable_line | payment_receivable_line
for payment, amounts in split_receivables_bank.items():
@@ -805,6 +873,9 @@ class PosSession(models.Model):
payment_receivable_line = self._create_split_account_payment(payment, amounts)
payment_to_receivable_lines[payment] = split_receivable_line | payment_receivable_line
for bank_payment_method in self.payment_method_ids.filtered(lambda pm: pm.type == 'bank' and pm.split_transactions):
self._create_diff_account_move_for_split_payment_method(bank_payment_method, bank_payment_method_diffs.get(bank_payment_method.id) or 0)
data['payment_method_to_receivable_lines'] = payment_method_to_receivable_lines
data['payment_to_receivable_lines'] = payment_to_receivable_lines
return data
@@ -821,7 +892,7 @@ class PosSession(models.Model):
MoveLine.create(vals)
return data
def _create_combine_account_payment(self, payment_method, amounts):
def _create_combine_account_payment(self, payment_method, amounts, diff_amount):
outstanding_account = payment_method.outstanding_account_id or self.company_id.account_journal_payment_debit_account_id
destination_account = self._get_receivable_account(payment_method)
@@ -838,9 +909,29 @@ class PosSession(models.Model):
'pos_payment_method_id': payment_method.id,
'pos_session_id': self.id,
})
diff_amount_compare_to_zero = self.currency_id.compare_amounts(diff_amount, 0)
if diff_amount_compare_to_zero != 0:
self._apply_diff_on_account_payment_move(account_payment, payment_method, diff_amount)
account_payment.action_post()
return account_payment.move_id.line_ids.filtered(lambda line: line.account_id == account_payment.destination_account_id)
def _apply_diff_on_account_payment_move(self, account_payment, payment_method, diff_amount):
source_vals, dest_vals = self._get_diff_vals(payment_method.id, diff_amount)
outstanding_line = account_payment.move_id.line_ids.filtered(lambda line: line.account_id.id == source_vals['account_id'])
new_balance = outstanding_line.balance + diff_amount
new_balance_compare_to_zero = self.currency_id.compare_amounts(new_balance, 0)
account_payment.move_id.write({
'line_ids': [
Command.create(dest_vals),
Command.update(outstanding_line.id, {
'debit': new_balance_compare_to_zero > 0 and new_balance or 0.0,
'credit': new_balance_compare_to_zero < 0 and -new_balance or 0.0
})
]
})
def _create_split_account_payment(self, payment, amounts):
payment_method = payment.payment_method_id
if not payment_method.journal_id:
@@ -1318,6 +1409,17 @@ class PosSession(models.Model):
},
}
def _get_other_related_moves(self):
# TODO This is not an ideal way to get the diff account.move's for
# the session. It would be better if there is a relation field where
# these moves are saved.
# Unfortunately, the 'ref' of account.move is not indexed, so
# we are querying over the account.move.line because its 'ref' is indexed.
# And yes, we are only concern for split bank payment methods.
diff_lines_ref = [self._get_diff_account_move_ref(pm) for pm in self.payment_method_ids if pm.type == 'bank' and pm.split_transactions]
return self.env['account.move.line'].search([('ref', 'in', diff_lines_ref)]).mapped('move_id')
def _get_related_account_moves(self):
pickings = self.picking_ids | self.order_ids.mapped('picking_ids')
invoices = self.mapped('order_ids.account_move')
@@ -1325,7 +1427,8 @@ class PosSession(models.Model):
stock_account_moves = pickings.mapped('move_lines.account_move_ids')
cash_moves = self.cash_register_id.line_ids.mapped('move_id')
bank_payment_moves = self.bank_payment_ids.mapped('move_id')
return invoices | invoice_payments | self.move_id | stock_account_moves | cash_moves | bank_payment_moves
other_related_moves = self._get_other_related_moves()
return invoices | invoice_payments | self.move_id | stock_account_moves | cash_moves | bank_payment_moves | other_related_moves
def _get_receivable_account(self, payment_method):
"""Returns the default pos receivable account if no receivable_account_id is set on the payment method."""
@@ -39,12 +39,12 @@ odoo.define('point_of_sale.ClosePosPopup', function(require) {
// component state and refs definition
const state = {notes: '', acceptClosing: false, payments: {}};
if (this.cashControl) {
state.payments[this.defaultCashDetails.id] = {counted: 0, difference: -this.defaultCashDetails.amount};
state.payments[this.defaultCashDetails.id] = {counted: 0, difference: -this.defaultCashDetails.amount, number: 0};
}
if (this.otherPaymentMethods.length > 0) {
this.otherPaymentMethods.forEach(pm => {
if (pm.type === 'bank') {
state.payments[pm.id] = {counted: this.env.pos.round_decimals_currency(pm.amount), difference: 0}
state.payments[pm.id] = {counted: this.env.pos.round_decimals_currency(pm.amount), difference: 0, number: pm.number}
}
})
}
@@ -141,10 +141,13 @@ odoo.define('point_of_sale.ClosePosPopup', function(require) {
args: [this.env.pos.pos_session.id, this.state.notes]
})
try {
const bankPaymentMethodDiffPairs = this.otherPaymentMethods
.filter((pm) => pm.type == 'bank')
.map((pm) => [pm.id, this.state.payments[pm.id].difference]);
response = await this.rpc({
model: 'pos.session',
method: 'close_session_from_ui',
args: [this.env.pos.pos_session.id],
args: [this.env.pos.pos_session.id, bankPaymentMethodDiffPairs],
});
if (!response.successful) {
return this.handleClosingError(response);
@@ -176,6 +179,9 @@ odoo.define('point_of_sale.ClosePosPopup', function(require) {
window.location = '/web#action=point_of_sale.action_client_pos_menu';
}
}
_getShowDiff(pm) {
return pm.type == 'bank' && pm.number !== 0;
}
}
ClosePosPopup.template = 'ClosePosPopup';
@@ -78,8 +78,9 @@
<tr t-foreach="otherPaymentMethods" t-as="pm" t-key="pm.id">
<td t-esc="pm.name"/>
<td t-esc="env.pos.format_currency(pm.amount)"/>
<td t-if="pm.type === 'bank'" t-on-input="handleInputChange(pm.id)"><input class="pos-input" type="number" t-model.number="state.payments[pm.id].counted"/></td>
<td t-if="pm.type === 'bank'" t-esc="env.pos.format_currency(state.payments[pm.id].difference)" t-att-class="{'warning': state.payments[pm.id].difference}"/>
<t t-set="_showDiff" t-value="_getShowDiff(pm)" />
<td t-if="_showDiff" t-on-input="handleInputChange(pm.id)"><input class="pos-input" type="number" t-model.number="state.payments[pm.id].counted"/></td>
<td t-if="_showDiff" t-esc="env.pos.format_currency(state.payments[pm.id].difference)" t-att-class="{'warning': state.payments[pm.id].difference}"/>
</tr>
</tbody>
</table>