diff --git a/sf_manufacturing/models/product_template.py b/sf_manufacturing/models/product_template.py index 57676d90..19bce23f 100644 --- a/sf_manufacturing/models/product_template.py +++ b/sf_manufacturing/models/product_template.py @@ -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): diff --git a/sf_sale/models/quick_easy_order.py b/sf_sale/models/quick_easy_order.py index d164d8cc..3a46a00e 100644 --- a/sf_sale/models/quick_easy_order.py +++ b/sf_sale/models/quick_easy_order.py @@ -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 diff --git a/sf_sale/models/quick_easy_order_old.py b/sf_sale/models/quick_easy_order_old.py index cbf0f8f1..459b9914 100644 --- a/sf_sale/models/quick_easy_order_old.py +++ b/sf_sale/models/quick_easy_order_old.py @@ -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 diff --git a/sf_warehouse/__manifest__.py b/sf_warehouse/__manifest__.py index 9959e5d5..456d3b96 100644 --- a/sf_warehouse/__manifest__.py +++ b/sf_warehouse/__manifest__.py @@ -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, diff --git a/sf_warehouse/migrations/1.2/post-migrate.py b/sf_warehouse/migrations/1.2/post-migrate.py new file mode 100644 index 00000000..f4a0d9a4 --- /dev/null +++ b/sf_warehouse/migrations/1.2/post-migrate.py @@ -0,0 +1,15 @@ +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"] + shelves = sf_shelf_model.search([]) + for shelf in shelves: + 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 \ No newline at end of file diff --git a/sf_warehouse/models/model.py b/sf_warehouse/models/model.py index b1f66eba..33373600 100644 --- a/sf_warehouse/models/model.py +++ b/sf_warehouse/models/model.py @@ -1,1268 +1,1288 @@ -# -*- coding: utf-8 -*- -import re - -import datetime -import logging -import base64 -import qrcode -import io - -import requests - -from odoo import api, fields, models, _ -from odoo.osv import expression -from odoo.exceptions import UserError, ValidationError - - -class SfLocation(models.Model): - _inherit = 'stock.location' - - # 重写字段定义 - name = fields.Char('Location Name', required=True, size=20) - barcode = fields.Char('Barcode', copy=False, size=15) - - # 仓库类别(selection:库区、库位、货位) - # location_type = fields.Selection([ - # ('库区', '库区'), - # ('货架', '货架'), - # ('货位', '货位') - # ], string='存储类型') - location_type = fields.Selection([ - ('库区', '库区') - ], string='存储类型') - # 库区类型(selection:拣货区、存货区、收货区、退货区、次品区) - area_type = fields.Selection([ - ('拣货区', '拣货区'), - ('存货区', '存货区'), - ('收货区', '收货区'), - ('退货区', '退货区'), - ('次品区', '次品区') - ], string='库区类型') - # 当前位置 - current_location_id = fields.Many2one('sf.shelf.location', string='当前位置') - # 目的位置 - destination_location_id = fields.Many2one('sf.shelf.location', string='目的位置') - # 存储类型(selection:库区、货架) - # storage_type = fields.Selection([ - # ('库区', '库区'), - # ('货架', '货架') - # ], string='存储类型') - # 产品类别 (关联:product.category) - product_type = fields.Many2many('product.category', string='产品类别') - # 货架独有字段:通道、方向、货架高度(m)、货架层数、层数容量 - channel = fields.Char(string='通道') - direction = fields.Selection([ - ('R', 'R'), - ('L', 'L') - ], string='方向') - shelf_height = fields.Float(string='货架高度(m)') - shelf_layer = fields.Integer(string='货架层数') - layer_capacity = fields.Integer(string='层数容量') - - # 货位独有字段:货位状态、产品(关联产品对象)、产品序列号(关联产品序列号对象) - location_status = fields.Selection([ - ('空闲', '空闲'), - ('占用', '占用'), - ('禁用', '禁用') - ], string='货位状态', default='空闲') - # product_id = fields.Many2one('product.template', string='产品') - product_id = fields.Many2one('product.product', string='产品', compute='_compute_product_id', readonly=True) - product_sn_id = fields.Many2one('stock.lot', string='产品序列号') - # time_test = fields.Char(string='time') - # 添加SQL约束 - # _sql_constraints = [ - # ('name_uniq', 'unique(name)', '位置名称必须唯一!'), - # ] - - hide_location_type = fields.Boolean(compute='_compute_hide_what', string='隐藏仓库') - hide_area = fields.Boolean(compute='_compute_hide_what', string='隐藏库区') - hide_shelf = fields.Boolean(compute='_compute_hide_what', string='隐藏货架') - hide_location = fields.Boolean(compute='_compute_hide_what', string='隐藏货位') - - # @api.model - # def create(self, vals): - # """ - # 重写create方法,添加自定义的约束 - # """ - # print('create', vals) - # if vals.get('location_id'): - # location = self.env['stock.location'].browse(vals.get('location_id')) - # if location.storage_type == '库区': - # raise UserError('库区不能作为父级仓库') - # return super().create(vals) - # - # @api.onchange('location_id') - # def _onchange_location_id(self): - # """ - # 重写onchange方法,添加自定义的约束 - # """ - # if self.location_id: - # if self.location_id.storage_type == '库区': - # raise UserError('库区不能作为父级仓库') - - # @api.constrains('shelf_height') - # def _check_shelf_height(self): - # for record in self: - # if not (0 <= record.shelf_height < 1000): # 限制字段值在0到999之间 - # raise UserError('shelf_height的值必须在0到1000之间') - # - # @api.constrains('shelf_layer') - # def _check_shelf_layer(self): - # for record in self: - # if not (0 < record.shelf_layer < 1000): - # raise UserError('shelf_layer的值必须在0到999之间,且不能为0') - # - # @api.constrains('layer_capacity') - # def _check_layer_capacity(self): - # for record in self: - # if not (0 <= record.layer_capacity < 1000): - # raise UserError('layer_capacity的值必须在0到999之间,且不能为0') - - @api.depends('product_sn_id') - def _compute_product_id(self): - """ - 根据产品序列号,获取产品 - """ - for record in self: - if record.product_sn_id: - record.product_id = record.product_sn_id.product_id - # record.location_status = '占用' - else: - record.product_id = False - # record.location_status = '空闲' - - @api.depends('location_type') - def _compute_hide_what(self): - """ - 根据仓库类别,隐藏不需要的字段 - :return: - """ - for record in self: - record.hide_location_type = False - record.hide_area = False - record.hide_shelf = False - record.hide_location = False - if record.location_type and record.location_type == '仓库': - record.hide_location_type = True - elif record.location_type and record.location_type == '库区': - record.hide_area = True - elif record.location_type and record.location_type == '货架': - record.hide_shelf = True - elif record.location_type and record.location_type == '货位': - record.hide_location = True - else: - pass - - # # 添加Python约束 - # @api.constrains('name', 'barcode') - # def _check_len(self): - # for rec in self: - # if len(rec.name) > 20: - # raise ValidationError("Location Name length must be less equal than 20!") - # if len(rec.barcode) > 15: - # raise ValidationError("Barcode length must be less equal than 15!") - - # @api.model - # def default_get(self, fields): - # print('fields:', fields) - # res = super(SfLocation, self).default_get(fields) - # print('res:', res) - # if 'barcode' in fields and 'barcode' not in res: - # # 这里是你生成barcode的代码 - # pass - # # res['barcode'] = self.generate_barcode() # 假设你有一个方法generate_barcode来生成barcode - # return res - # @api.model - # def create(self, vals): - # """ - # 重写create方法,当仓库类型为货架时,自动生成其下面的货位,数量为货架层数*层数容量 - # """ - # res = super(SfLocation, self).create(vals) - # if res.location_type == '货架': - # for i in range(res.shelf_layer): - # for j in range(res.layer_capacity): - # self.create({ - # 'name': res.name + '-' + str(i+1) + '-' + str(j+1), - # 'location_id': res.id, - # 'location_type': '货位', - # 'barcode': self.generate_barcode(res, i, j), - # 'location_status': '空闲' - # }) - # return res - - # 生成货位 - - def create_location(self): - """ - 当仓库类型为货架时,自动生成其下面的货位,数量为货架层数*层数容量 - """ - pass - # if self.location_type == '货架': - # for i in range(self.shelf_layer): - # for j in range(self.layer_capacity): - # self.create({ - # 'name': self.name + '-' + str(i + 1) + '层' + '-' + str(j + 1) + '位置', - # 'location_id': self.id, - # 'location_type': '货位', - # 'barcode': self.generate_barcode(i, j), - # 'location_status': '空闲' - # }) - - def generate_barcode(self, i, j): - """ - 生成货位条码 - """ - pass - # # 这里是你生成barcode的代码 - # # area_type_barcode = self.location_id.barcode - # 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 - - -class SfShelf(models.Model): - _name = 'sf.shelf' - _inherit = ['printing.utils'] - _description = '货架' - _order = 'create_date desc' - - name = fields.Char('货架名称', required=True, size=20) - active = fields.Boolean("有效", default=True) - barcode = fields.Char('编码', copy=False, size=15, required=True) - - # 货位 - location_ids = fields.One2many('sf.shelf.location', 'shelf_id', string='货位') - - check_state = fields.Selection([ - ('enable', '启用'), - ('close', '关闭') - ], string='审核状态', default='close') - - def action_check(self): - self.check_state = 'enable' - - # 绑定库区 - shelf_location_id = fields.Many2one('stock.location', string='所属库区') - - # 货架独有字段:通道、方向、货架高度(m)、货架层数、层数容量 - channel = fields.Char(string='通道', required=True, size=10) - direction = fields.Selection([ - ('R', 'R'), - ('L', 'L') - ], string='方向', required=True) - shelf_height = fields.Float(string='货架高度(m)') - shelf_layer = fields.Integer(string='货架层数') - layer_capacity = fields.Integer(string='层数容量') - shelf_rotative_Boolean = fields.Boolean('循环货架', default=False) - - # 是否有货位 - is_there_area = fields.Boolean(string='是否有货位', compute='_compute_is_there_area', default=False, store=True) - - @api.depends('location_ids') - def _compute_is_there_area(self): - for record in self: - record.is_there_area = bool(record.location_ids) - - @api.onchange('shelf_location_id') - def _onchange_shelf_location_id(self): - """ - 根据货架的所属库区修改货位的所属库区 - """ - if self.name: - all_location = self.env['sf.shelf.location'].search([('name', 'ilike', self.name)]) - for record in self: - for location in all_location: - location.location_id = record.shelf_location_id.id - - def create_location(self): - """ - 当仓库类型为货架时,自动生成其下面的货位,数量为货架层数*层数容量 - """ - area_obj = self.env['sf.shelf.location'] - for i in range(self.shelf_layer): - for j in range(self.layer_capacity): - location_name = self.name + '-' + str(i + 1) + '层' + '-' + str(j + 1) + '位置' - # 检查是否已经有同名的位置存在 - existing_location = area_obj.search([('name', '=', location_name)]) - if not existing_location: - area_obj.create({ - 'name': location_name, - 'location_id': self.shelf_location_id.id, - 'barcode': self.generate_barcode(i, j), - 'location_status': '空闲', - 'shelf_id': self.id - }) - - def generate_barcode(self, i, j): - """ - 生成货位条码 - """ - # 这里是你生成barcode的代码 - # area_type_barcode = self.location_id.barcode - 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 - - def print_all_location_barcode(self): - """ - 打印所有货位编码 - """ - print('=======打印货架所有货位编码=========') - for record in self.location_ids: - print('record', record) - if not record.barcode: - continue - record.ensure_one() - # qr_code_data = record.lot_qr_code - # if not qr_code_data: - # raise UserError("没有找到二维码数据。") - barcode = record.barcode - # todo 待控制 - if not barcode: - raise ValidationError("请先分配序列号") - # host = "192.168.50.110" # 可以根据实际情况修改 - # port = 9100 # 可以根据实际情况修改 - - # 获取默认打印机配置 - printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) - if not printer_config: - raise UserError('请先配置打印机') - host = printer_config.printer_id.ip_address - port = printer_config.printer_id.port - self.print_qr_code(barcode, host, port) - - -class ShelfLocation(models.Model): - _name = 'sf.shelf.location' - _inherit = ['printing.utils'] - _description = '货位' - # _rec_name = 'barcode' - _order = 'id asc, create_date asc' - - # current_location_id = fields.Many2one('sf.shelf.location', string='当前位置') - # # 目的位置 - # destination_location_id = fields.Many2one('sf.shelf.location', string='目的位置') - current_move_ids = fields.One2many('stock.move.line', 'current_location_id', '当前位置调拨单') - destination_move_ids = fields.One2many('stock.move.line', 'destination_location_id', '目标位置调拨单') - storage_time = fields.Datetime('入库时间', compute='_compute_location_status') - production_id = fields.Many2one('mrp.production', string='制造订单') - active = fields.Boolean("有效", default=True) - - @api.depends('location_status') - def _compute_location_status(self): - for record in self: - if record.location_status == '占用': - record.storage_time = datetime.datetime.now() - if record.location_status == '空闲': - record.storage_time = False - if record.location_status == '禁用': - record.storage_time = False - - name = fields.Char('货位名称', required=True, size=20) - barcode = fields.Char('货位编码', copy=False, size=50) - rotative_Boolean = fields.Boolean('循环货位', related='shelf_id.shelf_rotative_Boolean', store=True) - qr_code = fields.Binary(string='二维码', compute='_compute_location_qr_code', store=True) - - # 货架 - shelf_id = fields.Many2one('sf.shelf', string='货架') - - check_state = fields.Selection([ - ('enable', '启用'), - ('close', '关闭') - ], string='审核状态', default='close') - - def action_check(self): - self.check_state = 'enable' - - @api.depends('barcode') - def _compute_location_qr_code(self): - for record in self: - if record.barcode: - # 创建一个QRCode对象 - qr = qrcode.QRCode( - version=1, # 设置版本, 1-40,控制二维码的大小 - error_correction=qrcode.constants.ERROR_CORRECT_L, # 设置错误校正等级 - box_size=10, # 设置每个格子的像素大小 - border=4, # 设置边框的格子宽度 - ) - # 添加数据 - qr.add_data(record.barcode) - qr.make(fit=True) - # 创建二维码图像 - img = qr.make_image(fill_color="black", back_color="white") - # 创建一个内存文件 - buffer = io.BytesIO() - img.save(buffer, format="PNG") # 将图像保存到内存文件中 - # 获取二进制数据 - binary_data = buffer.getvalue() - # 使用Base64编码这些二进制数据 - data = base64.b64encode(binary_data) - self.qr_code = data - else: - record.qr_code = False - - def print_single_location_qr_code(self): - self.ensure_one() - qr_code_data = self.qr_code - if not qr_code_data: - raise UserError("没有找到二维码数据。") - barcode = self.barcode - # host = "192.168.50.110" # 可以根据实际情况修改 - # port = 9100 # 可以根据实际情况修改 - # 获取默认打印机配置 - printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) - if not printer_config: - raise UserError('请先配置打印机') - host = printer_config.printer_id.ip_address - port = printer_config.printer_id.port - self.print_qr_code(barcode, host, port) - # # 获取当前wizard的视图ID或其他标识信息 - # view_id = self.env.context.get('view_id') - # # 构造返回wizard页面的action字典 - # action = { - # 'type': 'ir.actions.act_window', - # 'name': '返回 Wizard', - # 'res_model': 'sf.shelf', # 替换为你的wizard模型名称 - # 'view_mode': 'form', - # 'view_id': view_id, # 如果需要基于特定的视图返回 - # 'target': 'new', # 如果需要在新的窗口或标签页打开 - # 'res_id': self.shelf_id, # 如果你想要返回当前记录的视图 - # } - # return action - - # # 仓库类别(selection:库区、库位、货位) - # location_type = fields.Selection([ - # ('货架', '货架'), - # ('货位', '货位') - # ], string='存储类型') - # 绑定库区 - # shelf_location_id = fields.Many2one('stock.location', string='所属库区', domain=[('location_type', '=', '库区')]) - location_id = fields.Many2one('stock.location', string='所属库区') - # 产品类别 (关联:product.category) - # product_type = fields.Many2many('product.category', string='产品类别') - - # picking_product_type = fields.Many2many('stock.picking', string='调拨产品类别', related='location_dest_id.product_type') - - # 货位独有字段:货位状态、产品(关联产品对象)、产品序列号(关联产品序列号对象) - location_status = fields.Selection([ - ('空闲', '空闲'), - ('占用', '占用'), - ('禁用', '禁用') - ], string='货位状态', default='空闲', compute='_compute_product_num', store=True) - # product_id = fields.Many2one('product.template', string='产品') - product_id = fields.Many2one('product.product', string='产品', compute='_compute_product_id', store=True) - product_sn_id = fields.Many2one('stock.lot', string='产品序列号') - product_sn_ids = fields.One2many('sf.shelf.location.lot', 'shelf_location_id', string='产品批次号') - # 产品数量 - product_num = fields.Integer('总数量', compute='_compute_number', store=True) - - @api.depends('product_num') - def _compute_product_num(self): - for record in self: - if record.product_num > 0: - record.location_status = '占用' - elif record.product_num == 0: - record.location_status = '空闲' - - @api.depends('product_sn_ids.qty') - def _compute_number(self): - for item in self: - if item.product_sn_ids: - qty = 0 - for product_sn_id in item.product_sn_ids: - qty += product_sn_id.qty - item.product_num = qty - - # 修改货位状态为禁用 - def action_location_status_disable(self): - self.location_status = '禁用' - - # 修改货位状态为空闲 - def action_location_status_enable(self): - self.location_status = '空闲' - - @api.depends('product_sn_id', 'product_sn_ids') - def _compute_product_id(self): - """ - 根据产品序列号,获取产品 - """ - for record in self: - if record.product_sn_id: - try: - record.sudo().product_id = record.product_sn_id.product_id - # record.sudo().location_status = '占用' - record.sudo().product_num = 1 - except Exception as e: - print('eeeeeee占用', e) - elif record.product_sn_ids: - return True - else: - try: - record.sudo().product_id = False - # record.sudo().location_status = '空闲' - record.sudo().product_num = 0 - except Exception as e: - print('eeeeeee空闲', e) - - # 调取获取货位信息接口 - def get_sf_shelf_location_info(self, device_id='Cabinet-AL'): - - config = self.env['res.config.settings'].get_values() - headers = {'Authorization': config['center_control_Authorization']} - crea_url = config['center_control_url'] + "/AutoDeviceApi/GetLocationInfos" - - params = {'DeviceId': device_id} - # r = requests.get(crea_url, params=params, headers=headers) - r = self.env['api.request.log'].log_request( - 'get', - crea_url, - name='库位信息', - responser='中控系统', - params=params, - headers=headers - ) - - ret = r.json() - - print(ret) - if ret['Succeed'] == True: - return ret['Datas'] - else: - raise UserError("该库位无产品") - - @api.model_create_multi - def create(self, vals_list): - # 编码重复校验 - barcode_list = [] - for val in vals_list: - location = self.search([('barcode', '=', val['barcode'])]) - if location: - barcode_list.append(val['name']) - if barcode_list: - raise UserError("货位编码【%s】存在重复" % barcode_list) - records = super(ShelfLocation, self).create(vals_list) - return records - - -class SfShelfLocationLot(models.Model): - _name = 'sf.shelf.location.lot' - _description = '批次数量' - - name = fields.Char('名称', related='lot_id.name') - shelf_location_id = fields.Many2one('sf.shelf.location', string="货位") - lot_id = fields.Many2one('stock.lot', string='批次号') - qty = fields.Integer('数量') - qty_num = fields.Integer('变更数量') - - @api.onchange('qty_num') - def _onchange_qty_num(self): - for item in self: - if item.qty_num > item.qty: - raise ValidationError('变更数量不能比库存数量大!!!') - - -class SfStockMoveLine(models.Model): - _name = 'stock.move.line' - _inherit = ['stock.move.line', 'printing.utils'] - - stock_lot_name = fields.Char('序列号名称', related='lot_id.name') - current_location_id = fields.Many2one( - 'sf.shelf.location', string='当前货位', compute='_compute_current_location_id', store=True, readonly=False, - domain="[('product_id', '=', product_id),'|',('product_sn_id.name', '=', stock_lot_name),('product_sn_ids.lot_id.name','=',stock_lot_name)]") - # location_dest_id = fields.Many2one('stock.location', string='目标库位') - location_dest_id_product_type = fields.Many2many(related='location_dest_id.product_type') - location_dest_id_value = fields.Integer(compute='_compute_location_dest_id_value', store=True) - # lot_qr_code = fields.Binary(string='二维码', compute='_compute_lot_qr_code', store=True) - lot_qr_code = fields.Binary(string='二维码', compute='_compute_lot_qr_code', store=True) - current_product_id = fields.Integer(compute='_compute_location_dest_id_value', store=True) - there_is_no_sn = fields.Boolean('是否有序列号', default=False) - - rfid = fields.Char('Rfid') - rfid_barcode = fields.Char('Rfid', compute='_compute_rfid') - - @api.depends('lot_id') - def _compute_rfid(self): - for item in self: - item.rfid_barcode = item.lot_id.rfid - - def action_revert_inventory(self): - # 检查用户是否有执行操作的权限 - if not self.env.user.has_group('sf_warehouse.group_sf_stock_user'): - raise UserError(_('抱歉,只有库管人员可以执行此动作')) - - # 如果用户有权限,调用父类方法 - return super().action_revert_inventory() - - @api.depends('lot_name') - def _compute_lot_qr_code(self): - for record in self: - if record.lot_id: - # record.lot_qr_code = record.lot_id.lot_qr_code - # 创建一个QRCode对象 - qr = qrcode.QRCode( - version=1, # 设置版本, 1-40,控制二维码的大小 - error_correction=qrcode.constants.ERROR_CORRECT_L, # 设置错误校正等级 - box_size=10, # 设置每个格子的像素大小 - border=4, # 设置边框的格子宽度 - ) - - # 添加数据 - qr.add_data(record.lot_id.name) - qr.make(fit=True) - - # 创建二维码图像 - img = qr.make_image(fill_color="black", back_color="white") - - # 创建一个内存文件 - buffer = io.BytesIO() - img.save(buffer, format="PNG") # 将图像保存到内存文件中 - - # 获取二进制数据 - binary_data = buffer.getvalue() - - # 使用Base64编码这些二进制数据 - data = base64.b64encode(binary_data) - self.lot_qr_code = data - else: - record.lot_qr_code = False - - def print_single_method(self): - self.ensure_one() - qr_code_data = self.lot_qr_code - if not qr_code_data: - raise UserError("没有找到二维码数据。") - lot_name = self.lot_name - - # 增加"当为坯料时,只打印序列号的前面部分" - if self.lot_name: # 确保 lot_name 存在 - if self.product_id.categ_id.name == '坯料': - lot_name = lot_name.split('[', 1)[0] - # host = "192.168.50.110" # 可以根据实际情况修改 - # port = 9100 # 可以根据实际情况修改 - - # 获取默认打印机配置 - printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) - if not printer_config: - raise UserError('请先配置打印机') - host = printer_config.printer_id.ip_address - port = printer_config.printer_id.port - self.print_qr_code(lot_name, host, port) - - # 返回当前wizard页面 - # base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') - # return { - # 'type': 'ir.actions.act_url', - # 'url': str(base_url) + download_url, - # 'target': 'self', - # } - # 获取当前wizard的视图ID或其他标识信息 - view_id = self.env.context.get('view_id') - # 构造返回wizard页面的action字典 - action = { - 'type': 'ir.actions.act_window', - 'name': '返回 Wizard', - 'res_model': 'stock.move', # 替换为你的wizard模型名称 - 'view_mode': 'form', - 'view_id': view_id, # 如果需要基于特定的视图返回 - 'target': 'new', # 如果需要在新的窗口或标签页打开 - 'res_id': self.id, # 如果你想要返回当前记录的视图 - } - return action - - # def generate_zpl_code(self, code): - # # 初始化ZPL代码字符串 - # zpl_code = "^XA\n" - # zpl_code += "^CW1,E:SIMSUN.TTF^FS\n" - # zpl_code += "^CI28\n" - # - # # 设置二维码位置 - # zpl_code += "^FO50,50\n" # 调整二维码位置,使其与资产编号在同一行 - # zpl_code += f"^BQN,2,6^FDLM,B0093{code}^FS\n" - # - # # 设置资产编号文本位置 - # zpl_code += "^FO300,60\n" # 资产编号文本的位置,与二维码在同一行 - # zpl_code += "^A1N,45,45^FD编码名称: ^FS\n" - # - # # 设置{code}文本位置 - # # 假设{code}文本需要位于资产编号和二维码下方,中间位置 - # # 设置{code}文本位置并启用自动换行 - # zpl_code += "^FO300,120\n" # {code}文本的起始位置 - # zpl_code += "^FB500,4,0,L,0\n" # 定义一个宽度为500点的文本框,最多4行,左对齐 - # zpl_code += f"^A1N,40,40^FD{code}^FS\n" - # - # # 在{code}文本框周围绘制线框 - # # 假设线框的外部尺寸为宽度500点,高度200点 - # # zpl_code += "^FO300,110^GB500,200,2^FS\n" # 绘制线框,边框粗细为2点 - # - # zpl_code += "^PQ1,0,1,Y\n" - # zpl_code += "^XZ\n" - # return zpl_code - # - # def send_to_printer(self, host, port, zpl_code): - # # 将ZPL代码转换为字节串 - # print('zpl_code', zpl_code) - # zpl_bytes = zpl_code.encode('utf-8') - # print(zpl_bytes) - # - # # 创建socket对象 - # mysocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # try: - # mysocket.connect((host, port)) # 连接到打印机 - # mysocket.send(zpl_bytes) # 发送ZPL代码 - # print("ZPL code sent to printer successfully.") - # except Exception as e: - # print(f"Error with the connection: {e}") - # finally: - # mysocket.close() # 关闭连接 - # - # def print_qr_code(self): - # self.ensure_one() # 确保这个方法只为一个记录调用 - # # if not self.lot_id: - # # raise UserError("没有找到序列号。") - # # 假设_lot_qr_code方法已经生成了二维码并保存在字段中 - # qr_code_data = self.lot_qr_code - # if not qr_code_data: - # raise UserError("没有找到二维码数据。") - # # 生成ZPL代码 - # zpl_code = self.generate_zpl_code(self.lot_id.name) - # # 设置打印机的IP地址和端口号 - # host = "192.168.50.110" - # port = 9100 - # # 发送ZPL代码到打印机 - # self.send_to_printer(host, port, zpl_code) - # - # # # 生成下载链接或直接触发下载 - # # # 此处的实现依赖于你的具体需求,以下是触发下载的一种示例 - # # attachment = self.env['ir.attachment'].sudo().create({ - # # 'datas': self.lot_qr_code, - # # 'type': 'binary', - # # 'description': '二维码图片', - # # 'name': self.lot_name + '.png', - # # # 'res_id': invoice.id, - # # # 'res_model': 'stock.picking', - # # 'public': True, - # # 'mimetype': 'application/x-png', - # # # 'model_name': 'stock.picking', - # # }) - # # # 返回附件的下载链接 - # # download_url = '/web/content/%s?download=true' % attachment.id - # # base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') - # # return { - # # 'type': 'ir.actions.act_url', - # # 'url': str(base_url) + download_url, - # # 'target': 'self', - # # } - - # # # 定义一个方法,用于根据序列号生成二维码 - # # @api.depends('lot_id') - # def generate_lot_qr_code(self): - # # 创建一个QRCode对象 - # qr = qrcode.QRCode( - # version=1, # 设置版本, 1-40,控制二维码的大小 - # error_correction=qrcode.constants.ERROR_CORRECT_L, # 设置错误校正等级 - # box_size=10, # 设置每个格子的像素大小 - # border=4, # 设置边框的格子宽度 - # ) - # - # # 添加数据 - # qr.add_data(self.lot_id.name) - # qr.make(fit=True) - # - # # 创建二维码图像 - # img = qr.make_image(fill_color="black", back_color="white") - # - # # 创建一个内存文件 - # buffer = io.BytesIO() - # img.save(buffer, format="PNG") # 将图像保存到内存文件中 - # - # # 获取二进制数据 - # binary_data = buffer.getvalue() - # - # # 使用Base64编码这些二进制数据 - # data = base64.b64encode(binary_data) - # self.lot_qr_code = data - # attachment = self.env['ir.attachment'].sudo().create({ - # 'datas': data, - # 'type': 'binary', - # 'description': '二维码图片', - # 'name': self.lot_id.name + '.png', - # # 'res_id': invoice.id, - # # 'res_model': 'stock.picking', - # 'public': True, - # 'mimetype': 'application/pdf', - # # 'model_name': 'stock.picking', - # }) - - # def button_test(self): - # print(self.picking_id.name) - # stock_picking = self.env['stock.picking'].search([('name', '=', self.picking_id.name)], limit=1) - # print(self.picking_id.name) - # print(aa.move_line_ids.lot_id.name) - # # 获取当前的stock.picking对象 - # current_picking = self.env['stock.picking'].search([('name', '=', self.picking_id.name)], limit=1) - # - # # 获取当前picking的第一个stock.move对象 - # current_move = current_picking.move_ids[0] if current_picking.move_ids else False - # - # # 如果存在相关的stock.move对象 - # if current_move: - # # 获取源stock.move对象 - # origin_move = current_move.move_orig_ids[0] if current_move.move_orig_ids else False - # - # # 从源stock.move对象获取源stock.picking对象 - # origin_picking = origin_move.picking_id if origin_move else False - # # 现在,origin_picking就是current_picking的上一步 - # # 获取目标stock.move对象 - # dest_move = current_move.move_dest_ids[0] if current_move.move_dest_ids else False - # - # # 从目标stock.move对象获取目标stock.picking对象 - # dest_picking = dest_move.picking_id if dest_move else False - # # 现在,dest_picking就是current_picking的下一步 - # 添加所有需要的依赖字段 - @api.depends('location_id') - def _compute_current_location_id(self): - # 批量获取所有相关记录的picking - pickings = self.mapped('picking_id') - - # 构建源picking的移库行与目标位置的映射 - origin_location_map = {} - for picking in pickings: - # 获取源picking - origin_move = picking.move_ids[:1].move_orig_ids[:1] - if not origin_move: - continue - - origin_picking = origin_move.picking_id - if not origin_picking: - continue - - # 为每个picking构建lot_id到location的映射 - origin_location_map[picking.id] = { - move_line.lot_id.id: move_line.destination_location_id - for move_line in origin_picking.move_line_ids.filtered( - lambda ml: ml.destination_location_id and ml.lot_id - ) - } - - # 批量更新current_location_id - for record in self: - current_picking = record.picking_id - if not current_picking: - record.current_location_id = False - continue - - # 获取当前picking对应的lot_location映射 - lot_dest_map = origin_location_map.get(current_picking.id, {}) - - # 查找匹配的lot_id - for move_line in current_picking.move_line_ids: - if move_line.lot_id and move_line.lot_id.id in lot_dest_map: - record.current_location_id = lot_dest_map[move_line.lot_id.id] - break - else: - record.current_location_id = False - - # 是一张单据一张单据往下走的,所以这里的目标货位是上一张单据的当前货位,且这样去计算是可以的。 - @api.depends('location_dest_id') - def _compute_location_dest_id_value(self): - for record in self: - record.location_dest_id_value = record.location_dest_id.id if record.location_dest_id else False - record.current_product_id = record.product_id.id if record.product_id else False - - destination_location_id = fields.Many2one( - 'sf.shelf.location', string='目标货位') - - def compute_destination_location_id(self): - for record in self: - obj = self.env['sf.shelf.location'].search([('name', '=', - record.destination_location_id.name)]) - if obj and obj.product_id and obj.product_id != record.product_id: - raise ValidationError('目标货位【%s】已被【%s】产品占用!' % (obj.code, obj.product_id)) - if record.lot_id: - if record.product_id.tracking == 'serial': - shelf_location_obj = self.env['sf.shelf.location'].search( - [('product_sn_id', '=', record.lot_id.id)]) - if shelf_location_obj: - shelf_location_obj.product_sn_id = False - if obj: - obj.product_sn_id = record.lot_id.id - else: - if obj: - obj.product_sn_id = record.lot_id.id - elif record.product_id.tracking == 'lot': - record.put_shelf_location(record) - if not obj.product_id: - obj.product_id = record.product_id.id - else: - if obj: - obj.product_id = record.product_id.id - # obj.location_status = '占用' - obj.product_num += record.qty_done - - @api.onchange('destination_location_id') - def _check_destination_location_id(self): - for item in self: - if item: - barcode = item.destination_location_id.barcode - for line in item.picking_id.move_line_ids_without_package: - if line.destination_location_id: - if (barcode and barcode == line.destination_location_id.barcode - and item.product_id != line.product_id): - raise ValidationError( - '【%s】货位已经被占用,请重新选择!!!' % item.destination_location_id.barcode) - - def put_shelf_location(self, vals): - """ - 对货位的批量数据进行数量计算 - """ - for record in vals: - if record.lot_id and record.product_id.tracking == 'lot': - if record.current_location_id: - location_lot = self.env['sf.shelf.location.lot'].sudo().search( - [('shelf_location_id', '=', record.current_location_id.id), ('lot_id', '=', record.lot_id.id)]) - if location_lot: - location_lot.qty -= record.qty_done - if location_lot.qty == 0: - location_lot.unlink() - elif location_lot.qty < 0: - raise ValidationError('【%s】货位【%s】批次的【%s】产品数量不足!' % ( - record.current_location_id.barcode, record.lot_id.name, record.product_id.name)) - else: - raise ValidationError('【%s】货位不存在【%s】批次的【%s】产品' % ( - record.current_location_id.barcode, record.lot_id.name, record.product_id.name)) - if record.destination_location_id: - location_lot = self.env['sf.shelf.location.lot'].sudo().search( - [('shelf_location_id', '=', record.destination_location_id.id), - ('lot_id', '=', record.lot_id.id)]) - if location_lot: - location_lot.qty += record.qty_done - else: - self.env['sf.shelf.location.lot'].sudo().create({ - 'shelf_location_id': record.destination_location_id.id, - 'lot_id': record.lot_id.id, - 'qty': record.qty_done - }) - if not record.destination_location_id.product_id: - record.destination_location_id.product_id = record.product_id.id - - -class SfStockPicking(models.Model): - _inherit = 'stock.picking' - - check_in = fields.Char(string='查询是否为入库单', compute='_check_is_in') - product_uom_qty_sp = fields.Float('需求数量', compute='_compute_product_uom_qty_sp', store=True) - - @api.depends('move_ids_without_package', 'move_ids_without_package.product_uom_qty') - def _compute_product_uom_qty_sp(self): - for sp in self: - if sp.move_ids_without_package: - sp.product_uom_qty_sp = 0 - for move_id in sp.move_ids_without_package: - sp.product_uom_qty_sp += move_id.product_uom_qty - else: - sp.product_uom_qty_sp = 0 - - def batch_stock_move(self): - """ - 批量调拨,非就绪状态的会被忽略,完成后有通知提示 - """ - # 对所以调拨单的质检单进行是否完成校验 - sp_ids = [sp.id for sp in self] - qc_ids = self.env['quality.check'].sudo().search( - [('picking_id', 'in', sp_ids), ('quality_state', 'in', ['waiting', 'none'])]) - if qc_ids: - raise ValidationError(f'单据{list(set(qc.picking_id.name for qc in qc_ids))}未完成质量检查,完成后再试。') - for record in self: - if record.state != 'assigned': - continue - for move in record.move_ids: - move.action_show_details() - record.action_set_quantities_to_reservation() - record.button_validate() - - notification_message = '批量调拨完成!请注意,状态非就绪的单据会被忽略' - return { - 'effect': { - 'fadeout': 'fast', - 'message': notification_message, - 'img_url': '/web/image/%s/%s/image_1024' % ( - self.create_uid._name, self.create_uid.id) if 0 else '/web/static/img/smile.svg', - 'type': 'rainbow_man', - } - } - - @api.depends('name') - def _check_is_in(self): - """ - 判断是否为出库单 - """ - for record in self: - if record.name: - is_check_in = record.name.split('/') - record.check_in = is_check_in[1] - - def button_validate(self): - """ - 重写验证方法,当验证时意味着调拨单已经完成,已经移动到了目标货位,所以需要将当前货位的状态改为空闲 - """ - res = super(SfStockPicking, self).button_validate() - for line in self.move_line_ids: - if line: - if line.destination_location_id: - # 调用入库方法进行入库刀货位 - line.compute_destination_location_id() - else: - # 对除刀柄之外的刀具物料入库到刀具房进行 目标货位必填校验 - if self.location_dest_id.name == '刀具房' and line.product_id.cutting_tool_material_id.name not in ( - '刀柄', False): - raise ValidationError('请选择【%s】产品的目标货位!' % line.product_id.name) - if line.current_location_id: - # 对货位的批次产品进行出货 - line.put_shelf_location(line) - - if line.current_location_id: - # 按序列号管理的产品 - if line.current_location_id.product_sn_id: - line.current_location_id.product_sn_id = False - # line.current_location_id.location_status = '空闲' - line.current_location_id.product_num = 0 - line.current_location_id.product_id = False - else: - # 对除刀柄之外的刀具物料从刀具房出库进行 当前货位必填校验 - if self.location_id.name == '刀具房' and line.product_id.cutting_tool_material_id.name not in ( - '刀柄', False): - raise ValidationError('请选择【%s】产品的当前货位!' % line.product_id.name) - - # 对入库作业的刀柄和托盘进行Rfid绑定校验 - for move in self.move_ids: - if move and move.product_id.cutting_tool_material_id.name == '刀柄' or '托盘' in ( - move.product_id.fixture_material_id.name or ''): - for item in move.move_line_nosuggest_ids: - if item.rfid: - if self.env['stock.lot'].search([('rfid', '=', item.rfid)]): - raise ValidationError('该Rfid【%s】在系统中已经存在,请重新录入!' % item.rfid) - if item.location_dest_id.name == '进货': - if not item.rfid: - raise ValidationError('你需要提供%s的Rfid' % move.product_id.name) - self.env['stock.lot'].search([('name', '=', item.lot_name)]).write({'rfid': item.rfid}) - return res - - -# def print_all_barcode(self): -# """ -# 打印所有编码 -# """ -# print('================') -# for record in self.move_ids_without_package: -# print('record', record) -# print('record.move_line_ids', record.move_line_ids) -# -# # record.move_line_ids.print_qr_code() -# -# print('record.move_line_ids.lot_id', record.move_line_ids.lot_id) -# print('record.move_line_ids.lot_id.name', record.move_line_ids.lot_id.name) - - -class SfProcurementGroup(models.Model): - _inherit = 'procurement.group' - - @api.model - def _search_rule(self, route_ids, packaging_id, product_id, warehouse_id, domain): - """ - 修改路线多规则条件选取 - """ - if warehouse_id: - domain = expression.AND( - [['|', ('warehouse_id', '=', warehouse_id.id), ('warehouse_id', '=', False)], domain]) - Rule = self.env['stock.rule'] - res = self.env['stock.rule'] - if route_ids: - res_list = Rule.search(expression.AND([[('route_id', 'in', route_ids.ids)], domain]), - order='route_sequence, sequence') - for res1 in res_list: - if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ - res1.location_src_id.product_type: - res = res1 - if not res: - res = Rule.search(expression.AND([[('route_id', 'in', route_ids.ids)], domain]), - order='route_sequence, sequence', limit=1) - - if not res and packaging_id: - packaging_routes = packaging_id.route_ids - if packaging_routes: - res_list = Rule.search(expression.AND([[('route_id', 'in', packaging_routes.ids)], domain]), - order='route_sequence, sequence') - for res1 in res_list: - if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ - res1.location_src_id.product_type: - res = res1 - if not res: - res = Rule.search(expression.AND([[('route_id', 'in', packaging_routes.ids)], domain]), - order='route_sequence, sequence', limit=1) - if not res: - product_routes = product_id.route_ids | product_id.categ_id.total_route_ids - if product_routes: - res_list = Rule.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), - order='route_sequence, sequence') - for res1 in res_list: - if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ - res1.location_src_id.product_type: - res = res1 - if not res: - res = Rule.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), - order='route_sequence, sequence', limit=1) - if not res and warehouse_id: - warehouse_routes = warehouse_id.route_ids - if warehouse_routes: - res_list = Rule.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), - order='route_sequence, sequence') - for res1 in res_list: - if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ - res1.location_src_id.product_type: - res = res1 - if not res: - res = Rule.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), - order='route_sequence, sequence', limit=1) - return res - - -# class SfPickingType(models.Model): -# _inherit = 'stock.picking.type' -# -# def _default_show_operations(self): -# return self.user_has_groups('stock.group_production_lot,' -# 'stock.group_stock_multi_locations,' -# 'stock.group_tracking_lot', -# 'sf_warehouse.group_sf_stock_user', -# 'sf_warehouse.group_sf_stock_manager') - -class SfPickingType(models.Model): - _inherit = 'stock.picking.type' - - code = fields.Selection([('incoming', 'Receipt'), ('outgoing', 'Delivery'), ('internal', '厂内出入库')], - 'Type of Operation', required=True) - - def _default_show_operations(self): - return self.user_has_groups( - 'stock.group_production_lot,' - 'stock.group_stock_multi_locations,' - 'stock.group_tracking_lot,' - 'sf_warehouse.group_sf_stock_user,' - 'sf_warehouse.group_sf_stock_manager' - ) - - def _get_action(self, action_xmlid): - action = super(SfPickingType, self)._get_action(action_xmlid) - if not self.env.user.has_group('base.group_system'): - action['context']['create'] = False - if self.sequence_code in ['INT', 'PC']: - action['context']['search_default_retrospect'] = 1 - return action - - -class CustomStockMove(models.Model): - _name = 'stock.move' - _inherit = ['stock.move', 'printing.utils', 'barcodes.barcode_events_mixin'] - - def on_barcode_scanned(self, barcode): - """ - 采购入库扫码绑定Rfid码 - """ - for record in self: - logging.info('Rfid:%s' % barcode) - if record: - lot = self.env['stock.lot'].sudo().search([('rfid', '=', barcode)]) - if lot: - if lot.product_id.cutting_tool_material_id: - material = lot.product_id.cutting_tool_material_id.name - else: - material = lot.product_id.fixture_material_id.name - raise ValidationError( - '该Rfid【%s】已经被序列号为【%s】的【%s】物料所占用!' % (barcode, lot.name, material)) - if '刀柄' in (record.product_id.cutting_tool_material_id.name or '') or '托盘' in ( - record.product_id.fixture_material_id.name or ''): - logging.info('开始录入Rfid:%s' % record.move_line_nosuggest_ids) - for move_line_nosuggest_id in record.move_line_nosuggest_ids: - logging.info('录入的记录%s , Rfid:%s' % (move_line_nosuggest_id, move_line_nosuggest_id.rfid)) - if move_line_nosuggest_id.rfid: - if move_line_nosuggest_id.rfid == barcode: - if record.product_id.cutting_tool_material_id.name: - raise ValidationError('该刀柄的Rfid已经录入,请勿重复录入!!!') - else: - raise ValidationError('该托盘的Rfid已经录入,请勿重复录入!!!') - else: - line_id = int(re.sub(r"\D", "", str(move_line_nosuggest_id.id))) - res = self.env['stock.move.line'].sudo().search([('id', '=', line_id)]).write( - {'rfid': barcode}) - logging.info('Rfid是否录入:%s' % res) - move_line_nosuggest_id.rfid = barcode - break - else: - raise ValidationError('该产品不需要录入Rfid!!!') - - def action_assign_serial_show_details(self): - # 首先执行原有逻辑 - result = super(CustomStockMove, self).action_assign_serial_show_details() - # 接着为每个 lot_name 生成二维码 - move_lines = self.move_line_ids # 获取当前 stock.move 对应的所有 stock.move.line 记录 - for line in move_lines: - if line.lot_name: # 确保 lot_name 存在 - lot_name = line.lot_name - if line.product_id.categ_id.name == '坯料': - lot_name = lot_name.split('[', 1)[0] - qr_data = self.compute_lot_qr_code(lot_name) - # 假设 stock.move.line 模型中有一个字段叫做 lot_qr_code 用于存储二维码数据 - line.lot_qr_code = qr_data - return result - - def compute_lot_qr_code(self, lot_name): - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - qr.add_data(lot_name) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - buffer = io.BytesIO() - img.save(buffer, format="PNG") - binary_data = buffer.getvalue() - data = base64.b64encode(binary_data).decode() # 确保返回的是字符串形式的数据 - return data - - def print_all_barcode(self): - """ - 打印所有编码 - """ - print('================') - for record in self.move_line_ids: - print('record', record) - if not record.lot_name: - continue - record.ensure_one() - # qr_code_data = record.lot_qr_code - # if not qr_code_data: - # raise UserError("没有找到二维码数据。") - lot_name = record.lot_name - # todo 待控制 - if not lot_name: - raise ValidationError("请先分配序列号") - - # 增加"当为坯料时,只打印序列号的前面部分" - if record.lot_name: # 确保 lot_name 存在 - if record.product_id.categ_id.name == '坯料': - lot_name = lot_name.split('[', 1)[0] - - # host = "192.168.50.110" # 可以根据实际情况修改 - # port = 9100 # 可以根据实际情况修改 - - # 获取默认打印机配置 - printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) - if not printer_config: - raise UserError('请先配置打印机') - host = printer_config.printer_id.ip_address - port = printer_config.printer_id.port - record.print_qr_code(lot_name, host, port) +# -*- coding: utf-8 -*- +import re + +import datetime +import logging +import base64 +import qrcode +import io + +import requests + +from odoo import api, fields, models, _ +from odoo.osv import expression +from odoo.exceptions import UserError, ValidationError + + +class SfLocation(models.Model): + _inherit = 'stock.location' + + # 重写字段定义 + name = fields.Char('Location Name', required=True, size=20) + barcode = fields.Char('Barcode', copy=False, size=15) + + # 仓库类别(selection:库区、库位、货位) + # location_type = fields.Selection([ + # ('库区', '库区'), + # ('货架', '货架'), + # ('货位', '货位') + # ], string='存储类型') + location_type = fields.Selection([ + ('库区', '库区') + ], string='存储类型') + # 库区类型(selection:拣货区、存货区、收货区、退货区、次品区) + area_type = fields.Selection([ + ('拣货区', '拣货区'), + ('存货区', '存货区'), + ('收货区', '收货区'), + ('退货区', '退货区'), + ('次品区', '次品区') + ], string='库区类型') + # 当前位置 + current_location_id = fields.Many2one('sf.shelf.location', string='当前位置') + # 目的位置 + destination_location_id = fields.Many2one('sf.shelf.location', string='目的位置') + # 存储类型(selection:库区、货架) + # storage_type = fields.Selection([ + # ('库区', '库区'), + # ('货架', '货架') + # ], string='存储类型') + # 产品类别 (关联:product.category) + product_type = fields.Many2many('product.category', string='产品类别') + # 货架独有字段:通道、方向、货架高度(m)、货架层数、层数容量 + channel = fields.Char(string='通道') + direction = fields.Selection([ + ('R', 'R'), + ('L', 'L') + ], string='方向') + shelf_height = fields.Float(string='货架高度(m)') + shelf_layer = fields.Integer(string='货架层数') + layer_capacity = fields.Integer(string='层数容量') + + # 货位独有字段:货位状态、产品(关联产品对象)、产品序列号(关联产品序列号对象) + location_status = fields.Selection([ + ('空闲', '空闲'), + ('占用', '占用'), + ('禁用', '禁用') + ], string='货位状态', default='空闲') + # product_id = fields.Many2one('product.template', string='产品') + product_id = fields.Many2one('product.product', string='产品', compute='_compute_product_id', readonly=True) + product_sn_id = fields.Many2one('stock.lot', string='产品序列号') + # time_test = fields.Char(string='time') + # 添加SQL约束 + # _sql_constraints = [ + # ('name_uniq', 'unique(name)', '位置名称必须唯一!'), + # ] + + hide_location_type = fields.Boolean(compute='_compute_hide_what', string='隐藏仓库') + hide_area = fields.Boolean(compute='_compute_hide_what', string='隐藏库区') + hide_shelf = fields.Boolean(compute='_compute_hide_what', string='隐藏货架') + hide_location = fields.Boolean(compute='_compute_hide_what', string='隐藏货位') + + # @api.model + # def create(self, vals): + # """ + # 重写create方法,添加自定义的约束 + # """ + # print('create', vals) + # if vals.get('location_id'): + # location = self.env['stock.location'].browse(vals.get('location_id')) + # if location.storage_type == '库区': + # raise UserError('库区不能作为父级仓库') + # return super().create(vals) + # + # @api.onchange('location_id') + # def _onchange_location_id(self): + # """ + # 重写onchange方法,添加自定义的约束 + # """ + # if self.location_id: + # if self.location_id.storage_type == '库区': + # raise UserError('库区不能作为父级仓库') + + # @api.constrains('shelf_height') + # def _check_shelf_height(self): + # for record in self: + # if not (0 <= record.shelf_height < 1000): # 限制字段值在0到999之间 + # raise UserError('shelf_height的值必须在0到1000之间') + # + # @api.constrains('shelf_layer') + # def _check_shelf_layer(self): + # for record in self: + # if not (0 < record.shelf_layer < 1000): + # raise UserError('shelf_layer的值必须在0到999之间,且不能为0') + # + # @api.constrains('layer_capacity') + # def _check_layer_capacity(self): + # for record in self: + # if not (0 <= record.layer_capacity < 1000): + # raise UserError('layer_capacity的值必须在0到999之间,且不能为0') + + @api.depends('product_sn_id') + def _compute_product_id(self): + """ + 根据产品序列号,获取产品 + """ + for record in self: + if record.product_sn_id: + record.product_id = record.product_sn_id.product_id + # record.location_status = '占用' + else: + record.product_id = False + # record.location_status = '空闲' + + @api.depends('location_type') + def _compute_hide_what(self): + """ + 根据仓库类别,隐藏不需要的字段 + :return: + """ + for record in self: + record.hide_location_type = False + record.hide_area = False + record.hide_shelf = False + record.hide_location = False + if record.location_type and record.location_type == '仓库': + record.hide_location_type = True + elif record.location_type and record.location_type == '库区': + record.hide_area = True + elif record.location_type and record.location_type == '货架': + record.hide_shelf = True + elif record.location_type and record.location_type == '货位': + record.hide_location = True + else: + pass + + # # 添加Python约束 + # @api.constrains('name', 'barcode') + # def _check_len(self): + # for rec in self: + # if len(rec.name) > 20: + # raise ValidationError("Location Name length must be less equal than 20!") + # if len(rec.barcode) > 15: + # raise ValidationError("Barcode length must be less equal than 15!") + + # @api.model + # def default_get(self, fields): + # print('fields:', fields) + # res = super(SfLocation, self).default_get(fields) + # print('res:', res) + # if 'barcode' in fields and 'barcode' not in res: + # # 这里是你生成barcode的代码 + # pass + # # res['barcode'] = self.generate_barcode() # 假设你有一个方法generate_barcode来生成barcode + # return res + # @api.model + # def create(self, vals): + # """ + # 重写create方法,当仓库类型为货架时,自动生成其下面的货位,数量为货架层数*层数容量 + # """ + # res = super(SfLocation, self).create(vals) + # if res.location_type == '货架': + # for i in range(res.shelf_layer): + # for j in range(res.layer_capacity): + # self.create({ + # 'name': res.name + '-' + str(i+1) + '-' + str(j+1), + # 'location_id': res.id, + # 'location_type': '货位', + # 'barcode': self.generate_barcode(res, i, j), + # 'location_status': '空闲' + # }) + # return res + + # 生成货位 + + def create_location(self): + """ + 当仓库类型为货架时,自动生成其下面的货位,数量为货架层数*层数容量 + """ + pass + # if self.location_type == '货架': + # for i in range(self.shelf_layer): + # for j in range(self.layer_capacity): + # self.create({ + # 'name': self.name + '-' + str(i + 1) + '层' + '-' + str(j + 1) + '位置', + # 'location_id': self.id, + # 'location_type': '货位', + # 'barcode': self.generate_barcode(i, j), + # 'location_status': '空闲' + # }) + + def generate_barcode(self, i, j): + """ + 生成货位条码 + """ + pass + # # 这里是你生成barcode的代码 + # # area_type_barcode = self.location_id.barcode + # 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 + + +class SfShelf(models.Model): + _name = 'sf.shelf' + _inherit = ['printing.utils'] + _description = '货架' + _order = 'create_date desc' + + name = fields.Char('货架名称', required=True, size=20) + active = fields.Boolean("有效", default=True) + barcode = fields.Char('编码', copy=False, size=15, required=True) + + # 货位 + location_ids = fields.One2many('sf.shelf.location', 'shelf_id', string='货位') + + check_state = fields.Selection([ + ('enable', '启用'), + ('close', '关闭') + ], string='审核状态', default='close') + + def action_check(self): + self.check_state = 'enable' + + # 绑定库区 + shelf_location_id = fields.Many2one('stock.location', string='所属库区') + + # 货架独有字段:通道、方向、货架高度(m)、货架层数、层数容量 + channel = fields.Char(string='通道', required=True, size=10) + direction = fields.Selection([ + ('R', 'R'), + ('L', 'L') + ], string='方向', required=True) + shelf_height = fields.Float(string='货架高度(m)') + shelf_layer = fields.Integer(string='货架层数') + layer_capacity = fields.Integer(string='层数容量') + shelf_rotative_Boolean = fields.Boolean('循环货架', default=False) + + # 是否有货位 + is_there_area = fields.Boolean(string='是否有货位', compute='_compute_is_there_area', default=False, store=True) + + @api.depends('location_ids') + def _compute_is_there_area(self): + for record in self: + record.is_there_area = bool(record.location_ids) + + @api.onchange('shelf_location_id') + def _onchange_shelf_location_id(self): + """ + 根据货架的所属库区修改货位的所属库区 + """ + if self.name: + all_location = self.env['sf.shelf.location'].search([('name', 'ilike', self.name)]) + for record in self: + for location in all_location: + location.location_id = record.shelf_location_id.id + + def create_location(self): + """ + 当仓库类型为货架时,自动生成其下面的货位,数量为货架层数*层数容量 + """ + area_obj = self.env['sf.shelf.location'] + for i in range(self.shelf_layer): + for j in range(self.layer_capacity): + location_name = self.name + '-' + str(i + 1) + '层' + '-' + str(j + 1) + '位置' + # 检查是否已经有同名的位置存在 + existing_location = area_obj.search([('name', '=', location_name)]) + if not existing_location: + area_obj.create({ + 'name': location_name, + 'location_id': self.shelf_location_id.id, + 'barcode': self.generate_barcode(i, j), + 'location_status': '空闲', + 'shelf_id': self.id + }) + + def generate_barcode(self, i, j): + """ + 生成货位条码 + """ + # 这里是你生成barcode的代码 + # area_type_barcode = self.location_id.barcode + area_type_barcode = self.barcode + i_str = str(i + 1).zfill(3) # 确保是两位数,如果不足两位,左侧补0 + j_str = str(j + 1).zfill(3) # 确保是两位数,如果不足两位,左侧补0 + 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): + """ + 打印所有货位编码 + """ + print('=======打印货架所有货位编码=========') + for record in self.location_ids: + print('record', record) + if not record.barcode: + continue + record.ensure_one() + # qr_code_data = record.lot_qr_code + # if not qr_code_data: + # raise UserError("没有找到二维码数据。") + barcode = record.barcode + # todo 待控制 + if not barcode: + raise ValidationError("请先分配序列号") + # host = "192.168.50.110" # 可以根据实际情况修改 + # port = 9100 # 可以根据实际情况修改 + + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + host = printer_config.printer_id.ip_address + port = printer_config.printer_id.port + self.print_qr_code(barcode, host, port) + + +class ShelfLocation(models.Model): + _name = 'sf.shelf.location' + _inherit = ['printing.utils'] + _description = '货位' + # _rec_name = 'barcode' + _order = 'id asc, create_date asc' + + # current_location_id = fields.Many2one('sf.shelf.location', string='当前位置') + # # 目的位置 + # destination_location_id = fields.Many2one('sf.shelf.location', string='目的位置') + current_move_ids = fields.One2many('stock.move.line', 'current_location_id', '当前位置调拨单') + destination_move_ids = fields.One2many('stock.move.line', 'destination_location_id', '目标位置调拨单') + storage_time = fields.Datetime('入库时间', compute='_compute_location_status') + production_id = fields.Many2one('mrp.production', string='制造订单') + active = fields.Boolean("有效", default=True) + + @api.depends('location_status') + def _compute_location_status(self): + for record in self: + if record.location_status == '占用': + record.storage_time = datetime.datetime.now() + if record.location_status == '空闲': + record.storage_time = False + if record.location_status == '禁用': + record.storage_time = False + + name = fields.Char('货位名称', required=True, size=20) + barcode = fields.Char('货位编码', copy=False, size=50) + rotative_Boolean = fields.Boolean('循环货位', related='shelf_id.shelf_rotative_Boolean', store=True) + qr_code = fields.Binary(string='二维码', compute='_compute_location_qr_code', store=True) + + # 货架 + shelf_id = fields.Many2one('sf.shelf', string='货架') + + check_state = fields.Selection([ + ('enable', '启用'), + ('close', '关闭') + ], string='审核状态', default='close') + + def action_check(self): + self.check_state = 'enable' + + @api.depends('barcode') + def _compute_location_qr_code(self): + for record in self: + if record.barcode: + # 创建一个QRCode对象 + qr = qrcode.QRCode( + version=1, # 设置版本, 1-40,控制二维码的大小 + error_correction=qrcode.constants.ERROR_CORRECT_L, # 设置错误校正等级 + box_size=10, # 设置每个格子的像素大小 + border=4, # 设置边框的格子宽度 + ) + # 添加数据 + qr.add_data(record.barcode) + qr.make(fit=True) + # 创建二维码图像 + img = qr.make_image(fill_color="black", back_color="white") + # 创建一个内存文件 + buffer = io.BytesIO() + img.save(buffer, format="PNG") # 将图像保存到内存文件中 + # 获取二进制数据 + binary_data = buffer.getvalue() + # 使用Base64编码这些二进制数据 + data = base64.b64encode(binary_data) + self.qr_code = data + else: + record.qr_code = False + + def print_single_location_qr_code(self): + self.ensure_one() + qr_code_data = self.qr_code + if not qr_code_data: + raise UserError("没有找到二维码数据。") + barcode = self.barcode + # host = "192.168.50.110" # 可以根据实际情况修改 + # port = 9100 # 可以根据实际情况修改 + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + host = printer_config.printer_id.ip_address + port = printer_config.printer_id.port + self.print_qr_code(barcode, host, port) + # # 获取当前wizard的视图ID或其他标识信息 + # view_id = self.env.context.get('view_id') + # # 构造返回wizard页面的action字典 + # action = { + # 'type': 'ir.actions.act_window', + # 'name': '返回 Wizard', + # 'res_model': 'sf.shelf', # 替换为你的wizard模型名称 + # 'view_mode': 'form', + # 'view_id': view_id, # 如果需要基于特定的视图返回 + # 'target': 'new', # 如果需要在新的窗口或标签页打开 + # 'res_id': self.shelf_id, # 如果你想要返回当前记录的视图 + # } + # return action + + # # 仓库类别(selection:库区、库位、货位) + # location_type = fields.Selection([ + # ('货架', '货架'), + # ('货位', '货位') + # ], string='存储类型') + # 绑定库区 + # shelf_location_id = fields.Many2one('stock.location', string='所属库区', domain=[('location_type', '=', '库区')]) + location_id = fields.Many2one('stock.location', string='所属库区') + # 产品类别 (关联:product.category) + # product_type = fields.Many2many('product.category', string='产品类别') + + # picking_product_type = fields.Many2many('stock.picking', string='调拨产品类别', related='location_dest_id.product_type') + + # 货位独有字段:货位状态、产品(关联产品对象)、产品序列号(关联产品序列号对象) + location_status = fields.Selection([ + ('空闲', '空闲'), + ('占用', '占用'), + ('禁用', '禁用') + ], string='货位状态', default='空闲', compute='_compute_product_num', store=True) + # product_id = fields.Many2one('product.template', string='产品') + product_id = fields.Many2one('product.product', string='产品', compute='_compute_product_id', store=True) + product_sn_id = fields.Many2one('stock.lot', string='产品序列号') + product_sn_ids = fields.One2many('sf.shelf.location.lot', 'shelf_location_id', string='产品批次号') + # 产品数量 + product_num = fields.Integer('总数量', compute='_compute_number', store=True) + + @api.depends('product_num') + def _compute_product_num(self): + for record in self: + if record.product_num > 0: + record.location_status = '占用' + elif record.product_num == 0: + record.location_status = '空闲' + + @api.depends('product_sn_ids.qty') + def _compute_number(self): + for item in self: + if item.product_sn_ids: + qty = 0 + for product_sn_id in item.product_sn_ids: + qty += product_sn_id.qty + item.product_num = qty + + # 修改货位状态为禁用 + def action_location_status_disable(self): + self.location_status = '禁用' + + # 修改货位状态为空闲 + def action_location_status_enable(self): + self.location_status = '空闲' + + @api.depends('product_sn_id', 'product_sn_ids') + def _compute_product_id(self): + """ + 根据产品序列号,获取产品 + """ + for record in self: + if record.product_sn_id: + try: + record.sudo().product_id = record.product_sn_id.product_id + # record.sudo().location_status = '占用' + record.sudo().product_num = 1 + except Exception as e: + print('eeeeeee占用', e) + elif record.product_sn_ids: + return True + else: + try: + record.sudo().product_id = False + # record.sudo().location_status = '空闲' + record.sudo().product_num = 0 + except Exception as e: + print('eeeeeee空闲', e) + + # 调取获取货位信息接口 + def get_sf_shelf_location_info(self, device_id='Cabinet-AL'): + + config = self.env['res.config.settings'].get_values() + headers = {'Authorization': config['center_control_Authorization']} + crea_url = config['center_control_url'] + "/AutoDeviceApi/GetLocationInfos" + + params = {'DeviceId': device_id} + # r = requests.get(crea_url, params=params, headers=headers) + r = self.env['api.request.log'].log_request( + 'get', + crea_url, + name='库位信息', + responser='中控系统', + params=params, + headers=headers + ) + + ret = r.json() + + print(ret) + if ret['Succeed'] == True: + return ret['Datas'] + else: + raise UserError("该库位无产品") + + @api.model_create_multi + def create(self, vals_list): + # 编码重复校验 + barcode_list = [] + for val in vals_list: + location = self.search([('barcode', '=', val['barcode'])]) + if location: + barcode_list.append(val['name']) + if barcode_list: + raise UserError("货位编码【%s】存在重复" % barcode_list) + 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 + record.kanban_show_layer_info=f"{_layer}-{_layer_capacity}" + record.kanban_show_center_control_code=f"{_cc_code}" + +class SfShelfLocationLot(models.Model): + _name = 'sf.shelf.location.lot' + _description = '批次数量' + + name = fields.Char('名称', related='lot_id.name') + shelf_location_id = fields.Many2one('sf.shelf.location', string="货位") + lot_id = fields.Many2one('stock.lot', string='批次号') + qty = fields.Integer('数量') + qty_num = fields.Integer('变更数量') + + @api.onchange('qty_num') + def _onchange_qty_num(self): + for item in self: + if item.qty_num > item.qty: + raise ValidationError('变更数量不能比库存数量大!!!') + + +class SfStockMoveLine(models.Model): + _name = 'stock.move.line' + _inherit = ['stock.move.line', 'printing.utils'] + + stock_lot_name = fields.Char('序列号名称', related='lot_id.name') + current_location_id = fields.Many2one( + 'sf.shelf.location', string='当前货位', compute='_compute_current_location_id', store=True, readonly=False, + domain="[('product_id', '=', product_id),'|',('product_sn_id.name', '=', stock_lot_name),('product_sn_ids.lot_id.name','=',stock_lot_name)]") + # location_dest_id = fields.Many2one('stock.location', string='目标库位') + location_dest_id_product_type = fields.Many2many(related='location_dest_id.product_type') + location_dest_id_value = fields.Integer(compute='_compute_location_dest_id_value', store=True) + # lot_qr_code = fields.Binary(string='二维码', compute='_compute_lot_qr_code', store=True) + lot_qr_code = fields.Binary(string='二维码', compute='_compute_lot_qr_code', store=True) + current_product_id = fields.Integer(compute='_compute_location_dest_id_value', store=True) + there_is_no_sn = fields.Boolean('是否有序列号', default=False) + + rfid = fields.Char('Rfid') + rfid_barcode = fields.Char('Rfid', compute='_compute_rfid') + + @api.depends('lot_id') + def _compute_rfid(self): + for item in self: + item.rfid_barcode = item.lot_id.rfid + + def action_revert_inventory(self): + # 检查用户是否有执行操作的权限 + if not self.env.user.has_group('sf_warehouse.group_sf_stock_user'): + raise UserError(_('抱歉,只有库管人员可以执行此动作')) + + # 如果用户有权限,调用父类方法 + return super().action_revert_inventory() + + @api.depends('lot_name') + def _compute_lot_qr_code(self): + for record in self: + if record.lot_id: + # record.lot_qr_code = record.lot_id.lot_qr_code + # 创建一个QRCode对象 + qr = qrcode.QRCode( + version=1, # 设置版本, 1-40,控制二维码的大小 + error_correction=qrcode.constants.ERROR_CORRECT_L, # 设置错误校正等级 + box_size=10, # 设置每个格子的像素大小 + border=4, # 设置边框的格子宽度 + ) + + # 添加数据 + qr.add_data(record.lot_id.name) + qr.make(fit=True) + + # 创建二维码图像 + img = qr.make_image(fill_color="black", back_color="white") + + # 创建一个内存文件 + buffer = io.BytesIO() + img.save(buffer, format="PNG") # 将图像保存到内存文件中 + + # 获取二进制数据 + binary_data = buffer.getvalue() + + # 使用Base64编码这些二进制数据 + data = base64.b64encode(binary_data) + self.lot_qr_code = data + else: + record.lot_qr_code = False + + def print_single_method(self): + self.ensure_one() + qr_code_data = self.lot_qr_code + if not qr_code_data: + raise UserError("没有找到二维码数据。") + lot_name = self.lot_name + + # 增加"当为坯料时,只打印序列号的前面部分" + if self.lot_name: # 确保 lot_name 存在 + if self.product_id.categ_id.name == '坯料': + lot_name = lot_name.split('[', 1)[0] + # host = "192.168.50.110" # 可以根据实际情况修改 + # port = 9100 # 可以根据实际情况修改 + + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + host = printer_config.printer_id.ip_address + port = printer_config.printer_id.port + self.print_qr_code(lot_name, host, port) + + # 返回当前wizard页面 + # base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + # return { + # 'type': 'ir.actions.act_url', + # 'url': str(base_url) + download_url, + # 'target': 'self', + # } + # 获取当前wizard的视图ID或其他标识信息 + view_id = self.env.context.get('view_id') + # 构造返回wizard页面的action字典 + action = { + 'type': 'ir.actions.act_window', + 'name': '返回 Wizard', + 'res_model': 'stock.move', # 替换为你的wizard模型名称 + 'view_mode': 'form', + 'view_id': view_id, # 如果需要基于特定的视图返回 + 'target': 'new', # 如果需要在新的窗口或标签页打开 + 'res_id': self.id, # 如果你想要返回当前记录的视图 + } + return action + + # def generate_zpl_code(self, code): + # # 初始化ZPL代码字符串 + # zpl_code = "^XA\n" + # zpl_code += "^CW1,E:SIMSUN.TTF^FS\n" + # zpl_code += "^CI28\n" + # + # # 设置二维码位置 + # zpl_code += "^FO50,50\n" # 调整二维码位置,使其与资产编号在同一行 + # zpl_code += f"^BQN,2,6^FDLM,B0093{code}^FS\n" + # + # # 设置资产编号文本位置 + # zpl_code += "^FO300,60\n" # 资产编号文本的位置,与二维码在同一行 + # zpl_code += "^A1N,45,45^FD编码名称: ^FS\n" + # + # # 设置{code}文本位置 + # # 假设{code}文本需要位于资产编号和二维码下方,中间位置 + # # 设置{code}文本位置并启用自动换行 + # zpl_code += "^FO300,120\n" # {code}文本的起始位置 + # zpl_code += "^FB500,4,0,L,0\n" # 定义一个宽度为500点的文本框,最多4行,左对齐 + # zpl_code += f"^A1N,40,40^FD{code}^FS\n" + # + # # 在{code}文本框周围绘制线框 + # # 假设线框的外部尺寸为宽度500点,高度200点 + # # zpl_code += "^FO300,110^GB500,200,2^FS\n" # 绘制线框,边框粗细为2点 + # + # zpl_code += "^PQ1,0,1,Y\n" + # zpl_code += "^XZ\n" + # return zpl_code + # + # def send_to_printer(self, host, port, zpl_code): + # # 将ZPL代码转换为字节串 + # print('zpl_code', zpl_code) + # zpl_bytes = zpl_code.encode('utf-8') + # print(zpl_bytes) + # + # # 创建socket对象 + # mysocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # try: + # mysocket.connect((host, port)) # 连接到打印机 + # mysocket.send(zpl_bytes) # 发送ZPL代码 + # print("ZPL code sent to printer successfully.") + # except Exception as e: + # print(f"Error with the connection: {e}") + # finally: + # mysocket.close() # 关闭连接 + # + # def print_qr_code(self): + # self.ensure_one() # 确保这个方法只为一个记录调用 + # # if not self.lot_id: + # # raise UserError("没有找到序列号。") + # # 假设_lot_qr_code方法已经生成了二维码并保存在字段中 + # qr_code_data = self.lot_qr_code + # if not qr_code_data: + # raise UserError("没有找到二维码数据。") + # # 生成ZPL代码 + # zpl_code = self.generate_zpl_code(self.lot_id.name) + # # 设置打印机的IP地址和端口号 + # host = "192.168.50.110" + # port = 9100 + # # 发送ZPL代码到打印机 + # self.send_to_printer(host, port, zpl_code) + # + # # # 生成下载链接或直接触发下载 + # # # 此处的实现依赖于你的具体需求,以下是触发下载的一种示例 + # # attachment = self.env['ir.attachment'].sudo().create({ + # # 'datas': self.lot_qr_code, + # # 'type': 'binary', + # # 'description': '二维码图片', + # # 'name': self.lot_name + '.png', + # # # 'res_id': invoice.id, + # # # 'res_model': 'stock.picking', + # # 'public': True, + # # 'mimetype': 'application/x-png', + # # # 'model_name': 'stock.picking', + # # }) + # # # 返回附件的下载链接 + # # download_url = '/web/content/%s?download=true' % attachment.id + # # base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + # # return { + # # 'type': 'ir.actions.act_url', + # # 'url': str(base_url) + download_url, + # # 'target': 'self', + # # } + + # # # 定义一个方法,用于根据序列号生成二维码 + # # @api.depends('lot_id') + # def generate_lot_qr_code(self): + # # 创建一个QRCode对象 + # qr = qrcode.QRCode( + # version=1, # 设置版本, 1-40,控制二维码的大小 + # error_correction=qrcode.constants.ERROR_CORRECT_L, # 设置错误校正等级 + # box_size=10, # 设置每个格子的像素大小 + # border=4, # 设置边框的格子宽度 + # ) + # + # # 添加数据 + # qr.add_data(self.lot_id.name) + # qr.make(fit=True) + # + # # 创建二维码图像 + # img = qr.make_image(fill_color="black", back_color="white") + # + # # 创建一个内存文件 + # buffer = io.BytesIO() + # img.save(buffer, format="PNG") # 将图像保存到内存文件中 + # + # # 获取二进制数据 + # binary_data = buffer.getvalue() + # + # # 使用Base64编码这些二进制数据 + # data = base64.b64encode(binary_data) + # self.lot_qr_code = data + # attachment = self.env['ir.attachment'].sudo().create({ + # 'datas': data, + # 'type': 'binary', + # 'description': '二维码图片', + # 'name': self.lot_id.name + '.png', + # # 'res_id': invoice.id, + # # 'res_model': 'stock.picking', + # 'public': True, + # 'mimetype': 'application/pdf', + # # 'model_name': 'stock.picking', + # }) + + # def button_test(self): + # print(self.picking_id.name) + # stock_picking = self.env['stock.picking'].search([('name', '=', self.picking_id.name)], limit=1) + # print(self.picking_id.name) + # print(aa.move_line_ids.lot_id.name) + # # 获取当前的stock.picking对象 + # current_picking = self.env['stock.picking'].search([('name', '=', self.picking_id.name)], limit=1) + # + # # 获取当前picking的第一个stock.move对象 + # current_move = current_picking.move_ids[0] if current_picking.move_ids else False + # + # # 如果存在相关的stock.move对象 + # if current_move: + # # 获取源stock.move对象 + # origin_move = current_move.move_orig_ids[0] if current_move.move_orig_ids else False + # + # # 从源stock.move对象获取源stock.picking对象 + # origin_picking = origin_move.picking_id if origin_move else False + # # 现在,origin_picking就是current_picking的上一步 + # # 获取目标stock.move对象 + # dest_move = current_move.move_dest_ids[0] if current_move.move_dest_ids else False + # + # # 从目标stock.move对象获取目标stock.picking对象 + # dest_picking = dest_move.picking_id if dest_move else False + # # 现在,dest_picking就是current_picking的下一步 + # 添加所有需要的依赖字段 + @api.depends('location_id') + def _compute_current_location_id(self): + # 批量获取所有相关记录的picking + pickings = self.mapped('picking_id') + + # 构建源picking的移库行与目标位置的映射 + origin_location_map = {} + for picking in pickings: + # 获取源picking + origin_move = picking.move_ids[:1].move_orig_ids[:1] + if not origin_move: + continue + + origin_picking = origin_move.picking_id + if not origin_picking: + continue + + # 为每个picking构建lot_id到location的映射 + origin_location_map[picking.id] = { + move_line.lot_id.id: move_line.destination_location_id + for move_line in origin_picking.move_line_ids.filtered( + lambda ml: ml.destination_location_id and ml.lot_id + ) + } + + # 批量更新current_location_id + for record in self: + current_picking = record.picking_id + if not current_picking: + record.current_location_id = False + continue + + # 获取当前picking对应的lot_location映射 + lot_dest_map = origin_location_map.get(current_picking.id, {}) + + # 查找匹配的lot_id + for move_line in current_picking.move_line_ids: + if move_line.lot_id and move_line.lot_id.id in lot_dest_map: + record.current_location_id = lot_dest_map[move_line.lot_id.id] + break + else: + record.current_location_id = False + + # 是一张单据一张单据往下走的,所以这里的目标货位是上一张单据的当前货位,且这样去计算是可以的。 + @api.depends('location_dest_id') + def _compute_location_dest_id_value(self): + for record in self: + record.location_dest_id_value = record.location_dest_id.id if record.location_dest_id else False + record.current_product_id = record.product_id.id if record.product_id else False + + destination_location_id = fields.Many2one( + 'sf.shelf.location', string='目标货位') + + def compute_destination_location_id(self): + for record in self: + obj = self.env['sf.shelf.location'].search([('name', '=', + record.destination_location_id.name)]) + if obj and obj.product_id and obj.product_id != record.product_id: + raise ValidationError('目标货位【%s】已被【%s】产品占用!' % (obj.code, obj.product_id)) + if record.lot_id: + if record.product_id.tracking == 'serial': + shelf_location_obj = self.env['sf.shelf.location'].search( + [('product_sn_id', '=', record.lot_id.id)]) + if shelf_location_obj: + shelf_location_obj.product_sn_id = False + if obj: + obj.product_sn_id = record.lot_id.id + else: + if obj: + obj.product_sn_id = record.lot_id.id + elif record.product_id.tracking == 'lot': + record.put_shelf_location(record) + if not obj.product_id: + obj.product_id = record.product_id.id + else: + if obj: + obj.product_id = record.product_id.id + # obj.location_status = '占用' + obj.product_num += record.qty_done + + @api.onchange('destination_location_id') + def _check_destination_location_id(self): + for item in self: + if item: + barcode = item.destination_location_id.barcode + for line in item.picking_id.move_line_ids_without_package: + if line.destination_location_id: + if (barcode and barcode == line.destination_location_id.barcode + and item.product_id != line.product_id): + raise ValidationError( + '【%s】货位已经被占用,请重新选择!!!' % item.destination_location_id.barcode) + + def put_shelf_location(self, vals): + """ + 对货位的批量数据进行数量计算 + """ + for record in vals: + if record.lot_id and record.product_id.tracking == 'lot': + if record.current_location_id: + location_lot = self.env['sf.shelf.location.lot'].sudo().search( + [('shelf_location_id', '=', record.current_location_id.id), ('lot_id', '=', record.lot_id.id)]) + if location_lot: + location_lot.qty -= record.qty_done + if location_lot.qty == 0: + location_lot.unlink() + elif location_lot.qty < 0: + raise ValidationError('【%s】货位【%s】批次的【%s】产品数量不足!' % ( + record.current_location_id.barcode, record.lot_id.name, record.product_id.name)) + else: + raise ValidationError('【%s】货位不存在【%s】批次的【%s】产品' % ( + record.current_location_id.barcode, record.lot_id.name, record.product_id.name)) + if record.destination_location_id: + location_lot = self.env['sf.shelf.location.lot'].sudo().search( + [('shelf_location_id', '=', record.destination_location_id.id), + ('lot_id', '=', record.lot_id.id)]) + if location_lot: + location_lot.qty += record.qty_done + else: + self.env['sf.shelf.location.lot'].sudo().create({ + 'shelf_location_id': record.destination_location_id.id, + 'lot_id': record.lot_id.id, + 'qty': record.qty_done + }) + if not record.destination_location_id.product_id: + record.destination_location_id.product_id = record.product_id.id + + +class SfStockPicking(models.Model): + _inherit = 'stock.picking' + + check_in = fields.Char(string='查询是否为入库单', compute='_check_is_in') + product_uom_qty_sp = fields.Float('需求数量', compute='_compute_product_uom_qty_sp', store=True) + + @api.depends('move_ids_without_package', 'move_ids_without_package.product_uom_qty') + def _compute_product_uom_qty_sp(self): + for sp in self: + if sp.move_ids_without_package: + sp.product_uom_qty_sp = 0 + for move_id in sp.move_ids_without_package: + sp.product_uom_qty_sp += move_id.product_uom_qty + else: + sp.product_uom_qty_sp = 0 + + def batch_stock_move(self): + """ + 批量调拨,非就绪状态的会被忽略,完成后有通知提示 + """ + # 对所以调拨单的质检单进行是否完成校验 + sp_ids = [sp.id for sp in self] + qc_ids = self.env['quality.check'].sudo().search( + [('picking_id', 'in', sp_ids), ('quality_state', 'in', ['waiting', 'none'])]) + if qc_ids: + raise ValidationError(f'单据{list(set(qc.picking_id.name for qc in qc_ids))}未完成质量检查,完成后再试。') + for record in self: + if record.state != 'assigned': + continue + for move in record.move_ids: + move.action_show_details() + record.action_set_quantities_to_reservation() + record.button_validate() + + notification_message = '批量调拨完成!请注意,状态非就绪的单据会被忽略' + return { + 'effect': { + 'fadeout': 'fast', + 'message': notification_message, + 'img_url': '/web/image/%s/%s/image_1024' % ( + self.create_uid._name, self.create_uid.id) if 0 else '/web/static/img/smile.svg', + 'type': 'rainbow_man', + } + } + + @api.depends('name') + def _check_is_in(self): + """ + 判断是否为出库单 + """ + for record in self: + if record.name: + is_check_in = record.name.split('/') + record.check_in = is_check_in[1] + + def button_validate(self): + """ + 重写验证方法,当验证时意味着调拨单已经完成,已经移动到了目标货位,所以需要将当前货位的状态改为空闲 + """ + res = super(SfStockPicking, self).button_validate() + for line in self.move_line_ids: + if line: + if line.destination_location_id: + # 调用入库方法进行入库刀货位 + line.compute_destination_location_id() + else: + # 对除刀柄之外的刀具物料入库到刀具房进行 目标货位必填校验 + if self.location_dest_id.name == '刀具房' and line.product_id.cutting_tool_material_id.name not in ( + '刀柄', False): + raise ValidationError('请选择【%s】产品的目标货位!' % line.product_id.name) + if line.current_location_id: + # 对货位的批次产品进行出货 + line.put_shelf_location(line) + + if line.current_location_id: + # 按序列号管理的产品 + if line.current_location_id.product_sn_id: + line.current_location_id.product_sn_id = False + # line.current_location_id.location_status = '空闲' + line.current_location_id.product_num = 0 + line.current_location_id.product_id = False + else: + # 对除刀柄之外的刀具物料从刀具房出库进行 当前货位必填校验 + if self.location_id.name == '刀具房' and line.product_id.cutting_tool_material_id.name not in ( + '刀柄', False): + raise ValidationError('请选择【%s】产品的当前货位!' % line.product_id.name) + + # 对入库作业的刀柄和托盘进行Rfid绑定校验 + for move in self.move_ids: + if move and move.product_id.cutting_tool_material_id.name == '刀柄' or '托盘' in ( + move.product_id.fixture_material_id.name or ''): + for item in move.move_line_nosuggest_ids: + if item.rfid: + if self.env['stock.lot'].search([('rfid', '=', item.rfid)]): + raise ValidationError('该Rfid【%s】在系统中已经存在,请重新录入!' % item.rfid) + if item.location_dest_id.name == '进货': + if not item.rfid: + raise ValidationError('你需要提供%s的Rfid' % move.product_id.name) + self.env['stock.lot'].search([('name', '=', item.lot_name)]).write({'rfid': item.rfid}) + return res + + +# def print_all_barcode(self): +# """ +# 打印所有编码 +# """ +# print('================') +# for record in self.move_ids_without_package: +# print('record', record) +# print('record.move_line_ids', record.move_line_ids) +# +# # record.move_line_ids.print_qr_code() +# +# print('record.move_line_ids.lot_id', record.move_line_ids.lot_id) +# print('record.move_line_ids.lot_id.name', record.move_line_ids.lot_id.name) + + +class SfProcurementGroup(models.Model): + _inherit = 'procurement.group' + + @api.model + def _search_rule(self, route_ids, packaging_id, product_id, warehouse_id, domain): + """ + 修改路线多规则条件选取 + """ + if warehouse_id: + domain = expression.AND( + [['|', ('warehouse_id', '=', warehouse_id.id), ('warehouse_id', '=', False)], domain]) + Rule = self.env['stock.rule'] + res = self.env['stock.rule'] + if route_ids: + res_list = Rule.search(expression.AND([[('route_id', 'in', route_ids.ids)], domain]), + order='route_sequence, sequence') + for res1 in res_list: + if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ + res1.location_src_id.product_type: + res = res1 + if not res: + res = Rule.search(expression.AND([[('route_id', 'in', route_ids.ids)], domain]), + order='route_sequence, sequence', limit=1) + + if not res and packaging_id: + packaging_routes = packaging_id.route_ids + if packaging_routes: + res_list = Rule.search(expression.AND([[('route_id', 'in', packaging_routes.ids)], domain]), + order='route_sequence, sequence') + for res1 in res_list: + if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ + res1.location_src_id.product_type: + res = res1 + if not res: + res = Rule.search(expression.AND([[('route_id', 'in', packaging_routes.ids)], domain]), + order='route_sequence, sequence', limit=1) + if not res: + product_routes = product_id.route_ids | product_id.categ_id.total_route_ids + if product_routes: + res_list = Rule.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), + order='route_sequence, sequence') + for res1 in res_list: + if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ + res1.location_src_id.product_type: + res = res1 + if not res: + res = Rule.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), + order='route_sequence, sequence', limit=1) + if not res and warehouse_id: + warehouse_routes = warehouse_id.route_ids + if warehouse_routes: + res_list = Rule.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), + order='route_sequence, sequence') + for res1 in res_list: + if product_id.categ_id in res1.location_dest_id.product_type or product_id.categ_id in \ + res1.location_src_id.product_type: + res = res1 + if not res: + res = Rule.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), + order='route_sequence, sequence', limit=1) + return res + + +# class SfPickingType(models.Model): +# _inherit = 'stock.picking.type' +# +# def _default_show_operations(self): +# return self.user_has_groups('stock.group_production_lot,' +# 'stock.group_stock_multi_locations,' +# 'stock.group_tracking_lot', +# 'sf_warehouse.group_sf_stock_user', +# 'sf_warehouse.group_sf_stock_manager') + +class SfPickingType(models.Model): + _inherit = 'stock.picking.type' + + code = fields.Selection([('incoming', 'Receipt'), ('outgoing', 'Delivery'), ('internal', '厂内出入库')], + 'Type of Operation', required=True) + + def _default_show_operations(self): + return self.user_has_groups( + 'stock.group_production_lot,' + 'stock.group_stock_multi_locations,' + 'stock.group_tracking_lot,' + 'sf_warehouse.group_sf_stock_user,' + 'sf_warehouse.group_sf_stock_manager' + ) + + def _get_action(self, action_xmlid): + action = super(SfPickingType, self)._get_action(action_xmlid) + if not self.env.user.has_group('base.group_system'): + action['context']['create'] = False + if self.sequence_code in ['INT', 'PC']: + action['context']['search_default_retrospect'] = 1 + return action + + +class CustomStockMove(models.Model): + _name = 'stock.move' + _inherit = ['stock.move', 'printing.utils', 'barcodes.barcode_events_mixin'] + + def on_barcode_scanned(self, barcode): + """ + 采购入库扫码绑定Rfid码 + """ + for record in self: + logging.info('Rfid:%s' % barcode) + if record: + lot = self.env['stock.lot'].sudo().search([('rfid', '=', barcode)]) + if lot: + if lot.product_id.cutting_tool_material_id: + material = lot.product_id.cutting_tool_material_id.name + else: + material = lot.product_id.fixture_material_id.name + raise ValidationError( + '该Rfid【%s】已经被序列号为【%s】的【%s】物料所占用!' % (barcode, lot.name, material)) + if '刀柄' in (record.product_id.cutting_tool_material_id.name or '') or '托盘' in ( + record.product_id.fixture_material_id.name or ''): + logging.info('开始录入Rfid:%s' % record.move_line_nosuggest_ids) + for move_line_nosuggest_id in record.move_line_nosuggest_ids: + logging.info('录入的记录%s , Rfid:%s' % (move_line_nosuggest_id, move_line_nosuggest_id.rfid)) + if move_line_nosuggest_id.rfid: + if move_line_nosuggest_id.rfid == barcode: + if record.product_id.cutting_tool_material_id.name: + raise ValidationError('该刀柄的Rfid已经录入,请勿重复录入!!!') + else: + raise ValidationError('该托盘的Rfid已经录入,请勿重复录入!!!') + else: + line_id = int(re.sub(r"\D", "", str(move_line_nosuggest_id.id))) + res = self.env['stock.move.line'].sudo().search([('id', '=', line_id)]).write( + {'rfid': barcode}) + logging.info('Rfid是否录入:%s' % res) + move_line_nosuggest_id.rfid = barcode + break + else: + raise ValidationError('该产品不需要录入Rfid!!!') + + def action_assign_serial_show_details(self): + # 首先执行原有逻辑 + result = super(CustomStockMove, self).action_assign_serial_show_details() + # 接着为每个 lot_name 生成二维码 + move_lines = self.move_line_ids # 获取当前 stock.move 对应的所有 stock.move.line 记录 + for line in move_lines: + if line.lot_name: # 确保 lot_name 存在 + lot_name = line.lot_name + if line.product_id.categ_id.name == '坯料': + lot_name = lot_name.split('[', 1)[0] + qr_data = self.compute_lot_qr_code(lot_name) + # 假设 stock.move.line 模型中有一个字段叫做 lot_qr_code 用于存储二维码数据 + line.lot_qr_code = qr_data + return result + + def compute_lot_qr_code(self, lot_name): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(lot_name) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buffer = io.BytesIO() + img.save(buffer, format="PNG") + binary_data = buffer.getvalue() + data = base64.b64encode(binary_data).decode() # 确保返回的是字符串形式的数据 + return data + + def print_all_barcode(self): + """ + 打印所有编码 + """ + print('================') + for record in self.move_line_ids: + print('record', record) + if not record.lot_name: + continue + record.ensure_one() + # qr_code_data = record.lot_qr_code + # if not qr_code_data: + # raise UserError("没有找到二维码数据。") + lot_name = record.lot_name + # todo 待控制 + if not lot_name: + raise ValidationError("请先分配序列号") + + # 增加"当为坯料时,只打印序列号的前面部分" + if record.lot_name: # 确保 lot_name 存在 + if record.product_id.categ_id.name == '坯料': + lot_name = lot_name.split('[', 1)[0] + + # host = "192.168.50.110" # 可以根据实际情况修改 + # port = 9100 # 可以根据实际情况修改 + + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + host = printer_config.printer_id.ip_address + port = printer_config.printer_id.port + record.print_qr_code(lot_name, host, port) diff --git a/sf_warehouse/static/src/css/kanban_location_custom.scss b/sf_warehouse/static/src/css/kanban_location_custom.scss new file mode 100644 index 00000000..3bb002e5 --- /dev/null +++ b/sf_warehouse/static/src/css/kanban_location_custom.scss @@ -0,0 +1,128 @@ +// 定义一个 mixin 来处理重复的样式 +@mixin kanban-common-styles($record-count-each-row, + $record-gap: 16px, + $color-guide-width: 70px) { + $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: 0px !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: 0px !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; + + &:hover { + transform: translateY(-1px) !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15) !important; + } + + .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: 14px; + color: #000000; + } + + .sf_kanban_no { + display: flex !important; + justify-content: center !important; + align-items: center !important; + font-size: 18px; + color: #000000; + } + } + } +} + +// 使用 mixin 为不同的列数生成样式 +.o_kanban_view { + .sf_kanban_location_style { + // 设置卡片样式 + .o_kanban_record { + + &:hover { + transform: translateY(-1px) !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15) !important; + } + + .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: 14px; + color: #000000; + } + + .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); + } +} \ No newline at end of file diff --git a/sf_warehouse/static/src/js/custom_kanban_controller.js b/sf_warehouse/static/src/js/custom_kanban_controller.js index 62c83505..ab3a57d6 100644 --- a/sf_warehouse/static/src/js/custom_kanban_controller.js +++ b/sf_warehouse/static/src/js/custom_kanban_controller.js @@ -1,21 +1,177 @@ -/** @odoo-module */ - -import {KanbanController} from "@web/views/kanban/kanban_controller"; -import {kanbanView} from "@web/views/kanban/kanban_view"; -import {registry} from "@web/core/registry"; - -// 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() -} - -CustomKanbanController.template = "sf_warehouse.CustomKanbanView"; - -export const customKanbanView = { - ...kanbanView, // contains the default Renderer/Controller/Model - Controller: CustomKanbanController, -}; - -// Register it to the views registry -registry.category("views").add("custom_kanban", customKanbanView); +/** @odoo-module */ + +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 { + +} + +// 自定义看板控制器 +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 () => { + 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'); + this.onShelfChange(shelfDomain[2]); + } 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]; + // 如果货架ID存在,则设置相应的样式 + if (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); + } + 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, + Controller: CustomKanbanController, + Renderer: CustomKanbanRenderer, +}; + +registry.category("views").add("custom_kanban", customKanbanView); \ No newline at end of file diff --git a/sf_warehouse/views/shelf_location.xml b/sf_warehouse/views/shelf_location.xml index 747c25a2..c39f61c6 100644 --- a/sf_warehouse/views/shelf_location.xml +++ b/sf_warehouse/views/shelf_location.xml @@ -1,330 +1,319 @@ - - - - - - Sf Shelf - sf.shelf - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - - shelf.location.kanban - sf.shelf.location - - - - -
- -
-
- -
-
- -
- -
-
- - | - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - - - shelf.location.search - sf.shelf.location - - - - - - - - - - - 货位看板 - ir.actions.act_window - sf.shelf.location - kanban,form - - - - - - - - - - - - - - - - - - - - 货位 - ir.actions.act_window - sf.shelf.location - tree,form - - - - - - - - - - - - - - - - - - - - - - -
-
+ + + + + + Sf Shelf + sf.shelf + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + shelf.location.kanban + sf.shelf.location + + + + + +
+ + +
+ +
+ + + +
+
+ +
+
+
+ + | + +
+
+ + +
+ +
+
+ +
+
+
+
+
+
+
+
+ + + + shelf.location.search + sf.shelf.location + + + + + + + + + + + 货位看板 + ir.actions.act_window + sf.shelf.location + kanban,form + + + + + + + + + + + + + + + + + + + + 货位 + ir.actions.act_window + sf.shelf.location + tree,form + + + + + + + + + + + + + + + + + + + + + + +
+