diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 0f39b6c32f7..2c082616a52 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -877,8 +877,8 @@ class AccountMoveLine(models.Model): # Create list of debit and list of credit move ordered by date-currency debit_moves = self.filtered(lambda r: r.debit != 0 or r.amount_currency > 0) credit_moves = self.filtered(lambda r: r.credit != 0 or r.amount_currency < 0) - debit_moves.sorted(key=lambda a: (a.date, a.currency_id)) - credit_moves.sorted(key=lambda a: (a.date, a.currency_id)) + debit_moves = debit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id)) + credit_moves = credit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id)) # Compute on which field reconciliation should be based upon: field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' #if all lines share the same currency, use amount_residual_currency to avoid currency rounding error diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index cff4d7b91be..b9504bd6ee6 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -376,9 +376,17 @@ var StatementModel = BasicModel.extend({ }; }); }); + var domainReconcile = []; + if (context && context.company_ids) { + domainReconcile.push(['company_id', 'in', context.company_ids]); + } + if (context && context.active_model === 'account.journal' && context.active_ids) { + domainReconcile.push(['journal_id', 'in', [false].concat(context.active_ids)]); + } var def_reconcileModel = this._rpc({ model: 'account.reconcile.model', method: 'search_read', + domain: domainReconcile, }) .then(function (reconcileModels) { self.reconcileModels = reconcileModels; @@ -1253,9 +1261,14 @@ var ManualModel = StatementModel.extend({ self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code')); }); + var domainReconcile = []; + if (context && context.company_ids) { + domainReconcile.push(['company_id', 'in', context.company_ids]); + } var def_reconcileModel = this._rpc({ model: 'account.reconcile.model', method: 'search_read', + domain: domainReconcile, }) .then(function (reconcileModels) { self.reconcileModels = reconcileModels; diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index f7ffd6fc49f..353f9dcb310 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -407,6 +407,32 @@ class TestPayment(AccountingTestCase): self.assertTrue(payment.move_line_ids.filtered(lambda l: l.account_id == invoice.account_id)[0].full_reconcile_id) self.assertEqual(invoice.state, 'paid') + def test_payment_and_writeoff_out_refund(self): + # Use case: + # Company is in EUR, create a credit note for 100 EUR and register payment of 90. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=100, type='out_refund', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'outbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 90, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 0.0, 'credit': 90.0, 'amount_currency': 0.0, 'currency_id': False}, + {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 10.0, 'amount_currency': 0.0, 'currency_id': False}, + {'account_id': self.account_receivable.id, 'debit': 100.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, + ]) + self.assertEqual(invoice.state, 'paid') + def test_payment_and_writeoff_in_other_currency_2(self): # Use case: # Company is in EUR, create a supplier bill of 5325.6 USD and register payment of 5325 USD, at a different rate diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 53cdc1a6be5..14f1d589d86 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -971,3 +971,63 @@ class TestReconciliation(AccountingTestCase): # because they owe us still 50 CC. self.assertEqual(invoice_cust_1.state, 'open', 'Invoice is in status %s' % invoice_cust_1.state) + + def test_multiple_term_reconciliation_opw_1906665(self): + '''Test that when registering a payment to an invoice with multiple + payment term lines the reconciliation happens against the line + with the earliest date_maturity + ''' + + payment_term = self.env['account.payment.term'].create({ + 'name': 'Pay in 2 installments', + 'line_ids': [ + # Pay 50% immediately + (0, 0, { + 'value': 'percent', + 'value_amount': 50, + }), + # Pay the rest after 14 days + (0, 0, { + 'value': 'balance', + 'days': 14, + }) + ], + }) + + # can't use self.create_invoice because it validates and we need to set payment_term_id + invoice = self.account_invoice_model.create({ + 'partner_id': self.partner_agrolait_id, + 'payment_term_id': payment_term.id, + 'reference_type': 'none', + 'currency_id': self.currency_usd_id, + 'name': 'Multiple payment terms', + 'account_id': self.account_rcv.id, + 'type': 'out_invoice', + 'date_invoice': time.strftime('%Y') + '-07-01', + }) + self.account_invoice_line_model.create({ + 'product_id': self.product.id, + 'quantity': 1, + 'price_unit': 50, + 'invoice_id': invoice.id, + 'name': self.product.display_name, + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + }) + + invoice.action_invoice_open() + + payment = self.env['account.payment'].create({ + 'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_usd.id, + }) + payment.post() + + invoice.assign_outstanding_credit(payment.move_line_ids.filtered('credit').id) + + receivable_lines = invoice.move_id.line_ids.filtered(lambda line: line.account_id == self.account_rcv).sorted('date_maturity')[0] + self.assertTrue(receivable_lines.matched_credit_ids) diff --git a/addons/l10n_uk/data/l10n_uk_chart_data.xml b/addons/l10n_uk/data/l10n_uk_chart_data.xml index 70a81b228a2..4125fd23b98 100644 --- a/addons/l10n_uk/data/l10n_uk_chart_data.xml +++ b/addons/l10n_uk/data/l10n_uk_chart_data.xml @@ -1,6 +1,6 @@ - + diff --git a/addons/mail/static/src/js/followers.js b/addons/mail/static/src/js/followers.js index 2b440aa3477..0a760516607 100644 --- a/addons/mail/static/src/js/followers.js +++ b/addons/mail/static/src/js/followers.js @@ -28,7 +28,9 @@ var Followers = AbstractField.extend({ 'click .o_remove_follower': '_onRemoveFollower', 'click .o_mail_redirect': '_onRedirect', }, - supportedFieldTypes: ['one2many'], + // this widget only supports one2many but is not generic enough to claim + // that it supports all one2many fields + // supportedFieldTypes: ['one2many'], // inherited init: function (parent, name, record, options) { diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index bcbd21350fc..cd1c2f993ac 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -50,6 +50,23 @@ class AccountInvoice(models.Model): company = self.company_id or self.env.user.company_id self.comment = company.with_context(lang=self.partner_id.lang).sale_note + @api.multi + def action_invoice_open(self): + # OVERRIDE + # Auto-reconcile the invoice with payments coming from transactions. + # It's useful when you have a "paid" sale order (using a payment transaction) and you invoice it later. + res = super(AccountInvoice, self).action_invoice_open() + + if not self: + return res + + for invoice in self: + payments = invoice.mapped('transaction_ids.payment_id') + move_lines = payments.mapped('move_line_ids').filtered(lambda line: not line.reconciled and line.credit > 0.0) + for line in move_lines: + invoice.assign_outstanding_credit(line.id) + return res + @api.multi def action_invoice_paid(self): res = super(AccountInvoice, self).action_invoice_paid() diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 7401ded6d8d..08ee2e8d381 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -7,3 +7,4 @@ from . import test_sale_pricelist from . import test_onchange from . import test_reinvoice from . import test_access_rights +from . import test_sale_transaction diff --git a/addons/sale/tests/test_sale_transaction.py b/addons/sale/tests/test_sale_transaction.py new file mode 100644 index 00000000000..d63db15dd5a --- /dev/null +++ b/addons/sale/tests/test_sale_transaction.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from odoo import tests +from odoo.addons.account.tests.account_test_classes import AccountingTestCase + + +@tests.tagged('post_install', '-at_install') +class TestSaleTransaction(AccountingTestCase): + def test_sale_invoicing_from_transaction(self): + ''' Test the following scenario: + - Create a sale order + - Create a transaction for the sale order. + - Confirm the transaction but no invoice generated automatically. + - Create manually an invoice for this sale order. + => The invoice must be paid. + ''' + product = self.env['product.product'].create({ + 'name': 'Product A', + }) + + order = self.env['sale.order'].create({ + 'partner_id': self.env.ref('base.res_partner_1').id, + 'order_line': [ + (0, False, { + 'product_id': product.id, + 'name': '1 Product', + 'price_unit': 100.0, + }), + ], + }) + + transaction = order._create_payment_transaction({ + 'acquirer_id': self.env.ref('payment.payment_acquirer_transfer').id, + }) + transaction._set_transaction_done() + transaction._post_process_after_done() + + # Assert a posted payment has been generated at this point. + self.assertTrue(transaction.payment_id) + self.assertEqual(transaction.payment_id.state, 'posted') + + invoice_ids = order.action_invoice_create() + invoice = self.env['account.invoice'].browse(invoice_ids) + invoice.action_invoice_open() + + self.assertEqual(invoice.state, 'paid') diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index b071ea16a95..50a8ab084ff 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -169,8 +169,10 @@ class Website(models.Model): affiliate_id = request.session.get('affiliate_id') salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id addr = partner.address_get(['delivery']) - if len(partner.sale_order_ids): # first = me - addr['delivery'] = partner.sale_order_ids[0].partner_shipping_id.id + if not request.website.is_public_user(): + last_sale_order = self.env['sale.order'].search([('partner_id', '=', partner.id)], limit=1, order="date_order desc, id desc") + if last_sale_order: # first = me + addr['delivery'] = last_sale_order.partner_shipping_id.id default_user_id = partner.parent_id.user_id.id or partner.user_id.id values = { 'partner_id': partner.id, diff --git a/odoo/addons/base/tests/test_orm.py b/odoo/addons/base/tests/test_orm.py index 2663658ac3c..af515299d21 100644 --- a/odoo/addons/base/tests/test_orm.py +++ b/odoo/addons/base/tests/test_orm.py @@ -357,6 +357,17 @@ class TestInherits(TransactionCase): self.assertEqual(user_bar.name, 'Bar', "name is given from specific partner") self.assertEqual(user_bar.signature, user_foo.signature, "signature should be copied") + @mute_logger('odoo.models') + def test_write_date(self): + """ modifying inherited fields must update write_date """ + user = self.env.user + write_date_before = user.write_date + + # write base64 image + user.write({'image': 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='}) + write_date_after = user.write_date + self.assertNotEqual(write_date_before, write_date_after) + CREATE = lambda values: (0, False, values) UPDATE = lambda id, values: (1, id, values) diff --git a/odoo/addons/base/wizard/base_module_uninstall.py b/odoo/addons/base/wizard/base_module_uninstall.py index ac224dcc82b..a3f53f38c31 100644 --- a/odoo/addons/base/wizard/base_module_uninstall.py +++ b/odoo/addons/base/wizard/base_module_uninstall.py @@ -40,10 +40,12 @@ class BaseModuleUninstall(models.TransientModel): for wizard in self: if wizard.module_id: module_names = set(wizard._get_modules().mapped('name')) - # find the models that have all their XIDs in the given modules + def lost(model): - return all(xid.split('.')[0] in module_names - for xid in ir_models_xids.get(model.id, ())) + xids = ir_models_xids.get(model.id, ()) + return xids and all(xid.split('.')[0] in module_names for xid in xids) + + # find the models that have all their XIDs in the given modules self.model_ids = ir_models.filtered(lost).sorted('name') @api.onchange('module_id') diff --git a/odoo/models.py b/odoo/models.py index dae750654f5..1af6cd9c173 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -3253,7 +3253,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): with self.env.protecting(protected_fields, self): # write stored fields with (low-level) method _write - if store_vals: + if store_vals or inverse_vals or inherited_vals: self._write(store_vals) # update parent records (after possibly updating parent fields)