Files
odoo_source/addons/purchase_requisition/models/purchase_requisition.py
T
Arnold Moyaux d8355a0087 [FIX] purchase_requistion: call for tender zero lines
Usecase to reproduce:
- Create a purchase requistion with type Call For Tender
- Update a line and set the price to zero.

UserError 'You cannot confirm the blanket order without price.' raised.
It happens because the write don't process the same check than create
and don't check if the purchase_requistion is a blanket order or a call
for tender. It also doens't check the current state of the
purchase_requisition.

closes odoo/odoo#39980

Task: 2120211
X-original-commit: 7d69f01477a2d5dd93974737978d7af11cfc8b6d
Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
2019-11-07 16:18:13 +00:00

268 lines
13 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')],
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: 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')
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.Text()
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', readonly=False)
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):
self.state_blanket_order = self.state
@api.onchange('vendor_id')
def _onchange_vendor(self):
requisitions = self.env['purchase.requisition'].search([
('vendor_id', '=', self.vendor_id.id),
('state', '=', 'ongoing'),
('type_id.quantity_copy', '=', 'none'),
])
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 to use to 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 all(obj.line_ids for obj in self):
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':
if self.is_quantity_copy != 'none':
self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.purchase.tender')
else:
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'})
def _prepare_tender_values(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values):
return{
'origin': origin,
'date_end': values['date_planned'],
'user_id': False,
'warehouse_id': values.get('warehouse_id') and values['warehouse_id'].id or False,
'company_id': company_id.id,
'line_ids': [(0, 0, {
'product_id': product_id.id,
'product_uom_id': product_uom.id,
'product_qty': product_qty,
})],
}
def unlink(self):
if any(requisition.state not in ('draft', 'cancel') for requisition in self):
raise UserError(_('You can only delete draft requisitions.'))
# 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"
_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', string='Product Unit of Measure', 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')
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, default= lambda self: self.env.company)
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
schedule_date = fields.Date(string='Scheduled Date')
supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')
@api.model
def create(self,vals):
res = super(PurchaseRequisitionLine, self).create(vals)
if res.requisition_id.state not in ['draft', 'cancel', 'done'] and res.requisition_id.is_quantity_copy == 'none':
supplier_infos = self.env['product.supplierinfo'].search([
('product_id', '=', vals.get('product_id')),
('name', '=', res.requisition_id.vendor_id.id),
])
if not any([s.purchase_requisition_id for s in supplier_infos]):
res.create_supplier_info()
if vals['price_unit'] <= 0.0:
raise UserError(_('You cannot confirm the blanket order without price.'))
return res
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({
'name': 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):
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
line.qty_ordered = total
@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 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,
'account_analytic_id': self.account_analytic_id.id,
'analytic_tag_ids': self.analytic_tag_ids.ids,
}