Compare commits

..

6 Commits

Author SHA1 Message Date
guyaodong
aec2d1c516 恢复刀具房样式和编码 2025-06-19 16:18:04 +08:00
guyaodong
8b66fda899 增加kanban悬停tip 2025-06-18 14:26:53 +08:00
huziyang@jikimo.com
6321e7ef23 货位看板详情回退修复,删除幽灵卡片。 2025-06-12 15:38:56 +08:00
huziyang@jikimo.com
23dd88b7ba 解决冲突 2025-06-09 10:01:57 +08:00
huziyang@jikimo.com
f164488e48 屏蔽OCC导入 2025-06-09 09:59:47 +08:00
huziyang@jikimo.com
25b53794bb 处理#6973任务:
1货位编码规则简化
2货位看板直观化展示
2025-06-09 09:49:21 +08:00
20 changed files with 2222 additions and 1752 deletions

View File

@@ -41,14 +41,13 @@ class PurchaseRequest(models.Model):
if lines:
for line in lines:
for line_item in line.order_line:
if line_item.state == 'purchase':
product_id = line_item.product_id.id
qty = line_item.product_qty
product_rounding[product_id] = line_item.product_id.uom_id.rounding
if product_id in product_summary:
product_summary[product_id] += qty
else:
product_summary[product_id] = qty
product_id = line_item.product_id.id
qty = line_item.product_qty
product_rounding[product_id] = line_item.product_id.uom_id.rounding
if product_id in product_summary:
product_summary[product_id] += qty
else:
product_summary[product_id] = qty
# 校验产品数量
discrepancies = []
@@ -61,10 +60,10 @@ class PurchaseRequest(models.Model):
if discrepancies:
# 弹出提示框
message = "产品与采购数量不一致:\n"
message = "产品数量不一致:\n"
for product_id, required_qty, order_qty in discrepancies:
product_name = self.env['product.product'].browse(product_id).display_name # 获取产品名称
message += f"产品 {product_name},需求数量 {required_qty},关联采购订单确认的数量 {order_qty}\n"
message += f"产品 {product_name},需求数量 {required_qty},关联采购订单数量 {order_qty}(含询价状态)\n"
# 添加确认框
message += "确认关闭?"
return {

View File

@@ -445,7 +445,32 @@ class Manufacturing_Connect(http.Controller):
shelfinfo = list(filter(lambda x: x.get('DeviceId') == DeciveId,
request.env['sf.shelf.location'].sudo().get_sf_shelf_location_info(
DeciveId)))
request.env['sf.shelf.location.datasync'].sudo().set_shelf_location(shelfinfo)
total_data = request.env['sf.shelf.location.datasync'].sudo().get_total_data()
for item in shelfinfo:
logging.info('货架已获取信息:%s' % item)
shelf_barcode = request.env['sf.shelf.location.datasync'].sudo().find_our_code(
total_data, item['Postion'])
location_id = request.env['sf.shelf.location'].sudo().search(
[('barcode', '=', shelf_barcode)],
limit=1)
if location_id:
# 如果是线边刀库信息,则对功能刀具移动生成记录
if 'Tool' in item['Postion']:
tool = request.env['sf.functional.cutting.tool.entity'].sudo().search(
[('rfid', '=', item['RfidCode']), ('functional_tool_status', '!=', '已拆除')])
tool.sudo().tool_in_out_stock_location(location_id)
if tool:
location_id.product_sn_id = tool.barcode_id.id
# 修改功能刀具状态
if item.get('State') == '报警':
if tool.functional_tool_status != item.get('State'):
tool.write({
'functional_tool_status': item['State']
})
else:
location_id.product_sn_id = False
if item['RfidCode']:
logging.info('Rfid为【%s】的功能刀具在系统中不存在!' % item['RfidCode'])
else:
equipment_id = request.env['maintenance.equipment'].sudo().search([('name', '=', DeciveId)])
if equipment_id:

View File

@@ -27,7 +27,7 @@ class JikimoSaleRoutePicking(Sf_Bf_Connect):
bfm_process_order_list = json.loads(kw['bfm_process_order_list'])
order_id = request.env['sale.order'].with_user(request.env.ref("base.user_admin")).sale_order_create(
company_id, kw['delivery_name'], kw['delivery_telephone'], kw['delivery_address'],
kw['delivery_end_date'], kw['payments_way'], kw['pay_way'], kw['order_number'], kw['remark'], state='draft',
kw['delivery_end_date'], kw['payments_way'], kw['pay_way'], kw['order_number'], state='draft',
model_display_version=kw.get('model_display_version'))
i = 1
# 给sale_order的default_code字段赋值
@@ -47,7 +47,7 @@ class JikimoSaleRoutePicking(Sf_Bf_Connect):
i += 1
if kw.get('contract_file_name') and kw.get('contract_file') and kw.get('contract_code'):
order_id.create_sale_documents(kw.get('contract_file_name'), kw.get('contract_file'))
order_id.write({'contract_code': kw.get('contract_code'), 'contract_date': kw.get('contract_date')})
order_id.write({'contract_code': kw.get('contract_code')})
res['factory_order_no'] = order_id.name
order_id.confirm_to_supply_method()
except Exception as e:

View File

@@ -227,30 +227,22 @@ class ResMrpWorkOrder(models.Model):
# finish_move.move_dest_ids.move_line_ids.reserved_uom_qty = 0
else:
next_workorder = sorted_workorders[position + 1]
# 持续获取下一个工单,直到找到一个不是返工的工单
while next_workorder and next_workorder.state == 'rework':
position += 1
if position + 1 < len(sorted_workorders):
next_workorder = sorted_workorders[position + 1]
next_state = next_workorder.state
if next_state not in ['pending', 'waiting', 'ready']:
raise UserError('下工序已经开始,无法回退')
if next_workorder.is_subcontract:
next_workorder.picking_ids.write({'state': 'waiting'})
next_workorder.state = 'pending'
self.time_ids.date_end = None
cur_workorder.state = 'progress'
cur_workorder.production_id.state = 'progress'
quality_check = self.env['quality.check'].search(
[('workorder_id', '=', self.id)])
for check_order in quality_check:
if check_order.point_id.is_inspect:
check_order.quality_state = 'waiting'
else:
next_workorder = None
if next_workorder:
next_state = next_workorder.state
if next_state not in ['pending', 'waiting', 'ready']:
raise UserError('下工序已经开始,无法回退')
if next_workorder.is_subcontract:
next_workorder.picking_ids.write({'state': 'waiting'})
next_workorder.state = 'pending'
self.time_ids.date_end = None
cur_workorder.state = 'progress'
cur_workorder.production_id.state = 'progress'
quality_check = self.env['quality.check'].search(
[('workorder_id', '=', self.id)])
for check_order in quality_check:
if check_order.point_id.is_inspect:
check_order.quality_state = 'waiting'
else:
check_order.quality_state = 'none'
check_order.quality_state = 'none'
def _compute_working_users(self):
super()._compute_working_users()

View File

@@ -10,8 +10,8 @@ from odoo.exceptions import ValidationError, UserError
from odoo.modules import get_resource_path
from OCC.Extend.DataExchange import read_step_file
from OCC.Extend.DataExchange import write_stl_file
# from OCC.Extend.DataExchange import read_step_file
# from OCC.Extend.DataExchange import write_stl_file
class ResProductMo(models.Model):

View File

@@ -210,8 +210,8 @@
<field name="msgtype">markdown</field>
<field name="urgency">normal</field>
<field name="content">### 功能刀具寿命到期提醒:
单号:拆解单[{{code}}]({{request_url}})
事项:{{name}}寿命已到期,需拆解</field>
单号:拆解单[{{code}}]({{tool_expired_remind_special_url}})
事项:{{functional_tool_id.tool_name_id.name}}寿命已到期,需拆解</field>
</record>
<record id="template_tool_assembly_remind" model="jikimo.message.template">

View File

@@ -1,5 +1,4 @@
from odoo import models, api
from urllib.parse import urlencode
class SFMessagefunctionalToolDismantle(models.Model):
@@ -19,39 +18,12 @@ class SFMessagefunctionalToolDismantle(models.Model):
obj.add_queue('功能刀具寿命到期')
return result
def _get_message(self, message_queue_ids):
contents = []
for message_queue_id in message_queue_ids:
if message_queue_id.message_template_id.name == '功能刀具寿命到期':
content = message_queue_id.message_template_id.content
td_line = self.search([('id', '=', int(message_queue_id.res_id))])
url = self.request_url(int(message_queue_id.res_id))
content = content.replace('{{code}}', td_line.code).replace(
'{{request_url}}', url).replace('{{name}}', td_line.name)
contents.append(content)
return contents, message_queue_ids
def request_url(self, id):
url = self.env['ir.config_parameter'].get_param('web.base.url')
action_id = self.env.ref('sf_tool_management.sf_functional_tool_dismantle_view_act').id
menu_id = self.env.ref('sf_tool_management.menu_sf_functional_tool_dismantle').id
# 查询参数
params = {'id': id, 'menu_id': menu_id, 'action': action_id,
'model': 'sf.functional.tool.dismantle',
'view_type': 'form'}
# 拼接查询参数
tool_string = urlencode(params)
# 拼接URL
full_url = url + "/web#" + tool_string
return full_url
def get_special_url(self, id, tmplate_name, special_name, model_id):
def get_special_url(self,id,tmplate_name,special_name,model_id):
menu_id = 0
action_id = 0
if tmplate_name == '调拨入库' and special_name == 'tool_expired_remind_special_url':
if tmplate_name=='调拨入库' and special_name== 'tool_expired_remind_special_url':
menu_id = self.env.ref('mrp.menu_mrp_root').id
action_id = self.env.ref('sf_tool_management.sf_functional_tool_dismantle_view_act').id
return super(SFMessagefunctionalToolDismantle, self).get_url(id, menu_id, action_id, model_id)
return super(SFMessagefunctionalToolDismantle, self).get_url(id, menu_id, action_id,model_id)
else:
return super(SFMessagefunctionalToolDismantle, self).get_special_url(id, tmplate_name, special_name,
model_id)
return super(SFMessagefunctionalToolDismantle, self).get_special_url(id, tmplate_name, special_name, model_id)

View File

@@ -43,17 +43,14 @@ class SFMessageQualityCheck(models.Model):
# ('message_template_id', '=', message_template.id)])
# return jikimo_message_queue
def write(self, vals):
original_state = {}
for item in self:
original_state.update({f'{item.id}': item.quality_state})
original_state = self.quality_state
res = super(SFMessageQualityCheck, self).write(vals)
for item in self:
if vals.get('quality_state') == 'none' and original_state.get(f'{item.id}') != 'none':
message_template_id = self.env['jikimo.message.template'].sudo().search([('name', '=', '待质检提醒')])
queue_id = self.env['jikimo.message.queue'].sudo().search(
[('message_template_id', '=', message_template_id[-1].id), ('res_id', '=', item.id)])
if not queue_id and '制造' not in [pt.name for pt in item.point_id.picking_type_ids]:
item.add_queue('待质检')
if res and vals.get('quality_state') == 'none' and original_state != 'none':
message_template_id = self.env['jikimo.message.template'].sudo().search([('name', '=', '待质检提醒')])
queue_id = self.env['jikimo.message.queue'].sudo().search(
[('message_template_id', '=', message_template_id[-1].id), ('res_id', '=', self.id)])
if not queue_id and '制造' not in [pt.name for pt in self.point_id.picking_type_ids]:
self.add_queue('待质检')
return res
def _get_message(self, message_queue_ids):

View File

@@ -8,8 +8,8 @@ from datetime import datetime
import requests
from odoo import http
from odoo.http import request
from OCC.Extend.DataExchange import read_step_file
from OCC.Extend.DataExchange import write_stl_file
# from OCC.Extend.DataExchange import read_step_file
# from OCC.Extend.DataExchange import write_stl_file
from odoo import models, fields, api
from odoo.modules import get_resource_path
from odoo.exceptions import ValidationError, UserError

View File

@@ -5,8 +5,8 @@ import requests
import os
from datetime import datetime
# from OCC.Core.GProp import GProp_GProps
from OCC.Extend.DataExchange import read_step_file
from OCC.Extend.DataExchange import write_stl_file
# from OCC.Extend.DataExchange import read_step_file
# from OCC.Extend.DataExchange import write_stl_file
from odoo.addons.sf_base.commons.common import Common
from odoo import models, fields, api
from odoo.modules import get_resource_path

View File

@@ -64,14 +64,13 @@ class ReSaleOrder(models.Model):
model_display_version = fields.Char('模型展示版本', default="v1")
contract_code = fields.Char('合同编号')
contract_date = fields.Date('合同日期')
contract_document_id = fields.Many2one('documents.document', string='合同文件')
contract_file = fields.Binary(related='contract_document_id.datas', string='合同文件内容')
contract_file_name = fields.Char(related='contract_document_id.attachment_id.name', string='文件名')
# 业务平台分配工厂后在智能工厂先创建销售订单
def sale_order_create(self, company_id, delivery_name, delivery_telephone, delivery_address,
deadline_of_delivery, payments_way, pay_way, order_number, remark, state='sale',
deadline_of_delivery, payments_way, pay_way, order_number, state='sale',
model_display_version='v1'):
now_time = datetime.datetime.now()
partner = self.get_customer()
@@ -90,7 +89,6 @@ class ReSaleOrder(models.Model):
'pay_way': pay_way,
'order_code': order_number,
'model_display_version': model_display_version,
'remark': remark,
}
if deadline_of_delivery:
# deadline_of_delivery字段存在为false字符串情况

