# -*- coding: utf-8 -*- import base64 import logging from re import findall as regex_findall from datetime import datetime from re import split as regex_split import requests from odoo import SUPERUSER_ID, _, api, fields, models from odoo.tools import float_compare from collections import defaultdict, namedtuple from odoo.addons.stock.models.stock_rule import ProcurementException from odoo.addons.sf_base.commons.common import Common 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) ''' 创建生产计划 ''' # 工单耗时 workorder_duration = 0 for workorder in production.workorder_ids: workorder_duration += workorder.duration_expected sale_order = self.env['sale.order'].sudo().search([('name', '=', production.origin)]) if sale_order: bb = sale_order.deadline_of_delivery productions = self.env['sf.production.plan'].with_user(SUPERUSER_ID).sudo().with_company(company_id). \ create({ 'name': production.name, 'order_deadline': sale_order.deadline_of_delivery, 'production_id': production.id, 'date_planned_start': production.date_planned_start, 'origin': production.origin, 'product_qty': production.product_qty, 'product_id': production.product_id.id, 'state': 'draft', }) 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] now = datetime.now().strftime("%Y-%m-%d") # formatted_date = now.strftime("%Y-%m-%d") if product.cutting_tool_model_id: return "%s-%s-%03d" % (product.cutting_tool_model_id.code, now, 1) return "%s-%03d" % (product.name, 1) class StockPicking(models.Model): _inherit = 'stock.picking' workorder_in_id = fields.One2many('mrp.workorder', 'picking_in_id') workorder_out_id = fields.One2many('mrp.workorder', 'picking_out_id') # 设置外协出入单的名称 def _get_name_Res(self, rescode): last_picking = self.sudo().search([('name', 'like', rescode)], order='create_date DESC', limit=1) logging.info('编号:' + last_picking.name) if not last_picking: num = "%04d" % 1 else: m = int(last_picking.name[-3:]) + 1 num = "%04d" % m return '%s%s' % (rescode, num) def button_validate(self): # 出库单验证 if self.workorder_out_id: workorder_in = self.workorder_out_id.filtered(lambda p: p.state == 'progress' and p.is_subcontract is True) if workorder_in: picking_in = self.sudo().search([('id', '=', workorder_in.picking_in_id.id)]) if picking_in: picking_in.write({'state': 'assigned'}) else: workorder_subcontract = self.workorder_out_id.filtered( lambda p: p.state == 'pending' and p.is_subcontract is True) if workorder_subcontract: raise UserError( _('该出库单里源单据内的单号为%s的工单还未开始,不能进行验证操作!' % workorder_subcontract[ 0].name)) # 入库单验证 if self.workorder_in_id: workorder_out = self.workorder_in_id.filtered(lambda p: p.state == 'progress' and p.is_subcontract is True) if workorder_out: picking_out = self.sudo().search([('id', '=', workorder_out.picking_out_id.id)]) if picking_out.state != 'done': raise UserError( _('该入库单对应的单号为%s的出库单还未完成,不能进行验证操作!' % picking_out.name)) res = super().button_validate() # 采购单验证(夹具) # for item in self.move_ids_without_package: # if item.quantity_done > 0: # if item.product_id.categ_type == '夹具': # item._register_fixture() # elif item.product_id.categ_type == '刀具': # item._register_cutting_tool() return res # 创建 外协出库入单 def create_outcontract_picking(self, sorted_workorders_arr, item): m = 0 for sorted_workorders in sorted_workorders_arr: if m == 0: outcontract_stock_move = self.env['stock.move'].search( [('workorder_id', '=', sorted_workorders.id), ('production_id', '=', item.id)]) if not outcontract_stock_move: location_id = self.env.ref( 'sf_manufacturing.stock_location_locations_virtual_outcontract').id, location_dest_id = self.env['stock.location'].search( [('barcode', '=', 'WH-PREPRODUCTION')]).id, outcontract_picking_type_in = self.env.ref( 'sf_manufacturing.outcontract_picking_in').id, outcontract_picking_type_out = self.env.ref( 'sf_manufacturing.outcontract_picking_out').id, moves_in = self.env['stock.move'].sudo().create( item._get_stock_move_values_Res(item, location_id, location_dest_id, outcontract_picking_type_in)) moves_out = self.env['stock.move'].sudo().create( item._get_stock_move_values_Res(item, location_dest_id, location_id, outcontract_picking_type_out)) new_picking = True picking_in = self.create( moves_in._get_new_picking_values_Res(item, sorted_workorders, 'WH/OCIN/')) picking_out = self.create( moves_out._get_new_picking_values_Res(item, sorted_workorders, 'WH/OCOUT/')) moves_in.write({'picking_id': picking_in.id, 'state': 'confirmed'}) moves_out.write({'picking_id': picking_out.id, 'state': 'confirmed'}) moves_in._assign_picking_post_process(new=new_picking) moves_out._assign_picking_post_process(new=new_picking) m += 1 sorted_workorders.write({'picking_in_id': picking_in.id, 'picking_out_id': picking_out.id}) class ReStockMove(models.Model): _inherit = 'stock.move' materiel_length = fields.Float(string='物料长度', digits=(16, 4)) materiel_width = fields.Float(string='物料宽度', digits=(16, 4)) materiel_height = fields.Float(string='物料高度', digits=(16, 4)) def _get_new_picking_values_Res(self, item, sorted_workorders, rescode): logging.info('new_picking-rescode: %s' % rescode) return { 'name': self.env['stock.picking']._get_name_Res(rescode), 'origin': item.name, 'company_id': self.mapped('company_id').id, 'user_id': False, 'move_type': self.mapped('group_id').move_type or 'direct', 'partner_id': sorted_workorders.supplier_id.id, 'picking_type_id': self.mapped('picking_type_id').id, 'location_id': self.mapped('location_id').id, 'location_dest_id': self.mapped('location_dest_id').id, 'state': 'confirmed', } # 将采购到的夹具注册到Cloud def _register_fixture(self): create_url = '/api/factory_fixture_material/create' config = self.env['res.config.settings'].get_values() headers = Common.get_headers(self, config['token'], config['sf_secret_key']) strurl = config['sf_url'] + create_url for item in self: val = { 'token': config['token'], 'name': item.product_id.name, 'brand_code': self.env['sf.machine.brand'].search([('id', '=', item.product_id.brand_id.id)]).code, 'fixture_material_code': self.env['sf.fixture.material'].search( [('id', '=', item.product_id.fixture_material_id.id)]).code, 'fixture_multi_mounting_type_code': self.env['sf.multi_mounting.type'].search( [('id', '=', item.product_id.fixture_multi_mounting_type_id.id)]).code, 'fixture_materials_type_code': self.env['sf.materials.model'].search( [('id', '=', item.product_id.materials_type_id.id)]).materials_no, 'fixture_clamping_way': item.product_id.fixture_clamping_way, 'fixture_port_type': item.product_id.fixture_port_type, 'fixture_length': item.product_id.tool_length, 'fixture_width': item.product_id.tool_width, 'fixture_height': item.product_id.tool_height, 'fixture_weight': item.product_id.tool_weight, 'fixture_amount': int(item.quantity_done), 'fixture_model_file': '' if not item.product_id.fixture_model_file else base64.b64encode( item.product_id.fixture_model_file).decode( 'utf-8'), 'fixture_clamp_workpiece_length_max': item.product_id.fixture_clamp_workpiece_length_max, 'fixture_clamp_workpiece_width_max': item.product_id.fixture_clamp_workpiece_width_max, 'fixture_clamp_workpiece_height_max': item.product_id.fixture_clamp_workpiece_height_max, 'fixture_clamp_workpiece_diameter_max': item.product_id.fixture_clamp_workpiece_diameter_max, 'fixture_maximum_carrying_weight': item.product_id.fixture_maximum_carrying_weight, 'fixture_maximum_clamping_force': item.product_id.fixture_maximum_clamping_force, 'fixture_driving_way': '' if not item.product_id.fixture_driving_way else item.product_id.fixture_driving_way, 'fixture_apply_machine_tool_type_codes': self.env[ 'product.template']._json_apply_machine_tool_type_item_code(item), 'fixture_through_hole_size': item.product_id.fixture_through_hole_size, 'fixture_screw_size': item.product_id.fixture_screw_size, } try: if item.product_id.industry_code: val['industry_code'] = item.product_id.industry_code ret = requests.post(strurl, json={}, data=val, headers=headers) ret = ret.json() if ret['status'] == 200: if not item.product_id.industry_code: item.product_id.write({'register_state': '已注册', 'industry_code': ret['industry_code']}) else: item.product_id.write({'register_state': '已注册'}) else: item.product_id.write({'register_state': '注册失败'}) except Exception as e: raise UserError("注册夹具到云端失败,请联系管理员!") # 将采购到的刀具注册到Cloud def _register_cutting_tool(self): create_url = '/api/factory_cutting_tool_material/create' config = self.env['res.config.settings'].get_values() headers = Common.get_headers(self, config['token'], config['sf_secret_key']) strurl = config['sf_url'] + create_url for item in self: val = { 'token': config['token'], 'name': item.product_id.name, 'brand_code': self.env['sf.machine.brand'].search([('id', '=', item.product_id.brand_id.id)]).code, 'cutting_tool_material_code': self.env['sf.cutting.tool.material'].search( [('id', '=', item.product_id.cutting_tool_material_id.id)]).code, 'cutting_tool_type_code': self.env['sf.cutting.tool.type'].search( [('id', '=', item.product_id.cutting_tool_type_id.id)]).code, 'material_model_code': self.env['sf.materials.model'].search( [('id', '=', item.product_id.materials_type_id.id)]).materials_no, 'tool_length': item.product_id.tool_length, 'tool_width': item.product_id.tool_width, 'tool_height': item.product_id.tool_height, 'tool_thickness': item.product_id.tool_thickness, 'tool_weight': item.product_id.tool_weight, 'tool_hardness': item.product_id.tool_hardness, 'coating_material': item.product_id.coating_material, 'amount': int(item.quantity_done), # 'model_file': '' if not item.product_id.fixture_model_file else base64.b64encode( # item.product_id.fixture_model_file).decode( # 'utf-8'), 'total_length': item.product_id.cutting_tool_total_length, 'shank_length': item.product_id.cutting_tool_shank_length, 'blade_length': item.product_id.cutting_tool_blade_length, 'neck_length': item.product_id.cutting_tool_neck_length, 'neck_diameter': item.product_id.cutting_tool_neck_diameter, 'shank_diameter': item.product_id.cutting_tool_shank_diameter, 'blade_tip_diameter': item.product_id.cutting_tool_blade_tip_diameter, 'blade_tip_taper': item.product_id.cutting_tool_blade_tip_taper, 'blade_helix_angle': item.product_id.cutting_tool_blade_helix_angle, 'blade_type': item.product_id.cutting_tool_blade_type, 'coarse_medium_fine': '' if item.product_id.cutting_tool_coarse_medium_fine is False else item.product_id.cutting_tool_coarse_medium_fine, 'run_out_accuracy_max': item.product_id.cutting_tool_run_out_accuracy_max, 'run_out_accuracy_min': item.product_id.cutting_tool_run_out_accuracy_min, 'head_diameter': item.product_id.cutting_tool_head_diameter, 'diameter': item.product_id.cutting_tool_diameter, 'blade_number': '' if item.product_id.cutting_tool_blade_number is False else item.product_id.cutting_tool_blade_number, 'front_angle': item.product_id.cutting_tool_front_angle, 'rear_angle': item.product_id.cutting_tool_rear_angle, 'main_included_angle': item.product_id.cutting_tool_main_included_angle, 'chuck_codes': self.env['product.template']._json_chuck_item_code(item), 'cutter_bar_codes': self.env['product.template']._json_cutter_bar_item_code(item), 'cutter_pad_codes': self.env['product.template']._json_cutter_pad_item_code(item), 'blade_codes': self.env['product.template']._json_blade_item_code(item), 'handle_codes': self.env['product.template']._json_handle_item_code(item), 'nut': item.product_id.cutting_tool_nut, 'top_angle': item.product_id.cutting_tool_top_angle, 'jump_accuracy': item.product_id.cutting_tool_jump_accuracy, 'working_hardness': item.product_id.cutting_tool_working_hardness, 'blade_diameter': item.product_id.cutting_tool_blade_diameter, 'wrench': item.product_id.cutting_tool_wrench, 'accuracy_level': item.product_id.cutting_tool_accuracy_level, 'clamping_way': item.product_id.cutting_tool_clamping_way, 'clamping_length': item.product_id.cutting_tool_clamping_length, 'clamping_tolerance': item.product_id.cutting_tool_clamping_tolerance, 'diameter_max': item.product_id.cutting_tool_diameter_max, 'clamping_diameter_min': item.product_id.cutting_tool_clamping_diameter_min, 'clamping_diameter_max': item.product_id.cutting_tool_clamping_diameter_max, 'detection_accuracy_max': item.product_id.cutting_tool_detection_accuracy_max, 'detection_accuracy_min': item.product_id.cutting_tool_detection_accuracy_min, 'is_rough_finish': item.product_id.cutting_tool_is_rough_finish, 'is_finish': item.product_id.cutting_tool_is_finish, 'is_drill_hole': item.product_id.cutting_tool_is_drill_hole, 'is_safety_lock': item.product_id.cutting_tool_is_safety_lock, 'is_high_speed_cutting': item.product_id.cutting_tool_is_high_speed_cutting, 'dynamic_balance_class': item.product_id.cutting_tool_dynamic_balance_class, 'change_time': item.product_id.cutting_tool_change_time, 'standard_speed': item.product_id.cutting_tool_standard_speed, 'speed_max': item.product_id.cutting_tool_speed_max, 'cooling_type': item.product_id.cutting_tool_cooling_type, 'body_accuracy': item.product_id.cutting_tool_body_accuracy, 'apply_lock_nut_model': item.product_id.apply_lock_nut_model, 'apply_lock_wrench_model': item.product_id.apply_lock_wrench_model, 'tool_taper': item.product_id.cutting_tool_taper, 'flange_length': item.product_id.cutting_tool_flange_length, 'flange_diameter': item.product_id.cutting_tool_flange_diameter, 'outer_diameter': item.product_id.cutting_tool_outer_diameter, 'inner_diameter': item.product_id.cutting_tool_inner_diameter, 'cooling_suit_type_ids': item.product_id.cooling_suit_type_ids, 'er_size_model': item.product_id.cutting_tool_er_size_model, 'image': '' if not item.product_id.image_1920 else base64.b64encode(item.product_id.image_1920).decode('utf-8'), } try: if item.product_id.industry_code: val['industry_code'] = item.product_id.industry_code ret = requests.post(strurl, json={}, data=val, headers=headers) ret = ret.json() if ret['status'] == 200: if not item.product_id.industry_code: item.product_id.write({'register_state': '已注册', 'industry_code': ret['industry_code']}) else: item.product_id.write({'register_state': '已注册'}) else: item.product_id.write({'register_state': '注册失败'}) except Exception as e: raise UserError("注册刀具到云端失败,请联系管理员!") class ReStockQuant(models.Model): _inherit = 'stock.quant' def action_apply_inventory(self): inventory_diff_quantity = self.inventory_diff_quantity super(ReStockQuant, self).action_apply_inventory() if inventory_diff_quantity >= 1: stock = self.env['stock.move'].search([('product_id', '=', self.product_id.id), ('is_inventory', '=', True), ('reference', '=', '更新的产品数量'), ('state', '=', 'done')], limit=1, order='id desc') if self.product_id.categ_type == '夹具': stock._register_fixture() elif self.product_id.categ_type == '刀具': stock._register_cutting_tool() return True