233 lines
11 KiB
Python
233 lines
11 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from markupsafe import Markup
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.misc import clean_context
|
|
|
|
|
|
class ApprovalRequest(models.Model):
|
|
_inherit = 'approval.request'
|
|
|
|
purchase_order_count = fields.Integer(compute='_compute_purchase_order_count')
|
|
|
|
@api.depends('product_line_ids.purchase_order_line_id')
|
|
def _compute_purchase_order_count(self):
|
|
for request in self:
|
|
purchases = request.product_line_ids.purchase_order_line_id.order_id
|
|
request.purchase_order_count = len(purchases)
|
|
|
|
def action_approve(self, approver=None):
|
|
if self.approval_type == 'purchase' and any(not line.product_id for line in self.product_line_ids):
|
|
raise UserError(_("You must select a product for each line of requested products."))
|
|
return super().action_approve(approver)
|
|
|
|
def action_cancel(self):
|
|
""" Override to unlink the products of the cancelled Approval Request from Purchase Orders """
|
|
res = super().action_cancel()
|
|
purchase_orders_data_per_state = self._cancel_approval_request()
|
|
if any(purchase_orders_data_per_state.values()):
|
|
self._log_po_cancellation_to_chatter(purchase_orders_data_per_state)
|
|
return res
|
|
|
|
def _cancel_approval_request(self):
|
|
purchase_orders = self.sudo().product_line_ids.purchase_order_line_id.order_id
|
|
purchase_orders_data_per_state = {"removed": [], "require_manual_action": []}
|
|
for purchase_order in purchase_orders:
|
|
product_lines = self.sudo().product_line_ids.filtered(
|
|
lambda line: line.purchase_order_line_id.order_id.id == purchase_order.id
|
|
)
|
|
|
|
if purchase_order.state == 'draft':
|
|
purchase_orders_data_per_state["removed"].extend(self._get_order_data_from_product_lines(product_lines))
|
|
removed_lines = self.sudo().env['purchase.order.line']
|
|
for product_line in product_lines:
|
|
purchase_order_line = product_line.purchase_order_line_id
|
|
if purchase_order_line.product_qty == product_line.quantity:
|
|
removed_lines |= purchase_order_line
|
|
else:
|
|
purchase_order_line.product_qty -= product_line.quantity
|
|
if len(purchase_order.order_line) == len(removed_lines):
|
|
# Purchase orders must be canceled first before deletion
|
|
purchase_order.write({'state': 'cancel'})
|
|
purchase_order.unlink()
|
|
elif removed_lines:
|
|
removed_lines.unlink()
|
|
|
|
else:
|
|
purchase_orders_data_per_state["require_manual_action"].extend(self._get_order_data_from_product_lines(product_lines, include_raw_data=True))
|
|
|
|
product_lines.purchase_order_line_id = False
|
|
|
|
return purchase_orders_data_per_state
|
|
|
|
def action_confirm(self):
|
|
for request in self:
|
|
if request.approval_type == 'purchase' and not request.product_line_ids:
|
|
raise UserError(_("You cannot create an empty purchase request."))
|
|
return super().action_confirm()
|
|
|
|
def action_create_purchase_orders(self):
|
|
""" Create and/or modifier Purchase Orders. """
|
|
self.ensure_one()
|
|
if self.purchase_order_count:
|
|
return
|
|
self._create_purchase_orders()
|
|
self._log_po_creation_to_chatter()
|
|
|
|
def _create_purchase_orders(self):
|
|
self.product_line_ids._check_products_vendor()
|
|
for line in self.product_line_ids:
|
|
seller = line.seller_id or line.product_id.with_company(line.company_id)._select_seller(
|
|
quantity=line.po_uom_qty,
|
|
uom_id=line.product_id.uom_id,
|
|
)
|
|
vendor = seller.partner_id
|
|
po_domain = line._get_purchase_orders_domain(vendor)
|
|
purchase_orders = self.env['purchase.order'].search(po_domain)
|
|
|
|
if purchase_orders:
|
|
# Existing RFQ found: check if we must modify an existing
|
|
# purchase order line or create a new one.
|
|
purchase_line = self.env['purchase.order.line'].search([
|
|
('order_id', 'in', purchase_orders.ids),
|
|
('product_id', '=', line.product_id.id),
|
|
('product_uom_id', '=', line.seller_id.product_uom_id.id or line.product_id.uom_id.id),
|
|
], limit=1)
|
|
purchase_order = self.env['purchase.order']
|
|
if purchase_line:
|
|
# Compatible po line found, only update the quantity.
|
|
line.purchase_order_line_id = purchase_line.id
|
|
purchase_line.product_qty += line.po_uom_qty
|
|
purchase_order = purchase_line.order_id
|
|
else:
|
|
# No purchase order line found, create one.
|
|
purchase_order = purchase_orders[0]
|
|
seller_uom_qty = line.product_uom_id._compute_quantity(line.quantity, seller.product_uom_id)
|
|
po_line_vals = self.env['purchase.order.line']._prepare_purchase_order_line(
|
|
line.product_id,
|
|
seller_uom_qty,
|
|
seller.product_uom_id,
|
|
line.company_id,
|
|
vendor,
|
|
purchase_order,
|
|
)
|
|
new_po_line = self.env['purchase.order.line'].create(po_line_vals)
|
|
line.purchase_order_line_id = new_po_line.id
|
|
purchase_order.order_line = [(4, new_po_line.id)]
|
|
|
|
# Add the request name on the purchase order `origin` field.
|
|
new_origin = set([self.name])
|
|
if purchase_order.origin:
|
|
missing_origin = new_origin - set(purchase_order.origin.split(', '))
|
|
if missing_origin:
|
|
purchase_order.write({'origin': purchase_order.origin + ', ' + ', '.join(missing_origin)})
|
|
else:
|
|
purchase_order.write({'origin': ', '.join(new_origin)})
|
|
else:
|
|
# No RFQ found: create a new one.
|
|
po_vals = line._get_purchase_order_values(vendor)
|
|
new_purchase_order = self.env['purchase.order'].create(po_vals)
|
|
seller_uom_qty = line.product_uom_id._compute_quantity(line.quantity, seller.product_uom_id)
|
|
po_line_vals = self.env['purchase.order.line']._prepare_purchase_order_line(
|
|
line.product_id,
|
|
seller_uom_qty,
|
|
seller.product_uom_id,
|
|
line.company_id,
|
|
vendor,
|
|
new_purchase_order,
|
|
)
|
|
new_po_line = self.env['purchase.order.line'].create(po_line_vals)
|
|
line.purchase_order_line_id = new_po_line.id
|
|
new_purchase_order.order_line = [(4, new_po_line.id)]
|
|
|
|
def action_open_purchase_orders(self):
|
|
""" Return the list of purchase orders the approval request created or
|
|
affected in quantity. """
|
|
self.ensure_one()
|
|
purchase_ids = self.product_line_ids.purchase_order_line_id.order_id.ids
|
|
domain = [('id', 'in', purchase_ids)]
|
|
action = {
|
|
'name': _('Purchase Orders'),
|
|
'view_type': 'list',
|
|
'view_mode': 'list,form',
|
|
'res_model': 'purchase.order',
|
|
'type': 'ir.actions.act_window',
|
|
'context': clean_context(self.env.context), # avoid 'default_name' context key propagation
|
|
'domain': domain,
|
|
}
|
|
return action
|
|
|
|
def _log_po_creation_to_chatter(self):
|
|
purchase_orders_data = self._get_order_data_from_product_lines(self.product_line_ids)
|
|
po_creation_chatter_msg = self._generate_po_log_message("created", purchase_orders_data)
|
|
self._message_log(body=po_creation_chatter_msg)
|
|
|
|
def _log_po_cancellation_to_chatter(self, purchase_orders_data_per_state):
|
|
po_cancellation_states = ["removed", "require_manual_action"]
|
|
cancellation_log_msg = ""
|
|
for po_state in po_cancellation_states:
|
|
purchase_orders_data = purchase_orders_data_per_state[po_state]
|
|
if purchase_orders_data:
|
|
cancellation_log_msg += self._generate_po_log_message(po_state, purchase_orders_data)
|
|
if po_state == "require_manual_action":
|
|
self._log_cancellation_exception_to_po_chatter(purchase_orders_data)
|
|
self._message_log(body=cancellation_log_msg)
|
|
|
|
def _log_cancellation_exception_to_po_chatter(self, purchase_orders_data):
|
|
for purchase_order_data in purchase_orders_data:
|
|
purchase_order_data["order"]._activity_schedule_with_view(
|
|
'mail.mail_activity_data_warning',
|
|
views_or_xmlid='approvals_purchase.exception_approval_request_canceled',
|
|
user_id=self.env.user.id,
|
|
render_context={
|
|
'approval_requests': self,
|
|
'product_lines': purchase_order_data["product_lines"],
|
|
}
|
|
)
|
|
|
|
def _get_order_data_from_product_lines(self, product_lines, include_raw_data=False):
|
|
"""
|
|
Returns a list of dictionaries, each representing the data of a single order.
|
|
Each dictionary has the following structure:
|
|
{
|
|
"order": The purchase order (optional)
|
|
"product_lines": The product lines of the approval request (optional)
|
|
"order_name": The name of the purchase order
|
|
"products": [(product_name, product_quantity), ...] A list of tuples. Each tuple contains product name and product quantity.
|
|
}
|
|
"""
|
|
orders_data = list()
|
|
grouped_product_lines = product_lines.grouped(lambda product_line: product_line.purchase_order_line_id.order_id)
|
|
for order_id, lines in grouped_product_lines.items():
|
|
order_data = dict()
|
|
if include_raw_data:
|
|
order_data["order"] = order_id
|
|
order_data["product_lines"] = lines
|
|
order_data["order_name"] = order_id.name
|
|
order_data["products"] = [(order_line.product_id.name, order_line.quantity) for order_line in lines]
|
|
orders_data.append(order_data)
|
|
return orders_data
|
|
|
|
def _generate_po_log_message(self, po_state, purchase_orders_data):
|
|
log_msg_header_by_state = {
|
|
'created': _("The following products have been added to the RFQs:"),
|
|
'removed': _("The following products have been removed from the RFQs:"),
|
|
'require_manual_action': _("The following products couldn't be removed because the RFQs are not in draft state:")
|
|
}
|
|
|
|
return Markup("%(log_msg_header)s<br> <br> %(purchase_log)s <hr>") % {
|
|
'log_msg_header': log_msg_header_by_state.get(po_state, ""),
|
|
'purchase_log': Markup().join(
|
|
Markup("RFQ %(po_name)s: <br> <ul>%(products_lines)s</ul>") % {
|
|
'po_name': purchase_order['order_name'],
|
|
'products_lines': Markup().join(
|
|
Markup("<li>%(quantity)s %(name)s</li>") % {
|
|
'name': product_line[0],
|
|
'quantity': product_line[1]
|
|
} for product_line in purchase_order["products"]
|
|
)
|
|
} for purchase_order in purchase_orders_data
|
|
)
|
|
}
|