View File

@@ -58,6 +58,20 @@ class FunctionalCuttingToolEntity(models.Model):
safe_inventory_id = fields.Many2one('sf.real.time.distribution.of.functional.tools',
string='功能刀具安全库存', readonly=True)
@api.onchange('functional_tool_status')
def _onchange_functional_tool_status(self):
for item in self:
if item:
if item.functional_tool_status == '报警':
self.create_tool_dismantle()
def set_functional_tool_status(self):
# self.write({
# 'functional_tool_status': '报警'
# })
self.functional_tool_status = '报警'
self.create_tool_dismantle()
def create_tool_dismantle(self):
for item in self:
# 创建报警刀具拆解单

View File

@@ -42,6 +42,7 @@
<field name="arch" type="xml">
<form create="0" edit="0" delete="0">
<header>
<button name="set_functional_tool_status" string="报警" type="object" invisible="1"/>
<!-- <button name="enroll_functional_tool_entity" string="功能刀具注册" type="object"-->
<!-- class="btn-primary"/>-->
<field name="functional_tool_status" widget="statusbar" statusbar_visible="正常,报警,已拆除"/>

View File

@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': '机企猫智能工厂 库存管理',
'version': '1.0',
'version': '1.2',
'summary': '智能工厂库存管理',
'sequence': 1,
'description': """
@@ -23,17 +23,16 @@
'demo': [
],
'assets': {
'web.assets_qweb': [
],
'web.assets_backend': [
# 'sf_warehouse/static/src/js/vanilla-masker.min.js',
'sf_warehouse/static/src/css/kanban_color_change.scss',
'sf_warehouse/static/src/js/custom_kanban_controller.js',
'sf_warehouse/static/src/xml/custom_kanban_controller.xml',
'sf_warehouse/static/src/css/kanban_location_custom.scss',
'sf_warehouse/static/src/js/shelf_location_search.js',
]
},
'license': 'LGPL-3',
'installable': True,

View File

@@ -0,0 +1,21 @@
from odoo import api, SUPERUSER_ID
def migrate(cr, version):
env = api.Environment(cr, SUPERUSER_ID, {})
sf_shelf_model = env["sf.shelf"]
sf_shelf_location_model = env["sf.shelf.location"]
preproduction_shelf_ids = sf_shelf_location_model.get_preproduction_shelf_ids()
shelves = sf_shelf_model.search([])
for shelf in shelves:
if shelf.id not in preproduction_shelf_ids:
continue
shelf_barcode = shelf.barcode or ""
if not shelf_barcode:
continue
locations = sf_shelf_location_model.search([("shelf_id", "=", shelf.id)], order="id asc")
for index, location in enumerate(locations, start=1):
new_barcode = f"{shelf_barcode}-{index:03d}"
location.barcode = new_barcode

View File

@@ -303,7 +303,9 @@ class SfShelf(models.Model):
area_type_barcode = self.barcode
i_str = str(i + 1).zfill(3) # 确保是两位数如果不足两位左侧补0
j_str = str(j + 1).zfill(3) # 确保是两位数如果不足两位左侧补0
return area_type_barcode + self.channel + self.direction + '-' + self.barcode + '-' + i_str + '-' + j_str
num_str = str((i)*self.layer_capacity+j + 1).zfill(3)
# return area_type_barcode + self.channel + self.direction + '-' + self.barcode + '-' + i_str + '-' + j_str
return area_type_barcode + '-' + num_str
def print_all_location_barcode(self):
"""
@@ -457,7 +459,40 @@ class ShelfLocation(models.Model):
product_sn_ids = fields.One2many('sf.shelf.location.lot', 'shelf_location_id', string='产品批次号')
# 产品数量
product_num = fields.Integer('总数量', compute='_compute_number', store=True)
tool_rfid = fields.Char('Rfid', compute='_compute_tool', store=True)
tool_name_id = fields.Many2one('sf.functional.cutting.tool.entity', string='功能刀具名称', compute='_compute_tool', store=True)
display_rfid = fields.Char('RFID', compute='_compute_display_rfid', store=True)
@api.depends('product_sn_id')
def _compute_display_rfid(self):
"""计算显示 RFID"""
for record in self:
try:
record.display_rfid = record.product_sn_id.rfid if record.product_sn_id else ''
except Exception as e:
record.display_rfid = ''
@api.depends('product_id')
def _compute_tool(self):
"""计算工具 RFID"""
for record in self:
try:
if record.product_id:
if record.product_id.categ_id.name == '功能刀具':
# 搜索关联的功能刀具实体
tool_id = self.env['sf.functional.cutting.tool.entity'].search(
[('barcode_id', '=', record.product_sn_id.id)], limit=1
)
if tool_id:
record.tool_rfid = tool_id.rfid
record.tool_name_id = tool_id.id
continue
# 默认值
record.tool_rfid = ''
record.tool_name_id = False
except Exception as e:
record.tool_rfid = ''
record.tool_name_id = False
_logger.error(f"计算 tool_rfid 时出错: {e}")
@api.depends('product_num')
def _compute_product_num(self):
for record in self:
@@ -545,6 +580,43 @@ class ShelfLocation(models.Model):
records = super(ShelfLocation, self).create(vals_list)
return records
kanban_show_layer_info = fields.Char('展示货位的层信息', compute='_compute_kanban_show_info')
kanban_show_center_control_code = fields.Char('展示货位的货柜信息', compute='_compute_kanban_show_info')
@api.depends('shelf_id','barcode')
def _compute_kanban_show_info(self):
for record in self:
arr = record.barcode.split('-')
alen = len(arr)
if( alen >= 1):
_cc_code = int(arr[alen-1])
_layer = _cc_code // record.shelf_id.layer_capacity
_layer_capacity = _cc_code % record.shelf_id.layer_capacity
if _layer_capacity == 0:
_layer_capacity = record.shelf_id.layer_capacity
else:
_layer_capacity = _layer_capacity
_layer = _layer+1
_layer_capacity = f"{_layer_capacity:02d}"
record.kanban_show_layer_info=f"{_layer}-{_layer_capacity}"
record.kanban_show_center_control_code=f"{_cc_code}"
@api.model
def get_preproduction_shelf_ids(self):
"""
获取预生产区域的货架ID列表
Returns:
list: 货架ID列表
"""
query = """
SELECT DISTINCT b.shelf_id
FROM stock_location a
LEFT JOIN sf_shelf_location b ON a.id = b.location_id
WHERE a.barcode LIKE 'WH-PREPRODUCTION'
"""
self.env.cr.execute(query)
result = self.env.cr.fetchall()
# 将结果转换为ID列表
shelf_ids = [record[0] for record in result if record[0]]
return shelf_ids
class SfShelfLocationLot(models.Model):
_name = 'sf.shelf.location.lot'
@@ -563,6 +635,7 @@ class SfShelfLocationLot(models.Model):
raise ValidationError('变更数量不能比库存数量大!!!')
class SfStockMoveLine(models.Model):
_name = 'stock.move.line'
_inherit = ['stock.move.line', 'printing.utils']

