177 lines
8.9 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
report_grids = fields.Boolean(string="Print Variant Grids", default=True, help="If set, the matrix of configurable products will be shown on the report of this order.")
""" Matrix loading and update: fields and methods :
NOTE: The matrix functionality was done in python, server side, to avoid js
restriction. Indeed, the js framework only loads the x first lines displayed
in the client, which means in case of big matrices and lots of po_lines,
the js doesn't have access to the 41st and following lines.
To force the loading, a 'hack' of the js framework would have been needed...
"""
grid_product_tmpl_id = fields.Many2one('product.template', store=False, help="Technical field for product_matrix functionalities.")
grid_update = fields.Boolean(default=False, store=False, help="Whether the grid field contains a new matrix to apply or not.")
grid = fields.Char(store=False, help="Technical storage of grid. \nIf grid_update, will be loaded on the PO. \nIf not, represents the matrix to open.")
@api.onchange('grid_product_tmpl_id')
def _set_grid_up(self):
if self.grid_product_tmpl_id:
self.grid_update = False
self.grid = json.dumps(self._get_matrix(self.grid_product_tmpl_id))
def _must_delete_date_planned(self, field_name):
return super()._must_delete_date_planned(field_name) or field_name == "grid"
@api.onchange('grid')
def _apply_grid(self):
if self.grid and self.grid_update:
grid = json.loads(self.grid)
product_template = self.env['product.template'].browse(grid['product_template_id'])
product_ids = set()
dirty_cells = grid['changes']
Attrib = self.env['product.template.attribute.value']
default_po_line_vals = {}
new_lines = []
for cell in dirty_cells:
combination = Attrib.browse(cell['ptav_ids'])
no_variant_attribute_values = combination - combination._without_no_variant_attributes()
# create or find product variant from combination
product = product_template._create_product_variant(combination)
# TODO replace the check on product_id by a first check on the ptavs and pnavs?
# and only create/require variant after no line has been found ???
order_lines = self.order_line.filtered(lambda line: (line._origin or line).product_id == product and (line._origin or line).product_no_variant_attribute_value_ids == no_variant_attribute_values)
# if product variant already exist in order lines
old_qty = sum(order_lines.mapped('product_qty'))
qty = cell['qty']
diff = qty - old_qty
if not diff:
continue
product_ids.add(product.id)
if order_lines:
if qty == 0:
if self.state in ['draft', 'sent']:
# Remove lines if qty was set to 0 in matrix
# only if PO state = draft/sent
self.order_line -= order_lines
else:
order_lines.update({'product_qty': 0.0})
else:
"""
When there are multiple lines for same product and its quantity was changed in the matrix,
An error is raised.
A 'good' strategy would be to:
* Sets the quantity of the first found line to the cell value
* Remove the other lines.
But this would remove all business logic linked to the other lines...
Therefore, it only raises an Error for now.
"""
if len(order_lines) > 1:
raise ValidationError(_("You cannot change the quantity of a product present in multiple purchase lines."))
else:
order_lines[0].product_qty = qty
# If we want to support multiple lines edition:
# removal of other lines.
# For now, an error is raised instead
# if len(order_lines) > 1:
# # Remove 1+ lines
# self.order_line -= order_lines[1:]
else:
if not default_po_line_vals:
OrderLine = self.env['purchase.order.line']
default_po_line_vals = OrderLine.default_get(OrderLine._fields.keys())
last_sequence = self.order_line[-1:].sequence
if last_sequence:
default_po_line_vals['sequence'] = last_sequence
new_lines.append((0, 0, dict(
default_po_line_vals,
product_id=product.id,
product_qty=qty,
product_no_variant_attribute_value_ids=no_variant_attribute_values.ids)
))
if product_ids:
res = False
if new_lines:
# Add new PO lines
self.update(dict(order_line=new_lines))
# Recompute prices for new/modified lines:
for line in self.order_line.filtered(lambda line: line.product_id.id in product_ids):
line._product_id_change()
res = line.onchange_product_id_warning() or res
return res
def _get_matrix(self, product_template):
def has_ptavs(line, sorted_attr_ids):
ptav = line.product_template_attribute_value_ids.ids
pnav = line.product_no_variant_attribute_value_ids.ids
pav = pnav + ptav
pav.sort()
return pav == sorted_attr_ids
matrix = product_template._get_template_matrix(
company_id=self.company_id,
currency_id=self.currency_id)
if self.order_line:
lines = matrix['matrix']
order_lines = self.order_line.filtered(lambda line: line.product_template_id == product_template)
for line in lines:
for cell in line:
if not cell.get('name', False):
line = order_lines.filtered(lambda line: has_ptavs(line, cell['ptav_ids']))
if line:
cell.update({
'qty': sum(line.mapped('product_qty'))
})
return matrix
def get_report_matrixes(self):
"""Reporting method."""
matrixes = []
if self.report_grids:
grid_configured_templates = self.order_line.filtered('is_configurable_product').product_template_id
# TODO is configurable product and product_variant_count > 1
# configurable products are only configured through the matrix in purchase, so no need to check product_add_mode.
for template in grid_configured_templates:
if len(self.order_line.filtered(lambda line: line.product_template_id == template)) > 1:
matrix = self._get_matrix(template)
matrix_data = []
for row in matrix['matrix']:
if any(column['qty'] != 0 for column in row[1:]):
matrix_data.append(row)
matrix['matrix'] = matrix_data
matrixes.append(matrix)
return matrixes
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
product_template_id = fields.Many2one('product.template', string='Product Template', related="product_id.product_tmpl_id", domain=[('purchase_ok', '=', True)])
is_configurable_product = fields.Boolean('Is the product configurable?', related="product_template_id.has_configurable_attributes")
product_template_attribute_value_ids = fields.Many2many(related='product_id.product_template_attribute_value_ids', readonly=True)
product_no_variant_attribute_value_ids = fields.Many2many('product.template.attribute.value', string='Product attribute values that do not create variants', ondelete='restrict')
def _get_product_purchase_description(self, product):
name = super(PurchaseOrderLine, self)._get_product_purchase_description(product)
for no_variant_attribute_value in self.product_no_variant_attribute_value_ids:
name += "\n" + no_variant_attribute_value.attribute_id.name + ': ' + no_variant_attribute_value.name
return name