Skip to content

Commit

Permalink
[ADD] purchase_order_variant_mgmt (OCA#288)
Browse files Browse the repository at this point in the history
==================================================
Handle easily multiple variants on Purchase Orders
==================================================

This module allows to add/modify all the variants of a product in a direct
screen without the need of handling them one by one.

Configuration
=============

* Configure your user to have any permission from "Purchases" group.
* Create a product with 2 attributes and several values.

Usage
=====

* Go to Purchases > Purchase > Requests for Quotation
* Create a new quotation or edit an existing one.
* Press "Add variants" button located in the upper right corner of the
  "Order Lines" tab.
* A new screen will appear allowing you to select the products that have
  variants.
* Once you select the product, a 2D matrix will appear with the first
  attribute values as columns and the second one as rows.
* If there are already order lines for the product variants, the current
  quantity will be pre-filled in the matrix.
* Change the quantities for the variant you want and click on "Transfer to
  order"
* Order lines for the variants will be created/removed to comply with the
  input you have done.

As extra feature for saving steps, there's also a button on each existing line
that corresponds to a variant that opens the dialog directly with the product
selected.

Known issues / Roadmap
======================

* The inline button for modifying quantities for an existing line won't
  work correctly until these 2 PRs are merged in Odoo:

  * odoo/odoo#13558
  * odoo/odoo#13635

  The problems are already fixed in OCB.
  • Loading branch information
pedrobaeza committed Oct 27, 2017
1 parent 17f6f3b commit 276c148
Show file tree
Hide file tree
Showing 12 changed files with 431 additions and 0 deletions.
76 changes: 76 additions & 0 deletions purchase_order_variant_mgmt/README.rst
@@ -0,0 +1,76 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3

==================================================
Handle easily multiple variants on Purchase Orders
==================================================

This module allows to add/modify all the variants of a product in a direct
screen without the need of handling them one by one.

Configuration
=============

#. Configure your user to have any permission from "Purchases" group.
#. Create a product with 2 attributes and several values.

Usage
=====

#. Go to Purchases > Purchase > Requests for Quotation
#. Create a new quotation or edit an existing one.
#. Press "Add variants" button located in the upper right corner of the
"Order Lines" tab.
#. A new screen will appear allowing you to select the products that have
variants.
#. Once you select the product, a 2D matrix will appear with the first
attribute values as columns and the second one as rows.
#. If there are already order lines for the product variants, the current
quantity will be pre-filled in the matrix.
#. Change the quantities for the variant you want and click on "Transfer to
order"
#. Order lines for the variants will be created/removed to comply with the
input you have done.

As extra feature for saving steps, there's also a button on each existing line
that corresponds to a variant that opens the dialog directly with the product
selected.

.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/142/9.0

Known issues / Roadmap
======================

* The inline button for modifying quantities for an existing line won't
work correctly until these 2 PRs are merged in Odoo:

* https://github.com/odoo/odoo/pull/13558
* https://github.com/odoo/odoo/pull/13635

The problems are already fixed in OCB.

Credits
=======

Contributors
------------

* Pedro M. Baeza <pedro.baeza@tecnativa.com>

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 https://odoo-community.org.
5 changes: 5 additions & 0 deletions purchase_order_variant_mgmt/__init__.py
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import models
from . import wizard
25 changes: 25 additions & 0 deletions purchase_order_variant_mgmt/__openerp__.py
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
'name': 'Handle easily multiple variants on Purchase Orders',
'summary': 'Handle the addition/removal of multiple variants from '
'product template into the purchase order',
'version': '9.0.1.0.0',
'author': 'Tecnativa,'
'Odoo Community Association (OCA)',
'category': 'Purchases',
'license': 'AGPL-3',
'website': 'https://www.tecnativa.com',
'depends': [
'purchase',
'web_widget_x2many_2d_matrix',
],
'demo': [],
'data': [
'wizard/purchase_manage_variant_view.xml',
'views/purchase_order_view.xml',
],
'installable': True,
}
4 changes: 4 additions & 0 deletions purchase_order_variant_mgmt/models/__init__.py
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import purchase_order
19 changes: 19 additions & 0 deletions purchase_order_variant_mgmt/models/purchase_order.py
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from openerp import fields, models


class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'

# These field names are for avoiding conflicts with any other field with
# the same name declared by other modules and that can be a no related one
product_tmpl_id_purchase_order_variant_mgmt = fields.Many2one(
comodel_name="product.template", related="product_id.product_tmpl_id")
state_purchase_order_variant_mgmt = fields.Selection(
related="order_id.state")
product_attribute_value_ids = fields.Many2many(
comodel_name='product.attribute.value',
related="product_id.attribute_value_ids")
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions purchase_order_variant_mgmt/tests/__init__.py
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import test_purchase_order_variant_mgmt
102 changes: 102 additions & 0 deletions purchase_order_variant_mgmt/tests/test_purchase_order_variant_mgmt.py
@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from openerp.tests import common


