diff --git a/mrp_workorder/models/mrp_production.py b/mrp_workorder/models/mrp_production.py index d40718ed..91510baf 100644 --- a/mrp_workorder/models/mrp_production.py +++ b/mrp_workorder/models/mrp_production.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import fields, models, api class MrpProduction(models.Model): @@ -12,7 +12,8 @@ class MrpProduction(models.Model): check_ids = fields.One2many('quality.check', 'production_id', string="Checks") def _split_productions(self, amounts=False, cancel_remaining_qty=False, set_consumed_qty=False): - productions = super()._split_productions(amounts=amounts, cancel_remaining_qty=cancel_remaining_qty, set_consumed_qty=set_consumed_qty) + productions = super()._split_productions(amounts=amounts, cancel_remaining_qty=cancel_remaining_qty, + set_consumed_qty=set_consumed_qty) backorders = productions[1:] if not backorders: return productions @@ -20,3 +21,4 @@ class MrpProduction(models.Model): if wo.current_quality_check_id.component_id: wo.current_quality_check_id._update_component_quantity() return productions + diff --git a/mrp_workorder/views/mrp_production_views.xml b/mrp_workorder/views/mrp_production_views.xml index a033d501..66f92243 100644 --- a/mrp_workorder/views/mrp_production_views.xml +++ b/mrp_workorder/views/mrp_production_views.xml @@ -1,41 +1,44 @@ - - - mrp.production.tree.inherit.planning - mrp.production - - - - - - - - - - - - - - + + + mrp.production.tree.inherit.planning + mrp.production + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + mrp.production.search.view.inherit.planning @@ -43,7 +46,9 @@ - [('is_planned', '=', True), ('date_planned_start', '!=', False), ('date_planned_finished', '!=', False)] + [('is_planned', '=', True), ('date_planned_start', '!=', False), + ('date_planned_finished', '!=', False)] + @@ -51,30 +56,33 @@ Unplan orders - + list code records.button_unplan() - [('bom_id', '!=', False), ('bom_id.operation_ids.workcenter_id', '=', active_id), ('date_planned_start', '!=', False), ('date_planned_finished', '!=', False)] + [('bom_id', '!=', False), ('bom_id.operation_ids.workcenter_id', '=', active_id), + ('date_planned_start', '!=', False), ('date_planned_finished', '!=', False)] + + + name="Work Orders" + sequence="2" + parent="mrp.mrp_planning_menu_root" + groups="mrp.group_mrp_routings"/> + name="Planning by Production" + sequence="1" + action="mrp.action_mrp_workorder_production" + parent="mrp_workorder_menu_planning"/> + name="Planning by Workcenter" + sequence="2" + action="mrp_workorder.action_mrp_workorder_dependencies_workcenter" + parent="mrp_workorder_menu_planning"/> diff --git a/sf_base/views/common_view.xml b/sf_base/views/common_view.xml index c529354f..e49e25ba 100644 --- a/sf_base/views/common_view.xml +++ b/sf_base/views/common_view.xml @@ -12,7 +12,6 @@ - sf.production.process.parameter @@ -26,19 +25,19 @@ - - + + - - - + + + - + diff --git a/sf_base/views/tool_views.xml b/sf_base/views/tool_views.xml index 87dbd94c..b90d1a5c 100644 --- a/sf_base/views/tool_views.xml +++ b/sf_base/views/tool_views.xml @@ -80,10 +80,10 @@ sf.cutter.function.tree sf.functional.cutting.tool.model - - - - + + + + diff --git a/sf_bf_connect/models/process_status.py b/sf_bf_connect/models/process_status.py index f4c4d859..111254c7 100644 --- a/sf_bf_connect/models/process_status.py +++ b/sf_bf_connect/models/process_status.py @@ -53,7 +53,7 @@ class StatusChange(models.Model): if not ret.get('error'): logging.info('接口已经执行=============') else: - logging.error('工厂加工同步订单状态失败 {}'.format(ret.text)) + logging.error('工厂加工同步订单状态失败 {}'.format(ret)) raise UserError('工厂加工同步订单状态失败') except UserError as e: logging.error('工厂加工同步订单状态失败 {}'.format(e)) diff --git a/sf_maintenance/models/sf_equipment_maintenance_standards.py b/sf_maintenance/models/sf_equipment_maintenance_standards.py index 92d075e1..0cd3c81c 100644 --- a/sf_maintenance/models/sf_equipment_maintenance_standards.py +++ b/sf_maintenance/models/sf_equipment_maintenance_standards.py @@ -24,6 +24,7 @@ class SfEquipmentSaintenanceStandards(models.Model): remark = fields.Char('备注') maintenance_type = fields.Selection([('保养', '保养'), ("检修", "检修")], string='类型', default='保养') name = fields.Char(string='名称') + active = fields.Boolean(default=True) @api.model_create_multi def create(self, vals_list): diff --git a/sf_maintenance/views/equipment_maintenance_standards_views.xml b/sf_maintenance/views/equipment_maintenance_standards_views.xml index 1af9bfc5..1389633b 100644 --- a/sf_maintenance/views/equipment_maintenance_standards_views.xml +++ b/sf_maintenance/views/equipment_maintenance_standards_views.xml @@ -6,15 +6,16 @@ equipment.maintenance.standards.form equipment.maintenance.standards -
+ - - - - - - + + + + + + + @@ -50,7 +51,8 @@ equipment.maintenance.standards.tree equipment.maintenance.standards - + + diff --git a/sf_manufacturing/controllers/controllers.py b/sf_manufacturing/controllers/controllers.py index c249eda2..48144622 100644 --- a/sf_manufacturing/controllers/controllers.py +++ b/sf_manufacturing/controllers/controllers.py @@ -477,7 +477,7 @@ class Manufacturing_Connect(http.Controller): logging.info('LocationChange error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/AGVToProduct', type='json', auth='public', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/AGVToProduct', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def AGVToProduct(self, **kw): """ @@ -549,7 +549,7 @@ class Manufacturing_Connect(http.Controller): logging.info('AGVToProduct error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/AGVDownProduct', type='json', auth='public', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/AGVDownProduct', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def AGVDownProduct(self, **kw): """ diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 5095556d..405baf38 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -779,7 +779,8 @@ class MrpProduction(models.Model): routing_workcenter = self.env['mrp.routing.workcenter'].sudo().search( [('name', '=', work.routing_type)]) - work.write({'date_planned_start': date_planned_start, 'date_planned_finished': date_planned_end,'duration_expected':routing_workcenter.time_cycle}) + work.write({'date_planned_start': date_planned_start, 'date_planned_finished': date_planned_end, + 'duration_expected': routing_workcenter.time_cycle}) # 修改标记已完成方法 def button_mark_done1(self): @@ -1078,27 +1079,22 @@ class MrpProduction(models.Model): productions.write({'programming_no': self.programming_no, 'is_remanufacture': True}) # productions.procurement_group_id.mrp_production_ids.move_dest_ids.write( # {'group_id': self.env['procurement.group'].search([('name', '=', sale_order.name)])}) - stock_picking = None - pc_picking = self.env['stock.picking'].search( - [('origin', '=', productions.name), ('name', 'ilike', 'WH/PC/')]) - stock_picking = pc_picking - int_picking = self.env['stock.picking'].search( - [('origin', '=', productions.name), ('name', 'ilike', 'WH/INT/')]) - stock_picking |= int_picking - for pick in stock_picking: - if pick.move_ids: - product_type_id = pick.move_ids[0].product_id.categ_id - if product_type_id.name == '坯料': - location_id = self.env['stock.location'].search([('name', '=', '坯料存货区')]) - if not location_id: - logging.info(f'没有搜索到【坯料存货区】: {location_id}') - break - if pick.picking_type_id.name == '内部调拨': - if pick.location_dest_id.product_type != product_type_id: - pick.location_dest_id = location_id.id - elif pick.picking_type_id.name == '生产发料': - if pick.location_id.product_type != product_type_id: - pick.location_id = location_id.id + stock_picking_remanufacture = self.env['stock.picking'].search([('origin', '=', productions.name)]) + for pick in stock_picking_remanufacture: + if pick.name.startswith('WH/PC/') or pick.name.startswith('WH/INT/'): + if pick.move_ids: + product_type_id = pick.move_ids[0].product_id.categ_id + if product_type_id.name == '坯料': + location_id = self.env['stock.location'].search([('name', '=', '坯料存货区')]) + if not location_id: + logging.info(f'没有搜索到【坯料存货区】: {location_id}') + break + if pick.picking_type_id.name == '内部调拨': + if pick.location_dest_id.product_type != product_type_id: + pick.location_dest_id = location_id.id + elif pick.picking_type_id.name == '生产发料': + if pick.location_id.product_type != product_type_id: + pick.location_id = location_id.id scarp_process_parameter_workorder = self.env['mrp.workorder'].search( [('surface_technics_parameters_id', '!=', False), ('production_id', '=', self.id), ('is_subcontract', '=', True)]) @@ -1111,7 +1107,6 @@ class MrpProduction(models.Model): for process_item in scarp_process_parameter_workorder: if purchase_item.product_id.categ_type == '表面工艺': if purchase_item.product_id.server_product_process_parameters_id == process_item.surface_technics_parameters_id: - print(purchase_orders.origin.find(productions.name)) if purchase_orders.origin.find(productions.name) == -1: purchase_orders.origin += ',' + productions.name if item['is_reprogramming'] is False: diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py index 03597980..b5ec7580 100644 --- a/sf_manufacturing/models/mrp_workcenter.py +++ b/sf_manufacturing/models/mrp_workcenter.py @@ -1,7 +1,9 @@ import datetime +from datetime import timedelta, time from collections import defaultdict -from odoo import fields, models +from odoo import fields, models, api from odoo.addons.resource.models.resource import Intervals +from odoo.exceptions import UserError, ValidationError class ResWorkcenter(models.Model): @@ -41,14 +43,16 @@ class ResWorkcenter(models.Model): oee_target = fields.Float( string='OEE Target', help="Overall Effective Efficiency Target in percentage", default=90, tracking=True) - oee = fields.Float(compute='_compute_oee', help='Overall Equipment Effectiveness, based on the last month', store=True) + oee = fields.Float(compute='_compute_oee', help='Overall Equipment Effectiveness, based on the last month', + store=True) time_start = fields.Float('Setup Time', tracking=True) time_stop = fields.Float('Cleanup Time', tracking=True) costs_hour = fields.Float(string='Cost per hour', help='Hourly processing cost.', default=0.0, tracking=True) equipment_status = fields.Selection( - [("正常", "正常"), ("故障停机", "故障停机"), ("计划维保", "计划维保"), ("空闲", "空闲"), ("封存(报废)", "封存(报废)")], + [("正常", "正常"), ("故障停机", "故障停机"), ("计划维保", "计划维保"), ("空闲", "空闲"), + ("封存(报废)", "封存(报废)")], string="设备状态", related='equipment_id.state') # @api.depends('equipment_id') @@ -127,6 +131,102 @@ class ResWorkcenter(models.Model): # AGV是否可配送 is_agv_scheduling = fields.Boolean(string="AGV所属区域", tracking=True) + # 生产线优化 + available_machine_number = fields.Integer(string="可用机台数量") + single_machine_capacity = fields.Float(string="单台小时产能") + production_line_hour_capacity = fields.Float(string="生产线小时产能", readonly=True, + compute='_compute_production_line_hour_capacity') + effective_working_hours_day = fields.Float(string="日有效工作时长", default=0, readonly=True, + compute='_compute_effective_working_hours_day') + default_capacity = fields.Float( + string='生产线日产能', compute='_compute_production_line_day_capacity', readonly=True) + + # 计算生产线日产能 + @api.depends('production_line_hour_capacity', 'effective_working_hours_day') + def _compute_production_line_day_capacity(self): + for record in self: + record.default_capacity = round( + record.production_line_hour_capacity * record.effective_working_hours_day, 2) + + # 计算日有效工作时长 + @api.depends('resource_calendar_id', 'resource_calendar_id.attendance_ids', + 'resource_calendar_id.attendance_ids.hour_to', 'resource_calendar_id.attendance_ids.hour_from') + def _compute_effective_working_hours_day(self): + for record in self: + attendance_ids = [p for p in record.resource_calendar_id.attendance_ids if + p.dayofweek == self.get_current_day_of_week(datetime.datetime.now())] + if attendance_ids: + for attendance_id in attendance_ids: + if attendance_id.hour_from and attendance_id.hour_to: + record.effective_working_hours_day += attendance_id.hour_to - attendance_id.hour_from + else: + record.effective_working_hours_day = 0 + + # 获取传入时间是星期几 + def get_current_day_of_week(self, datetime): + day_num = datetime.weekday() + return str(day_num) + + # 计算生产线小时产能 + @api.depends('single_machine_capacity', 'available_machine_number') + def _compute_production_line_hour_capacity(self): + for record in self: + record.production_line_hour_capacity = round( + record.single_machine_capacity * record.available_machine_number, 2) + + # 判断计划开始时间是否在配置的工作中心的工作日历内 + def deal_with_workcenter_calendar(self, start_date): + start_date = start_date + timedelta(hours=8) # 转换为北京时间 + for record in self: + attendance_ids = [p for p in record.resource_calendar_id.attendance_ids if + p.dayofweek == record.get_current_day_of_week(start_date) and self.is_between_times( + p.hour_from, p.hour_to, start_date)] + return False if not attendance_ids else True + + # 判断传入时间是否在配置的工作中心的工作日历内 + def is_between_times(self, hour_from, hour_to, start_date): + integer_part, decimal_part = self.get_integer_and_decimal_parts(hour_from) + start_time = time(integer_part, decimal_part) + integer_part, decimal_part = self.get_integer_and_decimal_parts(hour_to) + end_time = time(integer_part, decimal_part) + return start_time <= start_date.time() <= end_time + + # 获取整数部分和小数部分 + def get_integer_and_decimal_parts(self, value): + integer_part = int(value) + decimal_part = value - integer_part + return int(integer_part), int(decimal_part) + + # 处理排程是否超过日产能 + def deal_available_default_capacity(self, date_planned): + date_planned_start = date_planned.strftime('%Y-%m-%d') + date_planned_end = date_planned + timedelta(days=1) + date_planned_end = date_planned_end.strftime('%Y-%m-%d') + plan_ids = self.env['sf.production.plan'].sudo().search([('date_planned_start', '>=', date_planned_start), + ('date_planned_start', '<', + date_planned_end), ('state', '!=', 'draft')]) + if plan_ids: + sum_qty = sum([p.product_qty for p in plan_ids]) + if sum_qty >= self.default_capacity: + return False + return True + + # 处理排程是否超过小时产能 + def deal_available_single_machine_capacity(self, date_planned): + + date_planned_start = date_planned.strftime('%Y-%m-%d %H:00:00') + date_planned_end = date_planned + timedelta(hours=1) + date_planned_end = date_planned_end.strftime('%Y-%m-%d %H:00:00') + plan_ids = self.env['sf.production.plan'].sudo().search([('date_planned_start', '>=', date_planned_start), + ('date_planned_start', '<', + date_planned_end), ('state', '!=', 'draft')]) + + if plan_ids: + sum_qty = sum([p.product_qty for p in plan_ids]) + if sum_qty >= self.production_line_hour_capacity: + return False + return True + class ResWorkcenterProductivity(models.Model): _inherit = 'mrp.workcenter.productivity' diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index 28f10ab4..2a492932 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -1100,7 +1100,7 @@ class ResMrpWorkOrder(models.Model): [('barcode', 'ilike', 'VL-SPOC')]).id), ('origin', '=', self.production_id.name)]) if move_out.state != 'done': - move_out.write({'state': 'assigned'}) + move_out.write({'state': 'assigned', 'production_id': False}) self.env['stock.move.line'].create(move_out.get_move_line(self.production_id, self)) # move_out._action_assign() diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index b9483a17..9d4e3161 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -574,6 +574,7 @@ class StockPicking(models.Model): ('origin', '=', self.origin), ('picking_id', '=', self.id)]) if self.location_id == move_in.location_id and self.location_dest_id == move_in.location_dest_id: if move_out.origin == move_in.origin: + move_in.write({'production_id': False}) if move_out.picking_id.state != 'done': raise UserError( _('该入库单对应的单号为%s的出库单还未完成,不能进行验证操作!' % move_out.picking_id.name)) @@ -659,6 +660,11 @@ class ReStockMove(models.Model): return move_values def _get_new_picking_values_Res(self, item, sorted_workorders, rescode): + picking_type_id = self.mapped('picking_type_id').id + if rescode == 'WH/OCOUT/': + picking_type_id = self.env.ref('sf_manufacturing.outcontract_picking_out').id + elif rescode == 'WH/OCIN/': + picking_type_id = self.env.ref('sf_manufacturing.outcontract_picking_in').id return { 'name': self.env['stock.picking']._get_name_Res(rescode), 'origin': item.name, @@ -667,7 +673,7 @@ class ReStockMove(models.Model): '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, + 'picking_type_id': picking_type_id, 'location_id': self.mapped('location_id').id, 'location_dest_id': self.mapped('location_dest_id').id, 'state': 'confirmed', diff --git a/sf_manufacturing/views/mrp_workcenter_views.xml b/sf_manufacturing/views/mrp_workcenter_views.xml index d0b4bee6..37ff8af5 100644 --- a/sf_manufacturing/views/mrp_workcenter_views.xml +++ b/sf_manufacturing/views/mrp_workcenter_views.xml @@ -6,9 +6,9 @@ mrp.production - - - + + +
+
+

销售订单价格同步

+
+
+
+
+
+
+
+
+
+
diff --git a/sf_plan/models/custom_plan.py b/sf_plan/models/custom_plan.py index 0e56bf22..c4043d33 100644 --- a/sf_plan/models/custom_plan.py +++ b/sf_plan/models/custom_plan.py @@ -191,7 +191,7 @@ class sf_production_plan(models.Model): return num - def do_production_schedule(self): + def do_production_schedule(self, date_planned_start): """ 排程方法 """ @@ -199,6 +199,10 @@ class sf_production_plan(models.Model): if not record.production_line_id: raise ValidationError("未选择生产线") else: + + is_schedule = self.deal_processing_schedule(date_planned_start) + if not is_schedule: + raise ValidationError("排程失败") workorder_id_list = record.production_id.workorder_ids.ids if record.production_id: if record.production_id.workorder_ids: @@ -249,6 +253,26 @@ class sf_production_plan(models.Model): 'target': 'current', # 跳转的目标窗口,可以是'current'或'new' } + # 处理是否可排程 + def deal_processing_schedule(self, date_planned_start): + for record in self: + workcenter_ids = record.production_line_id.mrp_workcenter_ids + if not workcenter_ids: + raise UserError('生产线没有配置工作中心') + production_lines = workcenter_ids.filtered(lambda b: "自动生产线" in b.name) + if not production_lines: # 判断是否配置了自动生产线 + raise UserError('生产线没有配置自动生产线') + if date_planned_start < datetime.now(): # 判断计划开始时间是否小于当前时间 + raise UserError('计划开始时间不能小于当前时间') + if all(not production_line.deal_with_workcenter_calendar(date_planned_start) for production_line in + production_lines): # 判断计划开始时间是否在配置的工作中心的工作日历内 + raise UserError('当前计划开始时间不能预约排程,请在工作时间内排程') + if not production_lines.deal_available_default_capacity(date_planned_start): # 判断生产线是否可排程 + raise UserError('当前计划开始时间不能预约排程,生产线今日没有可排程的资源') + if not production_lines.deal_available_single_machine_capacity(date_planned_start): # 判断生产线是否可排程 + raise UserError('当前计划开始时间不能预约排程,生产线该时间段没有可排程的资源') + return True + def calculate_plan_time_before(self, item, workorder_id_list): """ 根据CNC工单的时间去计算之前的其他工单的开始结束时间 diff --git a/sf_plan/views/view.xml b/sf_plan/views/view.xml index d0d3c84e..10992f4a 100644 --- a/sf_plan/views/view.xml +++ b/sf_plan/views/view.xml @@ -291,6 +291,7 @@ ir.actions.act_window mrp.production tree,form + [('picking_type_id.active', '=', True)] diff --git a/sf_plan/wizard/action_plan_some.py b/sf_plan/wizard/action_plan_some.py index 706714ee..9410efd5 100644 --- a/sf_plan/wizard/action_plan_some.py +++ b/sf_plan/wizard/action_plan_some.py @@ -36,7 +36,7 @@ class Action_Plan_All_Wizard(models.TransientModel): plan_obj = self.env['sf.production.plan'].browse(plan.id) plan_obj.production_line_id = self.production_line_id.id plan.date_planned_start = self.date_planned_start - plan_obj.do_production_schedule() + plan_obj.do_production_schedule(self.date_planned_start) # plan_obj.state = 'done' print('处理计划:', plan.id, '完成') diff --git a/sf_stock/__init__.py b/sf_stock/__init__.py new file mode 100644 index 00000000..511a0ca3 --- /dev/null +++ b/sf_stock/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models \ No newline at end of file diff --git a/sf_stock/__manifest__.py b/sf_stock/__manifest__.py new file mode 100644 index 00000000..40f9113b --- /dev/null +++ b/sf_stock/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +{ + 'name': "sf_stock", + + 'summary': """ + 处理代发货业务""", + + 'description': """ + Long description of module's purpose + """, + + 'author': "My Company", + 'website': "https://www.yourcompany.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Uncategorized', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['sf_sale', 'stock'], + + # always loaded + 'data': [ + # 'security/ir.model.access.csv', + 'views/stock_picking.xml', + ], + # only loaded in demonstration mode + 'demo': [ + 'demo/demo.xml', + ], + 'installable': True, + 'application': True, +} diff --git a/sf_stock/controllers/__init__.py b/sf_stock/controllers/__init__.py new file mode 100644 index 00000000..457bae27 --- /dev/null +++ b/sf_stock/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/sf_stock/controllers/controllers.py b/sf_stock/controllers/controllers.py new file mode 100644 index 00000000..af35ee1d --- /dev/null +++ b/sf_stock/controllers/controllers.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# from odoo import http + + +# class SfStock(http.Controller): +# @http.route('/sf_stock/sf_stock', auth='public') +# def index(self, **kw): +# return "Hello, world" + +# @http.route('/sf_stock/sf_stock/objects', auth='public') +# def list(self, **kw): +# return http.request.render('sf_stock.listing', { +# 'root': '/sf_stock/sf_stock', +# 'objects': http.request.env['sf_stock.sf_stock'].search([]), +# }) + +# @http.route('/sf_stock/sf_stock/objects/', auth='public') +# def object(self, obj, **kw): +# return http.request.render('sf_stock.object', { +# 'object': obj +# }) diff --git a/sf_stock/demo/demo.xml b/sf_stock/demo/demo.xml new file mode 100644 index 00000000..726c51be --- /dev/null +++ b/sf_stock/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/sf_stock/models/__init__.py b/sf_stock/models/__init__.py new file mode 100644 index 00000000..c62a4dff --- /dev/null +++ b/sf_stock/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import stock_picking \ No newline at end of file diff --git a/sf_stock/models/stock_picking.py b/sf_stock/models/stock_picking.py new file mode 100644 index 00000000..82ce7ea5 --- /dev/null +++ b/sf_stock/models/stock_picking.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +import json +import requests +from odoo import models, fields, api + +from odoo.exceptions import UserError +import logging +from odoo.tools import date_utils + +_logger = logging.getLogger(__name__) + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + cancel_backorder_ids = fields.Boolean(default=False, string='是否取消后置单据') + + # 重写验证,下发发货到bfm + def button_validate(self): + info = super(StockPicking, self).button_validate() + if self.picking_type_code == 'outgoing': + self.send_to_bfm() + return info + + def deal_move_ids(self, send_move_ids, send_move_line_ids): + move_ids = [] # 本次发货单 + move_line_ids = [] # 本次发货单行 + if send_move_ids: + for item in send_move_ids: + val = { + 'name': item.product_id.upload_model_file.display_name, + 'quantity_done': item.quantity_done, + 'date': date_utils.json_default(item.date) if item.date else None, + 'description_picking': item.description_picking, + 'date_deadline': date_utils.json_default(item.date_deadline) if item.date_deadline else None, + 'product_uom_qty': item.product_uom_qty, + 'sequence': item.sequence, + 'price_unit': item.price_unit, + 'priority': item.priority, + 'state': item.state, + } + move_ids.append(val) + for item in send_move_line_ids: + val = { + 'qty_done': item.qty_done, + 'reserved_qty': item.reserved_qty, + 'reserved_uom_qty': item.reserved_uom_qty, + 'date': date_utils.json_default(item.date) if item.date else None, + 'description_picking': item.description_picking, + 'state': item.state, + } + move_line_ids.append(val) + return move_ids, move_line_ids + + def deal_send_backorder_id(self, backorder_ids1): + backorder_ids = [] + + if backorder_ids1: + for item in backorder_ids1: + move_ids, move_line_ids = self.deal_move_ids(item.move_ids, item.move_line_ids) + val = { + 'receiverName': item.receiverName, + 'name': item.sale_id.default_code, + 'send_no': item.name, + 'scheduled_date': date_utils.json_default(item.scheduled_date) if item.scheduled_date else None, + 'date': date_utils.json_default(item.date) if item.date else None, + 'date_deadline': date_utils.json_default(item.date_deadline) if item.date_deadline else None, + 'date_done': date_utils.json_default(item.date_done) if item.date_done else None, + 'move_ids': move_ids, + 'move_line_ids': move_line_ids, + 'state': item.state, + 'move_type': item.move_type, + } + backorder_ids.append(val) + return backorder_ids + + def send_to_bfm(self): + skip_backorder = self.env.context.get('skip_backorder') + # 下发发货到bfm + config = self.env['res.config.settings'].get_values() + move_ids, move_line_ids = self.deal_move_ids(self.move_ids, self.move_line_ids) + data = { + 'params': { + 'receiverName': self.receiverName, + 'priority': self.priority, + 'name': self.sale_id.default_code, + 'send_no': self.name, + 'scheduled_date': date_utils.json_default(self.scheduled_date) if self.scheduled_date else None, + 'date': date_utils.json_default(self.date) if self.date else None, + 'date_deadline': date_utils.json_default(self.date_deadline) if self.date_deadline else None, + 'date_done': date_utils.json_default(self.date_done) if self.date_done else None, + 'move_ids': move_ids, + 'move_line_ids': move_line_ids, + 'state': self.state, + 'backorder_id': self.deal_send_backorder_id(self.backorder_id), + 'backorder_ids': self.deal_send_backorder_id(self.backorder_ids), + 'cancel_backorder_ids': skip_backorder, + 'move_type': self.move_type, + }, + } + url1 = config['bfm_url_new'] + '/api/stock/deliver_goods' + json_str = json.dumps(data) + print('json_str', json_str) + r = requests.post(url1, json=data, data=None) + if r.status_code == 200: + result = json.loads(r.json()['result']) + if result['code'] != 200: + raise UserError(result['message'] or '工厂发货下发bfm失败') + else: + raise UserError('工厂发货下发bfm失败') diff --git a/sf_stock/security/ir.model.access.csv b/sf_stock/security/ir.model.access.csv new file mode 100644 index 00000000..af1bcbaf --- /dev/null +++ b/sf_stock/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sf_stock_sf_stock,sf_stock.sf_stock,model_sf_stock_sf_stock,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/sf_stock/views/stock_picking.xml b/sf_stock/views/stock_picking.xml new file mode 100644 index 00000000..7750ca76 --- /dev/null +++ b/sf_stock/views/stock_picking.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sf_tool_management/__manifest__.py b/sf_tool_management/__manifest__.py index a1a88fb0..97c26e40 100644 --- a/sf_tool_management/__manifest__.py +++ b/sf_tool_management/__manifest__.py @@ -10,7 +10,7 @@ """, 'category': 'sf', 'website': 'https://www.sf.jikimo.com', - 'depends': ['sf_manufacturing'], + 'depends': ['sf_manufacturing', 'sf_base'], 'data': [ 'security/group_security.xml', 'security/ir.model.access.csv', @@ -24,6 +24,11 @@ 'views/menu_view.xml', 'views/stock.xml', 'data/tool_data.xml', + 'wizard/jikimo_bom_wizard.xml', + 'views/tool_inventory.xml', + 'views/jikimo_bom.xml', + 'views/tool_views.xml', + ], 'demo': [ ], diff --git a/sf_tool_management/models/__init__.py b/sf_tool_management/models/__init__.py index 90776ca7..e4f2a622 100644 --- a/sf_tool_management/models/__init__.py +++ b/sf_tool_management/models/__init__.py @@ -8,4 +8,6 @@ from . import fixture_material_search from . import fixture_enroll from . import temporary_data_processing_methods from . import stock - +from . import jikimo_bom +from . import tool_inventory +from . import functional_cutting_tool_model \ No newline at end of file diff --git a/sf_tool_management/models/functional_cutting_tool_model.py b/sf_tool_management/models/functional_cutting_tool_model.py new file mode 100644 index 00000000..db04b762 --- /dev/null +++ b/sf_tool_management/models/functional_cutting_tool_model.py @@ -0,0 +1,6 @@ +from odoo import models, fields + + +class SyncFunctionalCuttingToolModel(models.Model): + _inherit = 'sf.functional.cutting.tool.model' + cutting_tool_type_ids = fields.Many2many('sf.cutting.tool.type', string='刀具物料类型') \ No newline at end of file diff --git a/sf_tool_management/models/functional_tool_enroll.py b/sf_tool_management/models/functional_tool_enroll.py index ba3553e1..ab391511 100644 --- a/sf_tool_management/models/functional_tool_enroll.py +++ b/sf_tool_management/models/functional_tool_enroll.py @@ -50,7 +50,7 @@ class ToolDatasync(models.Model): # self.env['sf.real.time.distribution.of.functional.tools'].sudo().sync_enroll_functional_tool_real_time_distribution_all() # logging.info("功能刀具安全库存每日同步成功") except Exception as e: - logging.info("捕获错误信息:%s" % e) + logging.info("刀具物料、刀具信息同步失败:%s" % e) raise ValidationError("数据错误导致同步失败,请联系管理员") @@ -312,7 +312,7 @@ class FunctionalToolWarning(models.Model): else: logging.info('没有注册功能刀具预警信息') except Exception as e: - logging.info("捕获错误信息:%s" % e) + logging.info("功能刀具预警同步失败:%s" % e) class StockMoveLine(models.Model): @@ -373,7 +373,7 @@ class StockMoveLine(models.Model): else: logging.info('没有注册功能刀具出入库记录信息') except Exception as e: - logging.info("捕获错误信息:%s" % e) + logging.info("出入库记录信息同步失败:%s" % e) class RealTimeDistributionFunctionalTools(models.Model): @@ -446,4 +446,4 @@ class RealTimeDistributionFunctionalTools(models.Model): else: logging.info('没有注册功能刀具出入库记录信息') except Exception as e: - logging.info("捕获错误信息:%s" % e) + logging.info("实时功能刀具同步失败:%s" % e) diff --git a/sf_tool_management/models/jikimo_bom.py b/sf_tool_management/models/jikimo_bom.py new file mode 100644 index 00000000..627a8892 --- /dev/null +++ b/sf_tool_management/models/jikimo_bom.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +from xml import etree + +from odoo import models, fields, api, Command +from odoo.exceptions import UserError +from odoo.http import request + + +class jikimo_bom(models.Model): + _name = 'jikimo.bom' + _description = '功能刀具物料清单' + tool_inventory_id = fields.Many2one('sf.tool.inventory', '功能刀具清单') + tool_name = fields.Char(related="tool_inventory_id.name", string='功能刀具名称') + functional_cutting_tool_model_id = fields.Many2one(related='tool_inventory_id.functional_cutting_tool_model_id', + string='功能刀具类型') + tool_groups_id = fields.Many2one(related='tool_inventory_id.tool_groups_id', string='刀具组') + tool_length = fields.Float(related='tool_inventory_id.tool_length', string='刀具总长(mm)') + diameter = fields.Float(related='tool_inventory_id.diameter', string='直径(mm)') + angle = fields.Float(related='tool_inventory_id.angle', string='R角(mm)') + extension = fields.Float(related='tool_inventory_id.extension', string='伸出长度(mm)') + product_ids = fields.Many2many('product.product', string='产品') + knife_handle_model = fields.Selection(related='tool_inventory_id.knife_handle_model', string='使用刀柄型号') + options = fields.Char('产品清单') + + def name_get(self): + result = [] + for bom in self: + result.append((bom.id, '功能刀具物料清单')) + return result + + def bom_product_domains(self, assembly_options): + self.options = assembly_options + cutting_tool_materials = self.env['sf.cutting.tool.material'].search( + [('name', 'in', assembly_options.split('+'))]) + domains = [] + for index, option in enumerate(cutting_tool_materials): + domain = ['&', ('cutting_tool_material_id', '=', option.id), + ("cutting_tool_type_id", "in", + self.tool_inventory_id.functional_cutting_tool_model_id.cutting_tool_type_ids.ids)] + if option.name == '刀柄': + domain = ['&'] + domain + [ + ("cutting_tool_taper_shank_model", "=", self.tool_inventory_id.knife_handle_model)] + + if option.name == '整体式刀具': + domain = ['&'] + domain + [ + '|', + # 刀具直径 + ('cutting_tool_blade_diameter', '=', self.tool_inventory_id.diameter), + + # r角 + ('cutting_tool_blade_tip_working_size', '=', self.tool_inventory_id.angle)] + if option.name == '刀杆': + domain = ['&'] + domain + [ + ("cutting_tool_cutter_arbor_diameter", "=", self.tool_inventory_id.diameter)] + if option.name == '刀片': + domain = ['&'] + domain + [ + ("cutting_tool_blade_tip_circular_arc_radius", "=", self.tool_inventory_id.angle)] + if option.name == '刀盘': + domain = ['&'] + domain + [ + ("cutting_tool_cutter_head_diameter", "=", self.tool_inventory_id.diameter)] + domains = domains + domain + if index != 0: + domains = ['|'] + domains + # wqwqwe = self.env['product.product'].search(ddd) + # product = self.env['product.product'].search(domain) + # if product: + # products = products + product + return domains + + def generate_bill_materials(self, assembly_options): + domains = self.bom_product_domains(assembly_options) + products = self.env['product.product'].search(domains) + if products: + self.product_ids = [Command.set(products.ids)] + # if option.name == '刀盘': + # hilt = self.env['product.product'].search( + # [('cutting_tool_blade_diameter', '=', self.tool_inventory_id.diameter), + # ('cutting_tool_material_id', '=', option.id)]) + # self.product_ids = [Command.set(hilt.ids)]k + + +class jikimo_bom_line(models.Model): + _name = 'jikimo.bom.line' + _description = 'jikimo.bom.line' + + name = fields.Char() + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + def search(self, args, offset=0, limit=None, order=None, count=False): + # 你可以在这里修改 `args` 以调整搜索条件 + # 例如,添加额外的搜索条件 + if self.env.context.get('jikimo_bom_product'): + bom_id = self.env['jikimo.bom'].browse(request.session.get('jikimo_bom_product').get('bom_id')) + + if not bom_id.options: + raise UserError('请先选择组装方式') + domains = bom_id.bom_product_domains(bom_id.options) + args = args + domains + return super(ProductProduct, self).search(args, offset=offset, limit=limit, order=order, count=count) diff --git a/sf_tool_management/models/tool_inventory.py b/sf_tool_management/models/tool_inventory.py new file mode 100644 index 00000000..db15834a --- /dev/null +++ b/sf_tool_management/models/tool_inventory.py @@ -0,0 +1,34 @@ +from odoo import models, fields +from odoo.http import request + + +class ToolInventory(models.Model): + _inherit = 'sf.tool.inventory' + _description = '功能刀具清单' + knife_handle_model = fields.Selection([('BT30', 'BT30'), ('BT40', 'BT40'), ('BT50', 'BT50'), ('GSK30', 'GSK30'), ('GSK40', 'GSK40'), ('GSK50', 'GSK50')], string='使用刀柄型号') + jikimo_bom_ids = fields.One2many('jikimo.bom','tool_inventory_id', 'bom单') + def bom_mainfest(self): + + jikimo_bom_ids = self.mapped('jikimo_bom_ids') + if not jikimo_bom_ids: + self._bom_mainfest() + return self.bom_mainfest() + request.session['jikimo_bom_product'] = {'bom_id': int(self.jikimo_bom_ids)} + # context = dict(self.env.context) + # context.update({'jikimo_bom_product': self.jikimo_bom_ids.options}) + # if self.functional_cutting_tool_model_id.cutting_tool_type_ids: + # context.update({'jikimo_bom_product_cutting_tool_type': self.functional_cutting_tool_model_id.cutting_tool_type_ids.ids}) + return { + 'type': 'ir.actions.act_window', + 'name': '刀具组装清单', + 'res_model': 'jikimo.bom', + 'view_mode': 'form', + 'view_id': self.env.ref('sf_tool_management.view_jikimo_bom_form').id, + 'res_id': int(self.jikimo_bom_ids), + 'target': 'current', # Use 'new' to open in a new window/tab + # {'jikimo_bom_product': self.jikimo_bom_ids.options} + } + + # 创建bom单 + def _bom_mainfest(self): + self.env['jikimo.bom'].create({'tool_inventory_id':self.id}) \ No newline at end of file diff --git a/sf_tool_management/security/ir.model.access.csv b/sf_tool_management/security/ir.model.access.csv index 26b45aeb..8c188464 100644 --- a/sf_tool_management/security/ir.model.access.csv +++ b/sf_tool_management/security/ir.model.access.csv @@ -38,3 +38,6 @@ access_sf_fixture_material_search_group_plan_dispatch,sf.fixture.material.search access_sf_functional_tool_dismantle,sf.functional.tool.dismantle,model_sf_functional_tool_dismantle,base.group_user,1,1,1,0 access_sf_functional_tool_dismantle_group_sf_tool_user,sf.functional.tool.dismantle_group_sf_tool_user,model_sf_functional_tool_dismantle,sf_base.group_sf_tool_user,1,1,1,0 access_sf_functional_tool_dismantle_group_plan_dispatch,sf.functional.tool.dismantle_group_plan_dispatch,model_sf_functional_tool_dismantle,sf_base.group_plan_dispatch,1,0,0,0 + +access_jikimo_bom,jikimo.bom,model_jikimo_bom,base.group_user,1,1,1,1 +access_jikimo_bom_wizard,jikimo.bom.wizard,model_jikimo_bom_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/sf_tool_management/views/jikimo_bom.xml b/sf_tool_management/views/jikimo_bom.xml new file mode 100644 index 00000000..90e3232e --- /dev/null +++ b/sf_tool_management/views/jikimo_bom.xml @@ -0,0 +1,52 @@ + + + + bom物料清单 + jikimo.bom + tree,form + + + jikimo.bom.form + jikimo.bom + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
\ No newline at end of file diff --git a/sf_tool_management/views/tool_inventory.xml b/sf_tool_management/views/tool_inventory.xml new file mode 100644 index 00000000..f7fbc6b5 --- /dev/null +++ b/sf_tool_management/views/tool_inventory.xml @@ -0,0 +1,15 @@ + + + + sf.tool.inventory.inherit.tree + sf.tool.inventory + + + + +