306 lines
15 KiB
Python
306 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
from collections import defaultdict, namedtuple
|
|
from odoo.addons.stock.models.stock_rule import ProcurementException
|
|
from re import findall as regex_findall
|
|
from re import split as regex_split
|
|
from odoo import SUPERUSER_ID, _, api, fields,models
|
|
from odoo.tools import float_compare
|
|
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class StockRule(models.Model):
|
|
_inherit = 'stock.rule'
|
|
|
|
@api.model
|
|
def _run_pull(self, procurements):
|
|
moves_values_by_company = defaultdict(list)
|
|
mtso_products_by_locations = defaultdict(list)
|
|
|
|
# To handle the `mts_else_mto` procure method, we do a preliminary loop to
|
|
# isolate the products we would need to read the forecasted quantity,
|
|
# in order to to batch the read. We also make a sanitary check on the
|
|
# `location_src_id` field.
|
|
|
|
# list1 = []
|
|
# for item in procurements:
|
|
# num = int(item[0].product_qty)
|
|
# if num > 1:
|
|
# for no in range(1, num+1):
|
|
#
|
|
# Procurement = namedtuple('Procurement', ['product_id', 'product_qty',
|
|
# 'product_uom', 'location_id', 'name', 'origin',
|
|
# 'company_id',
|
|
# 'values'])
|
|
# s = Procurement(product_id=item[0].product_id,product_qty=1.0,product_uom=item[0].product_uom,
|
|
# location_id=item[0].location_id,
|
|
# name=item[0].name,
|
|
# origin=item[0].origin,
|
|
# company_id=item[0].company_id,
|
|
# values=item[0].values,
|
|
# )
|
|
# item1 = list(item)
|
|
# item1[0]=s
|
|
#
|
|
# list1.append(tuple(item1))
|
|
# else:
|
|
# list1.append(item)
|
|
|
|
for procurement, rule in procurements:
|
|
if not rule.location_src_id:
|
|
msg = _('No source location defined on stock rule: %s!') % (rule.name,)
|
|
raise ProcurementException([(procurement, msg)])
|
|
|
|
if rule.procure_method == 'mts_else_mto':
|
|
mtso_products_by_locations[rule.location_src_id].append(procurement.product_id.id)
|
|
|
|
# Get the forecasted quantity for the `mts_else_mto` procurement.
|
|
forecasted_qties_by_loc = {}
|
|
for location, product_ids in mtso_products_by_locations.items():
|
|
products = self.env['product.product'].browse(product_ids).with_context(location=location.id)
|
|
forecasted_qties_by_loc[location] = {product.id: product.free_qty for product in products}
|
|
|
|
# Prepare the move values, adapt the `procure_method` if needed.
|
|
procurements = sorted(procurements, key=lambda proc: float_compare(proc[0].product_qty, 0.0,
|
|
precision_rounding=proc[
|
|
0].product_uom.rounding) > 0)
|
|
list2 = []
|
|
for item in procurements:
|
|
num = int(item[0].product_qty)
|
|
|
|
product = self.env['product.product'].search(
|
|
[("id", '=', item[0].product_id.id)])
|
|
product_tmpl = self.env['product.template'].search(
|
|
["&", ("id", '=', product.product_tmpl_id.id), ('single_manufacturing', "!=", False)])
|
|
if product_tmpl:
|
|
if num > 1:
|
|
for no in range(1, num + 1):
|
|
Procurement = namedtuple('Procurement', ['product_id', 'product_qty',
|
|
'product_uom', 'location_id', 'name', 'origin',
|
|
'company_id',
|
|
'values'])
|
|
s = Procurement(product_id=item[0].product_id, product_qty=1.0, product_uom=item[0].product_uom,
|
|
location_id=item[0].location_id,
|
|
name=item[0].name,
|
|
origin=item[0].origin,
|
|
company_id=item[0].company_id,
|
|
values=item[0].values,
|
|
)
|
|
item1 = list(item)
|
|
item1[0] = s
|
|
|
|
list2.append(tuple(item1))
|
|
else:
|
|
list2.append(item)
|
|
else:
|
|
list2.append(item)
|
|
|
|
for procurement, rule in list2:
|
|
procure_method = rule.procure_method
|
|
if rule.procure_method == 'mts_else_mto':
|
|
qty_needed = procurement.product_uom._compute_quantity(procurement.product_qty,
|
|
procurement.product_id.uom_id)
|
|
if float_compare(qty_needed, 0, precision_rounding=procurement.product_id.uom_id.rounding) <= 0:
|
|
procure_method = 'make_to_order'
|
|
for move in procurement.values.get('group_id', self.env['procurement.group']).stock_move_ids:
|
|
if move.rule_id == rule and float_compare(move.product_uom_qty, 0,
|
|
precision_rounding=move.product_uom.rounding) > 0:
|
|
procure_method = move.procure_method
|
|
break
|
|
forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed
|
|
elif float_compare(qty_needed, forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id],
|
|
precision_rounding=procurement.product_id.uom_id.rounding) > 0:
|
|
procure_method = 'make_to_order'
|
|
else:
|
|
forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed
|
|
procure_method = 'make_to_stock'
|
|
|
|
move_values = rule._get_stock_move_values(*procurement)
|
|
move_values['procure_method'] = procure_method
|
|
moves_values_by_company[procurement.company_id.id].append(move_values)
|
|
|
|
for company_id, moves_values in moves_values_by_company.items():
|
|
# create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
|
|
moves = self.env['stock.move'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(moves_values)
|
|
# Since action_confirm launch following procurement_group we should activate it.
|
|
moves._action_confirm()
|
|
|
|
return True
|
|
|
|
@api.model
|
|
def _run_manufacture(self, procurements):
|
|
productions_values_by_company = defaultdict(list)
|
|
errors = []
|
|
for procurement, rule in procurements:
|
|
if float_compare(procurement.product_qty, 0, precision_rounding=procurement.product_uom.rounding) <= 0:
|
|
# If procurement contains negative quantity, don't create a MO that would be for a negative value.
|
|
continue
|
|
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.filtered(lambda p: (not p.orderpoint_id and p.move_raw_ids) or \
|
|
(
|
|
p.move_dest_ids.procure_method != 'make_to_order' and not p.move_raw_ids and not p.workorder_ids)).action_confirm()
|
|
|
|
for production in productions:
|
|
'''
|
|
创建制造订单时生成序列号
|
|
'''
|
|
production.action_generate_serial()
|
|
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
|
|
|
|
|
|
class ProductionLot(models.Model):
|
|
_inherit = 'stock.lot'
|
|
|
|
@api.model
|
|
def generate_lot_names1(self, display_name, first_lot, count):
|
|
"""Generate `lot_names` from a string."""
|
|
if first_lot.__contains__(display_name):
|
|
first_lot = first_lot[(len(display_name) + 1):]
|
|
|
|
# We look if the first lot contains at least one digit.
|
|
caught_initial_number = regex_findall(r"\d+", first_lot)
|
|
if not caught_initial_number:
|
|
return self.generate_lot_names1(display_name, first_lot + "0", count)
|
|
# We base the series on the last number found in the base lot.
|
|
initial_number = caught_initial_number[-1]
|
|
padding = len(initial_number)
|
|
# We split the lot name to get the prefix and suffix.
|
|
splitted = regex_split(initial_number, first_lot)
|
|
# initial_number could appear several times, e.g. BAV023B00001S00001
|
|
prefix = initial_number.join(splitted[:-1])
|
|
suffix = splitted[-1]
|
|
initial_number = int(initial_number)
|
|
|
|
lot_names = []
|
|
for i in range(0, count):
|
|
lot_names.append('%s-%s%s%s' % (
|
|
display_name,
|
|
prefix,
|
|
str(initial_number + i).zfill(padding),
|
|
suffix
|
|
))
|
|
return lot_names
|
|
|
|
@api.model
|
|
def _get_next_serial(self, company, product):
|
|
"""Return the next serial number to be attributed to the product."""
|
|
if product.tracking == "serial":
|
|
last_serial = self.env['stock.lot'].search(
|
|
[('company_id', '=', company.id), ('product_id', '=', product.id)],
|
|
limit=1, order='id DESC')
|
|
if last_serial:
|
|
return self.env['stock.lot'].generate_lot_names1(product.name, last_serial.name, 2)[
|
|
1]
|
|
return "%s-%03d" % (product.name, 1)
|
|
|
|
|
|
class ResStockPicking(models.Model):
|
|
_inherit = 'stock.picking'
|
|
|
|
workorder_in_id = fields.Many2one('mrp.workorder')
|
|
workorder_out_id = fields.Many2one('mrp.workorder')
|
|
|
|
|
|
class ResPurchaseOrder(models.Model):
|
|
_inherit = 'purchase.order'
|
|
|
|
# 外协出库、入库单
|
|
def _prepare_picking_outcontract(self, sequence_code):
|
|
# if not self.group_id:
|
|
# self.group_id = self.group_id.create({
|
|
# 'name': self.name,
|
|
# 'partner_id': self.partner_id.id
|
|
# })
|
|
# if not self.partner_id.property_stock_supplier.id:
|
|
# raise UserError(_("You must set a Vendor Location for this partner %s", self.partner_id.name))
|
|
|
|
picking_type_id_oc = self.env['stock.picking.type'].search([('sequence_code', '=', sequence_code)])
|
|
return {
|
|
'picking_type_id': picking_type_id_oc.id if picking_type_id_oc else self.picking_type_id.id,
|
|
'partner_id': self.partner_id.id,
|
|
'user_id': False,
|
|
'date': self.date_order,
|
|
'origin': self.name,
|
|
'location_dest_id': self._get_destination_location(),
|
|
'location_id': self.partner_id.property_stock_supplier.id,
|
|
'company_id': self.company_id.id,
|
|
}
|
|
|
|
def _create_picking_outcontract(self):
|
|
StockPicking = self.env['stock.picking']
|
|
for order in self.filtered(lambda po: po.state in ('purchase', 'done')):
|
|
if any(product.type in ['product', 'consu'] for product in order.order_line.product_id):
|
|
order = order.with_company(order.company_id)
|
|
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
|
if not pickings:
|
|
res = order._prepare_picking_outcontract()
|
|
picking = StockPicking.with_user(SUPERUSER_ID).create(res)
|
|
pickings = picking
|
|
else:
|
|
picking = pickings[0]
|
|
moves = order.order_line._create_stock_moves(picking)
|
|
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
|
|
seq = 0
|
|
for move in sorted(moves, key=lambda move: move.date):
|
|
seq += 5
|
|
move.sequence = seq
|
|
moves._action_assign()
|
|
# Get following pickings (created by push rules) to confirm them as well.
|
|
forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves)
|
|
(pickings | forward_pickings).action_confirm()
|
|
picking.message_post_with_view('mail.message_origin_link',
|
|
values={'self': picking, 'origin': order},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
return True
|
|
|
|
|
|
class ResPurchaseOrderLine(models.Model):
|
|
_inherit = 'purchase.order.line'
|
|
|
|
def _create_stock_moves(self, picking):
|
|
values = []
|
|
for line in self.filtered(lambda l: not l.display_type):
|
|
for val in line._prepare_stock_moves(picking):
|
|
# val['production_id'] = 10
|
|
val['is_subcontract'] = True
|
|
values.append(val)
|
|
line.move_dest_ids.created_purchase_line_id = False
|
|
|
|
return self.env['stock.move'].create(values)
|