[IMP] point_of_sale,account: revamp of accounting in pos

This commit makes some major changes in the Point of Sale module
when posting journal entries which allowed the following new features:

1. Show that the invoice generated is paid unlike before where invoice
doesn't appear to be paid (both in the pdf and in the backend).
2. User can create a 'Pay Later' payment method (created by default as
'Customer Account') to allow the customers to make credit in their
purchases.
3. An extension possibility where POS can settle customers dues.

The above is accomplished by doing the following:

1. Invoices are now reconciled to created account moves by POS using
the pos payments registered to the pos order. And this is done
right after the order is synced to the backend.
2. A new type of pos payment called 'pay_later' is introduced.
  - When an order is invoiced and is paid with 'pay_later' payment,
    the customer in the invoice will have `due` in his account which is
    immediately reflected when the order is synced.
  - When an order is not invoiced and is paid with 'pay_later' payment,
    the customer's 'due' reflects when the session is closed.
3. When the session is closed, POS payments from cash payment methods
are moved to the default account of the cash journal specified in the
payment method via creation of `account.bank.statement.line`s. (This is
already the case but it is mentioned this is for the purpose of
transparency.)
4. As for the bank pos payments, `account.payment` records are created
to move the payments to the specified `outstanding_account_id` in the
pos payment method. The `account.payment` records are written the bank
journal specified in the payment method.
5. As for the `pay_later` payments, we keep the behavior when no
journal is specified in the payment method where payments become
receivables to the customer who made the payments.

Other notable changes:
- The default POSS journal is changed to have 'general' type in order
to able to view the journal entries written in it.
- `pos.payment.method` can now be categorized as 'cash', 'bank' or 'pay_later'.
- `account_default_pos_receivable_account_id` can now be configured from
the Point of Sale general settings.
- `pos_invoice_report` is removed in favor of `account.account_invoices`
- disallow creating new payment method from `pos.config` form
- 'Journal Items' button in the `pos.session` form are now always visible.
  - This allows seeing the list of invoices (and their payment moves)
    to be accessible thru this button.

See test cases from the following spreadsheet:
https://docs.google.com/spreadsheets/d/1mt2jRSDU7OONPBFjwyTcnhRjITQI8rGMLLQA5K3fAjo/edit?usp=sharing

