Files
odoo_source/addons/purchase_requisition/models/purchase.py
T
niyasraphy 9b603054ed [FIX] purchase_requisition: fix singleton error
Activate Product variants and create a new products with attributes Legs, Size and color.

Now enable Variant Grid Entry and Purchase Agreements from purchase settings and now create a Blanket Order from Purchase -> Orders -> Blanket Orders with any products from above created variants.

Confirm the created blanked order and click on New Quotation, now a new RFQ will be created, and click on add an item button and select the product we have created before, now Choose Product Variants matrix will be opened. Enter some random quantities in multiple lines and click on confirm button.

Exception will be raised

closes odoo/odoo#106572

X-original-commit: ca94ace181675b6e150c0f7486de9495198dc8e8
Signed-off-by: Tiffany Chang <tic@odoo.com>
2022-11-25 18:55:40 +01:00

303 lines
15 KiB
Python

# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import api, fields, models, _, Command
from odoo.tools import get_lang
class PurchaseOrderGroup(models.Model):
_name = 'purchase.order.group'
_description = "Technical model to group PO for call to tenders"
order_ids = fields.One2many('purchase.order', 'purchase_group_id')
def write(self, vals):
res = super().write(vals)
# when len(POs) == 1, only linking PO to itself at this point => self implode (delete) group
self.filtered(lambda g: len(g.order_ids) <= 1).unlink()
return res
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
requisition_id = fields.Many2one('purchase.requisition', string='Purchase Agreement', copy=False)
is_quantity_copy = fields.Selection(related='requisition_id.is_quantity_copy', readonly=False)
purchase_group_id = fields.Many2one('purchase.order.group')
alternative_po_ids = fields.One2many(
'purchase.order', related='purchase_group_id.order_ids', readonly=False,
domain="[('id', '!=', id), ('state', 'in', ['draft', 'sent', 'to approve'])]",
string="Alternative POs", check_company=True,
help="Other potential purchase orders for purchasing products")
has_alternatives = fields.Boolean(
"Has Alternatives", compute='_compute_has_alternatives',
help="Whether or not this purchase order is linked to another purchase order as an alternative.")
@api.depends('purchase_group_id')
def _compute_has_alternatives(self):
self.has_alternatives = False
self.filtered(lambda po: po.purchase_group_id).has_alternatives = True
@api.onchange('requisition_id')
def _onchange_requisition_id(self):
if not self.requisition_id:
return
self = self.with_company(self.company_id)
requisition = self.requisition_id
if self.partner_id:
partner = self.partner_id
else:
partner = requisition.vendor_id
payment_term = partner.property_supplier_payment_term_id
FiscalPosition = self.env['account.fiscal.position']
fpos = FiscalPosition.with_company(self.company_id)._get_fiscal_position(partner)
self.partner_id = partner.id
self.fiscal_position_id = fpos.id
self.payment_term_id = payment_term.id,
self.company_id = requisition.company_id.id
self.currency_id = requisition.currency_id.id
if not self.origin or requisition.name not in self.origin.split(', '):
if self.origin:
if requisition.name:
self.origin = self.origin + ', ' + requisition.name
else:
self.origin = requisition.name
self.notes = requisition.description
self.date_order = fields.Datetime.now()
if requisition.type_id.line_copy != 'copy':
return
# Create PO lines if necessary
order_lines = []
for line in requisition.line_ids:
# Compute name
product_lang = line.product_id.with_context(
lang=partner.lang or self.env.user.lang,
partner_id=partner.id
)
name = product_lang.display_name
if product_lang.description_purchase:
name += '\n' + product_lang.description_purchase
# Compute taxes
taxes_ids = fpos.map_tax(line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id)).ids
# Compute quantity and price_unit
if line.product_uom_id != line.product_id.uom_po_id:
product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id)
price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id)
else:
product_qty = line.product_qty
price_unit = line.price_unit
if requisition.type_id.quantity_copy != 'copy':
product_qty = 0
# Create PO line
order_line_values = line._prepare_purchase_order_line(
name=name, product_qty=product_qty, price_unit=price_unit,
taxes_ids=taxes_ids)
order_lines.append((0, 0, order_line_values))
self.order_line = order_lines
def button_confirm(self):
if self.alternative_po_ids and not self.env.context.get('skip_alternative_check', False):
alternative_po_ids = self.alternative_po_ids.filtered(lambda po: po.state in ['draft', 'sent', 'to approve'] and po.id not in self.ids)
if alternative_po_ids:
view = self.env.ref('purchase_requisition.purchase_requisition_alternative_warning_form')
return {
'name': _('Alternative Warning'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'purchase.requisition.alternative.warning',
'views': [(view.id, 'form')],
'target': 'new',
'context': dict(self.env.context, default_alternative_po_ids=alternative_po_ids.ids, default_po_ids=self.ids),
}
res = super(PurchaseOrder, self).button_confirm()
for po in self:
if not po.requisition_id:
continue
if po.requisition_id.type_id.exclusive == 'exclusive':
others_po = po.requisition_id.mapped('purchase_ids').filtered(lambda r: r.id != po.id)
others_po.button_cancel()
if po.state not in ['draft', 'sent', 'to approve']:
po.requisition_id.action_done()
return res
@api.model_create_multi
def create(self, vals_list):
orders = super().create(vals_list)
if self.env.context.get('origin_po_id'):
# po created as an alt to another PO:
origin_po_id = self.env['purchase.order'].browse(self.env.context.get('origin_po_id'))
if origin_po_id.purchase_group_id:
origin_po_id.purchase_group_id.order_ids |= orders
else:
self.env['purchase.order.group'].create({'order_ids': [Command.set(origin_po_id.ids + orders.ids)]})
mt_note = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
for order in orders:
if order.requisition_id:
order.message_post_with_view(
'mail.message_origin_link',
values={'self': order, 'origin': order.requisition_id},
subtype_id=mt_note)
return orders
def write(self, vals):
if vals.get('purchase_group_id', False):
# store in case linking to a PO with existing linkages
orig_purchase_group = self.purchase_group_id
result = super(PurchaseOrder, self).write(vals)
if vals.get('requisition_id'):
for order in self:
order.message_post_with_view(
'mail.message_origin_link',
values={'self': order, 'origin': order.requisition_id, 'edit': True},
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
)
if vals.get('alternative_po_ids', False):
if not self.purchase_group_id and len(self.alternative_po_ids + self) > len(self):
# this can create a new group + delete an existing one (or more) when linking to already linked PO(s), but this is
# simplier than additional logic checking if exactly 1 exists or merging multiple groups if > 1
self.env['purchase.order.group'].create({'order_ids': [Command.set(self.ids + self.alternative_po_ids.ids)]})
elif self.purchase_group_id and len(self.alternative_po_ids + self) <= 1:
# write in purchase group isn't called so we have to manually unlink obsolete groups here
self.purchase_group_id.unlink()
if vals.get('purchase_group_id', False):
# the write is for multiple POs => don't double count the POs of the final group
additional_groups = orig_purchase_group - self.purchase_group_id
if additional_groups:
additional_pos = (additional_groups.order_ids - self.purchase_group_id.order_ids)
additional_groups.unlink()
if additional_pos:
self.purchase_group_id.order_ids |= additional_pos
return result
def action_create_alternative(self):
ctx = dict(**self.env.context, default_origin_po_id=self.id)
return {
'name': _('Create alternative'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'purchase.requisition.create.alternative',
'view_id': self.env.ref('purchase_requisition.purchase_requisition_create_alternative_form').id,
'target': 'new',
'context': ctx,
}
def action_compare_alternative_lines(self):
ctx = dict(
self.env.context,
search_default_groupby_product=True,
purchase_order_id=self.id,
)
view_id = self.env.ref('purchase_requisition.purchase_order_line_compare_tree').id
return {
'name': _('Compare Order Lines'),
'type': 'ir.actions.act_window',
'view_mode': 'list',
'res_model': 'purchase.order.line',
'views': [(view_id, "list")],
'domain': [('order_id', 'in', (self | self.alternative_po_ids).ids)],
'context': ctx,
}
def get_tender_best_lines(self):
product_to_best_price_line = defaultdict(lambda: self.env['purchase.order.line'])
product_to_best_date_line = defaultdict(lambda: self.env['purchase.order.line'])
product_to_best_price_unit = defaultdict(lambda: self.env['purchase.order.line'])
order_lines = self.order_line | self.alternative_po_ids.order_line
for line in order_lines:
if not line.product_qty or not line.price_subtotal or line.state in ['cancel', 'purchase', 'done']:
continue
if not product_to_best_price_line[line.product_id] or product_to_best_price_line[line.product_id][0].price_subtotal > line.price_subtotal:
product_to_best_price_line[line.product_id] = line
elif product_to_best_price_line[line.product_id][0].price_subtotal == line.price_subtotal:
product_to_best_price_line[line.product_id] |= line
if not product_to_best_date_line[line.product_id] or product_to_best_date_line[line.product_id][0].date_planned > line.date_planned:
product_to_best_date_line[line.product_id] = line
elif product_to_best_date_line[line.product_id][0].date_planned == line.date_planned:
product_to_best_date_line[line.product_id] |= line
if not product_to_best_price_unit[line.product_id] or product_to_best_price_unit[line.product_id][0].price_unit > line.price_unit:
product_to_best_price_unit[line.product_id] = line
elif product_to_best_price_unit[line.product_id][0].price_unit == line.price_unit:
product_to_best_price_unit[line.product_id] |= line
best_price_ids = set()
best_date_ids = set()
best_price_unit_ids = set()
for lines in product_to_best_price_line.values():
best_price_ids.update(lines.ids)
for lines in product_to_best_date_line.values():
best_date_ids.update(lines.ids)
for lines in product_to_best_price_unit.values():
best_price_unit_ids.update(lines.ids)
return list(best_price_ids), list(best_date_ids), list(best_price_unit_ids)
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
def _compute_price_unit_and_date_planned_and_name(self):
po_lines_without_requisition = self.env['purchase.order.line']
for pol in self:
if pol.product_id.id not in pol.order_id.requisition_id.line_ids.product_id.ids:
po_lines_without_requisition |= pol
continue
for line in pol.order_id.requisition_id.line_ids:
if line.product_id == pol.product_id:
pol.price_unit = line.product_uom_id._compute_price(line.price_unit, pol.product_uom)
partner = pol.order_id.partner_id or pol.order_id.requisition_id.vendor_id
params = {'order_id': pol.order_id}
seller = pol.product_id._select_seller(
partner_id=partner,
quantity=pol.product_qty,
date=pol.order_id.date_order and pol.order_id.date_order.date(),
uom_id=line.product_uom_id,
params=params)
product_ctx = {'seller_id': seller.id, 'lang': get_lang(pol.env, partner.lang).code}
name = pol._get_product_purchase_description(pol.product_id.with_context(product_ctx))
if line.product_description_variants:
name += '\n' + line.product_description_variants
pol.name = name
break
super(PurchaseOrderLine, po_lines_without_requisition)._compute_price_unit_and_date_planned_and_name()
def action_clear_quantities(self):
zeroed_lines = self.filtered(lambda l: l.state not in ['cancel', 'purchase', 'done'])
zeroed_lines.write({'product_qty': 0})
if len(self) > len(zeroed_lines):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Some not cleared"),
'message': _("Some quantities were not cleared because their status is not a RFQ status."),
'sticky': False,
}
}
return False
def action_choose(self):
order_lines = (self.order_id | self.order_id.alternative_po_ids).mapped('order_line')
order_lines = order_lines.filtered(lambda l: l.product_qty and l.product_id.id in self.product_id.ids and l.id not in self.ids)
if order_lines:
return order_lines.action_clear_quantities()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Nothing to clear"),
'message': _("There are no quantities to clear."),
'sticky': False,
}
}