From ca4e313d385d6cdcd8dd6883c34a501376153c50 Mon Sep 17 00:00:00 2001 From: Laetitia Gangloff Date: Thu, 23 Jul 2015 10:46:00 +0200 Subject: [PATCH] add modules to use product packaging packaging_extended: to use uom in packaging purchase_packaging: to improve supplierinfo with purchase uom and packaging and use them in purchase order sale_packaging: to use packaging uom in sale order line --- purchase_packaging/README.rst | 70 +++++++++ purchase_packaging/__init__.py | 5 + purchase_packaging/__openerp__.py | 39 +++++ purchase_packaging/models/__init__.py | 4 + purchase_packaging/models/product.py | 49 ++++++ purchase_packaging/models/purchase.py | 157 ++++++++++++++++++++ purchase_packaging/tests/__init__.py | 3 + purchase_packaging/tests/test_packaging.py | 104 +++++++++++++ purchase_packaging/views/product_views.xml | 47 ++++++ purchase_packaging/views/purchase_views.xml | 54 +++++++ 10 files changed, 532 insertions(+) create mode 100644 purchase_packaging/README.rst create mode 100755 purchase_packaging/__init__.py create mode 100755 purchase_packaging/__openerp__.py create mode 100755 purchase_packaging/models/__init__.py create mode 100644 purchase_packaging/models/product.py create mode 100644 purchase_packaging/models/purchase.py create mode 100644 purchase_packaging/tests/__init__.py create mode 100644 purchase_packaging/tests/test_packaging.py create mode 100644 purchase_packaging/views/product_views.xml create mode 100644 purchase_packaging/views/purchase_views.xml diff --git a/purchase_packaging/README.rst b/purchase_packaging/README.rst new file mode 100644 index 000000000..441c4179b --- /dev/null +++ b/purchase_packaging/README.rst @@ -0,0 +1,70 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +Purchase Packaging +=========== + +- Use packaging in supplierinfo +- Use uom set in the packaging instead of purchase uom for purchase +- Add minimum quantity measure unit in supplierinfo +- On purchase order line compute the quantity with the quantity and unit of + measure + + + +Installation +============ + +To install this module, you need to: + +* Click on install button + +Configuration +============= + +To configure this module, you need to: + +* on product packaging define the unit of measure to use. +* on supplier info define the packaging and minimum unit of measure quantity + to use. +* use product packaging in purchase order to use the link unit of measure + +Usage +===== + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + +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 +`here `_. + + +Credits +======= + +Contributors +------------ + +* Laetitia Gangloff +* Laurent Mignon + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit http://odoo-community.org. \ No newline at end of file diff --git a/purchase_packaging/__init__.py b/purchase_packaging/__init__.py new file mode 100755 index 000000000..30e6bf2bc --- /dev/null +++ b/purchase_packaging/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import ( + models, +) diff --git a/purchase_packaging/__openerp__.py b/purchase_packaging/__openerp__.py new file mode 100755 index 000000000..f7c091666 --- /dev/null +++ b/purchase_packaging/__openerp__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Authors: Laetitia Gangloff +# Copyright (c) 2015 Acsone SA/NV (http://www.acsone.eu) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + "name": "Purchase Packaging", + "version": "0.1", + 'author': "Acsone, Odoo Community Association (OCA)", + "category": "Other", + "website": "http://www.acsone.eu", + 'summary': "In purchase, use package", + "depends": ["product", + "purchase", + "packaging_extended", + ], + "data": ["views/product_views.xml", + "views/purchase_views.xml", + ], + "license": "AGPL-3", + "installable": True, + "application": False, +} diff --git a/purchase_packaging/models/__init__.py b/purchase_packaging/models/__init__.py new file mode 100755 index 000000000..2ce3fc0ca --- /dev/null +++ b/purchase_packaging/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import product +from . import purchase diff --git a/purchase_packaging/models/product.py b/purchase_packaging/models/product.py new file mode 100644 index 000000000..d165cb531 --- /dev/null +++ b/purchase_packaging/models/product.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Authors: Laetitia Gangloff +# Copyright (c) 2015 Acsone SA/NV (http://www.acsone.eu) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +from openerp import api, fields, models + + +class ProductSupplierinfo(models.Model): + _inherit = "product.supplierinfo" + + @api.model + def _default_min_qty_uom_id(self): + return self.env.ref('product.product_uom_unit') + + packaging_id = fields.Many2one('product.packaging', 'Logisitical Units') + product_uom = fields.Many2one(compute='_compute_product_uom', + string="Supplier Unit of Measure", + readonly=True) + min_qty_uom_id = fields.Many2one('product.uom', + 'Minimal Unit of Measure Quantity', + required=True, + default=_default_min_qty_uom_id) + + @api.one + @api.depends('product_tmpl_id', 'packaging_id') + def _compute_product_uom(self): + """ Set product_uom as a computed field instead of a related field. + To use uom of link packaging + """ + self.product_uom = self.packaging_id.uom_id or \ + self.product_tmpl_id.uom_po_id diff --git a/purchase_packaging/models/purchase.py b/purchase_packaging/models/purchase.py new file mode 100644 index 000000000..6d819c1f1 --- /dev/null +++ b/purchase_packaging/models/purchase.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Authors: Laetitia Gangloff +# Copyright (c) 2015 Acsone SA/NV (http://www.acsone.eu) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +from openerp import api, fields, models +import openerp.addons.decimal_precision as dp + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + @api.cr_uid_context + def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, + group_id, context=None): + """ Set product_packaging on stock move + """ + result = super(PurchaseOrder, self)._prepare_order_line_move( + cr, uid, order, order_line, picking_id, group_id, + context=context) + if order_line.packaging_id: + for res in result: + res['product_packaging'] = order_line.packaging_id.id + return result + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + @api.model + def _default_product_purchase_uom_id(self): + return self.env.ref('product.product_uom_unit') + + product_tmpl_id = fields.Many2one(related='product_id.product_tmpl_id', + comodel_name='product.template') + packaging_id = fields.Many2one('product.packaging', 'Packaging') + product_purchase_qty = fields.Float( + 'Purchase quantity', + digits_compute=dp.get_precision('Product Unit of Measure'), + required=True, default=lambda *a: 1.0) + product_purchase_uom_id = fields.Many2one( + 'product.uom', 'Purchase Unit of Measure', required=True, + default=_default_product_purchase_uom_id) + product_qty = fields.Float( + compute="_compute_product_qty", string='Quantity') + + @api.one + @api.depends('product_purchase_uom_id', 'product_purchase_qty') + def _compute_product_qty(self): + """ + Compute the total quantity + """ + uom_obj = self.env['product.uom'] + to_uom = uom_obj.search( + [('category_id', '=', self.product_purchase_uom_id.category_id.id), + ('uom_type', '=', 'reference')], limit=1) + self.product_qty = uom_obj._compute_qty( + self.product_purchase_uom_id.id, + self.product_purchase_qty, + to_uom.id) + + @api.onchange("packaging_id") + def _onchange_packaging_id(self): + if self.packaging_id: + self.product_uom = self.packaging_id.uom_id + + @api.cr_uid_context + def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, + uom_id, partner_id, date_order=False, + fiscal_position_id=False, date_planned=False, + name=False, price_unit=False, state='draft', + context=None): + """ set domain on product_purchase_uom_id and packaging_id + if there is no qty (first pass), + set the first packagigng, purchase_uom and purchase_qty + """ + product_product = self.pool['product.product'] + product_purchase_qty = 0 + product_purchase_uom_id = False + packaging_id = False + new_uom_id = False + domain = {} + + if product_id and partner_id: + product = product_product.browse(cr, uid, product_id, + context=context) + first = True + packaging_ids = [] + po_uom_ids = [] + domain['packaging_id'] = [('id', 'in', packaging_ids)] + domain['product_purchase_uom_id'] = [('id', 'in', po_uom_ids)] + for supplier in product.seller_ids: + if (supplier.name.id == partner_id): + if first: + product_purchase_qty = supplier.min_qty + product_purchase_uom_id = supplier.min_qty_uom_id.id + new_uom_id = supplier.product_uom.id + if supplier.packaging_id: + packaging_id = supplier.packaging_id.id + first = False + po_uom_ids.append(supplier.min_qty_uom_id.id) + if supplier.packaging_id: + packaging_ids.append(supplier.packaging_id.id) + + uom_id = new_uom_id if not qty else uom_id + res = super(PurchaseOrderLine, self).onchange_product_id( + cr, uid, ids, pricelist_id, product_id, qty, uom_id, + partner_id, date_order=date_order, + fiscal_position_id=fiscal_position_id, date_planned=date_planned, + name=name, price_unit=price_unit, state=state, context=context) + + if not qty: + res['value']['product_purchase_qty'] = product_purchase_qty + res['value']['product_purchase_uom_id'] = product_purchase_uom_id + res['value']['packaging_id'] = packaging_id + if domain: + if res.get('domain'): + res['domain'].update(domain) + else: + res['domain'] = domain + return res + + @api.model + def update_vals(self, vals): + """ + When packaging_id is set, uom_id is readonly, + so we need to reset the uom value in the vals dict + """ + if vals.get('packaging_id'): + vals['product_uom'] = self.env['product.packaging'].browse( + vals['packaging_id']).uom_id.id + return vals + + @api.model + def create(self, vals): + return super(PurchaseOrderLine, self).create(self.update_vals(vals)) + + @api.multi + def write(self, vals): + return super(PurchaseOrderLine, self).write(self.update_vals(vals)) diff --git a/purchase_packaging/tests/__init__.py b/purchase_packaging/tests/__init__.py new file mode 100644 index 000000000..c8a922e42 --- /dev/null +++ b/purchase_packaging/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_packaging diff --git a/purchase_packaging/tests/test_packaging.py b/purchase_packaging/tests/test_packaging.py new file mode 100644 index 000000000..9cfa2d49b --- /dev/null +++ b/purchase_packaging/tests/test_packaging.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Authors: Laetitia Gangloff +# Copyright (c) 2015 Acsone SA/NV (http://www.acsone.eu) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import openerp.tests.common as common + + +class TestPackaging(common.TransactionCase): + + def setUp(self): + """ Create a packagings with uom product_uom_dozen on + * product_product_34 (uom is product_uom_unit) + """ + super(TestPackaging, self).setUp() + self.product_packaging_34 = self.env['product.packaging'].create( + {'product_tmpl_id': self.env.ref('product.product_product_34' + ).product_tmpl_id.id, + 'uom_id': self.env.ref('product.product_uom_dozen').id}) + self.sp_30 = self.env.ref('product.product_supplierinfo_30') + + def test_supplierinfo_product_uom(self): + """ Check product_uom of product_supplierinfo_30 is product_uom_unit + Set packaging_id product_packaging_34 on product_supplierinfo_30 + Check product_uom of product_supplierinfo_30 is product_uom_dozen + """ + self.assertEqual(self.sp_30.product_uom.id, + self.env.ref('product.product_uom_unit').id) + self.sp_30.packaging_id = self.product_packaging_34 + self.assertEqual(self.sp_30.product_uom.id, + self.env.ref('product.product_uom_dozen').id) + + def test_po_line(self): + """ On supplierinfo set product_uom_8 as min_qty_uom_id + On supplierinfo set 2 as min_qty + Create purchase order line with product product_product_34 + Check packaging_id is product_packaging_34 + Check product_purchase_uom_id is product_uom_8 + Check product_purchase_qty is 2 + Check product_qty is 8*2 = 16 + Check price_unit is 12*38 = 456 + Check product_uom is product_uom_dozen + Confirm po + Check stock move packaging is product_packaging_34 + Check stock move product_uom is product_uom_dozen + Check stock move product_qty is 16 + """ + product_uom_8 = self.env['product.uom'].create( + {'category_id': self.env.ref('product.product_uom_categ_unit').id, + 'name': 'COL8', + 'factor_inv': 8, + 'uom_type': 'bigger' + }) + self.sp_30.min_qty_uom_id = product_uom_8 + self.sp_30.min_qty = 2 + self.sp_30.packaging_id = self.product_packaging_34 + + po = self.env['purchase.order'].create( + {'partner_id': self.env.ref('base.res_partner_16').id, + 'location_id': self.env.ref('stock.stock_location_stock').id, + 'pricelist_id': self.env.ref('purchase.list0').id + }) + + vals = self.env['purchase.order.line'].onchange_product_id( + [], + po.pricelist_id.id, self.env.ref('product.product_product_34').id, + 0, False, po.partner_id.id, date_order=po.date_order, + fiscal_position_id=po.fiscal_position.id, date_planned=False, + name=False, price_unit=False, state=po.state) + vals['value']['order_id'] = po.id + vals['value']['product_id'] = self.env.ref( + 'product.product_product_34').id + po_line = self.env['purchase.order.line'].create(vals['value']) + self.assertEqual(po_line.packaging_id.id, + self.product_packaging_34.id) + self.assertEqual(po_line.product_purchase_uom_id.id, product_uom_8.id) + self.assertAlmostEqual(po_line.product_purchase_qty, 2) + self.assertAlmostEqual(po_line.product_qty, 16) + self.assertAlmostEqual(po_line.price_unit, 456) + self.assertEqual(po_line.product_uom.id, + self.env.ref('product.product_uom_dozen').id) + po.signal_workflow('purchase_confirm') + sm = po.picking_ids[0].move_lines[0] + self.assertEqual(sm.product_packaging.id, + self.product_packaging_34.id) + self.assertEqual(sm.product_uom.id, + self.env.ref('product.product_uom_dozen').id) + self.assertAlmostEqual(sm.product_uom_qty, 16) diff --git a/purchase_packaging/views/product_views.xml b/purchase_packaging/views/product_views.xml new file mode 100644 index 000000000..13c8e2413 --- /dev/null +++ b/purchase_packaging/views/product_views.xml @@ -0,0 +1,47 @@ + + + + + + product.template.common.form (purchase_packaging) + product.template + + + + + {'default_product_tmpl_id': id} + + + + + + product.supplierinfo.tree.view" (purchase_packaging) + product.supplierinfo + + + + + + + + + + + product.supplierinfo.form.view (purchase_packaging) + product.supplierinfo + + + + + + + + + + + + + + + + diff --git a/purchase_packaging/views/purchase_views.xml b/purchase_packaging/views/purchase_views.xml new file mode 100644 index 000000000..95c37c6c3 --- /dev/null +++ b/purchase_packaging/views/purchase_views.xml @@ -0,0 +1,54 @@ + + + + + + purchase.order.form (purchase_packaging) + purchase.order + + + + + + + + + 1 + + + + + + + {'readonly' : [('packaging_id', '!=', False)]} + + + + + + + purchase.order.line.form (purchase_packaging) + purchase.order.line + + + + + + + + + 1 + + + + + + + +