class TestPurchaseOrderVariantMgmt(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestPurchaseOrderVariantMgmt, cls).setUpClass()
cls.partner = cls.env['res.partner'].create({'name': 'Test partner'})
cls.attribute1 = cls.env['product.attribute'].create({
'name': 'Test Attribute 1',
'value_ids': [
(0, 0, {'name': 'Value 1'}),
(0, 0, {'name': 'Value 2'}),
],
})
cls.attribute2 = cls.env['product.attribute'].create({
'name': 'Test Attribute 2',
'value_ids': [
(0, 0, {'name': 'Value X'}),
(0, 0, {'name': 'Value Y'}),
],
})
cls.product_tmpl = cls.env['product.template'].create({
'name': 'Test template',
'attribute_line_ids': [
(0, 0, {
'attribute_id': cls.attribute1.id,
'value_ids': [(6, 0, cls.attribute1.value_ids.ids)],
}),
(0, 0, {
'attribute_id': cls.attribute2.id,
'value_ids': [(6, 0, cls.attribute2.value_ids.ids)],
}),
],
})
assert len(cls.product_tmpl.product_variant_ids) == 4
order = cls.env['purchase.order'].new({'partner_id': cls.partner.id})
order.onchange_partner_id()
cls.order = order.create(order._convert_to_write(order._cache))
cls.Wizard = cls.env['purchase.manage.variant'].with_context(
active_ids=cls.order.ids, active_id=cls.order.id,
active_model=cls.order._name
)
cls.PurchaseOrderLine = cls.env['purchase.order.line']

def test_add_variants(self):
wizard = self.Wizard.new({'product_tmpl_id': self.product_tmpl.id})
wizard._onchange_product_tmpl_id()
wizard = wizard.create(wizard._convert_to_write(wizard._cache))
self.assertEqual(len(wizard.variant_line_ids), 4)
wizard.variant_line_ids[0].product_uom_qty = 1
wizard.variant_line_ids[1].product_uom_qty = 2
wizard.variant_line_ids[2].product_uom_qty = 3
wizard.variant_line_ids[3].product_uom_qty = 4
wizard.button_transfer_to_order()
self.assertEqual(len(self.order.order_line), 4,
"There should be 4 lines in the sale order")

def test_modify_variants(self):
product1 = self.product_tmpl.product_variant_ids[0]
order_line1 = self.PurchaseOrderLine.new({
'order_id': self.order.id,
'product_id': product1.id,
})
order_line1.onchange_product_id()
order_line1.product_qty = 1
order_line1._onchange_quantity()
product2 = self.product_tmpl.product_variant_ids[1]
order_line1 = self.PurchaseOrderLine.create(
order_line1._convert_to_write(order_line1._cache))
order_line2 = self.PurchaseOrderLine.new({
'order_id': self.order.id,
'product_id': product2.id,
})
order_line2.onchange_product_id()
order_line1.product_qty = 2
order_line2._onchange_quantity()
order_line2 = self.PurchaseOrderLine.create(
order_line2._convert_to_write(order_line2._cache))
Wizard2 = self.Wizard.with_context(
default_product_tmpl_id=self.product_tmpl.id,
active_model='purchase.order.line',
active_id=order_line1.id, active_ids=order_line1.ids
)
wizard = Wizard2.create({})
wizard._onchange_product_tmpl_id()
self.assertEqual(
len(wizard.variant_line_ids.filtered('product_uom_qty')), 2,
"There should be two fields with any quantity in the wizard."
)
wizard.variant_line_ids.filtered(
lambda x: x.product_id == product1).product_uom_qty = 0
wizard.variant_line_ids.filtered(
lambda x: x.product_id == product2).product_uom_qty = 10
wizard.button_transfer_to_order()
self.assertFalse(order_line1.exists(), "Order line not removed.")
self.assertEqual(
order_line2.product_qty, 10, "Order line quantity not changed.")
41 changes: 41 additions & 0 deletions purchase_order_variant_mgmt/views/purchase_order_view.xml
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>

