From a589c94bece6d71f268c1806b9af3e04c431985b Mon Sep 17 00:00:00 2001 From: Sergio Teruel Date: Tue, 29 Oct 2019 11:47:28 +0100 Subject: [PATCH 1/4] [ADD] product_cost_price_avco_sync: New module to sync product cost with stock move price unit --- product_cost_price_avco_sync/README.rst | 77 ++++ product_cost_price_avco_sync/__init__.py | 2 + product_cost_price_avco_sync/__manifest__.py | 16 + product_cost_price_avco_sync/i18n/es.po | 38 ++ .../i18n/product_cost_price_avco_sync.pot | 37 ++ .../models/__init__.py | 5 + .../models/product.py | 13 + .../models/purchase_order.py | 23 + .../models/stock_move.py | 64 +++ .../models/stock_picking.py | 12 + .../readme/CONTRIBUTORS.rst | 4 + .../readme/DESCRIPTION.rst | 2 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/icon.svg | 79 ++++ .../static/description/index.html | 427 ++++++++++++++++++ .../tests/__init__.py | 3 + .../test_product_cost_price_avco_sync.py | 139 ++++++ 17 files changed, 941 insertions(+) create mode 100644 product_cost_price_avco_sync/README.rst create mode 100644 product_cost_price_avco_sync/__init__.py create mode 100644 product_cost_price_avco_sync/__manifest__.py create mode 100644 product_cost_price_avco_sync/i18n/es.po create mode 100644 product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot create mode 100644 product_cost_price_avco_sync/models/__init__.py create mode 100644 product_cost_price_avco_sync/models/product.py create mode 100644 product_cost_price_avco_sync/models/purchase_order.py create mode 100644 product_cost_price_avco_sync/models/stock_move.py create mode 100644 product_cost_price_avco_sync/models/stock_picking.py create mode 100644 product_cost_price_avco_sync/readme/CONTRIBUTORS.rst create mode 100644 product_cost_price_avco_sync/readme/DESCRIPTION.rst create mode 100644 product_cost_price_avco_sync/static/description/icon.png create mode 100644 product_cost_price_avco_sync/static/description/icon.svg create mode 100644 product_cost_price_avco_sync/static/description/index.html create mode 100644 product_cost_price_avco_sync/tests/__init__.py create mode 100644 product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py diff --git a/product_cost_price_avco_sync/README.rst b/product_cost_price_avco_sync/README.rst new file mode 100644 index 000000000000..478e0f98431c --- /dev/null +++ b/product_cost_price_avco_sync/README.rst @@ -0,0 +1,77 @@ +============================ +Product cost price avco sync +============================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/11.0/product_cost_price_avco_sync + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-11-0/stock-logistics-workflow-11-0-product_cost_price_avco_sync + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/154/11.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to sync cost price products with average cost method from +stock moves price unit. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Carlos Dauden + * Sergio Teruel + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_cost_price_avco_sync/__init__.py b/product_cost_price_avco_sync/__init__.py new file mode 100644 index 000000000000..3275ac2adf3d --- /dev/null +++ b/product_cost_price_avco_sync/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models diff --git a/product_cost_price_avco_sync/__manifest__.py b/product_cost_price_avco_sync/__manifest__.py new file mode 100644 index 000000000000..6332b9dcbd21 --- /dev/null +++ b/product_cost_price_avco_sync/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2018 Tecnativa - Carlos Dauden +# Copyright 2018 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + 'name': 'Product cost price avco sync', + 'summary': 'Set product cost price from updated moves', + 'version': '11.0.1.0.0', + 'category': 'Stock', + 'website': 'https://github.com/OCA/stock-logistics-workflow', + 'author': 'Tecnativa, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'installable': True, + 'depends': [ + 'purchase', + ], +} diff --git a/product_cost_price_avco_sync/i18n/es.po b/product_cost_price_avco_sync/i18n/es.po new file mode 100644 index 000000000000..39772b0b69ef --- /dev/null +++ b/product_cost_price_avco_sync/i18n/es.po @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_price_avco_sync +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-10-29 10:28+0000\n" +"PO-Revision-Date: 2019-10-29 11:30+0100\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.6\n" +"Last-Translator: \n" +"Language: es\n" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_product_product +msgid "Product" +msgstr "Producto" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "LĂ­nea pedido de compra" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de existencias" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_picking +msgid "Transfer" +msgstr "Transferir" diff --git a/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot new file mode 100644 index 000000000000..366a55c38f1c --- /dev/null +++ b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_price_avco_sync +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-10-29 10:28+0000\n" +"PO-Revision-Date: 2019-10-29 10:28+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_product_product +msgid "Product" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_picking +msgid "Transfer" +msgstr "" + diff --git a/product_cost_price_avco_sync/models/__init__.py b/product_cost_price_avco_sync/models/__init__.py new file mode 100644 index 000000000000..4d0469d4165e --- /dev/null +++ b/product_cost_price_avco_sync/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import product +from . import purchase_order +from . import stock_move +from . import stock_picking diff --git a/product_cost_price_avco_sync/models/product.py b/product_cost_price_avco_sync/models/product.py new file mode 100644 index 000000000000..8e08857294fd --- /dev/null +++ b/product_cost_price_avco_sync/models/product.py @@ -0,0 +1,13 @@ +# Copyright 2019 Carlos Dauden - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + def get_cost_history_date(self, date): + return self.env['product.price.history'].search([ + ('product_id', '=', self.id), + ('datetime', '<', date), + ], limit=1, order='datetime DESC').cost diff --git a/product_cost_price_avco_sync/models/purchase_order.py b/product_cost_price_avco_sync/models/purchase_order.py new file mode 100644 index 000000000000..66829dcfde92 --- /dev/null +++ b/product_cost_price_avco_sync/models/purchase_order.py @@ -0,0 +1,23 @@ +# Copyright 2019 Carlos Dauden - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class PurchaseOrderLine(models.Model): + _inherit = 'purchase.order.line' + + def write(self, vals): + res = super().write(vals) + if 'price_unit' in vals and not self.env.context.get( + 'skip_cost_update'): + self.price_unit_update() + return res + + def price_unit_update(self): + for line in self: + if line.state not in ['purchase', 'done']: + continue + line.move_ids.write({ + 'price_unit': line.price_unit, + }) diff --git a/product_cost_price_avco_sync/models/stock_move.py b/product_cost_price_avco_sync/models/stock_move.py new file mode 100644 index 000000000000..adcfb4dad451 --- /dev/null +++ b/product_cost_price_avco_sync/models/stock_move.py @@ -0,0 +1,64 @@ +# Copyright 2019 Carlos Dauden - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.tools.float_utils import float_compare + + +class StockMove(models.Model): + _inherit = 'stock.move' + + def write(self, vals): + res = super(StockMove, self).write(vals) + if ('price_unit' in vals and + not self.env.context.get('skip_cost_update')): + self.price_unit_update() + return res + + def price_unit_update(self): + procesed_moves = [] + for move in self.sorted('date'): + if (move.id in procesed_moves or move.state != 'done' or + not move._is_in() or + move.product_id.cost_method != 'average'): + continue + # not take in account move's quantities + previous_qty = move.product_id.with_context( + to_date=move.date + ).qty_available - move.quantity_done + price_history = self.product_id.get_cost_history_date(move.date) + price = move.get_average_price(previous_qty, price_history) + previous_qty += move.quantity_done + affected_moves = self.search([ + ('product_id', '=', move.product_id.id), + ('date', '>', move.date), + ], order='date') + for af_move in affected_moves: + if af_move._is_in(): + if af_move.inventory_id or af_move.move_orig_ids: + af_move.with_context( + skip_cost_update=True).price_unit = price + # Avoid reprocess move if included in self.write + if af_move.id not in procesed_moves: + procesed_moves.append(af_move.id) + price = af_move.get_average_price(previous_qty, price) + previous_qty += af_move.quantity_done + elif af_move._is_out(): + af_move.with_context( + skip_cost_update=True).price_unit = -price + previous_qty -= af_move.quantity_done + if previous_qty < 0.0: + previous_qty = 0.0 + if float_compare(move.product_id.standard_price, price, + precision_rounding=move.product_uom.rounding): + # Write the standard price, as SUPERUSER_ID because a warehouse + # manager may not have the right to write on products + move.product_id.with_context( + force_company=move.company_id.id, + ).sudo().write({'standard_price': price}) + + def get_average_price(self, previous_qty, previous_price): + new_std_price = ((previous_qty * previous_price) + ( + self.price_unit * self.quantity_done)) / (previous_qty + + self.quantity_done) + return new_std_price diff --git a/product_cost_price_avco_sync/models/stock_picking.py b/product_cost_price_avco_sync/models/stock_picking.py new file mode 100644 index 000000000000..ff2f269d1c5a --- /dev/null +++ b/product_cost_price_avco_sync/models/stock_picking.py @@ -0,0 +1,12 @@ +# Copyright 2019 Carlos Dauden - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + def action_done(self): + return super(StockPicking, self.with_context( + skip_cost_update=True)).action_done() diff --git a/product_cost_price_avco_sync/readme/CONTRIBUTORS.rst b/product_cost_price_avco_sync/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..22e2c9556170 --- /dev/null +++ b/product_cost_price_avco_sync/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Tecnativa `_: + + * Carlos Dauden + * Sergio Teruel diff --git a/product_cost_price_avco_sync/readme/DESCRIPTION.rst b/product_cost_price_avco_sync/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..d800b605434b --- /dev/null +++ b/product_cost_price_avco_sync/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to sync cost price products with average cost method from +stock moves price unit. diff --git a/product_cost_price_avco_sync/static/description/icon.png b/product_cost_price_avco_sync/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/product_cost_price_avco_sync/static/description/icon.svg b/product_cost_price_avco_sync/static/description/icon.svg new file mode 100644 index 000000000000..a7a26d0932ab --- /dev/null +++ b/product_cost_price_avco_sync/static/description/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/product_cost_price_avco_sync/static/description/index.html b/product_cost_price_avco_sync/static/description/index.html new file mode 100644 index 000000000000..64036f7fc67d --- /dev/null +++ b/product_cost_price_avco_sync/static/description/index.html @@ -0,0 +1,427 @@ + + + + + + +Product cost price avco sync + + + +
+

