diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index b8a6d5f6c8a..d038cfab430 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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: diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 79048dc8176..5f533501a31 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -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: diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index 197e200d820..ec1659b0041 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -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', diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index f6ec809e5ce..83aaa1cdc15 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -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 diff --git a/addons/point_of_sale/models/account_journal.py b/addons/point_of_sale/models/account_journal.py index 01371496185..3e41f4476b8 100644 --- a/addons/point_of_sale/models/account_journal.py +++ b/addons/point_of_sale/models/account_journal.py @@ -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') diff --git a/addons/point_of_sale/models/account_move.py b/addons/point_of_sale/models/account_move.py index e662a21f60a..3768ce9ba08 100644 --- a/addons/point_of_sale/models/account_move.py +++ b/addons/point_of_sale/models/account_move.py @@ -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' diff --git a/addons/point_of_sale/models/account_payment.py b/addons/point_of_sale/models/account_payment.py new file mode 100644 index 00000000000..51bfee038dd --- /dev/null +++ b/addons/point_of_sale/models/account_payment.py @@ -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 diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 7f2a55ee76e..34fe57c8096 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -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, diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 565a6f0560a..2e2ec4714a7 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -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, diff --git a/addons/point_of_sale/models/pos_payment.py b/addons/point_of_sale/models/pos_payment.py index 4b571f009a0..5470c00e8ad 100644 --- a/addons/point_of_sale/models/pos_payment.py +++ b/addons/point_of_sale/models/pos_payment.py @@ -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 diff --git a/addons/point_of_sale/models/pos_payment_method.py b/addons/point_of_sale/models/pos_payment_method.py index 42891c6a590..a7d8fd0452a 100644 --- a/addons/point_of_sale/models/pos_payment_method.py +++ b/addons/point_of_sale/models/pos_payment_method.py @@ -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) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 316fa5a864a..3c7f15c90b6 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -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 { diff --git a/addons/point_of_sale/models/res_config_settings.py b/addons/point_of_sale/models/res_config_settings.py index c2cb589e624..c3cfc256d99 100644 --- a/addons/point_of_sale/models/res_config_settings.py +++ b/addons/point_of_sale/models/res_config_settings.py @@ -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() diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 25e4b37059a..7d17250dff5 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -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; diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js index 53b858ba492..f8e6dd6564c 100644 --- a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js +++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js @@ -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; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index 49a9a612999..1216e3b8032 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -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'), diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index a4d41f3055b..f937d23ce1a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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(); diff --git a/addons/point_of_sale/views/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml index 0f2fabb548e..120557b56f2 100644 --- a/addons/point_of_sale/views/point_of_sale_report.xml +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -10,15 +10,6 @@ report - - - Invoice - pos.order - qweb-pdf - point_of_sale.report_invoice - 'Invoice - %s' % (object.name) - - Sales Details report.point_of_sale.report_saledetails @@ -26,5 +17,4 @@ point_of_sale.report_saledetails -