mrp - when a MO is generated from replenishment report, a log note is added to chatter with the link of replenishment report, as this report got deleted when to to qty is 0.0 then link dost not work. Hence added the simple text for Relenishment report. purchase_stock - When click on the order once button of replenishment report, if vendor is not added on product then, added a new wizard with functionality to directly jump on the edit product. Added log note to the PO if po lines are generated through the Replenishment report. TaskId - 2427738 [IMP] stock: split method to create new or unlink unused OP As, server action was added to the Replenishment menu, unless until user clicks on the menu, new order point will not get generate. Hence splitted the method and added to auto vaccum for this. TaskId - 2427738 closes odoo/odoo#66130 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
189 lines
9.8 KiB
Python
189 lines
9.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, SUPERUSER_ID, _
|
|
from odoo.osv import expression
|
|
from odoo.addons.stock.models.stock_rule import ProcurementException
|
|
from odoo.tools import OrderedSet
|
|
|
|
|
|
class StockRule(models.Model):
|
|
_inherit = 'stock.rule'
|
|
action = fields.Selection(selection_add=[
|
|
('manufacture', 'Manufacture')
|
|
], ondelete={'manufacture': 'cascade'})
|
|
|
|
def _get_message_dict(self):
|
|
message_dict = super(StockRule, self)._get_message_dict()
|
|
source, destination, operation = self._get_message_values()
|
|
manufacture_message = _('When products are needed in <b>%s</b>, <br/> a manufacturing order is created to fulfill the need.') % (destination)
|
|
if self.location_src_id:
|
|
manufacture_message += _(' <br/><br/> The components will be taken from <b>%s</b>.') % (source)
|
|
message_dict.update({
|
|
'manufacture': manufacture_message
|
|
})
|
|
return message_dict
|
|
|
|
@api.depends('action')
|
|
def _compute_picking_type_code_domain(self):
|
|
remaining = self.browse()
|
|
for rule in self:
|
|
if rule.action == 'manufacture':
|
|
rule.picking_type_code_domain = 'mrp_operation'
|
|
else:
|
|
remaining |= rule
|
|
super(StockRule, remaining)._compute_picking_type_code_domain()
|
|
|
|
@api.model
|
|
def _run_manufacture(self, procurements):
|
|
productions_values_by_company = defaultdict(list)
|
|
errors = []
|
|
for procurement, rule in procurements:
|
|
bom = rule._get_matching_bom(procurement.product_id, procurement.company_id, procurement.values)
|
|
|
|
productions_values_by_company[procurement.company_id.id].append(rule._prepare_mo_vals(*procurement, bom))
|
|
|
|
if errors:
|
|
raise ProcurementException(errors)
|
|
|
|
for company_id, productions_values in productions_values_by_company.items():
|
|
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
|
|
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values)
|
|
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
|
|
self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
|
|
productions._create_workorder()
|
|
productions.action_confirm()
|
|
|
|
for production in productions:
|
|
origin_production = production.move_dest_ids and production.move_dest_ids[0].raw_material_production_id or False
|
|
orderpoint = production.orderpoint_id
|
|
if orderpoint and orderpoint.create_uid.id == SUPERUSER_ID and orderpoint.trigger == 'manual':
|
|
production.message_post(
|
|
body=_('This production order has been created from Replenishment Report.'),
|
|
message_type='comment',
|
|
subtype_xmlid='mail.mt_note')
|
|
elif orderpoint:
|
|
production.message_post_with_view(
|
|
'mail.message_origin_link',
|
|
values={'self': production, 'origin': orderpoint},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
elif origin_production:
|
|
production.message_post_with_view(
|
|
'mail.message_origin_link',
|
|
values={'self': production, 'origin': origin_production},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
return True
|
|
|
|
def _get_custom_move_fields(self):
|
|
fields = super(StockRule, self)._get_custom_move_fields()
|
|
fields += ['bom_line_id']
|
|
return fields
|
|
|
|
def _get_matching_bom(self, product_id, company_id, values):
|
|
if values.get('bom_id', False):
|
|
return values['bom_id']
|
|
return self.env['mrp.bom']._bom_find(product_id, picking_type=self.picking_type_id, bom_type='normal', company_id=company_id.id)[product_id]
|
|
|
|
def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values, bom):
|
|
date_planned = self._get_date_planned(product_id, company_id, values)
|
|
date_deadline = values.get('date_deadline') or date_planned + relativedelta(days=company_id.manufacturing_lead) + relativedelta(days=product_id.produce_delay)
|
|
return {
|
|
'origin': origin,
|
|
'product_id': product_id.id,
|
|
'product_description_variants': values.get('product_description_variants'),
|
|
'product_qty': product_qty,
|
|
'product_uom_id': product_uom.id,
|
|
'location_src_id': self.location_src_id.id or self.picking_type_id.default_location_src_id.id or location_id.id,
|
|
'location_dest_id': location_id.id,
|
|
'bom_id': bom.id,
|
|
'date_deadline': date_deadline,
|
|
'date_planned_start': date_planned,
|
|
'procurement_group_id': False,
|
|
'propagate_cancel': self.propagate_cancel,
|
|
'orderpoint_id': values.get('orderpoint_id', False) and values.get('orderpoint_id').id,
|
|
'picking_type_id': self.picking_type_id.id or values['warehouse_id'].manu_type_id.id,
|
|
'company_id': company_id.id,
|
|
'move_dest_ids': values.get('move_dest_ids') and [(4, x.id) for x in values['move_dest_ids']] or False,
|
|
'user_id': False,
|
|
}
|
|
|
|
def _get_date_planned(self, product_id, company_id, values):
|
|
format_date_planned = fields.Datetime.from_string(values['date_planned'])
|
|
date_planned = format_date_planned - relativedelta(days=product_id.produce_delay)
|
|
date_planned = date_planned - relativedelta(days=company_id.manufacturing_lead)
|
|
if date_planned == format_date_planned:
|
|
date_planned = date_planned - relativedelta(hours=1)
|
|
return date_planned
|
|
|
|
def _get_lead_days(self, product, **values):
|
|
"""Add the product and company manufacture delay to the cumulative delay
|
|
and cumulative description.
|
|
"""
|
|
delay, delay_description = super()._get_lead_days(product, **values)
|
|
bypass_delay_description = self.env.context.get('bypass_delay_description')
|
|
manufacture_rule = self.filtered(lambda r: r.action == 'manufacture')
|
|
if not manufacture_rule:
|
|
return delay, delay_description
|
|
manufacture_rule.ensure_one()
|
|
manufacture_delay = product.produce_delay
|
|
delay += manufacture_delay
|
|
if not bypass_delay_description:
|
|
delay_description.append((_('Manufacturing Lead Time'), _('+ %d day(s)', manufacture_delay)))
|
|
security_delay = manufacture_rule.picking_type_id.company_id.manufacturing_lead
|
|
delay += security_delay
|
|
if not bypass_delay_description:
|
|
delay_description.append((_('Manufacture Security Lead Time'), _('+ %d day(s)', security_delay)))
|
|
return delay, delay_description
|
|
|
|
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
|
|
new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date)
|
|
new_move_vals['production_id'] = False
|
|
return new_move_vals
|
|
|
|
|
|
class ProcurementGroup(models.Model):
|
|
_inherit = 'procurement.group'
|
|
|
|
mrp_production_ids = fields.One2many('mrp.production', 'procurement_group_id')
|
|
|
|
@api.model
|
|
def run(self, procurements, raise_user_error=True):
|
|
""" If 'run' is called on a kit, this override is made in order to call
|
|
the original 'run' method with the values of the components of that kit.
|
|
"""
|
|
procurements_without_kit = []
|
|
product_by_company = defaultdict(OrderedSet)
|
|
for procurement in procurements:
|
|
product_by_company[procurement.company_id].add(procurement.product_id.id)
|
|
kits_by_company = {
|
|
company: self.env['mrp.bom']._bom_find(self.env['product.product'].browse(product_ids), company_id=company.id, bom_type='phantom')
|
|
for company, product_ids in product_by_company.items()
|
|
}
|
|
for procurement in procurements:
|
|
bom_kit = kits_by_company[procurement.company_id].get(procurement.product_id)
|
|
if bom_kit:
|
|
order_qty = procurement.product_uom._compute_quantity(procurement.product_qty, bom_kit.product_uom_id, round=False)
|
|
qty_to_produce = (order_qty / bom_kit.product_qty)
|
|
boms, bom_sub_lines = bom_kit.explode(procurement.product_id, qty_to_produce)
|
|
for bom_line, bom_line_data in bom_sub_lines:
|
|
bom_line_uom = bom_line.product_uom_id
|
|
quant_uom = bom_line.product_id.uom_id
|
|
# recreate dict of values since each child has its own bom_line_id
|
|
values = dict(procurement.values, bom_line_id=bom_line.id)
|
|
component_qty, procurement_uom = bom_line_uom._adjust_uom_quantities(bom_line_data['qty'], quant_uom)
|
|
procurements_without_kit.append(self.env['procurement.group'].Procurement(
|
|
bom_line.product_id, component_qty, procurement_uom,
|
|
procurement.location_id, procurement.name,
|
|
procurement.origin, procurement.company_id, values))
|
|
else:
|
|
procurements_without_kit.append(procurement)
|
|
return super(ProcurementGroup, self).run(procurements_without_kit, raise_user_error=raise_user_error)
|
|
|
|
def _get_moves_to_assign_domain(self, company_id):
|
|
domain = super(ProcurementGroup, self)._get_moves_to_assign_domain(company_id)
|
|
domain = expression.AND([domain, [('production_id', '=', False)]])
|
|
return domain
|