View File

@@ -107,47 +107,38 @@ class MrsShelfLocationDataSync(models.Model):
equipment_id.register_equipment_tool()
shelfinfo = self.env['sf.shelf.location'].get_sf_shelf_location_info()
self.set_shelf_location(shelfinfo)
total_data = self.get_total_data()
print('shelfinfo:', shelfinfo)
for item in shelfinfo:
logging.info('货架已获取信息:%s' % item)
shelf_barcode = self.find_our_code(total_data, item['Postion'])
location_id = self.env['sf.shelf.location'].search([('barcode', '=', shelf_barcode)], limit=1)
if location_id:
# 如果是线边刀库信息,则对功能刀具移动生成记录
if 'Tool' in item['Postion']:
tool = self.env['sf.functional.cutting.tool.entity'].sudo().search(
[('rfid', '=', item['RfidCode']), ('functional_tool_status', '!=', '已拆除')])
tool.tool_in_out_stock_location(location_id)
if tool:
location_id.product_sn_id = tool.barcode_id.id
# 修改功能刀具标准状态值和已使用寿命值、功能刀具状态
if 'LifeStd' in item and 'LifeUse' in item:
tool.sudo().write({
'max_lifetime_value': item['LifeStd'],
'used_value': item['LifeUse'],
'functional_tool_status': item['State'],
})
else:
location_id.product_sn_id = False
if item['RfidCode']:
logging.info('Rfid为【%s】的功能刀具在系统中不存在!' % item['RfidCode'])
else:
stock_lot_obj = self.env['stock.lot'].search([('rfid', '=', item['RfidCode'])], limit=1)
if stock_lot_obj:
location_id.product_sn_id = stock_lot_obj.id
else:
location_id.product_sn_id = False
except Exception as e:
logging.info("库区信息同步失败:%s" % e)
raise ValidationError("数据错误导致同步失败,请联系管理员")
def set_shelf_location(self, shelfinfo):
print('shelfinfo:', shelfinfo)
total_data = self.get_total_data()
for item in shelfinfo:
logging.info('货架已获取信息:%s' % item)
shelf_barcode = self.env['sf.shelf.location.datasync'].sudo().find_our_code(
total_data, item['Postion'])
location_id = self.env['sf.shelf.location'].sudo().search(
[('barcode', '=', shelf_barcode)],
limit=1)
if location_id:
# 如果是线边刀库信息,则对功能刀具移动生成记录
if 'Tool' in item['Postion']:
tool = self.env['sf.functional.cutting.tool.entity'].sudo().search(
[('rfid', '=', item['RfidCode']), ('functional_tool_status', '!=', '已拆除')])
tool.sudo().tool_in_out_stock_location(location_id)
if tool:
location_id.product_sn_id = tool.barcode_id.id
if item.get('State') == '报警' and tool.functional_tool_status != '报警':
# 创建报警刀具拆解单和刀具报警记录
tool.create_tool_dismantle()
# 修改功能刀具标准状态值和已使用寿命值、功能刀具状态
if 'LifeStd' in item and 'LifeUse' in item:
tool.sudo().write({
'max_lifetime_value': item['LifeStd'],
'used_value': item['LifeUse'],
'functional_tool_status': item['State'],
})
else:
location_id.product_sn_id = False
if item['RfidCode']:
logging.info('Rfid为【%s】的功能刀具在系统中不存在!' % item['RfidCode'])
else:
stock_lot_obj = self.env['stock.lot'].search([('rfid', '=', item['RfidCode'])], limit=1)
if stock_lot_obj:
location_id.product_sn_id = stock_lot_obj.id
else:
location_id.product_sn_id = False

