- Please use the following communication for your payment : +
+ Please use the following communication for your payment :
diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml index 04a7e649e08..c472cd3ff68 100644 --- a/addons/account/views/report_payment_receipt_templates.xml +++ b/addons/account/views/report_payment_receipt_templates.xml @@ -6,8 +6,8 @@
| - | + | - - - + + + |
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index 9e2b8869f3c..fefa89d4e4a 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -432,16 +432,6 @@
Bank & Cash
-
-
-
-
-
-
- The payments which have not been matched with a bank statement will not be shown in bank reconciliation data if they were made before this date
-
-
diff --git a/addons/account/wizard/__init__.py b/addons/account/wizard/__init__.py
index 9be1a3ef8d1..a130d327938 100644
--- a/addons/account/wizard/__init__.py
+++ b/addons/account/wizard/__init__.py
@@ -16,3 +16,4 @@ from . import wizard_tax_adjustments
from . import account_invoice_send
from . import base_document_layout
from . import account_transfer_wizard
+from . import account_payment_register
diff --git a/addons/account/wizard/account_invoice_send.py b/addons/account/wizard/account_invoice_send.py
index 7ef17afc7c1..90861b0c91e 100644
--- a/addons/account/wizard/account_invoice_send.py
+++ b/addons/account/wizard/account_invoice_send.py
@@ -88,7 +88,7 @@ class AccountInvoiceSend(models.TransientModel):
if self.env.context.get('mark_invoice_as_sent'):
#Salesman send posted invoice, without the right to write
#but they should have the right to change this flag
- self.mapped('invoice_ids').sudo().write({'invoice_sent': True})
+ self.mapped('invoice_ids').sudo().write({'is_move_sent': True})
def _print_document(self):
""" to override for each type of models that will use this composer."""
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py
new file mode 100644
index 00000000000..c36f5bbfb9c
--- /dev/null
+++ b/addons/account/wizard/account_payment_register.py
@@ -0,0 +1,504 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+
+
+class AccountPaymentRegister(models.TransientModel):
+ _name = 'account.payment.register'
+ _description = 'Register Payment'
+
+ # == Business fields ==
+ payment_date = fields.Date(String="Payment Date", required=True,
+ default=fields.Date.context_today)
+ amount = fields.Monetary(currency_field='currency_id', store=True, readonly=False,
+ compute='_compute_amount')
+ communication = fields.Char(string="Memo", store=True, readonly=False,
+ compute='_compute_communication')
+ group_payment = fields.Boolean(string="Group Payments", store=True, readonly=False,
+ compute='_compute_group_payment',
+ help="Only one payment will be created by partner (bank)/ currency.")
+ currency_id = fields.Many2one('res.currency', string='Currency', store=True, readonly=False,
+ compute='_compute_currency_id',
+ help="The payment's currency.")
+ journal_id = fields.Many2one('account.journal', store=True, readonly=False,
+ compute='_compute_journal_id',
+ domain="[('company_id', '=', company_id), ('type', 'in', ('bank', 'cash'))]")
+ partner_bank_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account",
+ readonly=False, store=True,
+ compute='_compute_partner_bank_id',
+ domain="['|', ('company_id', '=', False), ('company_id', '=', company_id), ('partner_id', '=', partner_id)]")
+ company_currency_id = fields.Many2one('res.currency', string="Company Currency",
+ related='company_id.currency_id')
+
+ # == Fields given through the context ==
+ line_ids = fields.Many2many('account.move.line', 'account_payment_register_move_line_rel', 'wizard_id', 'line_id',
+ string="Journal items", readonly=True, copy=False,)
+ payment_type = fields.Selection([
+ ('outbound', 'Send Money'),
+ ('inbound', 'Receive Money'),
+ ], string='Payment Type', store=True, copy=False,
+ compute='_compute_from_lines')
+ partner_type = fields.Selection([
+ ('customer', 'Customer'),
+ ('supplier', 'Vendor'),
+ ], store=True, copy=False,
+ compute='_compute_from_lines')
+ source_amount = fields.Monetary(
+ string="Amount to Pay (company currency)", store=True, copy=False,
+ currency_field='company_currency_id',
+ compute='_compute_from_lines')
+ source_amount_currency = fields.Monetary(
+ string="Amount to Pay (foreign currency)", store=True, copy=False,
+ currency_field='source_currency_id',
+ compute='_compute_from_lines')
+ source_currency_id = fields.Many2one('res.currency',
+ string='Source Currency', store=True, copy=False,
+ compute='_compute_from_lines',
+ help="The payment's currency.")
+ can_edit_wizard = fields.Boolean(store=True, copy=False,
+ compute='_compute_from_lines',
+ help="Technical field used to indicate the user can edit the wizard content such as the amount.")
+ can_group_payments = fields.Boolean(store=True, copy=False,
+ compute='_compute_from_lines',
+ help="Technical field used to indicate the user can see the 'group_payments' box.")
+ company_id = fields.Many2one('res.company', store=True, copy=False,
+ compute='_compute_from_lines')
+ partner_id = fields.Many2one('res.partner',
+ string="Customer/Vendor", store=True, copy=False, ondelete='restrict',
+ compute='_compute_from_lines')
+
+ # == Payment methods fields ==
+ payment_method_id = fields.Many2one('account.payment.method', string='Payment Method',
+ readonly=False, store=True,
+ compute='_compute_payment_method_id',
+ domain="[('id', 'in', available_payment_method_ids)]",
+ help="Manual: Get paid by cash, check or any other method outside of Odoo.\n"\
+ "Electronic: Get paid automatically through a payment acquirer by requesting a transaction on a card saved by the customer when buying or subscribing online (payment token).\n"\
+ "Check: Pay bill by check and print it from Odoo.\n"\
+ "Batch Deposit: Encase several customer checks at once by generating a batch deposit to submit to your bank. When encoding the bank statement in Odoo, you are suggested to reconcile the transaction with the batch deposit.To enable batch deposit, module account_batch_payment must be installed.\n"\
+ "SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your bank. To enable sepa credit transfer, module account_sepa must be installed ")
+ available_payment_method_ids = fields.Many2many('account.payment.method',
+ compute='_compute_payment_method_fields')
+ hide_payment_method = fields.Boolean(
+ compute='_compute_payment_method_fields',
+ help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'")
+
+ # == Payment difference fields ==
+ payment_difference = fields.Monetary(
+ compute='_compute_payment_difference')
+ payment_difference_handling = fields.Selection([
+ ('open', 'Keep open'),
+ ('reconcile', 'Mark invoice as fully paid'),
+ ], default='open', string="Payment Difference Handling")
+ writeoff_account_id = fields.Many2one('account.account', string="Difference Account", copy=False,
+ domain="[('deprecated', '=', False), ('company_id', '=', company_id)]")
+ writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
+ help='Change label of the counterpart that will hold the payment difference')
+
+ # == Display purpose fields ==
+ show_partner_bank_account = fields.Boolean(
+ compute='_compute_show_require_partner_bank',
+ help="Technical field used to know whether the field `partner_bank_id` needs to be displayed or not in the payments form views")
+ require_partner_bank_account = fields.Boolean(
+ compute='_compute_show_require_partner_bank',
+ help="Technical field used to know whether the field `partner_bank_id` needs to be required or not in the payments form views")
+
+ # -------------------------------------------------------------------------
+ # HELPERS
+ # -------------------------------------------------------------------------
+
+ @api.model
+ def _get_batch_communication(self, batch_result):
+ ''' Helper to compute the communication based on the batch.
+ :param batch_result: A batch returned by '_get_batches'.
+ :return: A string representing a communication to be set on payment.
+ '''
+ return ' '.join(label for label in batch_result['lines'].mapped('name') if label)
+
+ @api.model
+ def _get_line_batch_key(self, line):
+ ''' Turn the line passed as parameter to a dictionary defining on which way the lines
+ will be grouped together.
+ :return: A python dictionary.
+ '''
+ return {
+ 'partner_id': line.partner_id.id,
+ 'account_id': line.account_id.id,
+ 'currency_id': (line.currency_id or line.company_currency_id).id,
+ 'partner_bank_id': line.move_id.partner_bank_id.id,
+ 'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier',
+ 'payment_type': 'inbound' if line.balance > 0.0 else 'outbound',
+ }
+
+ def _get_batches(self):
+ ''' Group the account.move.line linked to the wizard together.
+ :return: A list of batches, each one containing:
+ * key_values: The key as a dictionary used to group the journal items together.
+ * moves: An account.move recordset.
+ '''
+ self.ensure_one()
+
+ lines = self.line_ids
+
+ if len(lines.company_id) > 1:
+ raise UserError(_("You can't create payments for entries belonging to different companies."))
+ if not lines:
+ raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line."))
+
+ batches = {}
+ for line in lines:
+ batch_key = self._get_line_batch_key(line)
+
+ serialized_key = '-'.join(str(v) for v in batch_key.values())
+ batches.setdefault(serialized_key, {
+ 'key_values': batch_key,
+ 'lines': self.env['account.move.line'],
+ })
+ batches[serialized_key]['lines'] += line
+ return list(batches.values())
+
+ @api.model
+ def _get_wizard_values_from_batch(self, batch_result):
+ ''' Extract values from the batch passed as parameter (see '_get_batches')
+ to be mounted in the wizard view.
+ :param batch_result: A batch returned by '_get_batches'.
+ :return: A dictionary containing valid fields
+ '''
+ key_values = batch_result['key_values']
+ lines = batch_result['lines']
+ company = lines[0].company_id
+
+ source_amount = abs(sum(lines.mapped('amount_residual')))
+ if key_values['currency_id'] == company.currency_id.id:
+ source_amount_currency = source_amount
+ else:
+ source_amount_currency = abs(sum(lines.mapped('amount_residual_currency')))
+
+ return {
+ 'company_id': company.id,
+ 'partner_id': key_values['partner_id'],
+ 'partner_type': key_values['partner_type'],
+ 'payment_type': key_values['payment_type'],
+ 'source_currency_id': key_values['currency_id'],
+ 'source_amount': source_amount,
+ 'source_amount_currency': source_amount_currency,
+ }
+
+ # -------------------------------------------------------------------------
+ # COMPUTE METHODS
+ # -------------------------------------------------------------------------
+
+ @api.depends('line_ids')
+ def _compute_from_lines(self):
+ ''' Load initial values from the account.moves passed through the context. '''
+ for wizard in self:
+ batches = wizard._get_batches()
+
+ if len(batches) == 1:
+ # == Single batch to be mounted on the view ==
+ batch_result = batches[0]
+ wizard.update(wizard._get_wizard_values_from_batch(batch_result))
+
+ wizard.can_edit_wizard = True
+ wizard.can_group_payments = len(batch_result['lines']) != 1
+ else:
+ # == Multiple batches: The wizard is not editable ==
+ wizard.update({
+ 'company_id': batches[0]['lines'][0].company_id.id,
+ 'partner_id': False,
+ 'partner_type': False,
+ 'payment_type': False,
+ 'source_currency_id': False,
+ 'source_amount': False,
+ 'source_amount_currency': False,
+ })
+
+ wizard.can_edit_wizard = False
+ wizard.can_group_payments = any(len(batch_result['lines']) != 1 for batch_result in batches)
+
+ @api.depends('can_edit_wizard')
+ def _compute_communication(self):
+ # The communication can't be computed in '_compute_from_lines' because
+ # it's a compute editable field and then, should be computed in a separated method.
+ for wizard in self:
+ if wizard.can_edit_wizard:
+ batches = self._get_batches()
+ wizard.communication = wizard._get_batch_communication(batches[0])
+ else:
+ wizard.communication = False
+
+ @api.depends('can_edit_wizard')
+ def _compute_group_payment(self):
+ for wizard in self:
+ if wizard.can_edit_wizard:
+ batches = wizard._get_batches()
+ wizard.group_payment = len(batches[0]['lines'].move_id) == 1
+ else:
+ wizard.group_payment = False
+
+ @api.depends('company_id', 'source_currency_id')
+ def _compute_journal_id(self):
+ for wizard in self:
+ domain = [
+ ('type', 'in', ('bank', 'cash')),
+ ('company_id', '=', wizard.company_id.id),
+ ]
+ journal = None
+ if wizard.source_currency_id:
+ journal = self.env['account.journal'].search(domain + [('currency_id', '=', wizard.source_currency_id.id)], limit=1)
+ if not journal:
+ journal = self.env['account.journal'].search(domain, limit=1)
+ wizard.journal_id = journal
+
+ @api.depends('journal_id')
+ def _compute_currency_id(self):
+ for wizard in self:
+ wizard.currency_id = wizard.journal_id.currency_id or wizard.source_currency_id or wizard.company_id.currency_id
+
+ @api.depends('partner_id')
+ def _compute_partner_bank_id(self):
+ ''' The default partner_bank_id will be the first available on the partner. '''
+ for wizard in self:
+ available_partner_bank_accounts = wizard.partner_id.bank_ids
+ if available_partner_bank_accounts:
+ wizard.partner_bank_id = available_partner_bank_accounts[0]._origin
+ else:
+ wizard.partner_bank_id = False
+
+ @api.depends('journal_id')
+ def _compute_payment_method_id(self):
+ for wizard in self:
+ batches = wizard._get_batches()
+ payment_type = batches[0]['key_values']['payment_type']
+
+ if payment_type == 'inbound':
+ available_payment_methods = wizard.journal_id.inbound_payment_method_ids
+ else:
+ available_payment_methods = wizard.journal_id.outbound_payment_method_ids
+
+ # Select the first available one by default.
+ if available_payment_methods:
+ wizard.payment_method_id = available_payment_methods[0]._origin
+ else:
+ wizard.payment_method_id = False
+
+ @api.depends('payment_type',
+ 'journal_id.inbound_payment_method_ids',
+ 'journal_id.outbound_payment_method_ids')
+ def _compute_payment_method_fields(self):
+ for wizard in self:
+ if wizard.payment_type == 'inbound':
+ wizard.available_payment_method_ids = wizard.journal_id.inbound_payment_method_ids
+ else:
+ wizard.available_payment_method_ids = wizard.journal_id.outbound_payment_method_ids
+
+ wizard.hide_payment_method = len(wizard.available_payment_method_ids) == 1 and wizard.available_payment_method_ids.code == 'manual'
+
+ @api.depends('payment_type',
+ 'journal_id.inbound_payment_method_ids',
+ 'journal_id.outbound_payment_method_ids')
+ def _compute_payment_method_id(self):
+ for wizard in self:
+ if wizard.payment_type == 'inbound':
+ available_payment_methods = wizard.journal_id.inbound_payment_method_ids
+ else:
+ available_payment_methods = wizard.journal_id.outbound_payment_method_ids
+
+ # Select the first available one by default.
+ if available_payment_methods:
+ wizard.payment_method_id = available_payment_methods[0]._origin
+ else:
+ wizard.payment_method_id = False
+
+ @api.depends('payment_method_id')
+ def _compute_show_require_partner_bank(self):
+ """ Computes if the destination bank account must be displayed in the payment form view. By default, it
+ won't be displayed but some modules might change that, depending on the payment type."""
+ for wizard in self:
+ wizard.show_partner_bank_account = wizard.payment_method_id.code in self.env['account.payment']._get_method_codes_using_bank_account()
+ wizard.require_partner_bank_account = wizard.payment_method_id.code in self.env['account.payment']._get_method_codes_needing_bank_account()
+
+ @api.depends('source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
+ def _compute_amount(self):
+ for wizard in self:
+ if wizard.source_currency_id == wizard.currency_id:
+ # Same currency.
+ wizard.amount = wizard.source_amount_currency
+ elif wizard.currency_id == wizard.company_id.currency_id:
+ # Payment expressed on the company's currency.
+ wizard.amount = wizard.source_amount
+ else:
+ # Foreign currency on payment different than the one set on the journal entries.
+ amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date)
+ wizard.amount = amount_payment_currency
+
+ @api.depends('amount')
+ def _compute_payment_difference(self):
+ for wizard in self:
+ if wizard.source_currency_id == wizard.currency_id:
+ # Same currency.
+ wizard.payment_difference = wizard.source_amount_currency - wizard.amount
+ elif wizard.currency_id == wizard.company_id.currency_id:
+ # Payment expressed on the company's currency.
+ wizard.payment_difference = wizard.source_amount - wizard.amount
+ else:
+ # Foreign currency on payment different than the one set on the journal entries.
+ amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date)
+ wizard.payment_difference = amount_payment_currency - wizard.amount
+
+ # -------------------------------------------------------------------------
+ # LOW-LEVEL METHODS
+ # -------------------------------------------------------------------------
+
+ @api.model
+ def default_get(self, fields_list):
+ # OVERRIDE
+ res = super().default_get(fields_list)
+
+ if 'line_ids' in fields_list and 'line_ids' not in res:
+
+ # Retrieve moves to pay from the context.
+
+ if self._context.get('active_model') == 'account.move':
+ lines = self.env['account.move'].browse(self._context.get('active_ids', [])).line_ids
+ elif self._context.get('active_model') == 'account.move.line':
+ lines = self.env['account.move.line'].browse(self._context.get('active_ids', []))
+ else:
+ raise UserError(_(
+ "The register payment wizard should only be called on account.move or account.move.line records."
+ ))
+
+ # Keep lines having a residual amount to pay.
+ available_lines = self.env['account.move.line']
+ for line in lines:
+ if line.move_id.state != 'posted':
+ raise UserError(_("You can only register payment for posted journal entries."))
+
+ if line.account_internal_type not in ('receivable', 'payable'):
+ continue
+ if line.currency_id:
+ if line.currency_id.is_zero(line.amount_residual_currency):
+ continue
+ else:
+ if line.company_currency_id.is_zero(line.amount_residual):
+ continue
+ available_lines |= line
+
+ # Check.
+ if not available_lines:
+ raise UserError(_("You can't register a payment because there is nothing left to pay on the selected journal items."))
+ if len(lines.company_id) > 1:
+ raise UserError(_("You can't create payments for entries belonging to different companies."))
+ if len(set(available_lines.mapped('account_internal_type'))) > 1:
+ raise UserError(_("You can't register payments for journal items being either all inbound, either all outbound."))
+
+ res['line_ids'] = [(6, 0, available_lines.ids)]
+
+ return res
+
+ # -------------------------------------------------------------------------
+ # BUSINESS METHODS
+ # -------------------------------------------------------------------------
+
+ def _create_payment_vals_from_wizard(self):
+ payment_vals = {
+ 'date': self.payment_date,
+ 'amount': self.amount,
+ 'payment_type': self.payment_type,
+ 'partner_type': self.partner_type,
+ 'ref': self.communication,
+ 'journal_id': self.journal_id.id,
+ 'currency_id': self.currency_id.id,
+ 'partner_id': self.partner_id.id,
+ 'partner_bank_id': self.partner_bank_id.id,
+ 'payment_method_id': self.payment_method_id.id,
+ 'destination_account_id': self.line_ids[0].account_id.id
+ }
+
+ if self.payment_difference and self.payment_difference_handling == 'reconcile':
+ payment_vals['write_off_line_vals'] = {
+ 'name': self.writeoff_label,
+ 'amount': -self.payment_difference,
+ 'account_id': self.writeoff_account_id.id,
+ }
+ return payment_vals
+
+ def _create_payment_vals_from_batch(self, batch_result):
+ batch_values = self._get_wizard_values_from_batch(batch_result)
+ return {
+ 'date': self.payment_date,
+ 'amount': batch_values['source_amount_currency'],
+ 'payment_type': batch_values['payment_type'],
+ 'partner_type': batch_values['partner_type'],
+ 'ref': self._get_batch_communication(batch_result),
+ 'journal_id': self.journal_id.id,
+ 'currency_id': batch_values['source_currency_id'],
+ 'partner_id': batch_values['partner_id'],
+ 'partner_bank_id': batch_result['key_values']['partner_bank_id'],
+ 'payment_method_id': self.payment_method_id.id,
+ 'destination_account_id': batch_result['lines'][0].account_id.id
+ }
+
+ def _create_payments(self):
+ self.ensure_one()
+ batches = self._get_batches()
+
+ to_reconcile = []
+ if self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment):
+ payment_vals = self._create_payment_vals_from_wizard()
+ payment_vals_list = [payment_vals]
+ to_reconcile.append(batches[0]['lines'])
+ else:
+ # Don't group payments: Create one batch per move.
+ if not self.group_payment:
+ new_batches = []
+ for batch_result in batches:
+ for line in batch_result['lines']:
+ new_batches.append({
+ **batch_result,
+ 'lines': line,
+ })
+ batches = new_batches
+
+ payment_vals_list = []
+ for batch_result in batches:
+ payment_vals_list.append(self._create_payment_vals_from_batch(batch_result))
+ to_reconcile.append(batch_result['lines'])
+
+ payments = self.env['account.payment'].create(payment_vals_list)
+ payments.action_post()
+
+ domain = [('account_internal_type', 'in', ('receivable', 'payable')), ('reconciled', '=', False)]
+ for payment, lines in zip(payments, to_reconcile):
+ payment_lines = payment.line_ids.filtered_domain(domain)
+ for account in payment_lines.account_id:
+ (payment_lines + lines)\
+ .filtered_domain([('account_id', '=', account.id), ('reconciled', '=', False)])\
+ .reconcile()
+
+ return payments
+
+ def action_create_payments(self):
+ payments = self._create_payments()
+
+ if self._context.get('dont_redirect_to_payments'):
+ return True
+
+ action = {
+ 'name': _('Payments'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'account.payment',
+ 'context': {'create': False},
+ }
+ if len(payments) == 1:
+ action.update({
+ 'view_mode': 'form',
+ 'res_id': payments.id,
+ })
+ else:
+ action.update({
+ 'view_mode': 'tree,form',
+ 'domain': [('id', 'in', payments.ids)],
+ })
+ return action
diff --git a/addons/account/wizard/account_payment_register_views.xml b/addons/account/wizard/account_payment_register_views.xml
new file mode 100644
index 00000000000..15aee188635
--- /dev/null
+++ b/addons/account/wizard/account_payment_register_views.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
-
-
-
+
QR-bill payment part
@@ -19,11 +19,11 @@
Credit transfer
Account - + Creditor @@ -33,11 +33,11 @@ Reference number - + + diff --git a/addons/l10n_ch/tests/test_ch_qr_code.py b/addons/l10n_ch/tests/test_ch_qr_code.py index e46a3a2c1ec..e41dcdd03cc 100644 --- a/addons/l10n_ch/tests/test_ch_qr_code.py +++ b/addons/l10n_ch/tests/test_ch_qr_code.py @@ -28,9 +28,9 @@ class TestSwissQRCode(AccountTestInvoicingCommon): 'move_type': 'out_invoice', 'partner_id': cls.partner_a.id, 'currency_id': cls.env.ref('base.CHF').id, - 'invoice_partner_bank_id': cls.swiss_iban.id, + 'partner_bank_id': cls.swiss_iban.id, 'company_id': cls.company_data['company'].id, - 'invoice_payment_ref': "Papa a vu le fifi de lolo", + 'payment_reference': "Papa a vu le fifi de lolo", 'invoice_line_ids': [ (0, 0, {'quantity': 1, 'price_unit': 100}) ], @@ -61,13 +61,13 @@ class TestSwissQRCode(AccountTestInvoicingCommon): self.ch_qr_invoice.generate_qr_code() # Now, check with a QR-IBAN as the payment account - self.ch_qr_invoice.invoice_partner_bank_id = self.swiss_qr_iban + self.ch_qr_invoice.partner_bank_id = self.swiss_qr_iban with self.assertRaises(UserError, msg="It shouldn't be possible to generate a Swiss QR-cde for a QR-IBAN without giving it a valid QR-reference as payment reference."): self.ch_qr_invoice.generate_qr_code() # Assigning a QR reference should fix it - self.ch_qr_invoice.invoice_payment_ref = '210000000003139471430009017' + self.ch_qr_invoice.payment_reference = '210000000003139471430009017' def test_ch_qr_code_detection(self): """ Checks Swiss QR-code auto-detection when no specific QR-method diff --git a/addons/l10n_ch/views/account_invoice_view.xml b/addons/l10n_ch/views/account_invoice_view.xml index a5d56df6b99..f4f9abdee17 100644 --- a/addons/l10n_ch/views/account_invoice_view.xml +++ b/addons/l10n_ch/views/account_invoice_view.xml @@ -6,7 +6,7 @@ %s" % ( _("Bank account not found, useful informations from XML file:"), diff --git a/addons/l10n_lu/models/account_chart_template.py b/addons/l10n_lu/models/account_chart_template.py index 7eabdbb63d0..11d23619c36 100644 --- a/addons/l10n_lu/models/account_chart_template.py +++ b/addons/l10n_lu/models/account_chart_template.py @@ -7,11 +7,6 @@ from odoo import api, models, fields class AccountChartTemplate(models.Model): _inherit = 'account.chart.template' - def get_countries_posting_at_bank_rec(self): - rslt = super(AccountChartTemplate, self).get_countries_posting_at_bank_rec() - rslt.append('LU') - return rslt - @api.model def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None): journal_data = super(AccountChartTemplate, self)._prepare_all_journals( diff --git a/addons/l10n_pe/models/__init__.py b/addons/l10n_pe/models/__init__.py index 4ba6149ba4f..2037a75eaa2 100644 --- a/addons/l10n_pe/models/__init__.py +++ b/addons/l10n_pe/models/__init__.py @@ -1,7 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_tax from . import account_move -from . import account_journal from . import account_chart_template from . import l10n_latam_identification_type from . import res_partner diff --git a/addons/l10n_pe/models/account_journal.py b/addons/l10n_pe/models/account_journal.py deleted file mode 100644 index ff55a01cd45..00000000000 --- a/addons/l10n_pe/models/account_journal.py +++ /dev/null @@ -1,31 +0,0 @@ -from odoo import models, api - - -class AccountJournal(models.Model): - _inherit = "account.journal" - - @api.model - def _get_sequence_prefix(self, code, refund=False): - """For peruvian companies we can not use sequences with **/** due to the edi generation which need in the - sequence a plain text ended by a *-* and the length of this prefix""" - if self.env.company.country_id != self.env.ref('base.pe'): - return super()._get_sequence_prefix(code, refund=refund) - prefix = code.upper() - if len(prefix) > 3: - prefix = prefix[:3] - prefix = prefix.ljust(3, 'X') - if refund: - prefix = 'R' + prefix[:-1] - return prefix + '-' - - @api.model - def _create_sequence(self, vals, refund=False): - """For Peruvian companies, a number reset by date do not make sense due to the fact that we can not use a free - prefix the format does not have enough space to put a year on it or any other char, then with this approach we - are avoiding the default behavior there.""" - res = super()._create_sequence(vals, refund=refund) - # NOTE: the self element is coming filled just when write and not on create (which is Ok) - journal_type = self.type if not vals.get('type') else vals.get('type') - if self.env.company.country_id == self.env.ref('base.pe') or journal_type in ['sale', 'purchase']: - res.write({'use_date_range': False}) - return res diff --git a/addons/membership/tests/test_membership.py b/addons/membership/tests/test_membership.py index d315983138c..ae6c05aa094 100644 --- a/addons/membership/tests/test_membership.py +++ b/addons/membership/tests/test_membership.py @@ -53,14 +53,14 @@ class TestMembership(TestMembershipCommon): # the invoice is paid -> customer goes to paid status bank_journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'}) - self.env['account.payment'].create({ - 'payment_method_id': self.env.ref("account.account_payment_method_manual_in").id, - 'payment_type': 'inbound', - 'invoice_ids': [(6, False, invoice.ids)], - 'amount': 86.25, - 'journal_id': bank_journal.id, - 'partner_type': 'customer', - }).post() + + payment = self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=invoice.ids)\ + .create({ + 'amount': 86.25, + 'journal_id': bank_journal.id, + })\ + ._create_payments() self.assertEqual( self.partner_1.membership_state, 'paid', diff --git a/addons/payment/models/account_payment.py b/addons/payment/models/account_payment.py index 5c398c8f634..e324b033ce8 100644 --- a/addons/payment/models/account_payment.py +++ b/addons/payment/models/account_payment.py @@ -88,7 +88,7 @@ class AccountPayment(models.Model): self.mapped('payment_transaction_id').filtered(lambda x: x.state == 'done' and not x.is_processed)._post_process_after_done() return res - def post(self): + def action_post(self): # Post the payments "normally" if no transactions are needed. # If not, let the acquirer updates the state. # __________ ______________ @@ -109,7 +109,7 @@ class AccountPayment(models.Model): payments_need_trans = self.filtered(lambda pay: pay.payment_token_id and not pay.payment_transaction_id) transactions = payments_need_trans._create_payment_transaction() - res = super(AccountPayment, self - payments_need_trans).post() + res = super(AccountPayment, self - payments_need_trans).action_post() transactions.s2s_do_transaction() diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index f09951061f4..704b53c7c65 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -639,22 +639,42 @@ class PaymentTransaction(models.Model): for trans in self: trans.invoice_ids_nbr = len(trans.invoice_ids) - def _prepare_account_payment_vals(self): + def _create_payment(self, add_payment_vals={}): + ''' Create an account.payment record for the current payment.transaction. + If the transaction is linked to some invoices, the reconciliation will be done automatically. + :param add_payment_vals: Optional additional values to be passed to the account.payment.create method. + :return: An account.payment record. + ''' self.ensure_one() - return { + + payment_vals = { 'amount': self.amount, 'payment_type': 'inbound' if self.amount > 0 else 'outbound', 'currency_id': self.currency_id.id, 'partner_id': self.partner_id.id, 'partner_type': 'customer', - 'invoice_ids': [(6, 0, self.invoice_ids.ids)], 'journal_id': self.acquirer_id.journal_id.id, 'company_id': self.acquirer_id.company_id.id, 'payment_method_id': self.env.ref('payment.account_payment_method_electronic_in').id, 'payment_token_id': self.payment_token_id and self.payment_token_id.id or None, 'payment_transaction_id': self.id, - 'communication': self.reference, + 'ref': self.reference, + **add_payment_vals, } + payment = self.env['account.payment'].create(payment_vals) + payment.action_post() + + # Track the payment to make a one2one. + self.payment_id = payment + + if self.invoice_ids: + self.invoice_ids.filtered(lambda move: move.state == 'draft').post() + + (payment.line_ids + self.invoice_ids.line_ids)\ + .filtered(lambda line: line.account_id == payment.destination_account_id and not line.reconciled)\ + .reconcile() + + return payment def get_last_transaction(self): transactions = self.filtered(lambda t: t.state != 'draft') @@ -791,21 +811,11 @@ class PaymentTransaction(models.Model): invoices.post() # Create & Post the payments. - payments = defaultdict(lambda: self.env['account.payment']) for trans in self: if trans.payment_id: - payments[trans.acquirer_id.company_id.id] += trans.payment_id continue - payment_vals = trans._prepare_account_payment_vals() - payment = self.env['account.payment'].create(payment_vals) - payments[trans.acquirer_id.company_id.id] += payment - - # Track the payment to make a one2one. - trans.payment_id = payment - - for company in payments: - payments[company].with_company(company).with_context(company_id=company).post() + trans._create_payment() def _set_transaction_cancel(self): '''Move the transaction's payment to the cancel state(e.g. Paypal).''' diff --git a/addons/payment/wizards/payment_link_wizard.py b/addons/payment/wizards/payment_link_wizard.py index fb6e02b9cf5..7dd543d997b 100644 --- a/addons/payment/wizards/payment_link_wizard.py +++ b/addons/payment/wizards/payment_link_wizard.py @@ -24,7 +24,7 @@ class PaymentLinkWizard(models.TransientModel): if res_id and res_model == 'account.move': record = self.env[res_model].browse(res_id) res.update({ - 'description': record.invoice_payment_ref, + 'description': record.payment_reference, 'amount': record[amount_field], 'currency_id': record.currency_id.id, 'partner_id': record.partner_id.id, diff --git a/addons/point_of_sale/models/account_bank_statement.py b/addons/point_of_sale/models/account_bank_statement.py index 33533b8379d..0c19179cb62 100644 --- a/addons/point_of_sale/models/account_bank_statement.py +++ b/addons/point_of_sale/models/account_bank_statement.py @@ -11,11 +11,12 @@ class AccountBankStatement(models.Model): pos_session_id = fields.Many2one('pos.session', string="Session", copy=False) account_id = fields.Many2one('account.account', related='journal_id.default_debit_account_id', readonly=True) - def check_confirm_bank(self): - for bs in self: - if bs.pos_session_id.state in ('opened', 'closing_control') and bs.state == 'open': + def button_validate_or_action(self): + # OVERRIDE to check the consistency of the statement's state regarding the session's state. + for statement in self: + if statement.pos_session_id.state in ('opened', 'closing_control') and statement.state == 'open': raise UserError(_("You can't validate a bank statement that is used in an opened Session of a Point of Sale.")) - return super(AccountBankStatement, self).check_confirm_bank() + return super(AccountBankStatement, self).button_validate_or_action() def unlink(self): for bs in self: diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 9c45e5aa64a..e59b5e0fd25 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -384,7 +384,7 @@ class PosOrder(models.Model): raise UserError(_('Please provide a partner for the sale.')) move_vals = { - 'invoice_payment_ref': order.name, + 'payment_reference': order.name, 'invoice_origin': order.name, 'journal_id': order.session_id.config_id.invoice_journal_id.id, 'move_type': 'out_invoice' if order.amount_total >= 0 else 'out_refund', diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 537e54c3cb0..c96a445164e 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -207,7 +207,6 @@ class PosSession(models.Model): 'journal_id': cash_journal.id, 'user_id': self.env.user.id, 'name': pos_name, - 'balance_start': self.env["account.bank.statement"]._get_opening_balance(cash_journal.id) if cash_journal.type == 'cash' else 0 } statement_ids |= statement_ids.with_context(ctx).create(st_values) @@ -251,7 +250,6 @@ class PosSession(models.Model): values['start_at'] = fields.Datetime.now() values['state'] = 'opened' session.write(values) - session.statement_ids.button_open() return True def action_pos_session_closing_control(self): @@ -300,14 +298,13 @@ class PosSession(models.Model): self._create_picking_at_end_of_session() self.with_company(self.company_id)._create_account_move() if self.move_id.line_ids: - self.move_id.post() # Set the uninvoiced orders' state to 'done' self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'}) else: # The cash register needs to be confirmed for cash diffs # made thru cash in/out when sesion is in cash_control. if self.config_id.cash_control: - self.cash_register_id.button_confirm_bank() + self.cash_register_id.button_validate() self.move_id.unlink() self.write({'state': 'closed'}) return { @@ -340,7 +337,6 @@ class PosSession(models.Model): pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(location_dest_id, lines, picking_type) pickings.write({'pos_session_id': self.id, 'origin': self.name}) - def _create_account_move(self): """ Create account.move and account.move.line records for this session. @@ -365,6 +361,10 @@ class PosSession(models.Model): 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) + + if account_move.line_ids: + account_move.post() + data = self._reconcile_account_move_lines(data) def _accumulate_amounts(self, data): @@ -633,10 +633,11 @@ class PosSession(models.Model): for statement in self.statement_ids: if not self.config_id.cash_control: statement.write({'balance_end_real': statement.balance_end}) - statement.button_confirm_bank() + statement.button_post() + statement.button_validate() all_lines = ( - split_cash_statement_lines[statement].mapped('journal_entry_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable') - | combine_cash_statement_lines[statement].mapped('journal_entry_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable') + 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_receivable_lines[statement] | combine_cash_receivable_lines[statement] ) @@ -772,11 +773,11 @@ class PosSession(models.Model): def _get_statement_line_vals(self, statement, receivable_account, amount): return { - 'date': fields.Date.context_today(self), 'amount': amount, - 'name': self.name, + 'payment_ref': self.name, 'statement_id': statement.id, - 'account_id': receivable_account.id, + 'journal_id': statement.journal_id.id, + 'counterpart_account_id': receivable_account.id, } def _update_amounts(self, old_amounts, amounts_to_add, date, round=True, force_company_currency=False): diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index 8d0601b0c96..19a82d3b53d 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -31,9 +31,9 @@ class TestPointOfSaleCommon(StockAccountTestCommon): 'company_id': cls.company_id}) cls.company.write({ 'anglo_saxon_accounting': coa.use_anglo_saxon, - 'bank_account_code_prefix': coa.bank_account_code_prefix, - 'cash_account_code_prefix': coa.cash_account_code_prefix, - 'transfer_account_code_prefix': coa.transfer_account_code_prefix, + 'bank_account_code_prefix': 'test' + coa.bank_account_code_prefix, + 'cash_account_code_prefix': 'test' + coa.cash_account_code_prefix, + 'transfer_account_code_prefix': 'test' + coa.transfer_account_code_prefix, 'chart_template_id': coa.id,}) cls.product3 = cls.env['product.product'].create({ 'name': 'Product 3', diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index aa0ba016b4e..6611505aca3 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -472,6 +472,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I test that the total of the attached invoice is correct invoice = self.env['account.move'].browse(res['res_id']) + invoice.post() self.assertAlmostEqual( invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct") @@ -505,7 +506,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount': 1000, 'partner_id': self.partner4.id, 'statement_id': account_statement.id, - 'name': 'EXT001' + 'payment_ref': 'EXT001' }) # I modify the bank statement and set the Closing Balance. account_statement.write({ @@ -522,7 +523,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I confirm the bank statement using Confirm button - self.AccountBankStatement.button_confirm_bank() + self.AccountBankStatement.button_validate() def test_create_from_ui(self): """ @@ -869,6 +870,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I test that the total of the attached invoice is correct invoice = self.env['account.move'].browse(res['res_id']) + invoice.post() self.assertAlmostEqual( invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct") diff --git a/addons/point_of_sale/views/account_statement_view.xml b/addons/point_of_sale/views/account_statement_view.xml index fb31b360d25..781921d5008 100644 --- a/addons/point_of_sale/views/account_statement_view.xml +++ b/addons/point_of_sale/views/account_statement_view.xml @@ -7,7 +7,7 @@ Reconcile '), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier), +...tour.stepUtils.statusbarButtonsSteps('Post', _t('Processing'), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier), +...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('Reconcile'), ".o_statusbar_status .btn.dropdown-toggle:contains(Processing)").map(tour.stepUtils.editionEnterpriseModifier), { edition: "enterprise", trigger: "button.o_reconcile, button.o_validate", diff --git a/odoo/addons/test_main_flows/tests/test_flow.py b/odoo/addons/test_main_flows/tests/test_flow.py index d450c335253..4aaca90ed4d 100644 --- a/odoo/addons/test_main_flows/tests/test_flow.py +++ b/odoo/addons/test_main_flows/tests/test_flow.py @@ -47,8 +47,6 @@ class BaseTestUi(odoo.tests.HttpCase): 'name': 'Vendor Bills - Test', 'code': 'TEXJ', 'type': 'purchase', - 'default_credit_account_id': a_expense.id, - 'default_debit_account_id': a_expense.id, 'refund_sequence': True, }) self.bank_journal = self.env['account.journal'].create({ |