Part-of: odoo/odoo#74870
This commit is contained in:
Joseph Caburnay
2021-08-27 17:43:08 +00:00
parent 3f84e9b1f1
commit 6420b6d778
23 changed files with 493 additions and 247 deletions
+22 -21
View File
@@ -1558,31 +1558,32 @@ class AccountMove(models.Model):
def _get_reconciled_info_JSON_values(self):
self.ensure_one()
reconciled_vals = []
for partial, amount, counterpart_line in self._get_reconciled_invoices_partials():
if counterpart_line.move_id.ref:
reconciliation_ref = '%s (%s)' % (counterpart_line.move_id.name, counterpart_line.move_id.ref)
else:
reconciliation_ref = counterpart_line.move_id.name
reconciled_vals.append({
'name': counterpart_line.name,
'journal_name': counterpart_line.journal_id.name,
'amount': amount,
'currency': self.currency_id.symbol,
'digits': [69, self.currency_id.decimal_places],
'position': self.currency_id.position,
'date': counterpart_line.date,
'payment_id': counterpart_line.id,
'partial_id': partial.id,
'account_payment_id': counterpart_line.payment_id.id,
'payment_method_name': counterpart_line.payment_id.payment_method_line_id.name,
'move_id': counterpart_line.move_id.id,
'ref': reconciliation_ref,
})
reconciled_vals.append(self._get_reconciled_vals(partial, amount, counterpart_line))
return reconciled_vals
def _get_reconciled_vals(self, partial, amount, counterpart_line):
if counterpart_line.move_id.ref:
reconciliation_ref = '%s (%s)' % (counterpart_line.move_id.name, counterpart_line.move_id.ref)
else:
reconciliation_ref = counterpart_line.move_id.name
return {
'name': counterpart_line.name,
'journal_name': counterpart_line.journal_id.name,
'amount': amount,
'currency': self.currency_id.symbol,
'digits': [69, self.currency_id.decimal_places],
'position': self.currency_id.position,
'date': counterpart_line.date,
'payment_id': counterpart_line.id,
'partial_id': partial.id,
'account_payment_id': counterpart_line.payment_id.id,
'payment_method_name': counterpart_line.payment_id.payment_method_line_id.name,
'move_id': counterpart_line.move_id.id,
'ref': reconciliation_ref,
}
@api.depends('move_type', 'line_ids.amount_residual')
def _compute_payments_widget_reconciled_info(self):
for move in self:
+9 -6
View File
@@ -178,12 +178,7 @@ class AccountPayment(models.Model):
writeoff_lines = self.env['account.move.line']
for line in self.move_id.line_ids:
if line.account_id in (
self.journal_id.default_account_id,
self.payment_method_line_id.payment_account_id,
self.journal_id.company_id.account_journal_payment_debit_account_id,
self.journal_id.company_id.account_journal_payment_credit_account_id,
):
if line.account_id in self._get_valid_liquidity_accounts():
liquidity_lines += line
elif line.account_id.internal_type in ('receivable', 'payable') or line.partner_id == line.company_id.partner_id:
counterpart_lines += line
@@ -192,6 +187,14 @@ class AccountPayment(models.Model):
return liquidity_lines, counterpart_lines, writeoff_lines
def _get_valid_liquidity_accounts(self):
return (
self.journal_id.default_account_id,
self.payment_method_line_id.payment_account_id,
self.journal_id.company_id.account_journal_payment_debit_account_id,
self.journal_id.company_id.account_journal_payment_credit_account_id,
)
def _prepare_move_line_default_vals(self, write_off_line_vals=None):
''' Prepare the dictionary to create the default account.move.lines for the current payment.
:param write_off_line_vals: Optional dictionary to create a write-off account.move.line easily containing:
+2 -1
View File
@@ -27,6 +27,7 @@
'views/pos_category_view.xml',
'views/product_view.xml',
'views/account_journal_view.xml',
'views/res_config_settings_views.xml',
'views/pos_payment_method_views.xml',
'views/pos_payment_views.xml',
'views/pos_config_view.xml',
@@ -35,12 +36,12 @@
'data/point_of_sale_data.xml',
'views/pos_order_report_view.xml',
'views/account_statement_view.xml',
'views/res_config_settings_views.xml',
'views/digest_views.xml',
'views/res_partner_view.xml',
'views/report_userlabel.xml',
'views/report_saledetails.xml',
'views/point_of_sale_dashboard.xml',
'views/report_invoice.xml',
],
'demo': [
'data/point_of_sale_demo.xml',
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_bank_statement
from . import account_payment
from . import account_journal
from . import account_tax
from . import account_move
@@ -7,4 +7,4 @@ from odoo import fields, models, api
class AccountJournal(models.Model):
_inherit = 'account.journal'
pos_payment_method_ids = fields.One2many('pos.payment.method', 'cash_journal_id', string='Point of Sale Payment Methods')
pos_payment_method_ids = fields.One2many('pos.payment.method', 'journal_id', string='Point of Sale Payment Methods')
@@ -8,6 +8,7 @@ class AccountMove(models.Model):
_inherit = 'account.move'
pos_order_ids = fields.One2many('pos.order', 'account_move')
pos_payment_ids = fields.One2many('pos.payment', 'account_move_id')
def _stock_account_get_last_step_stock_moves(self):
stock_moves = super(AccountMove, self)._stock_account_get_last_step_stock_moves()
@@ -40,6 +41,13 @@ class AccountMove(models.Model):
return lot_values
def _get_reconciled_vals(self, partial, amount, counterpart_line):
"""Add pos_payment_name field in the reconciled vals to be able to show the payment method in the invoice."""
result = super()._get_reconciled_vals(partial, amount, counterpart_line)
if counterpart_line.move_id.pos_payment_ids:
pos_payment = counterpart_line.move_id.pos_payment_ids
result['pos_payment_name'] = pos_payment.payment_method_id.name
return result
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
class AccountPayment(models.Model):
_inherit = 'account.payment'
pos_payment_method_id = fields.Many2one('pos.payment.method', "POS Payment Method")
force_outstanding_account_id = fields.Many2one("account.account", "Forced Outstanding Account", check_company=True)
pos_session_id = fields.Many2one('pos.session', "POS Session")
def _get_valid_liquidity_accounts(self):
result = super()._get_valid_liquidity_accounts()
return result + (self.pos_payment_method_id.outstanding_account_id,)
@api.depends("force_outstanding_account_id")
def _compute_outstanding_account_id(self):
"""When force_outstanding_account_id is set, we use it as the outstanding_account_id."""
super()._compute_outstanding_account_id()
for payment in self:
if payment.force_outstanding_account_id:
payment.outstanding_account_id = payment.force_outstanding_account_id
+17 -29
View File
@@ -83,7 +83,7 @@ class PosConfig(models.Model):
return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).pos_type_id.id
def _default_sale_journal(self):
return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id), ('code', '=', 'POSS')], limit=1)
return self.env['account.journal'].search([('type', 'in', ('sale', 'general')), ('company_id', '=', self.env.company.id), ('code', '=', 'POSS')], limit=1)
def _default_invoice_journal(self):
return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)
@@ -111,9 +111,9 @@ class PosConfig(models.Model):
domain="[('code', '=', 'outgoing'), ('warehouse_id.company_id', '=', company_id)]",
ondelete='restrict')
journal_id = fields.Many2one(
'account.journal', string='Sales Journal',
domain=[('type', '=', 'sale')],
help="Accounting journal used to post sales entries.",
'account.journal', string='Point of Sale Journal',
domain=[('type', 'in', ('general', 'sale'))],
help="Accounting journal used to post POS session journal entries and POS invoice payments.",
default=_default_sale_journal,
ondelete='restrict')
invoice_journal_id = fields.Many2one(
@@ -364,7 +364,7 @@ class PosConfig(models.Model):
def _check_profit_loss_cash_journal(self):
if self.cash_control and self.payment_method_ids:
for method in self.payment_method_ids:
if method.is_cash_count and (not method.cash_journal_id.loss_account_id or not method.cash_journal_id.profit_account_id):
if method.is_cash_count and (not method.journal_id.loss_account_id or not method.journal_id.profit_account_id):
raise ValidationError(_("You need a loss and profit account on your cash journal."))
@api.constrains('company_id', 'invoice_journal_id')
@@ -393,24 +393,10 @@ class PosConfig(models.Model):
if any(
self.payment_method_ids\
.filtered(lambda pm: pm.is_cash_count)\
.mapped(lambda pm: self.currency_id not in (self.company_id.currency_id | pm.cash_journal_id.currency_id))
.mapped(lambda pm: self.currency_id not in (self.company_id.currency_id | pm.journal_id.currency_id))
):
raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))
@api.constrains('payment_method_ids')
def _check_payment_method_receivable_accounts(self):
# This is normally not supposed to happen to have a payment method without a receivable account set,
# as this is a required field. However, it happens the receivable account cannot be found during upgrades
# and this is a bommer to block the upgrade for that point, given the user can correct this by himself,
# without requiring a manual intervention from our upgrade support.
# However, this must be ensured this receivable is well set before opening a POS session.
invalid_payment_methods = self.payment_method_ids.filtered(lambda method: not method.receivable_account_id)
if invalid_payment_methods:
method_names = ", ".join(method.name for method in invalid_payment_methods)
raise ValidationError(
_("You must configure an intermediary account for the payment methods: %s.") % method_names
)
def _check_payment_method_ids(self):
self.ensure_one()
if not self.payment_method_ids:
@@ -642,7 +628,6 @@ class PosConfig(models.Model):
self._check_currencies()
self._check_profit_loss_cash_journal()
self._check_payment_method_ids()
self._check_payment_method_receivable_accounts()
self.env['pos.session'].create({
'user_id': self.env.uid,
'config_id': self.id
@@ -704,21 +689,24 @@ class PosConfig(models.Model):
if pos_config.payment_method_ids or pos_config.has_active_session:
continue
cash_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'cash')], limit=1)
pos_receivable_account = company.account_default_pos_receivable_account_id
bank_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'bank')], limit=1)
payment_methods = self.env['pos.payment.method']
if cash_journal:
payment_methods |= payment_methods.create({
'name': _('Cash'),
'receivable_account_id': pos_receivable_account.id,
'is_cash_count': True,
'cash_journal_id': cash_journal.id,
'journal_id': cash_journal.id,
'company_id': company.id,
})
if bank_journal:
payment_methods |= payment_methods.create({
'name': _('Bank'),
'journal_id': bank_journal.id,
'company_id': company.id,
})
payment_methods |= payment_methods.create({
'name': _('Bank'),
'receivable_account_id': pos_receivable_account.id,
'is_cash_count': False,
'name': _('Customer Account'),
'company_id': company.id,
'split_transactions': True,
})
pos_config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]})
@@ -729,7 +717,7 @@ class PosConfig(models.Model):
pos_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'POSS')])
if not pos_journal:
pos_journal = self.env['account.journal'].create({
'type': 'sale',
'type': 'general',
'name': 'Point of Sale',
'code': 'POSS',
'company_id': company.id,
+12 -2
View File
@@ -536,7 +536,6 @@ class PosOrder(models.Model):
self.ensure_one()
timezone = pytz.timezone(self._context.get('tz') or self.env.user.tz or 'UTC')
vals = {
'payment_reference': self.name,
'invoice_origin': self.name,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
'move_type': 'out_invoice' if self.amount_total >= 0 else 'out_refund',
@@ -574,6 +573,7 @@ class PosOrder(models.Model):
order.write({'account_move': new_move.id, 'state': 'invoiced'})
new_move.sudo().with_company(order.company_id)._post()
moves += new_move
order._apply_invoice_payments()
if not moves:
return {}
@@ -594,6 +594,16 @@ class PosOrder(models.Model):
def action_pos_order_cancel(self):
return self.write({'state': 'cancel'})
def _apply_invoice_payments(self):
receivable_account = self.env["res.partner"]._find_accounting_partner(self.partner_id).property_account_receivable_id
payment_moves = self.payment_ids._create_payment_moves()
invoice_receivable = self.account_move.line_ids.filtered(lambda line: line.account_id == receivable_account)
# Reconcile the invoice to the created payment moves.
# But not when the invoice's total amount is zero because it's already reconciled.
if not invoice_receivable.reconciled and receivable_account.reconcile:
payment_receivables = payment_moves.mapped('line_ids').filtered(lambda line: line.account_id == receivable_account)
(invoice_receivable | payment_receivables).reconcile()
@api.model
def create_from_ui(self, orders, draft=False):
""" Create and update Orders from the frontend PoS application.
@@ -715,7 +725,7 @@ class PosOrder(models.Model):
}
if self.mapped('account_move'):
report = self.env.ref('point_of_sale.pos_invoice_report')._render_qweb_pdf(self.ids[0])
report = self.env.ref('account.account_invoices')._render_qweb_pdf(self.account_move.ids[0])
filename = name + '.pdf'
attachment = self.env['ir.attachment'].create({
'name': filename,
+34 -1
View File
@@ -1,5 +1,5 @@
from odoo import api, fields, models, _
from odoo.tools import formatLang
from odoo.tools import formatLang, float_is_zero
from odoo.exceptions import ValidationError
@@ -31,6 +31,7 @@ class PosPayment(models.Model):
payment_status = fields.Char('Payment Status')
ticket = fields.Char('Payment Receipt Info')
is_change = fields.Boolean(string='Is this payment change?', default=False)
account_move_id = fields.Many2one('account.move')
@api.model
def name_get(self):
@@ -62,3 +63,35 @@ class PosPayment(models.Model):
def export_for_ui(self):
return self.mapped(self._export_for_ui) if self else []
def _create_payment_moves(self):
result = self.env['account.move']
for payment in self:
order = payment.pos_order_id
payment_method = payment.payment_method_id
if payment_method.type == 'pay_later' or float_is_zero(payment.amount, precision_rounding=order.currency_id.rounding):
continue
accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id)
pos_session = order.session_id
journal = pos_session.config_id.journal_id
payment_move = self.env['account.move'].with_context(default_journal_id=journal.id).create({
'journal_id': journal.id,
'date': fields.Date.context_today(payment),
'ref': _('Invoice payment for %s (%s) using %s') % (order.name, order.account_move.name, payment_method.name),
'pos_payment_ids': payment.ids,
})
result |= payment_move
payment.write({'account_move_id': payment_move.id})
amounts = pos_session._update_amounts({'amount': 0, 'amount_converted': 0}, {'amount': payment.amount}, payment.payment_date)
credit_line_vals = pos_session._credit_amounts({
'account_id': accounting_partner.property_account_receivable_id.id,
'partner_id': accounting_partner.id,
'move_id': payment_move.id,
}, amounts['amount'], amounts['amount_converted'])
debit_line_vals = pos_session._debit_amounts({
'account_id': pos_session.company_id.account_default_pos_receivable_account_id.id,
'move_id': payment_move.id,
}, amounts['amount'], amounts['amount_converted'])
self.env['account.move.line'].with_context(check_move_validity=False).create([credit_line_vals, debit_line_vals])
payment_move._post()
return result
@@ -3,18 +3,6 @@ from odoo.exceptions import UserError
class PosPaymentMethod(models.Model):
""" Used to classify pos.payment.
Generic characteristics of a pos.payment is described in this model.
E.g. A cash payment can be described by a pos.payment.method with
fields: is_cash_count = True and a cash_journal_id set to an
`account.journal` (type='cash') record.
When a pos.payment.method is cash, cash_journal_id is required as
it will be the journal where the account.bank.statement.line records
will be created.
"""
_name = "pos.payment.method"
_description = "Point of Sale Payment Methods"
_order = "id asc"
@@ -22,24 +10,31 @@ class PosPaymentMethod(models.Model):
def _get_payment_terminal_selection(self):
return []
name = fields.Char(string="Payment Method", required=True, translate=True)
name = fields.Char(string="Method", required=True, translate=True, help='Defines the name of the payment method that will be displayed in the Point of Sale when the payments are selected.')
outstanding_account_id = fields.Many2one('account.account',
string='Outstanding Account',
ondelete='restrict',
help='Leave empty to use the default account from the company setting.\n'
'Account used as outstanding account when creating accounting payment records for bank payments.')
receivable_account_id = fields.Many2one('account.account',
string='Intermediary Account',
required=True,
ondelete='restrict',
domain=[('reconcile', '=', True), ('user_type_id.type', '=', 'receivable')],
default=lambda self: self.env.company.account_default_pos_receivable_account_id,
help="Leave empty to use the default account from the company setting.\n"
"Overrides the company's receivable account (for Point of Sale) used in the journal entries.")
is_cash_count = fields.Boolean(string='Cash', compute="_compute_is_cash_count", store=True)
journal_id = fields.Many2one('account.journal',
string='Journal',
domain=[('type', 'in', ('cash', 'bank'))],
ondelete='restrict',
help='Account used as counterpart of the income account in the accounting entry representing the pos sales.')
is_cash_count = fields.Boolean(string='Cash')
cash_journal_id = fields.Many2one('account.journal',
string='Cash Journal',
domain=[('type', '=', 'cash')],
ondelete='restrict',
help='The payment method is of type cash. A cash statement will be automatically generated.')
help='Leave empty to use the receivable account of customer.\n'
'Defines the journal where to book the accumulated payments (or individual payment if Identify Customer is true) after closing the session.\n'
'For cash journal, we directly write to the default account in the journal via statement lines.\n'
'For bank journal, we write to the outstanding account specified in this payment method.')
split_transactions = fields.Boolean(
string='Split Transactions',
string='Identify Customer',
default=False,
help='If ticked, each payment will generate a separated journal item. Ticking that option will slow the closing of the PoS.')
help='Forces to set a customer when using this payment method and splits the journal entries for each customer. It could slow down the closing process.')
open_session_ids = fields.Many2many('pos.session', string='Pos Sessions', compute='_compute_open_session_ids', help='Open PoS sessions that are using this payment method.')
config_ids = fields.Many2many('pos.config', string='Point of Sale Configurations')
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
@@ -47,12 +42,13 @@ class PosPaymentMethod(models.Model):
hide_use_payment_terminal = fields.Boolean(compute='_compute_hide_use_payment_terminal', help='Technical field which is used to '
'hide use_payment_terminal when no payment interfaces are installed.')
active = fields.Boolean(default=True)
type = fields.Selection(selection=[('cash', 'Cash'), ('bank', 'Bank'), ('pay_later', 'Customer Account')], compute="_compute_type")
@api.depends('is_cash_count')
@api.depends('type')
def _compute_hide_use_payment_terminal(self):
no_terminals = not bool(self._fields['use_payment_terminal'].selection(self))
for payment_method in self:
payment_method.hide_use_payment_terminal = no_terminals or payment_method.is_cash_count
payment_method.hide_use_payment_terminal = no_terminals or payment_method.type in ('cash', 'pay_later')
@api.onchange('use_payment_terminal')
def _onchange_use_payment_terminal(self):
@@ -64,13 +60,24 @@ class PosPaymentMethod(models.Model):
for payment_method in self:
payment_method.open_session_ids = self.env['pos.session'].search([('config_id', 'in', payment_method.config_ids.ids), ('state', '!=', 'closed')])
@api.onchange('is_cash_count')
def _onchange_is_cash_count(self):
if not self.is_cash_count:
self.cash_journal_id = False
else:
@api.depends('journal_id', 'split_transactions')
def _compute_type(self):
for pm in self:
if pm.journal_id.type in {'cash', 'bank'}:
pm.type = pm.journal_id.type
else:
pm.type = 'pay_later'
@api.onchange('journal_id')
def _onchange_journal_id(self):
if self.is_cash_count:
self.use_payment_terminal = False
@api.depends('type')
def _compute_is_cash_count(self):
for pm in self:
pm.is_cash_count = pm.type == 'cash'
def _is_write_forbidden(self, fields):
return bool(fields and self.open_session_ids)
+219 -114
View File
@@ -97,6 +97,7 @@ class PosSession(models.Model):
total_payments_amount = fields.Float(compute='_compute_total_payments_amount', string='Total Payments Amount')
is_in_company_currency = fields.Boolean('Is Using Company Currency', compute='_compute_is_in_company_currency')
update_stock_at_closing = fields.Boolean('Stock should be updated at closing')
bank_payment_ids = fields.One2many('account.payment', 'pos_session_id', 'Bank Payments', help='Account payments representing aggregated and bank split payments.')
_sql_constraints = [('uniq_name', 'unique(name)', "The name of this POS Session must be unique !")]
@@ -159,7 +160,7 @@ class PosSession(models.Model):
if not cash_payment_methods:
continue
for statement in session.statement_ids:
if statement.journal_id == cash_payment_methods[0].cash_journal_id:
if statement.journal_id == cash_payment_methods[0].journal_id:
session.cash_control = session.config_id.cash_control
session.cash_journal_id = statement.journal_id.id
session.cash_register_id = statement.id
@@ -210,7 +211,7 @@ class PosSession(models.Model):
statement_ids = self.env['account.bank.statement']
if self.user_has_groups('point_of_sale.group_pos_user'):
statement_ids = statement_ids.sudo()
for cash_journal in cash_payment_methods.mapped('cash_journal_id'):
for cash_journal in cash_payment_methods.mapped('journal_id'):
ctx['journal_id'] = cash_journal.id if pos_config.cash_control and cash_journal.type == 'cash' else False
st_values = {
'journal_id': cash_journal.id,
@@ -344,11 +345,11 @@ class PosSession(models.Model):
if self.move_id.line_ids:
self.move_id._post()
self._reconcile_account_move_lines(data)
# Set the uninvoiced orders' state to 'done'
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
else:
self.move_id.unlink()
self._reconcile_account_move_lines(data)
else:
statement = self.cash_register_id
if not self.config_id.cash_control:
@@ -356,12 +357,7 @@ class PosSession(models.Model):
statement.button_post()
statement.button_validate()
self.write({'state': 'closed'})
return {
'type': 'ir.actions.client',
'name': 'Point of Sale Menu',
'tag': 'reload',
'params': {'menu_id': self.env.ref('point_of_sale.menu_point_root').id},
}
return True
def _close_session_action(self, amount_to_balance):
default_account = self._get_balancing_account()
@@ -452,6 +448,8 @@ class PosSession(models.Model):
data = {}
data = self._accumulate_amounts(data)
data = self._create_non_reconciliable_move_lines(data)
data = self._create_bank_payment_moves(data)
data = self._create_pay_later_receivable_lines(data)
data = self._create_cash_statement_lines_and_cash_move_lines(data)
data = self._create_invoice_receivable_lines(data)
data = self._create_stock_output_lines(data)
@@ -463,55 +461,78 @@ class PosSession(models.Model):
def _accumulate_amounts(self, data):
# Accumulate the amounts for each accounting lines group
# Each dict maps `key` -> `amounts`, where `key` is the group key.
# E.g. `combine_receivables` is derived from pos.payment records
# E.g. `combine_receivables_bank` is derived from pos.payment records
# in the self.order_ids with group key of the `payment_method_id`
# field of the pos.payment record.
amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0}
tax_amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0, 'base_amount': 0.0, 'base_amount_converted': 0.0}
split_receivables = defaultdict(amounts)
split_receivables_bank = defaultdict(amounts)
split_receivables_cash = defaultdict(amounts)
combine_receivables = defaultdict(amounts)
split_receivables_pay_later = defaultdict(amounts)
combine_receivables_bank = defaultdict(amounts)
combine_receivables_cash = defaultdict(amounts)
invoice_receivables = defaultdict(amounts)
combine_receivables_pay_later = defaultdict(amounts)
combine_invoice_receivables = defaultdict(amounts)
split_invoice_receivables = defaultdict(amounts)
sales = defaultdict(amounts)
taxes = defaultdict(tax_amounts)
stock_expense = defaultdict(amounts)
stock_return = 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
# Track the receivable lines of the order's invoice payment moves for reconciliation
# These receivable lines are reconciled to the corresponding invoice receivable lines
# of this session's move_id.
order_account_move_receivable_lines = defaultdict(lambda: self.env['account.move.line'])
combine_inv_payment_receivable_lines = defaultdict(lambda: self.env['account.move.line'])
split_inv_payment_receivable_lines = defaultdict(lambda: self.env['account.move.line'])
rounded_globally = self.company_id.tax_calculation_rounding_method == 'round_globally'
pos_receivable_account = self.company_id.account_default_pos_receivable_account_id
currency_rounding = self.currency_id.rounding
for order in self.order_ids:
# Combine pos receivable lines
# Separate cash payments for cash reconciliation later.
order_is_invoiced = order.is_invoiced
for payment in order.payment_ids:
amount, date = payment.amount, payment.payment_date
if payment.payment_method_id.split_transactions:
if payment.payment_method_id.is_cash_count:
split_receivables_cash[payment] = self._update_amounts(split_receivables_cash[payment], {'amount': amount}, date)
else:
split_receivables[payment] = self._update_amounts(split_receivables[payment], {'amount': amount}, date)
else:
key = payment.payment_method_id
if payment.payment_method_id.is_cash_count:
combine_receivables_cash[key] = self._update_amounts(combine_receivables_cash[key], {'amount': amount}, date)
else:
combine_receivables[key] = self._update_amounts(combine_receivables[key], {'amount': amount}, date)
amount = payment.amount
if float_is_zero(amount, precision_rounding=currency_rounding):
continue
date = payment.payment_date
payment_method = payment.payment_method_id
is_split_payment = payment.payment_method_id.split_transactions
payment_type = payment_method.type
if order.is_invoiced:
# Combine invoice receivable lines
key = order.partner_id
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' and not aml.reconciled):
order_account_move_receivable_lines[move_line.account_id.id] |= move_line
else:
# If not pay_later, we create the receivable vals for both invoiced and uninvoiced orders.
# Separate the split and aggregated payments.
# Moreover, if the order is invoiced, we create the pos receivable vals that will balance the
# pos receivable lines from the invoice payments.
if payment_type != 'pay_later':
if is_split_payment and payment_type == 'cash':
split_receivables_cash[payment] = self._update_amounts(split_receivables_cash[payment], {'amount': amount}, date)
elif not is_split_payment and payment_type == 'cash':
combine_receivables_cash[payment_method] = self._update_amounts(combine_receivables_cash[payment_method], {'amount': amount}, date)
elif is_split_payment and payment_type == 'bank':
split_receivables_bank[payment] = self._update_amounts(split_receivables_bank[payment], {'amount': amount}, date)
elif not is_split_payment and payment_type == 'bank':
combine_receivables_bank[payment_method] = self._update_amounts(combine_receivables_bank[payment_method], {'amount': amount}, date)
# Create the vals to create the pos receivables that will balance the pos receivables from invoice payment moves.
if order_is_invoiced:
if is_split_payment:
split_inv_payment_receivable_lines[payment] |= payment.account_move_id.line_ids.filtered(lambda line: line.account_id == pos_receivable_account)
split_invoice_receivables[payment] = self._update_amounts(split_invoice_receivables[payment], {'amount': payment.amount}, order.date_order)
else:
combine_inv_payment_receivable_lines[payment_method] |= payment.account_move_id.line_ids.filtered(lambda line: line.account_id == pos_receivable_account)
combine_invoice_receivables[payment_method] = self._update_amounts(combine_invoice_receivables[payment_method], {'amount': payment.amount}, order.date_order)
# If pay_later, we create the receivable lines.
# if split, with partner
# Otherwise, it's aggregated (combined)
# But only do if order is *not* invoiced because no account move is created for pay later invoice payments.
if payment_type == 'pay_later' and not order_is_invoiced:
if is_split_payment:
split_receivables_pay_later[payment] = self._update_amounts(split_receivables_pay_later[payment], {'amount': amount}, date)
elif not is_split_payment:
combine_receivables_pay_later[payment_method] = self._update_amounts(combine_receivables_pay_later[payment_method], {'amount': amount}, date)
if not order_is_invoiced:
order_taxes = defaultdict(tax_amounts)
for order_line in order.lines:
line = self._prepare_line(order_line)
@@ -589,16 +610,20 @@ class PosSession(models.Model):
'taxes': taxes,
'sales': sales,
'stock_expense': stock_expense,
'split_receivables': split_receivables,
'combine_receivables': combine_receivables,
'split_receivables_bank': split_receivables_bank,
'combine_receivables_bank': combine_receivables_bank,
'split_receivables_cash': split_receivables_cash,
'combine_receivables_cash': combine_receivables_cash,
'invoice_receivables': invoice_receivables,
'combine_invoice_receivables': combine_invoice_receivables,
'split_receivables_pay_later': split_receivables_pay_later,
'combine_receivables_pay_later': combine_receivables_pay_later,
'stock_return': stock_return,
'stock_output': stock_output,
'order_account_move_receivable_lines': order_account_move_receivable_lines,
'combine_inv_payment_receivable_lines': combine_inv_payment_receivable_lines,
'rounding_difference': rounding_difference,
'MoveLine': MoveLine
'MoveLine': MoveLine,
'split_invoice_receivables': split_invoice_receivables,
'split_inv_payment_receivable_lines': split_inv_payment_receivable_lines,
})
return data
@@ -612,8 +637,6 @@ class PosSession(models.Model):
taxes = data.get('taxes')
sales = data.get('sales')
stock_expense = data.get('stock_expense')
split_receivables = data.get('split_receivables')
combine_receivables = data.get('combine_receivables')
rounding_difference = data.get('rounding_difference')
MoveLine = data.get('MoveLine')
@@ -635,12 +658,74 @@ class PosSession(models.Model):
tax_vals
+ [self._get_sale_vals(key, amounts['amount'], amounts['amount_converted']) for key, amounts in sales.items()]
+ [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
)
return data
def _create_bank_payment_moves(self, data):
combine_receivables_bank = data.get('combine_receivables_bank')
split_receivables_bank = data.get('split_receivables_bank')
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_method_to_receivable_lines[payment_method] = combine_receivable_line | payment_receivable_line
for payment, amounts in split_receivables_bank.items():
split_receivable_line = MoveLine.create(self._get_split_receivable_vals(payment, amounts['amount'], amounts['amount_converted']))
payment_receivable_line = self._create_split_account_payment(payment, amounts)
payment_to_receivable_lines[payment] = split_receivable_line | payment_receivable_line
data['payment_method_to_receivable_lines'] = payment_method_to_receivable_lines
data['payment_to_receivable_lines'] = payment_to_receivable_lines
return data
def _create_pay_later_receivable_lines(self, data):
MoveLine = data.get('MoveLine')
combine_receivables_pay_later = data.get('combine_receivables_pay_later')
split_receivables_pay_later = data.get('split_receivables_pay_later')
vals = []
for payment_method, amounts in combine_receivables_pay_later.items():
vals.append(self._get_combine_receivable_vals(payment_method, amounts['amount'], amounts['amount_converted']))
for payment, amounts in split_receivables_pay_later.items():
vals.append(self._get_split_receivable_vals(payment, amounts['amount'], amounts['amount_converted']))
MoveLine.create(vals)
return data
def _create_combine_account_payment(self, payment_method, amounts):
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)
account_payment = self.env['account.payment'].create({
'amount': amounts['amount'],
'journal_id': payment_method.journal_id.id,
'force_outstanding_account_id': outstanding_account.id,
'destination_account_id': destination_account.id,
'ref': _('Combine %s POS payments from %s') % (payment_method.name, self.name),
'pos_payment_method_id': payment_method.id,
'pos_session_id': self.id,
})
account_payment.action_post()
return account_payment.move_id.line_ids.filtered(lambda line: line.account_id == account_payment.destination_account_id)
def _create_split_account_payment(self, payment, amounts):
payment_method = payment.payment_method_id
if not payment_method.journal_id:
return self.env['account.move.line']
outstanding_account = payment_method.outstanding_account_id or self.company_id.account_journal_payment_debit_account_id
account_payment = self.env['account.payment'].create({
'amount': amounts['amount'],
'partner_id': payment.partner_id.id,
'journal_id': payment_method.journal_id.id,
'force_outstanding_account_id': outstanding_account.id,
'ref': _('%s POS payment of %s in %s') % (payment_method.name, payment.partner_id.display_name, self.name),
'pos_payment_method_id': payment_method.id,
'pos_session_id': self.id,
})
account_payment.action_post()
return account_payment.move_id.line_ids.filtered(lambda line: line.account_id == account_payment.destination_account_id)
def _create_cash_statement_lines_and_cash_move_lines(self, data):
# Create the split and combine cash statement lines and account move lines.
# Keep the reference by statement for reconciliation.
@@ -657,16 +742,16 @@ class PosSession(models.Model):
split_cash_statement_line_vals = defaultdict(list)
split_cash_receivable_vals = defaultdict(list)
for payment, amounts in split_receivables_cash.items():
statement = statements_by_journal_id[payment.payment_method_id.cash_journal_id.id]
split_cash_statement_line_vals[statement].append(self._get_statement_line_vals(statement, payment.payment_method_id.receivable_account_id, amounts['amount'], date=payment.payment_date, partner=payment.pos_order_id.partner_id))
statement = statements_by_journal_id[payment.payment_method_id.journal_id.id]
split_cash_statement_line_vals[statement].append(self._get_split_statement_line_vals(statement, amounts['amount'], payment))
split_cash_receivable_vals[statement].append(self._get_split_receivable_vals(payment, amounts['amount'], amounts['amount_converted']))
# handle combine cash payments
combine_cash_statement_line_vals = defaultdict(list)
combine_cash_receivable_vals = defaultdict(list)
for payment_method, amounts in combine_receivables_cash.items():
if not float_is_zero(amounts['amount'] , precision_rounding=self.currency_id.rounding):
statement = statements_by_journal_id[payment_method.cash_journal_id.id]
combine_cash_statement_line_vals[statement].append(self._get_statement_line_vals(statement, payment_method.receivable_account_id, amounts['amount']))
statement = statements_by_journal_id[payment_method.journal_id.id]
combine_cash_statement_line_vals[statement].append(self._get_combine_statement_line_vals(statement, amounts['amount'], payment_method))
combine_cash_receivable_vals[statement].append(self._get_combine_receivable_vals(payment_method, amounts['amount'], amounts['amount_converted']))
# create the statement lines and account move lines
BankStatementLine = self.env['account.bank.statement.line']
@@ -675,8 +760,8 @@ class PosSession(models.Model):
split_cash_receivable_lines = {}
combine_cash_receivable_lines = {}
for statement in self.statement_ids:
split_cash_statement_lines[statement] = BankStatementLine.create(split_cash_statement_line_vals[statement])
combine_cash_statement_lines[statement] = BankStatementLine.create(combine_cash_statement_line_vals[statement])
split_cash_statement_lines[statement] = BankStatementLine.create(split_cash_statement_line_vals[statement]).mapped('move_id.line_ids').filtered(lambda line: line.account_id.internal_type == 'receivable')
combine_cash_statement_lines[statement] = BankStatementLine.create(combine_cash_statement_line_vals[statement]).mapped('move_id.line_ids').filtered(lambda line: line.account_id.internal_type == 'receivable')
split_cash_receivable_lines[statement] = MoveLine.create(split_cash_receivable_vals[statement])
combine_cash_receivable_lines[statement] = MoveLine.create(combine_cash_receivable_vals[statement])
@@ -691,27 +776,28 @@ class PosSession(models.Model):
def _create_invoice_receivable_lines(self, data):
# Create invoice receivable lines for this session's move_id.
# Keep reference of the invoice receivable lines because
# they are reconciled with the lines in order_account_move_receivable_lines
# they are reconciled with the lines in combine_inv_payment_receivable_lines
MoveLine = data.get('MoveLine')
invoice_receivables = data.get('invoice_receivables')
combine_invoice_receivables = data.get('combine_invoice_receivables')
split_invoice_receivables = data.get('split_invoice_receivables')
invoice_receivable_vals = defaultdict(list)
invoice_receivable_lines = {}
for partner, amounts in invoice_receivables.items():
commercial_partner = partner.commercial_partner_id
account_id = commercial_partner.property_account_receivable_id.id
invoice_receivable_vals[commercial_partner].append(self._get_invoice_receivable_vals(account_id, amounts['amount'], amounts['amount_converted'], partner=commercial_partner))
for commercial_partner, vals in invoice_receivable_vals.items():
account_id = commercial_partner.property_account_receivable_id.id
combine_invoice_receivable_vals = defaultdict(list)
split_invoice_receivable_vals = defaultdict(list)
combine_invoice_receivable_lines = {}
split_invoice_receivable_lines = {}
for payment_method, amounts in combine_invoice_receivables.items():
combine_invoice_receivable_vals[payment_method].append(self._get_invoice_receivable_vals(amounts['amount'], amounts['amount_converted']))
for payment, amounts in split_invoice_receivables.items():
split_invoice_receivable_vals[payment].append(self._get_invoice_receivable_vals(amounts['amount'], amounts['amount_converted']))
for payment_method, vals in combine_invoice_receivable_vals.items():
receivable_lines = MoveLine.create(vals)
for receivable_line in receivable_lines:
if (not receivable_line.reconciled):
if account_id not in invoice_receivable_lines:
invoice_receivable_lines[account_id] = receivable_line
else:
invoice_receivable_lines[account_id] |= receivable_line
combine_invoice_receivable_lines[payment_method] = receivable_lines
for payment, vals in split_invoice_receivable_vals.items():
receivable_lines = MoveLine.create(vals)
split_invoice_receivable_lines[payment] = receivable_lines
data.update({'invoice_receivable_lines': invoice_receivable_lines})
data.update({'combine_invoice_receivable_lines': combine_invoice_receivable_lines})
data.update({'split_invoice_receivable_lines': split_invoice_receivable_lines})
return data
def _create_stock_output_lines(self, data):
@@ -739,22 +825,26 @@ class PosSession(models.Model):
combine_cash_statement_lines = data.get('combine_cash_statement_lines')
split_cash_receivable_lines = data.get('split_cash_receivable_lines')
combine_cash_receivable_lines = data.get('combine_cash_receivable_lines')
order_account_move_receivable_lines = data.get('order_account_move_receivable_lines')
invoice_receivable_lines = data.get('invoice_receivable_lines')
combine_inv_payment_receivable_lines = data.get('combine_inv_payment_receivable_lines')
split_inv_payment_receivable_lines = data.get('split_inv_payment_receivable_lines')
combine_invoice_receivable_lines = data.get('combine_invoice_receivable_lines')
split_invoice_receivable_lines = data.get('split_invoice_receivable_lines')
stock_output_lines = data.get('stock_output_lines')
payment_method_to_receivable_lines = data.get('payment_method_to_receivable_lines')
payment_to_receivable_lines = data.get('payment_to_receivable_lines')
for statement in self.statement_ids:
if not self.config_id.cash_control:
statement.write({'balance_end_real': statement.balance_end})
statement.button_post()
all_lines = (
split_cash_statement_lines[statement].mapped('move_id.line_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
| combine_cash_statement_lines[statement].mapped('move_id.line_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
split_cash_statement_lines[statement]
| combine_cash_statement_lines[statement]
| split_cash_receivable_lines[statement]
| combine_cash_receivable_lines[statement]
)
accounts = all_lines.mapped('account_id')
lines_by_account = [all_lines.filtered(lambda l: l.account_id == account and not l.reconciled) for account in accounts]
lines_by_account = [all_lines.filtered(lambda l: l.account_id == account and not l.reconciled) for account in accounts if account.reconcile]
for lines in lines_by_account:
lines.reconcile()
# We try to validate the statement after the reconciliation is done
@@ -769,11 +859,26 @@ class PosSession(models.Model):
except UserError:
pass
# reconcile invoice receivable lines
for account_id in order_account_move_receivable_lines:
( order_account_move_receivable_lines[account_id]
| invoice_receivable_lines.get(account_id, self.env['account.move.line'])
).reconcile()
for payment_method, lines in payment_method_to_receivable_lines.items():
receivable_account = self._get_receivable_account(payment_method)
if receivable_account.reconcile:
lines.filtered(lambda line: not line.reconciled).reconcile()
for payment, lines in payment_to_receivable_lines.items():
if payment.partner_id.property_account_receivable_id.reconcile:
lines.filtered(lambda line: not line.reconciled).reconcile()
# Reconcile invoice payments' receivable lines. But we only do when the account is reconcilable.
# Though `account_default_pos_receivable_account_id` should be of type receivable, there is currently
# no constraint for it. Therefore, it is possible to put set a non-reconcilable account to it.
if self.company_id.account_default_pos_receivable_account_id.reconcile:
for payment_method in combine_inv_payment_receivable_lines:
lines = combine_inv_payment_receivable_lines[payment_method] | combine_invoice_receivable_lines.get(payment_method, self.env['account.move.line'])
lines.filtered(lambda line: not line.reconciled).reconcile()
for payment in split_inv_payment_receivable_lines:
lines = split_inv_payment_receivable_lines[payment] | split_invoice_receivable_lines.get(payment, self.env['account.move.line'])
lines.filtered(lambda line: not line.reconciled).reconcile()
# reconcile stock output lines
pickings = self.picking_ids.filtered(lambda p: not p.pos_order_id)
@@ -842,29 +947,28 @@ class PosSession(models.Model):
return self._credit_amounts(partial_args, amount, amount_converted)
def _get_split_receivable_vals(self, payment, amount, amount_converted):
accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id)
partial_vals = {
'account_id': payment.payment_method_id.receivable_account_id.id,
'account_id': accounting_partner.property_account_receivable_id.id,
'move_id': self.move_id.id,
'partner_id': self.env["res.partner"]._find_accounting_partner(payment.partner_id).id,
'partner_id': accounting_partner.id,
'name': '%s - %s' % (self.name, payment.payment_method_id.name),
}
return self._debit_amounts(partial_vals, amount, amount_converted)
def _get_combine_receivable_vals(self, payment_method, amount, amount_converted):
partial_vals = {
'account_id': payment_method.receivable_account_id.id,
'account_id': self._get_receivable_account(payment_method).id,
'move_id': self.move_id.id,
'name': '%s - %s' % (self.name, payment_method.name)
}
return self._debit_amounts(partial_vals, amount, amount_converted)
def _get_invoice_receivable_vals(self, account_id, amount, amount_converted, **kwargs):
partner = kwargs.get('partner', False)
def _get_invoice_receivable_vals(self, amount, amount_converted):
partial_vals = {
'account_id': account_id,
'account_id': self.company_id.account_default_pos_receivable_account_id.id,
'move_id': self.move_id.id,
'name': 'From invoiced orders',
'partner_id': partner and partner.id or False,
'name': _('From invoice payments'),
}
return self._credit_amounts(partial_vals, amount, amount_converted)
@@ -906,15 +1010,26 @@ class PosSession(models.Model):
partial_args = {'account_id': out_account.id, 'move_id': self.move_id.id}
return self._credit_amounts(partial_args, amount, amount_converted, force_company_currency=True)
def _get_statement_line_vals(self, statement, receivable_account, amount, date=False, partner=False):
def _get_combine_statement_line_vals(self, statement, amount, payment_method):
return {
'date': fields.Date.context_today(self, timestamp=date),
'date': fields.Date.context_today(self),
'amount': amount,
'payment_ref': self.name,
'statement_id': statement.id,
'journal_id': statement.journal_id.id,
'counterpart_account_id': receivable_account.id,
'partner_id': partner and self.env["res.partner"]._find_accounting_partner(partner).id
'counterpart_account_id': self._get_receivable_account(payment_method).id,
}
def _get_split_statement_line_vals(self, statement, amount, payment):
accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id)
return {
'date': fields.Date.context_today(self, timestamp=payment.payment_date),
'amount': amount,
'payment_ref': self.name,
'statement_id': statement.id,
'journal_id': statement.journal_id.id,
'counterpart_account_id': accounting_partner.property_account_receivable_id.id,
'partner_id': accounting_partner.id,
}
def _update_amounts(self, old_amounts, amounts_to_add, date, round=True, force_company_currency=False):
@@ -1063,27 +1178,17 @@ class PosSession(models.Model):
}
def _get_related_account_moves(self):
def get_matched_move_lines(aml):
if aml.credit > 0:
return [r.debit_move_id.id for r in aml.matched_debit_ids]
else:
return [r.credit_move_id.id for r in aml.matched_credit_ids]
pickings = self.picking_ids | self.order_ids.mapped('picking_ids')
invoices = self.mapped('order_ids.account_move')
invoice_payments = self.mapped('order_ids.payment_ids.account_move_id')
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
session_move = self.move_id
# get all the linked move lines to this account move.
non_reconcilable_lines = session_move.line_ids.filtered(lambda aml: not aml.account_id.reconcile)
reconcilable_lines = session_move.line_ids - non_reconcilable_lines
fully_reconciled_lines = reconcilable_lines.filtered(lambda aml: aml.full_reconcile_id)
partially_reconciled_lines = reconcilable_lines - fully_reconciled_lines
cash_move_lines = self.env['account.move.line'].search([('statement_id', '=', self.cash_register_id.id)])
ids = (non_reconcilable_lines.ids
+ fully_reconciled_lines.mapped('full_reconcile_id').mapped('reconciled_line_ids').ids
+ sum(partially_reconciled_lines.mapped(get_matched_move_lines), partially_reconciled_lines.ids)
+ cash_move_lines.ids)
return self.env['account.move.line'].browse(ids).mapped('move_id')
def _get_receivable_account(self, payment_method):
"""Returns the default pos receivable account if no receivable_account_id is set on the payment method."""
return payment_method.receivable_account_id or self.company_id.account_default_pos_receivable_account_id
def action_show_payments_list(self):
return {
@@ -12,6 +12,7 @@ class ResConfigSettings(models.TransientModel):
module_pos_six = fields.Boolean(string="Six Payment Terminal", help="The transactions are processed by Six. Set the IP address of the terminal on the related payment method.")
update_stock_quantities = fields.Selection(related="company_id.point_of_sale_update_stock_quantities", readonly=False)
module_pos_coupon = fields.Boolean("Coupon and Promotion Programs", help="Allow the use of coupon and promotion programs in PoS.")
account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False)
def set_values(self):
super(ResConfigSettings, self).set_values()
@@ -1511,6 +1511,10 @@ td {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
/* make the name of the payment line to be truncated with ellipsis */
white-space: nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
.paymentline .payment-amount {
margin-left: 10px;
@@ -42,11 +42,19 @@ odoo.define('point_of_sale.InvoiceButton', function (require) {
}
async _downloadInvoice(orderId) {
try {
await this.env.pos.do_action('point_of_sale.pos_invoice_report', {
additional_context: {
active_ids: [orderId],
},
const [orderWithInvoice] = await this.rpc({
method: 'read',
model: 'pos.order',
args: [orderId, ['account_move']],
kwargs: { load: false },
});
if (orderWithInvoice && orderWithInvoice.account_move) {
await this.env.pos.do_action('account.account_invoices', {
additional_context: {
active_ids: [orderWithInvoice.account_move],
},
});
}
} catch (error) {
if (error instanceof Error) {
throw error;
@@ -227,16 +227,29 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
return !this.error? 'ReceiptScreen' : 'ProductScreen';
}
async _isOrderValid(isForceValidate) {
if (this.currentOrder.get_orderlines().length === 0) {
if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) {
this.showPopup('ErrorPopup', {
title: this.env._t('Empty Order'),
body: this.env._t(
'There must be at least one product in your order before it can be validated'
'There must be at least one product in your order before it can be validated and invoiced.'
),
});
return false;
}
const splitPayments = this.paymentLines.filter(payment => payment.payment_method.split_transactions)
if (splitPayments.length && !this.currentOrder.get_client()) {
const paymentMethod = splitPayments[0].payment_method
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Customer Required'),
body: _.str.sprintf(this.env._t('Customer is required for %s payment method.'), paymentMethod.name),
});
if (confirmed) {
this.selectClient();
}
return false;
}
if ((this.currentOrder.is_to_invoice() || this.currentOrder.is_to_ship()) && !this.currentOrder.get_client()) {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Please select the Customer'),
+20 -9
View File
@@ -512,7 +512,7 @@ exports.PosModel = Backbone.Model.extend({
}
},{
model: 'pos.payment.method',
fields: ['name', 'is_cash_count', 'use_payment_terminal'],
fields: ['name', 'is_cash_count', 'use_payment_terminal', 'split_transactions', 'type'],
domain: function(self){return ['|',['active', '=', false], ['active', '=', true]]; },
loaded: function(self, payment_methods) {
self.payment_methods = payment_methods.sort(function(a,b){
@@ -1000,18 +1000,29 @@ exports.PosModel = Backbone.Model.extend({
});
// on success, get the order id generated by the server
transfer.then(function(order_server_id){
transfer.then(async function(order_server_id){
// generate the pdf and download it
if (order_server_id.length) {
self.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}}).then(function () {
resolveInvoiced(order_server_id);
resolveDone();
}).guardedCatch(function (error) {
const [orderWithInvoice] = await self.rpc({
method: 'read',
model: 'pos.order',
args: [order_server_id, ['account_move']],
kwargs: { load: false },
});
if (orderWithInvoice && orderWithInvoice.account_move) {
self.do_action('account.account_invoices',{additional_context:{
active_ids:[orderWithInvoice.account_move],
}}).then(function () {
resolveInvoiced(order_server_id);
resolveDone();
}).catch(error => {
rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}});
rejectDone();
});
} else {
rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}});
rejectDone();
});
}
} else if (order_server_id.length) {
resolveInvoiced(order_server_id);
resolveDone();
@@ -10,15 +10,6 @@
<field name="binding_type">report</field>
</record>
<!-- used from POS UI, no need to be in print menu -->
<record id="pos_invoice_report" model="ir.actions.report">
<field name="name">Invoice</field>
<field name="model">pos.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">point_of_sale.report_invoice</field>
<field name="print_report_name">'Invoice - %s' % (object.name)</field>
</record>
<record id="sale_details_report" model="ir.actions.report">
<field name="name">Sales Details</field>
<field name="model">report.point_of_sale.report_saledetails</field>
@@ -26,5 +17,4 @@
<field name="report_name">point_of_sale.report_saledetails</field>
</record>
<template id="report_invoice" inherit_id="account.report_invoice" primary="True"/>
</odoo>
@@ -408,7 +408,7 @@
Payment methods available
</div>
<div class="content-group mt16">
<field name="payment_method_ids" colspan="4" nolabel="1" widget="many2many_tags" required="1" attrs="{'readonly': [('has_active_session','=', True)]}"/>
<field name="payment_method_ids" colspan="4" nolabel="1" widget="many2many_tags" required="1" attrs="{'readonly': [('has_active_session','=', True)]}" options="{'no_create': True}" />
</div>
<div>
<button name="%(action_payment_methods_tree)d" icon="fa-arrow-right" type="action" string="Payment Methods" class="btn-link"/>
@@ -8,21 +8,35 @@
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="active" invisible="1"/>
<field name="type" invisible="1" />
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Cash" class="oe_inline"/></h1>
</div>
<div class="o_notification_alert alert alert-warning" attrs="{'invisible':['|', ('type', '!=', 'pay_later'), '&amp;', ('type', '=', 'pay_later'), ('split_transactions', '=', True)]}" role="alert">
Only cash and bank journals are allowed in the <strong>Journal</strong> field.
</div>
<group name="Payment methods">
<field name="hide_use_payment_terminal" invisible="1"/>
<group>
<field name="hide_use_payment_terminal" invisible="1"/>
<field name="receivable_account_id" groups="account.group_account_readonly" />
<field name="is_cash_count" />
<field name="cash_journal_id" attrs="{'invisible': [('is_cash_count', '=', False)], 'required': [('is_cash_count', '=', True)]}" />
<field name="split_transactions" groups="base.group_no_one" />
<field name="split_transactions"/>
<field name="journal_id" attrs="{'required': [('split_transactions', '=', False)]}" placeholder="Leave empty to use the receivable account of customer" />
<field name="outstanding_account_id" groups="account.group_account_readonly" attrs="{'invisible': [('type', '!=', 'bank')]}" placeholder="Leave empty to use the default account from the company setting" />
<field name="receivable_account_id" groups="account.group_account_readonly" attrs="{'invisible': [('split_transactions', '=', True)]}" placeholder="Leave empty to use the default account from the company setting" />
<field name="company_id" readonly="1" groups="base.group_multi_company" />
</group>
<group>
<field name="use_payment_terminal" attrs="{'invisible': [('hide_use_payment_terminal', '=', True)]}" />
<group attrs="{'invisible': ['|', ('hide_use_payment_terminal', '=', False), ('type', 'in', ['cash', 'pay_later'])]}">
<div>
<h2>Tips:</h2>
<p>
Go to <a href="#" name="%(action_pos_configuration)d" type="action" class="btn-link o_form_uri" role="button">Configurations > Settings</a>
<strong> > Payment Terminals</strong>
in order to install a Payment Terminal and make a fully integrated payment method.
</p>
</div>
</group>
<group attrs="{'invisible': [('hide_use_payment_terminal', '=', True)]}">
<field name="use_payment_terminal" />
</group>
</group>
</sheet>
@@ -34,13 +48,13 @@
<field name="name">pos.payment.method.tree</field>
<field name="model">pos.payment.method</field>
<field name="arch" type="xml">
<tree string="Payment Methods">
<field name="hide_use_payment_terminal" invisible="1"/>
<tree string="Payment Methods" create="1" delete="1">
<field name="type" invisible="1"/>
<field name="name" />
<field name="receivable_account_id" groups="account.group_account_readonly"/>
<field name="is_cash_count" />
<field name="split_transactions" groups="base.group_no_one" />
<field name="use_payment_terminal" attrs="{'invisible': [('hide_use_payment_terminal', '=', True)]}" />
<field name="split_transactions" optional="hide" />
<field name="journal_id" attrs="{'required': [('split_transactions', '=', False)]}" />
<field name="outstanding_account_id" groups="account.group_account_readonly" optional="hide" attrs="{'invisible': [('type', '!=', 'bank')]}" />
<field name="receivable_account_id" groups="account.group_account_readonly" optional="hide" attrs="{'invisible': [('split_transactions', '=', True)]}" />
<field name="company_id" groups="base.group_multi_company" />
</tree>
</field>
@@ -51,7 +51,6 @@
class="oe_stat_button"
icon="fa-bars"
string="Journal Items"
attrs="{'invisible':[('move_id', '=', False)]}"
groups="account.group_account_readonly"
>
</button>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="point_of_sale.report_invoice_document" inherit_id="account.report_invoice_document">
<xpath expr="//i[hasclass('oe_payment_label')]" position="inside">
<t t-if="payment_vals.get('pos_payment_name')">
using <t t-esc="payment_vals['pos_payment_name']" />
</t>
</xpath>
</template>
</odoo>
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Point of sale" string="Point of Sale" data-key="point_of_sale" groups="point_of_sale.group_pos_manager">
<h2>Taxes</h2>
<h2>Accounting</h2>
<div class="row mt16 o_settings_container" name="taxes_setting_container">
<div class="col-12 col-lg-6 o_setting_box"
id="default_sales_tax_setting"
@@ -28,6 +28,21 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
groups="account.group_account_readonly"
title="This account is used as intermediary account when nothing is set in a payment method.">
<div class="o_setting_right_pane">
<div>
<label string="Default Intermediary Account" for="sale_tax_id"/>
<div class="text-muted">
This account is used as intermediary account when nothing is set in a payment method.
</div>
<div class="content-group mt16">
<field name="account_default_pos_receivable_account_id" colspan="4" nolabel="1" domain="[('reconcile', '=', True), ('user_type_id.type', '=', 'receivable'), ('company_id', '=', company_id)]"/>
</div>
</div>
</div>
</div>
</div>
<h2>Pricing</h2>
<div class="row mt16 o_settings_container" name="pricing_setting_container">