Files
odoo_source/addons/purchase_requisition/models/purchase_requisition.py
T
Tiffany Chang (tic) 4e32d1fe37 [FIX] purchase_requsition: ensure product_uom_id exists in bo lines
Previous commit 0501bbd62e made it so
the `product_uom_id` for the `line_ids` of a purchase.requsition (i.e. a
blanket order) were no longer being saved when the UoM setting is not
active. This would cause an error to occur when the "New Quotation"
button is pushed because the missing uom is expected by purchase
_onchange_requisition_id.

Steps to reproduce:
- Have UoM setting NOT active
- Create a blanket order for any product/vendor
- Confirm the blanket order
- Click on "New Quotation"

Expected Behavior:
New RFQ created

Actual Behavior:
Stacktrace

We also properly restrict the product_uom_id in the form view of the
line_ids to when the uom setting is active.

closes odoo/odoo#104900

X-original-commit: e90200a7b927ecccb8d4d6f31e3ddecba24d0b6d
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
2022-11-17 11:58:50 +01:00

278 lines
14 KiB
Python

# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
from odoo import api, fields, models, _
from odoo.exceptions import UserError
PURCHASE_REQUISITION_STATES = [
('draft', 'Draft'),
('ongoing', 'Ongoing'),
('in_progress', 'Confirmed'),
('open', 'Bid Selection'),
('done', 'Closed'),
('cancel', 'Cancelled')
]
class PurchaseRequisitionType(models.Model):
_name = "purchase.requisition.type"
_description = "Purchase Requisition Type"
_order = "sequence"
name = fields.Char(string='Agreement Type', required=True, translate=True)
sequence = fields.Integer(default=1)
exclusive = fields.Selection([
('exclusive', 'Select only one RFQ (exclusive)'), ('multiple', 'Select multiple RFQ (non-exclusive)')],
string='Agreement Selection Type', required=True, default='multiple',
help="""Select only one RFQ (exclusive): when a purchase order is confirmed, cancel the remaining purchase order.\n
Select multiple RFQ (non-exclusive): allows multiple purchase orders. On confirmation of a purchase order it does not cancel the remaining orders""")
quantity_copy = fields.Selection([
('copy', 'Use quantities of agreement'), ('none', 'Set quantities manually')],
string='Quantities', required=True, default='none')
line_copy = fields.Selection([
('copy', 'Use lines of agreement'), ('none', 'Do not create RfQ lines automatically')],
string='Lines', required=True, default='copy')
active = fields.Boolean(default=True, help="Set active to false to hide the Purchase Agreement Types without removing it.")
class PurchaseRequisition(models.Model):
_name = "purchase.requisition"
_description = "Purchase Requisition"
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = "id desc"
def _get_type_id(self):
return self.env['purchase.requisition.type'].search([], limit=1)
name = fields.Char(string='Reference', required=True, copy=False, default='New', readonly=True)
origin = fields.Char(string='Source Document')
order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders')
vendor_id = fields.Many2one('res.partner', string="Vendor", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id)
ordering_date = fields.Date(string="Ordering Date", tracking=True)
date_end = fields.Datetime(string='Agreement Deadline', tracking=True)
schedule_date = fields.Date(string='Delivery Date', index=True, help="The expected and scheduled delivery date where all the products are received", tracking=True)
user_id = fields.Many2one(
'res.users', string='Purchase Representative',
default=lambda self: self.env.user, check_company=True)
description = fields.Html()
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]})
line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True)
product_id = fields.Many2one('product.product', related='line_ids.product_id', string='Product')
state = fields.Selection(PURCHASE_REQUISITION_STATES,
'Status', tracking=True, required=True,
copy=False, default='draft')
state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state')
is_quantity_copy = fields.Selection(related='type_id.quantity_copy', readonly=True)
currency_id = fields.Many2one('res.currency', 'Currency', required=True,
default=lambda self: self.env.company.currency_id.id)
@api.depends('state')
def _set_state(self):
for requisition in self:
requisition.state_blanket_order = requisition.state
@api.onchange('vendor_id')
def _onchange_vendor(self):
self = self.with_company(self.company_id)
if not self.vendor_id:
self.currency_id = self.env.company.currency_id.id
else:
self.currency_id = self.vendor_id.property_purchase_currency_id.id or self.env.company.currency_id.id
requisitions = self.env['purchase.requisition'].search([
('vendor_id', '=', self.vendor_id.id),
('state', '=', 'ongoing'),
('type_id.quantity_copy', '=', 'none'),
('company_id', '=', self.company_id.id),
])
if any(requisitions):
title = _("Warning for %s", self.vendor_id.name)
message = _("There is already an open blanket order for this supplier. We suggest you complete this open blanket order, instead of creating a new one.")
warning = {
'title': title,
'message': message
}
return {'warning': warning}
@api.depends('purchase_ids')
def _compute_orders_number(self):
for requisition in self:
requisition.order_count = len(requisition.purchase_ids)
def action_cancel(self):
# try to set all associated quotations to cancel state
for requisition in self:
for requisition_line in requisition.line_ids:
requisition_line.supplier_info_ids.unlink()
requisition.purchase_ids.button_cancel()
for po in requisition.purchase_ids:
po.message_post(body=_('Cancelled by the agreement associated to this quotation.'))
self.write({'state': 'cancel'})
def action_in_progress(self):
self.ensure_one()
if not self.line_ids:
raise UserError(_("You cannot confirm agreement '%s' because there is no product line.", self.name))
if self.type_id.quantity_copy == 'none' and self.vendor_id:
for requisition_line in self.line_ids:
if requisition_line.price_unit <= 0.0:
raise UserError(_('You cannot confirm the blanket order without price.'))
if requisition_line.product_qty <= 0.0:
raise UserError(_('You cannot confirm the blanket order without quantity.'))
requisition_line.create_supplier_info()
self.write({'state': 'ongoing'})
else:
self.write({'state': 'in_progress'})
# Set the sequence number regarding the requisition type
if self.name == 'New':
self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.blanket.order')
def action_open(self):
self.write({'state': 'open'})
def action_draft(self):
self.ensure_one()
self.name = 'New'
self.write({'state': 'draft'})
def action_done(self):
"""
Generate all purchase order based on selected lines, should only be called on one agreement at a time
"""
if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')):
raise UserError(_('You have to cancel or validate every RfQ before closing the purchase requisition.'))
for requisition in self:
for requisition_line in requisition.line_ids:
requisition_line.supplier_info_ids.unlink()
self.write({'state': 'done'})
@api.ondelete(at_uninstall=False)
def _unlink_if_draft_or_cancel(self):
if any(requisition.state not in ('draft', 'cancel') for requisition in self):
raise UserError(_('You can only delete draft requisitions.'))
def unlink(self):
# Draft requisitions could have some requisition lines.
self.mapped('line_ids').unlink()
return super(PurchaseRequisition, self).unlink()
class PurchaseRequisitionLine(models.Model):
_name = "purchase.requisition.line"
_inherit = 'analytic.mixin'
_description = "Purchase Requisition Line"
_rec_name = 'product_id'
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Product Unit of Measure',
compute='_compute_product_uom_id', store=True, readonly=False, precompute=True,
domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
product_qty = fields.Float(string='Quantity', digits='Product Unit of Measure')
product_description_variants = fields.Char('Custom Description')
price_unit = fields.Float(string='Unit Price', digits='Product Price')
qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities')
requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade')
company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True)
schedule_date = fields.Date(string='Scheduled Date')
supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')
@api.model_create_multi
def create(self, vals_list):
lines = super().create(vals_list)
for line, vals in zip(lines, vals_list):
if line.requisition_id.state not in ['draft', 'cancel', 'done'] and line.requisition_id.is_quantity_copy == 'none':
supplier_infos = self.env['product.supplierinfo'].search([
('product_id', '=', vals.get('product_id')),
('partner_id', '=', line.requisition_id.vendor_id.id),
])
if not any(s.purchase_requisition_id for s in supplier_infos):
line.create_supplier_info()
if vals['price_unit'] <= 0.0:
raise UserError(_('You cannot confirm the blanket order without price.'))
return lines
def write(self, vals):
res = super(PurchaseRequisitionLine, self).write(vals)
if 'price_unit' in vals:
if vals['price_unit'] <= 0.0 and any(
requisition.state not in ['draft', 'cancel', 'done'] and
requisition.is_quantity_copy == 'none' for requisition in self.mapped('requisition_id')):
raise UserError(_('You cannot confirm the blanket order without price.'))
# If the price is updated, we have to update the related SupplierInfo
self.supplier_info_ids.write({'price': vals['price_unit']})
return res
def unlink(self):
to_unlink = self.filtered(lambda r: r.requisition_id.state not in ['draft', 'cancel', 'done'])
to_unlink.mapped('supplier_info_ids').unlink()
return super(PurchaseRequisitionLine, self).unlink()
def create_supplier_info(self):
purchase_requisition = self.requisition_id
if purchase_requisition.type_id.quantity_copy == 'none' and purchase_requisition.vendor_id:
# create a supplier_info only in case of blanket order
self.env['product.supplierinfo'].create({
'partner_id': purchase_requisition.vendor_id.id,
'product_id': self.product_id.id,
'product_tmpl_id': self.product_id.product_tmpl_id.id,
'price': self.price_unit,
'currency_id': self.requisition_id.currency_id.id,
'purchase_requisition_line_id': self.id,
})
@api.depends('requisition_id.purchase_ids.state')
def _compute_ordered_qty(self):
line_found = set()
for line in self:
total = 0.0
for po in line.requisition_id.purchase_ids.filtered(lambda purchase_order: purchase_order.state in ['purchase', 'done']):
for po_line in po.order_line.filtered(lambda order_line: order_line.product_id == line.product_id):
if po_line.product_uom != line.product_uom_id:
total += po_line.product_uom._compute_quantity(po_line.product_qty, line.product_uom_id)
else:
total += po_line.product_qty
if line.product_id not in line_found:
line.qty_ordered = total
line_found.add(line.product_id)
else:
line.qty_ordered = 0
@api.depends('product_id')
def _compute_product_uom_id(self):
for line in self:
line.product_uom_id = line.product_id.uom_id
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.product_uom_id = self.product_id.uom_po_id
self.product_qty = 1.0
if not self.schedule_date:
self.schedule_date = self.requisition_id.schedule_date
def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False):
self.ensure_one()
requisition = self.requisition_id
if self.product_description_variants:
name += '\n' + self.product_description_variants
if requisition.schedule_date:
date_planned = datetime.combine(requisition.schedule_date, time.min)
else:
date_planned = datetime.now()
return {
'name': name,
'product_id': self.product_id.id,
'product_uom': self.product_id.uom_po_id.id,
'product_qty': product_qty,
'price_unit': price_unit,
'taxes_id': [(6, 0, taxes_ids)],
'date_planned': date_planned,
'analytic_distribution': self.analytic_distribution,
}