View File

@@ -0,0 +1,198 @@
// 定义看板公共样式的Mixin
@mixin kanban-common-styles($record-count-each-row, $record-gap: 16px) {
$record-gap-total-width: $record-gap * ($record-count-each-row - 1);
display: flex !important;
flex-wrap: wrap !important;
overflow-x: hidden !important;
overflow-y: auto !important;
padding: 0 !important;
gap: $record-gap !important;
width: 100% !important;
height: 100% !important;
// === 卡片基础样式(完全保留)===
.o_kanban_record {
flex: 0 0 calc((100% - #{$record-gap-total-width}) / #{$record-count-each-row}) !important;
height: calc((100% - #{$record-gap * 6}) / 6) !important;
margin: 0 !important;
padding: 0 !important;
background-color: white !important;
border: 1px solid #dee2e6 !important;
border-radius: 4px !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
min-width: calc((100% - #{$record-gap-total-width}) / #{$record-count-each-row}) !important;
max-width: calc((100% - #{$record-gap-total-width}) / #{$record-count-each-row}) !important;
position: relative;
transition: all 0.25s ease !important;
overflow: visible !important; // 允许悬停条溢出卡片边界
// === 状态标签(保留原设计)===
.status-label {
position: absolute;
top: 8px;
right: 8px;
padding: 3px 8px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #e0e0e0;
border-radius: 3px;
font-size: 11px;
color: #424242;
z-index: 2;
}
// === 优化:悬停信息条(核心改动)===
.status-hover-bar {
position: absolute;
bottom: calc(100% + 8px); // 默认显示在卡片上方
left: 0;
z-index: 1000;
min-width: max-content; // 宽度自适应内容
max-width: 300px; // 防止过宽
padding: 10px 12px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e0e0e0;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 12px;
color: #424242;
white-space: nowrap; // 强制单行显示
opacity: 0;
pointer-events: none; // 避免阻挡卡片交互
transition: opacity 0.2s ease, transform 0.2s ease;
transform: translateY(10px);
// 三角形指示器
&::after {
content: '';
position: absolute;
top: 100%;
left: 15px;
border: 6px solid transparent;
border-top-color: rgba(0, 0, 0, 0.85);
}
div {
margin-bottom: 4px;
line-height: 1.4;
}
}
// === 悬停触发逻辑 ===
&:hover {
transform: translateY(-4px) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
z-index: 10;
.status-hover-bar {
background: rgba(50, 50, 50, 0.9);
color: #fff !important;
font-size: 12px;
opacity: 0.9;
transform: translateY(0);
pointer-events: auto; // 悬停时允许交互
}
}
// === 边界保护(智能定位)===
// 左侧卡片:左对齐
&:nth-child(#{$record-count-each-row}n+1) .status-hover-bar {
left: 0;
right: auto;
&::after { left: 15px; }
}
// 右侧卡片:右对齐
&:nth-child(#{$record-count-each-row}n) .status-hover-bar {
left: auto;
right: 0;
&::after {
left: auto;
right: 15px;
}
}
&:nth-child(#{$record-count-each-row}n + #{$record-count-each-row - 1}) .status-hover-bar {
left: auto;
right: 0;
&::after {
left: auto;
right: 15px;
}
}
// 顶部卡片:悬停条显示在下方
&:nth-child(-n+#{$record-count-each-row}) .status-hover-bar {
bottom: auto;
top: calc(100% + 8px);
&::after {
top: auto;
bottom: 100%;
border-top-color: transparent;
border-bottom-color: rgba(255, 255, 255, 0.95);
}
}
// === 禁用状态样式(保留原效果)===
&.kanban_color_3 {
opacity: 0.6;
&:hover {
opacity: 0.85;
.status-hover-bar {
background:rgba(0, 0, 0, 0.85);
color: white !important;
border: 1px solid rgba(255, 255, 255, 0.15) !important;
}
}
}
}
}
// === 看板视图样式(完全保留)===
.o_kanban_view {
// 卡片内部结构(不修改)
.o_kanban_record {
.o_kanban_record_bottom {
margin: 0;
}
.oe_kanban_card.kanban_color_3,
.oe_kanban_card.kanban_color_1,
.oe_kanban_card.kanban_color_2 {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.sf_kanban_custom_location_info_style {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100%;
font-size: 15px;
color: #000000;
padding:0px;
}
.sf_kanban_no {
display: flex !important;
justify-content: center !important;
align-items: center !important;
font-size: 18px;
color: #000000;
}
}
}
// 不同列数的看板样式
.sf_kanban_location_style12 {
@include kanban-common-styles(12);
}
.sf_kanban_location_style19 {
@include kanban-common-styles(19);
}
.sf_kanban_location_style4 {
@include kanban-common-styles(4);
}
.sf_kanban_location_style3 {
@include kanban-common-styles(3);
}
}

View File

@@ -1,21 +1,197 @@
/** @odoo-module */
import {KanbanController} from "@web/views/kanban/kanban_controller";
import {kanbanView} from "@web/views/kanban/kanban_view";
import {registry} from "@web/core/registry";
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useState, onWillStart, onWillUnmount, onMounted } from "@odoo/owl";
// 自定义看板渲染器
class CustomKanbanRenderer extends KanbanRenderer {
// the controller usually contains the Layout and the renderer.
class CustomKanbanController extends KanbanController {
// Your logic here, override or insert new methods...
// if you override setup(), don't forget to call super.setup()
}
// 自定义看板控制器
class CustomKanbanController extends KanbanController {
setup() {
super.setup();
this.orm = useService("orm");
this.searchModel = this.model.env.searchModel;
this.defaultPagerLimit = this.getSystemDefaultLimit();
this._onUpdate = (payload) => {
this._handleSearchUpdate(payload);
};
this.env.services.user.updateContext({
isBaseStyle: true
});
let self = this;
onWillStart(async () => {
try {
this.preproductionShelfIds = await this.orm.call(
'sf.shelf.location',
'get_preproduction_shelf_ids',
[]
);
} catch (error) {
this.preproductionShelfIds = [];
}
this.searchModel.on('update', self, self._onUpdate);
await this.loadShelfLayersData();
});
// 组件销毁时移除监听
onWillUnmount(() => {
this.searchModel.off('update', self, self._onUpdate);
});
// 监听视图切换事件以监控面包屑
onMounted(() => {
this.handleRouteChange()
});
}
handleRouteChange() {
this.render(true);
let domain = this.searchModel.domain;
if (domain.length > 0) {
let shelfDomain = domain.find(item => item[0] === 'shelf_id');
if (shelfDomain && shelfDomain[2] && this.preproductionShelfIds && this.preproductionShelfIds.includes(shelfDomain[2])) {
this.onShelfChange(shelfDomain[2]);
} else {
this.setKanbanStyle('sf_kanban_location_style');
}
} else {
this.setKanbanStyle('sf_kanban_location_style');
}
}
_handleSearchUpdate() {
try {
let domain = this.searchModel.domain;
if (domain.length > 0) {
let shelfDomain = domain.find(item => item[0] === 'shelf_id');
if (shelfDomain) {
let shelfId = shelfDomain[2];
if (shelfId && this.preproductionShelfIds.includes(shelfId)) {
this.onShelfChange(shelfId);
return;
}
}
}
//设置默认样式
this.updatePagerLimit(this.defaultPagerLimit);
this.setKanbanStyle('sf_kanban_location_style');
} catch (error) {
}
}
// 加载所有货架的层数数据
async loadShelfLayersData() {
this.shelfLayersMap = {};
const shelfIds = await this.orm.search('sf.shelf', []);
const shelves = await this.orm.read('sf.shelf', shelfIds, ['id', 'layer_capacity']);
shelves.forEach(shelf => {
this.shelfLayersMap[shelf.id] = shelf.layer_capacity;
});
}
setKanbanStyle(style) {
this.env.services.user.updateContext({
isBaseStyle: style === 'sf_kanban_location_style'
});
const kanbanViewEl = document.querySelector('.o_kanban_renderer');
if (kanbanViewEl) {
let isHave = false;
// 移除所有现有的 sf_kanban_* 类
Array.from(kanbanViewEl.classList).forEach(cls => {
if (cls.startsWith('sf_kanban_location_style')) {
kanbanViewEl.classList.remove(cls);
isHave = true;
}
});
// 添加新类
if (isHave) kanbanViewEl.classList.add(style);
}
// 获取当前的搜索域
let domain = this.searchModel.domain;
let shelfDomain = domain.find(item => item[0] === 'shelf_id');
// 只有当shelf_id在preproductionShelfIds中时才删除幽灵看板
if (shelfDomain && this.preproductionShelfIds && this.preproductionShelfIds.includes(shelfDomain[2])) {
const ghostCards = document.querySelectorAll('.o_kanban_ghost');
ghostCards.forEach(card => {
card.remove();
});
}
}
updatePagerLimit(limit) {
if (this.model.root.limit !== limit) {
this.model.root.limit = limit;
this.render(true);
}
}
// 处理货架变更事件
async onShelfChange(shelfId) {
let style = 'sf_kanban_location_style';
let isBaseStyle = true;
if (shelfId) {
// 如果没有缓存,从服务器加载数据
if (!(shelfId in this.shelfLayersMap)) {
const [shelf] = await this.orm.read('sf.shelf', [shelfId], ['layer_capacity']);
this.shelfLayersMap[shelfId] = shelf.layer_capacity;
}
// 获取该货架的层数
const layerCapacity = this.shelfLayersMap[shelfId];
// 根据层数设置不同的样式
if (layerCapacity >= 1) {
style = `sf_kanban_location_style${layerCapacity}`;
isBaseStyle = false;
}
}
if (isBaseStyle) {
this.updatePagerLimit(this.defaultPagerLimit);
}
else {
this.updatePagerLimit(500);
}
this.setKanbanStyle(style);
}
/**
* 获取系统默认分页记录数
*/
getSystemDefaultLimit() {
// 方法1从用户服务获取默认值
const userService = this.env.services.user;
// 获取用户配置的默认分页大小
if (userService && userService.user_context && userService.user_context.limit) {
return userService.user_context.limit;
}
// 方法3使用Odoo核心默认值通常为80
return 80;
}
}
// 设置自定义模板
CustomKanbanController.template = "sf_warehouse.CustomKanbanView";
export const customKanbanView = {
...kanbanView, // contains the default Renderer/Controller/Model
...kanbanView,
Controller: CustomKanbanController,
Renderer: CustomKanbanRenderer,
};
// Register it to the views registry
registry.category("views").add("custom_kanban", customKanbanView);

View File

@@ -193,64 +193,78 @@
</field>
</record>
<record id="shelf_location_kanban_view" model="ir.ui.view">
<field name="name">shelf.location.kanban</field>
<field name="model">sf.shelf.location</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" js_class="custom_kanban" create="0">
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click
#{record.location_status.raw_value == '空闲' ? 'kanban_color_1' : ''}
#{record.location_status.raw_value == '占用' ? 'kanban_color_2' : ''}
#{record.location_status.raw_value == '用' ? 'kanban_color_3' : ''}">
<!-- 标题 -->
<div class="o_kanban_card_header">
<div class="o_kanban_card_header_title">
<field name="name"/>
</div>
</div>
<!-- 内容 -->
<div class="o_kanban_record_bottom">
<field name="location_status"/>
</div>
<div class="o_kanban_record_bottom">
<field name="product_sn_id"/>
<span>|</span>
<field name="product_id"/>
<record id="shelf_location_kanban_view" model="ir.ui.view">
<field name="name">shelf.location.kanban</field>
<field name="model">sf.shelf.location</field>
<field name="arch" type="xml">
<kanban class="sf_kanban_location_style" js_class="custom_kanban" create="0">
<templates>
<t t-name="kanban-box">
<t t-set='isBaseStyle' t-value="user_context.isBaseStyle"/>
<div t-attf-class="oe_kanban_card oe_kanban_global_click
#{record.location_status.raw_value == '空闲' ? 'kanban_color_1' : ''}
#{record.location_status.raw_value == '用' ? 'kanban_color_2' : ''}
#{record.location_status.raw_value == '禁用' ? 'kanban_color_3' : ''}">
<!-- 所有情况都需要的数据 (隐藏) -->
<div style="display:none">
<field name="location_status"/>
<field name="tool_name_id"/>
</div>
<t t-if="isBaseStyle">
<div class="o_kanban_card_header">
<div class="o_kanban_card_header_title">
<field name="name"/>
</div>
</div>
<div class="o_kanban_record_bottom">
<field name="product_sn_id"/>
<field name="product_id"/>
</div>
</t>
<!-- <t t-name="kanban-box"> -->
<!-- <div t-attf-class="oe_kanban_card oe_kanban_global_click -->
<!-- #{record.location_status.raw_value == '空闲' ? 'kanban_color_1' : ''} -->
<!-- #{record.location_status.raw_value == '占用' ? 'kanban_color_2' : ''} -->
<!-- #{record.location_status.raw_value == '禁用' ? 'kanban_color_3' : ''}"> -->
<!-- 看板内容 -->
<!-- </div> -->
<!-- <div t-attf-class="oe_kanban_card"> -->
<!-- 标题 -->
<!-- <div class="o_kanban_card_header"> -->
<!-- <div class="o_kanban_card_header_title"> -->
<!-- <field name="name"/> -->
<!-- </div> -->
<!-- </div> -->
<!-- 内容 -->
<!-- <div class="o_kanban_record_bottom"> -->
<!-- <field name="location_status"/> -->
<!-- </div> -->
<!-- <div class="o_kanban_record_bottom"> -->
<!-- <field name="product_sn_id"/> -->
<!-- <span> | </span> -->
<!-- <field name="product_id"/> -->
<!-- </div> -->
<!-- </div> -->
<!-- </t> -->
</templates>
</kanban>
</field>
</record>
<t t-else="">
<div class="o_kanban_record_bottom sf_kanban_custom_location_info_style">
<field name="kanban_show_layer_info"/>
</div>
<!-- 添加RFID字段 -->
<!-- <t t-if="record.data and record.data.display_rfid">
<div class="o_kanban_record_bottom">
<field name="display_rfid"/>
</div>
</t>
<t t-if="record.data and record.data.tool_rfid">
<div class="o_kanban_record_bottom">
<field name="tool_rfid"/>
</div>
</t> -->
<!-- 悬停时显示的详细信息 -->
<div class="status-hover-bar">
<t t-if="record.product_id.value">
<div>产品: <t t-esc="record.product_id.value"/></div>
</t>
<t t-if="record.product_sn_id.value">
<div>标签ID: <t t-esc="record.product_sn_id.value"/></div>
</t>
<!-- <t t-if="record.display_rfid.value">
<div>rfid: <t t-esc="record.display_rfid.value"/></div>
</t>
<t t-if="record.tool_rfid.value">
<div>rfid: <t t-esc="record.tool_rfid.value"/></div>
</t> -->
<t t-if="record.tool_name_id and record.tool_name_id.value">
<div>功能刀具名称: <t t-esc="record.tool_name_id.value"/></div>
</t>
<div>状态: <t t-esc="record.location_status.value"/></div>
</div>
</t>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- 搜索视图 -->
<record id="shelf_location_search_view" model="ir.ui.view">
<field name="name">shelf.location.search</field>