From b9a78f64867aa77ccdcc5ea73a93b65dc18851cd Mon Sep 17 00:00:00 2001 From: mariadforgeflow Date: Wed, 25 Aug 2021 10:58:56 +0200 Subject: [PATCH] [MIG] purchase_stock_picking_return_invoicing: Migration to 14.0 --- .../__manifest__.py | 8 +- .../models/account_invoice.py | 4 +- .../models/purchase_order.py | 173 +++++++++++++----- .../readme/CONTRIBUTORS.rst | 4 +- ...purchase_stock_picking_return_invoicing.py | 39 ++-- .../views/account_invoice_view.xml | 2 +- .../views/purchase_view.xml | 9 +- 7 files changed, 154 insertions(+), 85 deletions(-) diff --git a/purchase_stock_picking_return_invoicing/__manifest__.py b/purchase_stock_picking_return_invoicing/__manifest__.py index 765fabfe79d..ffa5194cf29 100644 --- a/purchase_stock_picking_return_invoicing/__manifest__.py +++ b/purchase_stock_picking_return_invoicing/__manifest__.py @@ -1,18 +1,18 @@ -# Copyright 2019 Eficent Business and IT Consulting Services +# Copyright 2019 ForgeFlow S.L. (https://www.forgeflow.com) # Copyright 2017-2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Purchase Stock Picking Return Invoicing", "summary": "Add an option to refund returned pickings", - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "category": "Purchases", "website": "https://github.com/OCA/account-invoicing", - "author": "Eficent," "Tecnativa," "Odoo Community Association (OCA)", + "author": "ForgeFlow, Tecnativa, Odoo Community Association (OCA)", "license": "AGPL-3", "installable": True, "development_status": "Mature", "depends": ["purchase_stock"], "data": ["views/account_invoice_view.xml", "views/purchase_view.xml"], - "maintainers": ["pedrobaeza"], + "maintainers": ["pedrobaeza", "MiquelRForgeFlow"], } diff --git a/purchase_stock_picking_return_invoicing/models/account_invoice.py b/purchase_stock_picking_return_invoicing/models/account_invoice.py index 605f7d204de..a98651dffd0 100644 --- a/purchase_stock_picking_return_invoicing/models/account_invoice.py +++ b/purchase_stock_picking_return_invoicing/models/account_invoice.py @@ -1,4 +1,4 @@ -# Copyright 2017 Eficent Business and IT Consulting Services +# Copyright 2017 ForgeFlow S.L. (https://www.forgeflow.com) # Copyright 2017-2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -13,7 +13,7 @@ class AccountMove(models.Model): def _onchange_purchase_auto_complete(self): """Remove lines with qty=0 when making refunds.""" res = super()._onchange_purchase_auto_complete() - if self.type == "in_refund": + if self.move_type == "in_refund": self.line_ids -= self.invoice_line_ids.filtered( lambda x: float_is_zero( x.quantity, precision_rounding=x.product_uom_id.rounding diff --git a/purchase_stock_picking_return_invoicing/models/purchase_order.py b/purchase_stock_picking_return_invoicing/models/purchase_order.py index 6ded4697b89..50637d60fea 100644 --- a/purchase_stock_picking_return_invoicing/models/purchase_order.py +++ b/purchase_stock_picking_return_invoicing/models/purchase_order.py @@ -1,11 +1,13 @@ -# Copyright 2017 Eficent Business and IT Consulting Services +# Copyright 2017 ForgeFlow S.L. (https://www.forgeflow.com) # Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import collections +from itertools import groupby -from odoo import api, fields, models -from odoo.tools import float_compare +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare, float_is_zero class PurchaseOrder(models.Model): @@ -45,7 +47,7 @@ def _get_invoiced(self): def _compute_invoice_refund_count(self): for order in self: invoices = order.mapped("order_line.invoice_lines.move_id").filtered( - lambda x: x.type == "in_refund" + lambda x: x.move_type == "in_refund" ) order.invoice_refund_count = len(invoices) @@ -60,45 +62,129 @@ def _compute_invoice(self): for order in self: order.invoice_count -= order.invoice_refund_count - def action_view_invoice_refund(self): + def action_create_invoice_refund(self): + """Create the refund associated to the PO.""" + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + # 1) Prepare refund vals and clean-up the section lines + invoice_vals_list = [] + for order in self: + if order.invoice_status != "to invoice": + continue + order = order.with_company(order.company_id) + pending_section = None + # Invoice values. + invoice_vals = order._prepare_invoice() + # Invoice line values (keep only necessary sections). + for line in order.order_line: + if line.display_type == "line_section": + pending_section = line + continue + if not float_is_zero(line.qty_to_invoice, precision_digits=precision): + if pending_section: + invoice_vals["invoice_line_ids"].append( + (0, 0, pending_section._prepare_account_move_line()) + ) + pending_section = None + invoice_vals["invoice_line_ids"].append( + (0, 0, line._prepare_account_move_line()) + ) + invoice_vals_list.append(invoice_vals) + if not invoice_vals_list: + raise UserError( + _( + "There is no invoiceable line. " + "If a product has a control policy based on received quantity, " + "please make sure that a quantity has been received." + ) + ) + # 2) group by (company_id, partner_id, currency_id) for batch creation + new_invoice_vals_list = [] + for _grouping_keys, invoices in groupby( + invoice_vals_list, + key=lambda x: ( + x.get("company_id"), + x.get("partner_id"), + x.get("currency_id"), + ), + ): + origins = set() + payment_refs = set() + refs = set() + ref_invoice_vals = None + for invoice_vals in invoices: + if not ref_invoice_vals: + ref_invoice_vals = invoice_vals + else: + ref_invoice_vals["invoice_line_ids"] += invoice_vals[ + "invoice_line_ids" + ] + origins.add(invoice_vals["invoice_origin"]) + payment_refs.add(invoice_vals["payment_reference"]) + refs.add(invoice_vals["ref"]) + ref_invoice_vals.update( + { + "ref": ", ".join(refs)[:2000], + "invoice_origin": ", ".join(origins), + "payment_reference": len(payment_refs) == 1 + and payment_refs.pop() + or False, + } + ) + new_invoice_vals_list.append(ref_invoice_vals) + invoice_vals_list = new_invoice_vals_list + # 3) Create refunds. + moves = self.env["account.move"] + AccountMove = self.env["account.move"].with_context( + default_move_type="in_refund" + ) + for vals in invoice_vals_list: + moves |= AccountMove.with_company(vals["company_id"]).create(vals) + return self.action_view_invoice_refund(moves) + + def action_view_invoice_refund(self, invoices=False): """This function returns an action that display existing vendor refund - bills of given purchase order id. + bills of given purchase order ids. When only one found, show the vendor bill immediately. """ - action = self.env.ref("account.action_move_in_refund_type") - result = action.read()[0] - create_refund = self.env.context.get("create_refund", False) - refunds = self.invoice_ids.filtered(lambda x: x.type == "in_refund") - # override the context to get rid of the default filtering - result["context"] = { - "default_type": "in_refund", - "default_company_id": self.company_id.id, - "default_purchase_id": self.id, - } + if not invoices: + # Invoice_ids may be filtered depending on the user. To ensure we get all + # invoices related to the purchase order, we read them in sudo to fill the + # cache. + self.sudo()._read(["invoice_ids"]) + invoices = self.invoice_ids + refunds = invoices.filtered(lambda x: x.move_type == "in_refund") + result = self.env["ir.actions.act_window"]._for_xml_id( + "account.action_move_in_refund_type" + ) # choose the view_mode accordingly - if len(refunds) > 1 and not create_refund: - result["domain"] = "[('id', 'in', " + str(refunds.ids) + ")]" + if len(refunds) > 1: + result["domain"] = [("id", "in", refunds.ids)] + elif len(refunds) == 1: + res = self.env.ref("account.view_move_form", False) + form_view = [(res and res.id or False, "form")] + if "views" in result: + result["views"] = form_view + [ + (state, view) for state, view in result["views"] if view != "form" + ] + else: + result["views"] = form_view + result["res_id"] = refunds.id else: - res = self.env.ref("account.invoice_supplier_form", False) - result["views"] = [(res and res.id or False, "form")] - # Do not set an move_id if we want to create a new refund. - if not create_refund: - result["res_id"] = refunds.id or False - result["context"]["default_origin"] = self.name - result["context"]["default_reference"] = self.partner_ref + result = {"type": "ir.actions.act_window_close"} return result - def action_view_invoice(self): + def action_view_invoice(self, invoices=False): """Change super action for displaying only normal invoices.""" - result = super(PurchaseOrder, self).action_view_invoice() - invoices = self.invoice_ids.filtered(lambda x: x.type == "in_invoice") - # choose the view_mode accordingly - if len(invoices) != 1: - result["domain"] = [("id", "in", invoices.ids)] - elif len(invoices) == 1: - res = self.env.ref("account.view_move_form", False) - result["views"] = [(res and res.id or False, "form")] - result["res_id"] = invoices.id + if not invoices: + # Invoice_ids may be filtered depending on the user. To ensure we get all + # invoices related to the purchase order, we read them in sudo to fill the + # cache. + self.sudo()._read(["invoice_ids"]) + invoices = self.invoice_ids + invoices = invoices.filtered(lambda x: x.move_type == "in_invoice") + result = super(PurchaseOrder, self).action_view_invoice(invoices) return result @@ -118,8 +204,8 @@ def _compute_qty_refunded(self): for line in self: inv_lines = line.invoice_lines.filtered( lambda x: ( - (x.move_id.type == "in_invoice" and x.quantity < 0.0) - or (x.move_id.type == "in_refund" and x.quantity > 0.0) + (x.move_id.move_type == "in_invoice" and x.quantity < 0.0) + or (x.move_id.move_type == "in_refund" and x.quantity > 0.0) ) ) line.qty_refunded = sum( @@ -160,14 +246,11 @@ def _compute_qty_returned(self): for line in self: line.qty_returned = line_qtys.get(line.id, 0) - def _prepare_account_move_line(self, move): + def _prepare_account_move_line(self, move=None): data = super()._prepare_account_move_line(move) - if self.product_id.purchase_method == "receive": - # This formula proceeds from the simplification of full expression: - # qty_received + qty_returned - (qty_invoiced + qty_refunded) - - # (qty_returned - qty_refunded) - qty = self.qty_received - self.qty_invoiced - data["quantity"] = qty - if move.type == "in_refund": + move_type = self.env.context.get("default_move_type", False) + if (move and move.move_type == "in_refund") or ( + not move and move_type and move_type == "in_refund" + ): data["quantity"] *= -1.0 return data diff --git a/purchase_stock_picking_return_invoicing/readme/CONTRIBUTORS.rst b/purchase_stock_picking_return_invoicing/readme/CONTRIBUTORS.rst index 65761931310..08ad716a0c5 100644 --- a/purchase_stock_picking_return_invoicing/readme/CONTRIBUTORS.rst +++ b/purchase_stock_picking_return_invoicing/readme/CONTRIBUTORS.rst @@ -1,6 +1,6 @@ -* Eficent : +* Forgeflow : - * Jordi Ballester Alomar + * Jordi Ballester Alomar * Tecnativa diff --git a/purchase_stock_picking_return_invoicing/tests/test_purchase_stock_picking_return_invoicing.py b/purchase_stock_picking_return_invoicing/tests/test_purchase_stock_picking_return_invoicing.py index 461691ebea1..76586d5883a 100644 --- a/purchase_stock_picking_return_invoicing/tests/test_purchase_stock_picking_return_invoicing.py +++ b/purchase_stock_picking_return_invoicing/tests/test_purchase_stock_picking_return_invoicing.py @@ -1,15 +1,13 @@ -# Copyright 2019 Eficent Business and IT Consulting Services +# Copyright 2019 ForgeFlow S.L. (https://www.forgeflow.com) # Copyright 2017-2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields -from odoo.tests.common import Form, SavepointCase +from odoo.tests.common import SavepointCase, tagged +@tagged("post_install", "-at_install") class TestPurchaseStockPickingReturnInvoicing(SavepointCase): - at_install = False - post_install = True - @classmethod def setUpClass(cls): """Add some defaults to let the test run without an accounts chart.""" @@ -86,6 +84,7 @@ def setUpClass(cls): ) cls.po_line = cls.po.order_line cls.po.button_confirm() + cls.po.button_approve() def check_values( self, @@ -118,13 +117,8 @@ def test_purchase_stock_return_1(self): pick.button_validate() self.check_values(self.po_line, 0, 5, 0, 0, "to invoice") # Make invoice - ctx = self.po.action_view_invoice()["context"] - active_model = self.env["account.move"].with_context(ctx) - view_id = "account.view_move_form" - with Form(active_model, view=view_id) as f: - f.partner_id = self.partner - f.purchase_id = self.po - inv_1 = f.save() + action = self.po.action_create_invoice() + inv_1 = self.env["account.move"].browse(action["res_id"]) self.check_values(self.po_line, 0, 5, 0, 5, "invoiced") self.assertAlmostEqual(inv_1.amount_untaxed_signed, -50, 2) @@ -137,14 +131,12 @@ def test_purchase_stock_return_1(self): return_pick.button_validate() self.check_values(self.po_line, 2, 3, 0, 5, "to invoice") # Make refund - ctx = self.po.action_view_invoice_refund()["context"] - active_model = self.env["account.move"].with_context(ctx) - view_id = "account.view_move_form" - with Form(active_model, view=view_id) as f: - f.partner_id = self.partner - f.purchase_id = self.po - inv_2 = f.save() + action2 = self.po.with_context( + default_move_type="in_refund" + ).action_create_invoice_refund() + inv_2 = self.env["account.move"].browse(action2["res_id"]) self.check_values(self.po_line, 2, 3, 2, 3, "invoiced") + self.assertAlmostEqual(inv_2.amount_untaxed_signed, 20, 2) action = self.po.action_view_invoice() self.assertEqual(action["res_id"], inv_1.id) @@ -171,12 +163,7 @@ def test_purchase_stock_return_2(self): return_pick.button_validate() self.check_values(self.po_line, 2, 3, 0, 0, "to invoice") # Make invoice - ctx = self.po.action_view_invoice()["context"] - active_model = self.env["account.move"].with_context(ctx) - view_id = "account.view_move_form" - with Form(active_model, view=view_id) as f: - f.partner_id = self.partner - f.purchase_id = self.po - inv_1 = f.save() + action = self.po.action_create_invoice() + inv_1 = self.env["account.move"].browse(action["res_id"]) self.check_values(self.po_line, 2, 3, 0, 3, "invoiced") self.assertAlmostEqual(inv_1.amount_untaxed_signed, -30, 2) diff --git a/purchase_stock_picking_return_invoicing/views/account_invoice_view.xml b/purchase_stock_picking_return_invoicing/views/account_invoice_view.xml index 67bd92b56ed..a036af530d1 100644 --- a/purchase_stock_picking_return_invoicing/views/account_invoice_view.xml +++ b/purchase_stock_picking_return_invoicing/views/account_invoice_view.xml @@ -8,7 +8,7 @@ - @@ -27,7 +26,7 @@ name="action_view_invoice_refund" class="oe_stat_button" icon="fa-pencil-square-o" - attrs="{'invisible':['|', ('invoice_count', '=', 0), ('state', 'in', ('draft','sent','to approve'))]}" + attrs="{'invisible':['|', ('invoice_refund_count', '=', 0), ('state', 'in', ('draft','sent','to approve'))]}" >