<record id="purchase_order_form" model="ir.ui.view">
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']" position="before">
<div class="oe_button_box" name="button_box">
<button name="%(action_purchase_manage_variant)d"
type="action"
string="Add or Modify Variants"
class="oe_edit_only"
states="draft,sent"
/>
</div>
</xpath>
<xpath expr="//field[@name='order_line']//tree" position="inside">
<field name="product_tmpl_id_purchase_order_variant_mgmt" invisible="1"/>
<field name="state_purchase_order_variant_mgmt" invisible="1"/>
<field name="product_attribute_value_ids" invisible="1"/>
<!-- Not working until https://github.com/odoo/odoo/pull/13558 -->
<!-- Also https://github.com/odoo/odoo/pull/13635 is needed for correct template selection -->
<button name="%(action_purchase_manage_variant)d"
type="action"
string="Modify Variants"
icon="fa-th"
class="oe_edit_only"
context="{'default_product_tmpl_id': product_tmpl_id_purchase_order_variant_mgmt}"
attrs="{'invisible': ['|', ('state_purchase_order_variant_mgmt', 'not in', ('draft', 'sent')), ('product_attribute_value_ids', '=', [])]}"
/>
</xpath>
<xpath expr="//field[@name='order_line']" position="attributes">
<attribute name="options">{'reload_on_button': true}</attribute>
</xpath>
</field>
</record>

</odoo>
4 changes: 4 additions & 0 deletions purchase_order_variant_mgmt/wizard/__init__.py
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import purchase_manage_variant
103 changes: 103 additions & 0 deletions purchase_order_variant_mgmt/wizard/purchase_manage_variant.py
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import openerp.addons.decimal_precision as dp
from openerp import api, fields, models


class PurchaseManageVariant(models.TransientModel):
_name = 'purchase.manage.variant'

product_tmpl_id = fields.Many2one(
comodel_name='product.template', string="Template", required=True)
# This is a many2many because Odoo fails to fill one2many in onchanges
variant_line_ids = fields.Many2many(
comodel_name='purchase.manage.variant.line', string="Variant Lines")

# HACK: https://github.com/OCA/server-tools/pull/492#issuecomment-237594285
@api.multi
def onchange(self, values, field_name, field_onchange): # pragma: no cover
if "variant_line_ids" in field_onchange:
for sub in ("product_id", "disabled", "value_x", "value_y",
"product_uom_qty"):
field_onchange.setdefault("variant_line_ids." + sub, u"")
return super(PurchaseManageVariant, self).onchange(
values, field_name, field_onchange)

@api.onchange('product_tmpl_id')
def _onchange_product_tmpl_id(self):
self.variant_line_ids = [(6, 0, [])]
template = self.product_tmpl_id
context = self.env.context
record = self.env[context['active_model']].browse(
context['active_id'])
if context['active_model'] == 'purchase.order.line':
purchase_order = record.order_id
else:
purchase_order = record
if template and len(template.attribute_line_ids) >= 2:
line_x = template.attribute_line_ids[0]
line_y = template.attribute_line_ids[1]
lines = []
for value_x in line_x.value_ids:
for value_y in line_y.value_ids:
# Filter the corresponding product for that values
product = template.product_variant_ids.filtered(
lambda x: (value_x in x.attribute_value_ids and
value_y in x.attribute_value_ids))
order_line = purchase_order.order_line.filtered(
lambda x: x.product_id == product)
lines.append((0, 0, {
'product_id': product,
'disabled': not bool(product),
'value_x': value_x,
'value_y': value_y,
'product_uom_qty': order_line.product_qty,
}))
self.variant_line_ids = lines

@api.multi
def button_transfer_to_order(self):
context = self.env.context
record = self.env[context['active_model']].browse(context['active_id'])
if context['active_model'] == 'purchase.order.line':
purchase_order = record.order_id
else:
purchase_order = record
OrderLine = self.env['purchase.order.line']
lines2unlink = OrderLine
for line in self.variant_line_ids:
order_line = purchase_order.order_line.filtered(
lambda x: x.product_id == line.product_id)
if order_line:
if not line.product_uom_qty:
# Done this way because there's a side effect removing here
lines2unlink |= order_line
else:
order_line.product_qty = line.product_uom_qty
elif line.product_uom_qty:
order_line = OrderLine.new({
'product_id': line.product_id.id,
'order_id': purchase_order.id,
})
order_line.onchange_product_id()
# This should be done later for handling supplier quantities
order_line.product_qty = line.product_uom_qty
order_line._onchange_quantity()
order_line_vals = order_line._convert_to_write(
order_line._cache)
purchase_order.order_line.create(order_line_vals)
lines2unlink.unlink()


class PurchaseManageVariantLine(models.TransientModel):
_name = 'purchase.manage.variant.line'

product_id = fields.Many2one(
comodel_name='product.product', string="Variant", readonly=True)
disabled = fields.Boolean()
value_x = fields.Many2one(comodel_name='product.attribute.value')
value_y = fields.Many2one(comodel_name='product.attribute.value')
product_uom_qty = fields.Float(
string="Quantity", digits_compute=dp.get_precision('Product UoS'))

0 comments on commit 276c148

Please sign in to comment.