Product cost price avco sync

+ + +

Beta License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runbot

+

This module allows to sync cost price products with average cost method from +stock moves price unit.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/stock-logistics-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_cost_price_avco_sync/tests/__init__.py b/product_cost_price_avco_sync/tests/__init__.py new file mode 100644 index 000000000000..54427389eae2 --- /dev/null +++ b/product_cost_price_avco_sync/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_product_cost_price_avco_sync diff --git a/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py new file mode 100644 index 000000000000..1971add3e501 --- /dev/null +++ b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py @@ -0,0 +1,139 @@ +# Copyright 2019 Tecnativa - Carlos Dauden +# Copyright 2019 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests.common import SavepointCase + + +class TestProductCostPriceAvcoSync(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestProductCostPriceAvcoSync, cls).setUpClass() + cls.PurchaseOrder = cls.env['purchase.order'] + cls.StockPicking = cls.env['stock.picking'] + cls.supplier_location = cls.env.ref('stock.stock_location_suppliers') + cls.customer_location = cls.env.ref('stock.stock_location_customers') + cls.warehouse = cls.env.ref('stock.warehouse0') + cls.stock_location = cls.env.ref('stock.stock_location_stock') + cls.picking_type_in = cls.env.ref('stock.picking_type_in') + cls.picking_type_out = cls.env.ref('stock.picking_type_out') + cls.partner = cls.env['res.partner'].create({ + 'customer': True, + 'supplier': True, + 'name': 'Test Partner', + }) + cls.picking = cls.env['stock.picking'].create({ + 'picking_type_id': cls.picking_type_in.id, + 'location_id': cls.supplier_location.id, + 'location_dest_id': cls.stock_location.id, + }) + cls.product = cls.env['product.product'].create({ + 'name': 'Product for test', + 'type': 'product', + 'tracking': 'none', + 'property_cost_method': 'average', + }) + cls.env['stock.move'].create({ + 'name': 'a move', + 'product_id': cls.product.id, + 'product_uom_qty': 5.0, + 'product_uom': cls.product.uom_id.id, + 'picking_id': cls.picking.id, + 'location_id': cls.supplier_location.id, + 'location_dest_id': cls.stock_location.id, + }) + cls.picking.action_assign() + cls.move_line = cls.picking.move_lines[:1] + + def test_sync_cost_price(self): + po = self.env['purchase.order'].new({ + 'partner_id': self.partner.id, + 'date_order': '2019-10-01', + }) + default_vals = self.PurchaseOrder.default_get( + list(self.PurchaseOrder._fields)) + po.update(default_vals) + po.onchange_partner_id() + po.order_line = [(0, 0, { + 'product_id': self.product.id, + })] + po.order_line.onchange_product_id() + po.order_line.product_qty = 100.0 + po.order_line.price_unit = 5.0 + purchase_order = self.PurchaseOrder.create( + po._convert_to_write(po._cache)) + purchase_order.button_confirm() + purchase_order.picking_ids.move_line_ids.qty_done = 100.0 + purchase_order.picking_ids.action_done() + purchase_order.picking_ids.move_lines.date = '2019-10-01' + + picking_in_vals = { + 'name': '/', + 'partner_id': self.partner.id, + 'picking_type_id': self.picking_type_in.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'move_lines': [(0, 0, { + 'name': self.product.name, + 'product_id': self.product.id, + 'product_uom_qty': 10.0, + 'product_uom': self.product.uom_id.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + })] + } + picking_in = self.StockPicking.create(picking_in_vals) + picking_in.move_line_ids.qty_done = 10.0 + picking_in.action_done() + picking_in.move_lines.date = '2019-10-02' + + picking_out_vals = { + 'name': '/', + 'partner_id': self.partner.id, + 'picking_type_id': self.picking_type_out.id, + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'move_lines': [(0, 0, { + 'name': self.product.name, + 'product_id': self.product.id, + 'product_uom_qty': 10.0, + 'product_uom': self.product.uom_id.id, + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + })] + } + + picking_out = self.StockPicking.create(picking_out_vals.copy()) + picking_out.action_assign() + picking_out.move_line_ids.qty_done = 5.0 + picking_out.action_done() + picking_out.move_lines.date = '2019-10-03' + + picking_out2 = self.StockPicking.create(picking_out_vals.copy()) + picking_out2.action_assign() + picking_out2.move_line_ids.qty_done = 5.0 + picking_out2.action_done() + picking_out2.move_lines.date = '2019-10-04' + + # Make an inventory + inventory = self.env['stock.inventory'].create({ + 'name': 'Initial inventory', + 'filter': 'partial', + 'location_id': self.warehouse.lot_stock_id.id, + 'line_ids': [(0, 0, { + 'product_id': self.product.id, + 'product_uom_id': self.product.uom_id.id, + 'product_qty': 200, + 'location_id': self.warehouse.lot_stock_id.id + })] + }) + inventory.action_done() + inventory.move_ids.date = '2019-10-05' + + self.assertEqual(self.product.standard_price, 5.0) + purchase_order.order_line.price_unit = 2.0 + self.assertEqual(self.product.standard_price, 2.27) + self.assertAlmostEqual( + round(picking_out.move_lines.price_unit, 2), -2.27) + self.assertAlmostEqual( + round(picking_out2.move_lines.price_unit, 2), -2.27) From fe6459b669c8500f2abeee6736e4249dfd918273 Mon Sep 17 00:00:00 2001 From: Carlos Dauden Date: Thu, 7 Nov 2019 12:13:09 +0100 Subject: [PATCH 2/4] [IMP] product_cost_price_avco_sync: Cover more use cases --- product_cost_price_avco_sync/__manifest__.py | 2 +- .../models/__init__.py | 2 - .../models/product.py | 13 -- .../models/purchase_order.py | 23 --- .../models/stock_move.py | 91 ++++++--- .../models/stock_picking.py | 3 +- .../test_product_cost_price_avco_sync.py | 193 ++++++++++-------- 7 files changed, 175 insertions(+), 152 deletions(-) delete mode 100644 product_cost_price_avco_sync/models/product.py delete mode 100644 product_cost_price_avco_sync/models/purchase_order.py diff --git a/product_cost_price_avco_sync/__manifest__.py b/product_cost_price_avco_sync/__manifest__.py index 6332b9dcbd21..0b692421cd80 100644 --- a/product_cost_price_avco_sync/__manifest__.py +++ b/product_cost_price_avco_sync/__manifest__.py @@ -11,6 +11,6 @@ 'license': 'AGPL-3', 'installable': True, 'depends': [ - 'purchase', + 'stock_account', ], } diff --git a/product_cost_price_avco_sync/models/__init__.py b/product_cost_price_avco_sync/models/__init__.py index 4d0469d4165e..fb7711831798 100644 --- a/product_cost_price_avco_sync/models/__init__.py +++ b/product_cost_price_avco_sync/models/__init__.py @@ -1,5 +1,3 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from . import product -from . import purchase_order from . import stock_move from . import stock_picking diff --git a/product_cost_price_avco_sync/models/product.py b/product_cost_price_avco_sync/models/product.py deleted file mode 100644 index 8e08857294fd..000000000000 --- a/product_cost_price_avco_sync/models/product.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2019 Carlos Dauden - Tecnativa -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models - - -class ProductProduct(models.Model): - _inherit = 'product.product' - - def get_cost_history_date(self, date): - return self.env['product.price.history'].search([ - ('product_id', '=', self.id), - ('datetime', '<', date), - ], limit=1, order='datetime DESC').cost diff --git a/product_cost_price_avco_sync/models/purchase_order.py b/product_cost_price_avco_sync/models/purchase_order.py deleted file mode 100644 index 66829dcfde92..000000000000 --- a/product_cost_price_avco_sync/models/purchase_order.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2019 Carlos Dauden - Tecnativa -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import models - - -class PurchaseOrderLine(models.Model): - _inherit = 'purchase.order.line' - - def write(self, vals): - res = super().write(vals) - if 'price_unit' in vals and not self.env.context.get( - 'skip_cost_update'): - self.price_unit_update() - return res - - def price_unit_update(self): - for line in self: - if line.state not in ['purchase', 'done']: - continue - line.move_ids.write({ - 'price_unit': line.price_unit, - }) diff --git a/product_cost_price_avco_sync/models/stock_move.py b/product_cost_price_avco_sync/models/stock_move.py index adcfb4dad451..d5b3201fab40 100644 --- a/product_cost_price_avco_sync/models/stock_move.py +++ b/product_cost_price_avco_sync/models/stock_move.py @@ -1,7 +1,9 @@ # Copyright 2019 Carlos Dauden - Tecnativa # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models +from datetime import timedelta + +from odoo import fields, models from odoo.tools.float_utils import float_compare @@ -9,46 +11,89 @@ class StockMove(models.Model): _inherit = 'stock.move' def write(self, vals): + """ Update cost price avco """ res = super(StockMove, self).write(vals) - if ('price_unit' in vals and - not self.env.context.get('skip_cost_update')): - self.price_unit_update() + if (('price_unit' in vals or 'quantity_done' in vals) and + not self.env.context.get('skip_avco_sync')): + self.cost_price_avco_sync() return res - def price_unit_update(self): - procesed_moves = [] + def _create_price_history(self, value): + PriceHistory = self.env['product.price.history'] + for move in self: + PriceHistory.create({ + 'product_id': move.product_id.id, + 'cost': value, + 'datetime': move.date, + 'company_id': move.company_id.id, + }) + + def _previous_instant_date(self): + """ Returns previous instant before move was done""" + self.ensure_one() + date_with_delta = fields.Datetime.from_string( + self.date) - timedelta(seconds=1) + return fields.Datetime.to_string(date_with_delta) + + def _remove_after_history_price(self): + PriceHistory = self.env['product.price.history'] + for move in self: + PriceHistory.search([ + ('company_id', '=', move.company_id.id), + ('product_id', '=', move.product_id.id), + ('datetime', '>=', move.date), + ]).unlink() + + def get_average_price(self, previous_qty, previous_price): + if self.quantity_done == 0.0: + price = previous_price + elif previous_qty < 0.0: + price = self.price_unit + else: + price = ((previous_qty * previous_price) + + (self.quantity_done * self.price_unit) + ) / (previous_qty + self.quantity_done) + return price + + def cost_price_avco_sync(self): + procesed_moves = set() for move in self.sorted('date'): if (move.id in procesed_moves or move.state != 'done' or not move._is_in() or move.product_id.cost_method != 'average'): continue - # not take in account move's quantities + previous_instant = move._previous_instant_date() previous_qty = move.product_id.with_context( - to_date=move.date - ).qty_available - move.quantity_done - price_history = self.product_id.get_cost_history_date(move.date) - price = move.get_average_price(previous_qty, price_history) + to_date=previous_instant + ).qty_available + previous_price = move.product_id.get_history_price( + move.company_id.id, previous_instant) + price = move.get_average_price(previous_qty, previous_price) + move._remove_after_history_price() + move._create_price_history(price) previous_qty += move.quantity_done - affected_moves = self.search([ + affected_moves = self.with_context(skip_avco_sync=True).search([ ('product_id', '=', move.product_id.id), ('date', '>', move.date), ], order='date') for af_move in affected_moves: if af_move._is_in(): if af_move.inventory_id or af_move.move_orig_ids: - af_move.with_context( - skip_cost_update=True).price_unit = price + af_move.write({ + 'price_unit': price, + 'value': price * af_move.quantity_done, + }) # Avoid reprocess move if included in self.write - if af_move.id not in procesed_moves: - procesed_moves.append(af_move.id) + procesed_moves.add(af_move.id) price = af_move.get_average_price(previous_qty, price) + af_move._create_price_history(price) previous_qty += af_move.quantity_done elif af_move._is_out(): - af_move.with_context( - skip_cost_update=True).price_unit = -price + af_move.write({ + 'price_unit': -price, + 'value': -price * af_move.quantity_done, + }) previous_qty -= af_move.quantity_done - if previous_qty < 0.0: - previous_qty = 0.0 if float_compare(move.product_id.standard_price, price, precision_rounding=move.product_uom.rounding): # Write the standard price, as SUPERUSER_ID because a warehouse @@ -56,9 +101,3 @@ def price_unit_update(self): move.product_id.with_context( force_company=move.company_id.id, ).sudo().write({'standard_price': price}) - - def get_average_price(self, previous_qty, previous_price): - new_std_price = ((previous_qty * previous_price) + ( - self.price_unit * self.quantity_done)) / (previous_qty + - self.quantity_done) - return new_std_price diff --git a/product_cost_price_avco_sync/models/stock_picking.py b/product_cost_price_avco_sync/models/stock_picking.py index ff2f269d1c5a..4e24880a4423 100644 --- a/product_cost_price_avco_sync/models/stock_picking.py +++ b/product_cost_price_avco_sync/models/stock_picking.py @@ -8,5 +8,6 @@ class StockPicking(models.Model): _inherit = 'stock.picking' def action_done(self): + """ Avoid AVCO cost price recomputation when validating picking """ return super(StockPicking, self.with_context( - skip_cost_update=True)).action_done() + skip_avco_sync=True)).action_done() diff --git a/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py index 1971add3e501..e88e6d1dfb2e 100644 --- a/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py +++ b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py @@ -9,7 +9,6 @@ class TestProductCostPriceAvcoSync(SavepointCase): @classmethod def setUpClass(cls): super(TestProductCostPriceAvcoSync, cls).setUpClass() - cls.PurchaseOrder = cls.env['purchase.order'] cls.StockPicking = cls.env['stock.picking'] cls.supplier_location = cls.env.ref('stock.stock_location_suppliers') cls.customer_location = cls.env.ref('stock.stock_location_customers') @@ -22,98 +21,62 @@ def setUpClass(cls): 'supplier': True, 'name': 'Test Partner', }) - cls.picking = cls.env['stock.picking'].create({ - 'picking_type_id': cls.picking_type_in.id, - 'location_id': cls.supplier_location.id, - 'location_dest_id': cls.stock_location.id, - }) cls.product = cls.env['product.product'].create({ 'name': 'Product for test', 'type': 'product', 'tracking': 'none', 'property_cost_method': 'average', + 'standard_price': 1, }) - cls.env['stock.move'].create({ - 'name': 'a move', - 'product_id': cls.product.id, - 'product_uom_qty': 5.0, - 'product_uom': cls.product.uom_id.id, - 'picking_id': cls.picking.id, + cls.picking_in = cls.env['stock.picking'].create({ + 'picking_type_id': cls.picking_type_in.id, 'location_id': cls.supplier_location.id, 'location_dest_id': cls.stock_location.id, - }) - cls.picking.action_assign() - cls.move_line = cls.picking.move_lines[:1] - - def test_sync_cost_price(self): - po = self.env['purchase.order'].new({ - 'partner_id': self.partner.id, - 'date_order': '2019-10-01', - }) - default_vals = self.PurchaseOrder.default_get( - list(self.PurchaseOrder._fields)) - po.update(default_vals) - po.onchange_partner_id() - po.order_line = [(0, 0, { - 'product_id': self.product.id, - })] - po.order_line.onchange_product_id() - po.order_line.product_qty = 100.0 - po.order_line.price_unit = 5.0 - purchase_order = self.PurchaseOrder.create( - po._convert_to_write(po._cache)) - purchase_order.button_confirm() - purchase_order.picking_ids.move_line_ids.qty_done = 100.0 - purchase_order.picking_ids.action_done() - purchase_order.picking_ids.move_lines.date = '2019-10-01' - - picking_in_vals = { - 'name': '/', - 'partner_id': self.partner.id, - 'picking_type_id': self.picking_type_in.id, - 'location_id': self.supplier_location.id, - 'location_dest_id': self.stock_location.id, 'move_lines': [(0, 0, { - 'name': self.product.name, - 'product_id': self.product.id, + 'name': 'a move', + 'product_id': cls.product.id, 'product_uom_qty': 10.0, - 'product_uom': self.product.uom_id.id, - 'location_id': self.supplier_location.id, - 'location_dest_id': self.stock_location.id, - })] - } - picking_in = self.StockPicking.create(picking_in_vals) - picking_in.move_line_ids.qty_done = 10.0 - picking_in.action_done() - picking_in.move_lines.date = '2019-10-02' - - picking_out_vals = { - 'name': '/', - 'partner_id': self.partner.id, - 'picking_type_id': self.picking_type_out.id, - 'location_id': self.stock_location.id, - 'location_dest_id': self.customer_location.id, + 'product_uom': cls.product.uom_id.id, + })], + }) + + cls.picking_out = cls.env['stock.picking'].create({ + 'picking_type_id': cls.picking_type_out.id, + 'location_id': cls.stock_location.id, + 'location_dest_id': cls.customer_location.id, 'move_lines': [(0, 0, { - 'name': self.product.name, - 'product_id': self.product.id, - 'product_uom_qty': 10.0, - 'product_uom': self.product.uom_id.id, - 'location_id': self.stock_location.id, - 'location_dest_id': self.customer_location.id, - })] - } + 'name': 'a move', + 'product_id': cls.product.id, + 'product_uom_qty': 5.0, + 'product_uom': cls.product.uom_id.id, + })], + }) + + def test_sync_cost_price(self): + move_in = self.picking_in.move_lines[:1] + move_in.product_uom_qty = 100 + move_in.price_unit = 5.0 + move_in.quantity_done = move_in.product_uom_qty + self.picking_in.action_done() + move_in.date = '2019-10-01 00:00:00' - picking_out = self.StockPicking.create(picking_out_vals.copy()) - picking_out.action_assign() - picking_out.move_line_ids.qty_done = 5.0 - picking_out.action_done() - picking_out.move_lines.date = '2019-10-03' + picking_in_2 = self.picking_in.copy() + move_in_2 = picking_in_2.move_lines[:1] + move_in_2.product_uom_qty = 10.0 + move_in_2.quantity_done = move_in_2.product_uom_qty + picking_in_2.action_done() + move_in_2.date = '2019-10-02 00:00:00' - picking_out2 = self.StockPicking.create(picking_out_vals.copy()) - picking_out2.action_assign() - picking_out2.move_line_ids.qty_done = 5.0 - picking_out2.action_done() - picking_out2.move_lines.date = '2019-10-04' + move_out = self.picking_out.move_lines[:1] + move_out.quantity_done = move_out.product_uom_qty + self.picking_out.action_done() + move_out.date = '2019-10-03 00:00:00' + + picking_out_2 = self.picking_out.copy() + move_out_2 = picking_out_2.move_lines[:1] + move_out_2.quantity_done = move_out_2.product_uom_qty + picking_out_2.action_done() + move_out_2.date = '2019-10-04 00:00:00' # Make an inventory inventory = self.env['stock.inventory'].create({ @@ -128,12 +91,70 @@ def test_sync_cost_price(self): })] }) inventory.action_done() - inventory.move_ids.date = '2019-10-05' + inventory.move_ids.date = '2019-10-05 00:00:00' self.assertEqual(self.product.standard_price, 5.0) - purchase_order.order_line.price_unit = 2.0 + move_in.price_unit = 2.0 self.assertEqual(self.product.standard_price, 2.27) - self.assertAlmostEqual( - round(picking_out.move_lines.price_unit, 2), -2.27) - self.assertAlmostEqual( - round(picking_out2.move_lines.price_unit, 2), -2.27) + self.assertAlmostEqual(move_out.price_unit, -2.27, 2) + self.assertAlmostEqual(move_out_2.price_unit, -2.27, 2) + + def test_sync_cost_price_and_history(self): + company_id = self.picking_in.company_id.id + move_in = self.picking_in.move_lines[:1] + move_in.quantity_done = move_in.product_uom_qty + self.picking_in.action_done() + move_in.date = '2019-10-01 00:00:00' + + move_out = self.picking_out.move_lines[:1] + move_out.quantity_done = move_out.product_uom_qty + self.picking_out.action_done() + move_out.date = '2019-10-01 01:00:00' + + picking_in_2 = self.picking_in.copy() + move_in_2 = picking_in_2.move_lines[:1] + move_in_2.quantity_done = move_in_2.product_uom_qty + picking_in_2.action_done() + move_in_2.date = '2019-10-01 02:00:00' + + picking_out_2 = self.picking_out.copy() + move_out_2 = picking_out_2.move_lines[:1] + move_out_2.product_uom_qty = 15 + move_out_2.quantity_done = move_out_2.product_uom_qty + picking_out_2.action_done() + move_out_2.date = '2019-10-01 03:00:00' + + picking_in_3 = self.picking_in.copy() + move_in_3 = picking_in_3.move_lines[:1] + move_in_3.quantity_done = move_in_3.product_uom_qty + move_in_3.price_unit = 2.0 + picking_in_3.action_done() + move_in_3.date = '2019-10-01 04:00:00' + + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + self.assertAlmostEqual(self.product.get_history_price( + company_id), 2.0, 2) + self.product.standard_price = 20.0 + self.assertAlmostEqual(self.product.get_history_price( + company_id), 20.0, 2) + + move_in.price_unit = 10.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + self.assertAlmostEqual(move_out.price_unit, -10.0, 2) + self.assertAlmostEqual(move_out_2.price_unit, -4.0, 2) + self.assertAlmostEqual(self.product.get_history_price( + company_id, move_in_3._previous_instant_date()), 4.0, 2) + + move_in_3.quantity_done = 5.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + move_in_3.quantity_done = 0.0 + self.assertAlmostEqual(self.product.standard_price, 4.0, 2) + + (move_in | move_in_2 | move_in_3).write({'price_unit': 9.0}) + self.assertAlmostEqual(self.product.standard_price, 9.0, 2) + + price_history_count = self.env['product.price.history'].search_count([ + ('company_id', '=', company_id), + ('product_id', '=', self.product.id), + ]) + self.assertEqual(price_history_count, 4) From bcc4642c2652579011f80fa7f5ecb70430bb0386 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Fri, 10 Jan 2020 19:11:24 +0000 Subject: [PATCH 3/4] [UPD] Update product_cost_price_avco_sync.pot --- .../i18n/product_cost_price_avco_sync.pot | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot index 366a55c38f1c..ac0fbfe1a519 100644 --- a/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot +++ b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot @@ -6,8 +6,6 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-10-29 10:28+0000\n" -"PO-Revision-Date: 2019-10-29 10:28+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -15,16 +13,6 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#. module: product_cost_price_avco_sync -#: model:ir.model,name:product_cost_price_avco_sync.model_product_product -msgid "Product" -msgstr "" - -#. module: product_cost_price_avco_sync -#: model:ir.model,name:product_cost_price_avco_sync.model_purchase_order_line -msgid "Purchase Order Line" -msgstr "" - #. module: product_cost_price_avco_sync #: model:ir.model,name:product_cost_price_avco_sync.model_stock_move msgid "Stock Move" From e49a6cb16c1b2db7fd6e5bff83352d5195e7ba46 Mon Sep 17 00:00:00 2001 From: Carlos Dauden Date: Thu, 16 Jan 2020 12:24:40 +0100 Subject: [PATCH 4/4] [MIG] product_cost_price_avco_sync: Migration to 12.0 --- product_cost_price_avco_sync/README.rst | 25 ++++-- product_cost_price_avco_sync/__manifest__.py | 8 +- product_cost_price_avco_sync/i18n/es.po | 2 +- .../i18n/product_cost_price_avco_sync.pot | 2 +- .../models/stock_move.py | 14 ++-- .../static/description/icon.svg | 79 ------------------- .../static/description/index.html | 8 +- .../test_product_cost_price_avco_sync.py | 2 +- 8 files changed, 37 insertions(+), 103 deletions(-) delete mode 100644 product_cost_price_avco_sync/static/description/icon.svg diff --git a/product_cost_price_avco_sync/README.rst b/product_cost_price_avco_sync/README.rst index 478e0f98431c..c4111fb654f1 100644 --- a/product_cost_price_avco_sync/README.rst +++ b/product_cost_price_avco_sync/README.rst @@ -7,20 +7,20 @@ Product cost price avco sync !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png :target: https://odoo-community.org/page/development-status - :alt: Beta + :alt: Production/Stable .. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github - :target: https://github.com/OCA/stock-logistics-workflow/tree/11.0/product_cost_price_avco_sync + :target: https://github.com/OCA/stock-logistics-workflow/tree/12.0/product_cost_price_avco_sync :alt: OCA/stock-logistics-workflow .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-11-0/stock-logistics-workflow-11-0-product_cost_price_avco_sync + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-12-0/stock-logistics-workflow-12-0-product_cost_price_avco_sync :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/154/11.0 + :target: https://runbot.odoo-community.org/runbot/154/12.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -39,7 +39,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -72,6 +72,17 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. +.. |maintainer-carlosdauden| image:: https://github.com/carlosdauden.png?size=40px + :target: https://github.com/carlosdauden + :alt: carlosdauden +.. |maintainer-sergio-teruel| image:: https://github.com/sergio-teruel.png?size=40px + :target: https://github.com/sergio-teruel + :alt: sergio-teruel + +Current `maintainers `__: + +|maintainer-carlosdauden| |maintainer-sergio-teruel| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_cost_price_avco_sync/__manifest__.py b/product_cost_price_avco_sync/__manifest__.py index 0b692421cd80..fac1547f341a 100644 --- a/product_cost_price_avco_sync/__manifest__.py +++ b/product_cost_price_avco_sync/__manifest__.py @@ -1,13 +1,15 @@ -# Copyright 2018 Tecnativa - Carlos Dauden -# Copyright 2018 Tecnativa - Sergio Teruel +# Copyright 2020 Tecnativa - Carlos Dauden +# Copyright 2020 Tecnativa - Sergio Teruel # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { 'name': 'Product cost price avco sync', 'summary': 'Set product cost price from updated moves', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', + 'development_status': 'Production/Stable', 'category': 'Stock', 'website': 'https://github.com/OCA/stock-logistics-workflow', 'author': 'Tecnativa, Odoo Community Association (OCA)', + 'maintainers': ['carlosdauden', 'sergio-teruel'], 'license': 'AGPL-3', 'installable': True, 'depends': [ diff --git a/product_cost_price_avco_sync/i18n/es.po b/product_cost_price_avco_sync/i18n/es.po index 39772b0b69ef..974048c716ec 100644 --- a/product_cost_price_avco_sync/i18n/es.po +++ b/product_cost_price_avco_sync/i18n/es.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 11.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-10-29 10:28+0000\n" "PO-Revision-Date: 2019-10-29 11:30+0100\n" diff --git a/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot index ac0fbfe1a519..b97c6191370c 100644 --- a/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot +++ b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 11.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: <>\n" "Language-Team: \n" diff --git a/product_cost_price_avco_sync/models/stock_move.py b/product_cost_price_avco_sync/models/stock_move.py index d5b3201fab40..2b66e0a1e0c5 100644 --- a/product_cost_price_avco_sync/models/stock_move.py +++ b/product_cost_price_avco_sync/models/stock_move.py @@ -19,14 +19,12 @@ def write(self, vals): return res def _create_price_history(self, value): - PriceHistory = self.env['product.price.history'] - for move in self: - PriceHistory.create({ - 'product_id': move.product_id.id, - 'cost': value, - 'datetime': move.date, - 'company_id': move.company_id.id, - }) + self.env['product.price.history'].create([{ + 'product_id': move.product_id.id, + 'cost': value, + 'datetime': move.date, + 'company_id': move.company_id.id, + } for move in self]) def _previous_instant_date(self): """ Returns previous instant before move was done""" diff --git a/product_cost_price_avco_sync/static/description/icon.svg b/product_cost_price_avco_sync/static/description/icon.svg deleted file mode 100644 index a7a26d0932ab..000000000000 --- a/product_cost_price_avco_sync/static/description/icon.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/product_cost_price_avco_sync/static/description/index.html b/product_cost_price_avco_sync/static/description/index.html index 64036f7fc67d..72af0b663d6a 100644 --- a/product_cost_price_avco_sync/static/description/index.html +++ b/product_cost_price_avco_sync/static/description/index.html @@ -367,7 +367,7 @@

Product cost price avco sync

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runbot

+

Production/Stable License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runbot

This module allows to sync cost price products with average cost method from stock moves price unit.

Table of contents

@@ -387,7 +387,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -418,7 +418,9 @@

Maintainers

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/stock-logistics-workflow project on GitHub.

+

Current maintainers:

+

carlosdauden sergio-teruel

+

This module is part of the OCA/stock-logistics-workflow project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py index e88e6d1dfb2e..0362f7afda53 100644 --- a/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py +++ b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py @@ -90,7 +90,7 @@ def test_sync_cost_price(self): 'location_id': self.warehouse.lot_stock_id.id })] }) - inventory.action_done() + inventory._action_done() inventory.move_ids.date = '2019-10-05 00:00:00' self.assertEqual(self.product.standard_price, 5.0)