diff --git a/sf_base/models/base.py b/sf_base/models/base.py index 5762822e..a6e1d4cc 100644 --- a/sf_base/models/base.py +++ b/sf_base/models/base.py @@ -77,6 +77,7 @@ class MachineTool(models.Model): b_axis = fields.Integer('B轴') c_axis = fields.Integer('C轴') remark = fields.Text('备注') + is_binding = fields.Boolean('是否绑定机床', default=False) precision = fields.Float('加工精度') control_system_id = fields.Many2one('sf.machine.control_system', string="控制系统") diff --git a/sf_bf_connect/controllers/controllers.py b/sf_bf_connect/controllers/controllers.py index 9e422ca7..5227592f 100644 --- a/sf_bf_connect/controllers/controllers.py +++ b/sf_bf_connect/controllers/controllers.py @@ -8,7 +8,7 @@ from odoo.http import request class Sf_Bf_Connect(http.Controller): - @http.route('/api/bfm_process_order/list', type='json', auth='none', methods=['GET', 'POST'], csrf=False, + @http.route('/api/bfm_process_order/list', type='http', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def get_bfm_process_order_list(self, **kw): """ @@ -19,48 +19,91 @@ class Sf_Bf_Connect(http.Controller): res = {'status': 1, 'factory_order_no': ''} logging.info('get_bfm_process_order_list:%s' % kw) try: - datas = request.httprequest.data - ret = json.loads(datas) - ret = json.loads(ret['result']) + # datas = request.httprequest.data + # ret = json.loads(datas) + # ret = json.loads(ret['result']) product_id = request.env.ref('sf_dlm.product_template_sf').sudo() + logging.info('product_id:%s' % product_id) self_machining_id = request.env.ref('sf_dlm.product_embryo_sf_self_machining').sudo() outsource_id = request.env.ref('sf_dlm.product_embryo_sf_outsource').sudo() purchase_id = request.env.ref('sf_dlm.product_embryo_sf_purchase').sudo() company_id = request.env.ref('base.main_company').sudo() + user_id = request.env.ref('base.user_admin').sudo() + logging.info('user_id:%s' % user_id) + bfm_process_order_list = json.loads(kw['bfm_process_order_list']) order_id = request.env['sale.order'].with_user(request.env.ref("base.user_admin")).sale_order_create( - company_id, ret['delivery_name'], ret['delivery_telephone'], ret['delivery_address'], - ret['delivery_end_date']) + company_id, kw['delivery_name'], kw['delivery_telephone'], kw['delivery_address'], + kw['delivery_end_date'], user_id) i = 1 - for item in ret['bfm_process_order_list']: + for item in bfm_process_order_list: + # product_has = request.env['product.template'].with_user(request.env.ref("base.user_admin")).search([('barcode','=', item['barcode'])]) + # if product_has: + # logging.info('product_has:%s' % product_has) + # logging.info('barcode:%s' % item['barcode']) + # order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product_has, item) + # else: product = request.env['product.template'].sudo().product_create(product_id, item, order_id, - ret['order_number'], i) - order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product, item) + kw['order_number'], i) + # order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product, item) + logging.info('order_id:%s' % order_id) + logging.info('product:%s' % product) bom_data = request.env['mrp.bom'].with_user(request.env.ref("base.user_admin")).get_bom(product) + logging.info('bom_data:%s' % bom_data) if bom_data: bom = request.env['mrp.bom'].with_user(request.env.ref("base.user_admin")).bom_create(product, - 'normal') - bom.with_user(request.env.ref("base.user_admin")).bom_create_Line(product) + 'normal', + False) + bom.with_user(request.env.ref("base.user_admin")).bom_create_line_has(bom_data) else: - if product.materials_id.gain_way == '自加工': - self_machining = request.env['product.template'].sudo().no_bom_product_create(self_machining_id, - item, - order_i) - bom = request.env['mrp.bom'].with_user(request.env.ref("base.user_admin")).bom_create( - self_machining, 'normal') - bom.with_user(request.env.ref("base.user_admin")).bom_create_Line(self_machining) - - elif product.materials_id.gain_way == '外协': - outsource = request.env['product.template'].sudo().no_bom_product_create(outsource_id, item, - order_id) - bom = request.env['mrp.bom'].with_user(request.env.ref("base.user_admin")).bom_create(outsource) - bom.with_user(request.env.ref("base.user_admin")).bom_create_Line(outsource, 'subcontract') - elif product.materials_id.gain_way == '采购': - purchase = request.env['product.template'].sudo().no_bom_product_create(purchase_id, item, - order_id) - - i += 1 - res['factory_order_no'] = order_id.name - return json.JSONEncoder().encode(res) + if product.materials_type_id.gain_way == '自加工': + # 创建胚料 + self_machining_embryo = request.env['product.template'].sudo().no_bom_product_create( + self_machining_id, + item, + order_id, 'self_machining') + # 创建胚料的bom + self_machining_bom = request.env['mrp.bom'].with_user( + request.env.ref("base.user_admin")).bom_create( + self_machining_embryo, 'normal', False) + # 创建胚料里bom的组件 + self_machining_bom.with_user(request.env.ref("base.user_admin")).bom_create_line( + self_machining_embryo) + # 产品配置bom + product_bom_self_machining = request.env['mrp.bom'].with_user( + request.env.ref("base.user_admin")).bom_create( + product, 'normal', False) + product_bom_self_machining.with_user(request.env.ref("base.user_admin")).bom_create_line_has( + self_machining_embryo) + elif product.materials_type_id.gain_way == '外协': + # 创建胚料 + outsource_embryo = request.env['product.template'].sudo().no_bom_product_create(outsource_id, + item, + order_id, + 'subcontract') + # 创建胚料的bom + outsource_bom = request.env['mrp.bom'].with_user(request.env.ref("base.user_admin")).bom_create( + outsource_embryo, + 'subcontract', True) + # 创建胚料的bom的组件 + outsource_bom.with_user(request.env.ref("base.user_admin")).bom_create_line(outsource_embryo) + # 产品配置bom + product_bom_outsource = request.env['mrp.bom'].with_user( + request.env.ref("base.user_admin")).bom_create(product, 'normal', False) + product_bom_outsource.with_user(request.env.ref("base.user_admin")).bom_create_line_has( + outsource_embryo) + elif product.materials_type_id.gain_way == '采购': + purchase_embryo = request.env['product.template'].sudo().no_bom_product_create(purchase_id, + item, + order_id, + 'purchase') + # 产品配置bom + product_bom_purchase = request.env['mrp.bom'].with_user( + request.env.ref("base.user_admin")).bom_create(product, 'normal', False) + product_bom_purchase.with_user(request.env.ref("base.user_admin")).bom_create_line_has( + purchase_embryo) + order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product, item) + i += 1 + res['factory_order_no'] = order_id.name except Exception as e: logging.info('get_bfm_process_order_list error:%s' % e) res['status'] = -1 diff --git a/sf_bf_connect/models/http.py b/sf_bf_connect/models/http.py index 7fee2b9a..23c66406 100644 --- a/sf_bf_connect/models/http.py +++ b/sf_bf_connect/models/http.py @@ -13,6 +13,10 @@ _logger = logging.getLogger(__name__) class AuthenticationError(Exception): pass +class AuthenticationError(Exception): + pass + + class Http(models.AbstractModel): _inherit = 'ir.http' diff --git a/sf_dlm/__manifest__.py b/sf_dlm/__manifest__.py index da6dba4b..27518d9f 100644 --- a/sf_dlm/__manifest__.py +++ b/sf_dlm/__manifest__.py @@ -10,7 +10,7 @@ """, 'category': 'sf', 'website': 'https://www.sf.jikimo.com', - 'depends': ['mrp', 'base', 'sf_manufacturing'], + 'depends': ['mrp', 'base', 'sf_manufacturing', 'web_widget_model_viewer', 'purchase', 'mrp_subcontracting'], 'data': [ 'data/product_data.xml', 'views/product_template_view.xml' diff --git a/sf_dlm/data/product_data.xml b/sf_dlm/data/product_data.xml index 847e6a80..aad3714c 100644 --- a/sf_dlm/data/product_data.xml +++ b/sf_dlm/data/product_data.xml @@ -1,57 +1,82 @@ - - CNC加工产品模板 - - delivery - product - false - - - - False - - 胚料 胚料 + + 成品 + 成品 + - - 自加工 - + + 原材料 + 原材料 + + + + CNC加工产品模板 + + + delivery product false - False + true + serial + + + + 胚料自加工模板 + + + + delivery + product + false + + + + true + serial + - 外协 - + 胚料外协加工模板 + + + delivery product - false + true - False + serial + - - 采购 - + 胚料采购模板 + + + delivery product - false + true - False + serial + \ No newline at end of file diff --git a/sf_dlm/models/product_template.py b/sf_dlm/models/product_template.py index aaf7b8f2..885e0c26 100644 --- a/sf_dlm/models/product_template.py +++ b/sf_dlm/models/product_template.py @@ -1,12 +1,16 @@ from odoo import models, fields, api from odoo.exceptions import ValidationError import logging - +import base64 +import os +from OCC.Extend.DataExchange import read_step_file, write_stl_file +from odoo.modules import get_resource_path class ResProductTemplate(models.Model): _inherit = 'product.template' # 模型的长,宽,高,体积,精度,材料 + model_name = fields.Char('模型名称') model_long = fields.Float('模型长[mm]', digits=(16, 3)) model_width = fields.Float('模型宽[mm]', digits=(16, 3)) model_height = fields.Float('模型高[mm]', digits=(16, 3)) @@ -21,163 +25,204 @@ class ResProductTemplate(models.Model): model_processing_panel = fields.Char('模型加工面板') model_surface_process_id = fields.Many2one('sf.production.process', string='表面工艺') model_process_parameters_id = fields.Many2one('sf.processing.technology', string='工艺参数') - model_price = fields.Float('模型单价', digits=(16, 3)) + # model_price = fields.Float('模型单价', digits=(16, 3)) model_remark = fields.Char('模型备注说明') length = fields.Float('长[mm]', digits=(16, 3)) width = fields.Float('宽[mm]', digits=(16, 3)) height = fields.Float('高[mm]', digits=(16, 3)) materials_id = fields.Many2one('sf.production.materials', string='材料') materials_type_id = fields.Many2one('sf.materials.model', string='材料型号') - # volume = fields.Float(compute='_compute_volume', store=True) single_manufacturing = fields.Boolean(string="单个制造") + upload_model_file = fields.Many2many('ir.attachment', 'upload_model_file_attachment_ref', string='上传模型文件') + model_file = fields.Binary('模型文件') - # @api.depends('long', 'width', 'height') - # def _compute_volume(self): - # self.volume = self.long * self.width * self.height + # 胚料的库存路线设置 + # def _get_routes(self, route_type): + # route_manufacture = self.env.ref('mrp.route_warehouse0_manufacture', raise_if_not_found=False).sudo() + # route_mto = self.env.ref('stock.route_warehouse0_mto', raise_if_not_found=False).sudo() + # route_purchase = self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False).sudo() + # if route_manufacture and route_mto: + # # 外协 + # if route_type == 'subcontract': + # route_subcontract = self.env.ref('mrp_subcontracting.route_resupply_subcontractor_mto', + # raise_if_not_found=False).sudo() + # return [route_mto.id, route_purchase.id, route_subcontract.id] + # elif route_type == 'purchase': + # # 采购 + # return [route_mto.id, route_purchase.id] + # else: + # return [route_mto.id, route_manufacture.id] + # return [] - - single_manufacturing = fields.Boolean(string="单个制造") - - @api.model - def _get_route(self): - route_manufacture = self.env.ref('stock.warehouse0', raise_if_not_found=False).manufacture_pull_id.route_id.id - route_mto = self.env.ref('stock.warehouse0', raise_if_not_found=False).mto_pull_id.route_id.id - if route_manufacture and route_mto: - return [route_manufacture, route_mto] - return [] - - route_ids = fields.Many2many(default=lambda self: self._get_route()) - - # @api.depends('long', 'width', 'height') - # def _compute_volume(self): - # self.volume = self.long * self.width * self.height - - # @api.depends('model_long', 'model_width', 'model_height') - # def _compute_model_volume(self): - # self.model_volume = self.model_long * self.model_width * self.model_height + # route_ids = fields.Many2many(default=lambda self: self._get_route()) # 业务平台分配工厂后在智能工厂先创建销售订单再创建该产品 def product_create(self, product_id, item, order_id, order_number, i): copy_product_id = product_id.with_user(self.env.ref("base.user_admin")).copy() copy_product_id.product_tmpl_id.active = True - logging.info('product_create:%s' % item) + model_type = self.env['sf.model.type'].search([], limit=1) + attachment = self.attachment_create(item['model_name'], item['model_data']) vals = { 'name': '%s-%s' % (order_id.name, i), - 'model_long': item['model_long'], - 'model_width': item['model_width'], - 'model_height': item['model_height'], - 'length': item['long'], - 'width': item['width'], - 'height': item['height'], - 'volume': item['long'] * item['width'] * item['height'], - 'model_price': item['price'], + 'model_long': item['model_long'] + model_type.embryo_tolerance, + 'model_width': item['model_width'] + model_type.embryo_tolerance, + 'model_height': item['model_height'] + model_type.embryo_tolerance, + 'model_volume': (item['model_long'] + model_type.embryo_tolerance) * ( + item['model_width'] + model_type.embryo_tolerance) * ( + item['model_height'] + model_type.embryo_tolerance), + 'model_type_id': 1, + # 'model_machining_precision': item['model_machining_precision'], + 'model_processing_panel': 'A', + 'model_machining_precision': '±0.10mm', 'length': item['model_long'], 'width': item['model_width'], 'height': item['model_height'], 'volume': item['model_long'] * item['model_width'] * item['model_height'], - 'model_price': item['price'], - 'tracking': 'serial', - 'single_manufacturing': True, + 'model_file': base64.b64decode(item['model_file']), + 'model_name': attachment.name, + 'upload_model_file': [(6, 0, [attachment.id])], + # 'single_manufacturing': True, 'list_price': item['price'], + # 'categ_id': self.env.ref('sf_dlm.product_category_finished_sf').id, 'materials_id': self.env['sf.production.materials'].search( [('materials_no', '=', item['texture_code'])]).id, 'materials_type_id': self.env['sf.materials.model'].search( [('materials_no', '=', item['texture_type_code'])]).id, - # 'model_surface_process_id': self.env['sf.production.process'].search( - # [('process_encode', '=', item['surface_process_code'])]).id, + 'model_surface_process_id': self.env['sf.production.process'].search( + [('process_encode', '=', item['surface_process_code'])]).id, # 'model_process_parameters_id': self.env['sf.processing.technology'].search( # [('process_encode', '=', item['process_parameters_code'])]).id, 'model_remark': item['remark'], - # 'default_code': '%s-%s' % (order_number, i), - # 'barcode': item['barcode'], - 'active': True + 'default_code': '%s-%s' % (order_number, i), + #'barcode': item['barcode'], + 'active': True, + # 'route_ids': self._get_routes('') } - logging.info('product_create1:%s' % item) copy_product_id.sudo().write(vals) + print(len(copy_product_id.model_file)) + product_id.product_tmpl_id.active = False return copy_product_id - def no_bom_product_create(self, product_id, item, order_id): - copy_product_id = product_id.with_user(self.env.ref("base.user_admin")).copy() - copy_product_id.product_tmpl_id.active = True - logging.info('no_bom_product_create:%s' % item) + def attachment_create(self, name, data): + attachment = self.env['ir.attachment'].create({ + 'datas': base64.b64decode(data), + 'type': 'binary', + 'description': '模型文件', + 'name': name + }) + return attachment + + # 创建胚料 + def no_bom_product_create(self, product_id, item, order_id, route_type): + no_bom_copy_product_id = product_id.with_user(self.env.ref("base.user_admin")).copy() + no_bom_copy_product_id.product_tmpl_id.active = True materials_id = self.env['sf.production.materials'].search( - [('materials_no', '=', item['texture_code'])]).id + [('materials_no', '=', item['texture_code'])]) materials_type_id = self.env['sf.materials.model'].search( - [('materials_no', '=', item['texture_type_code'])]).id + [('materials_no', '=', item['texture_type_code'])]) + model_type = self.env['sf.model.type'].search([], limit=1) + supplier = self.env['mrp.bom'].get_supplier(materials_type_id) + logging.info('no_bom_copy_product_supplier-vals:%s' % supplier) vals = { 'name': '%s %s %s %s * %s * %s' % ( order_id.name, materials_id.name, materials_type_id.name, item['model_long'], item['model_width'], item['model_height']), - 'model_long': item['model_long'], - 'model_width': item['model_width'], - 'model_height': item['model_height'], - 'model_volume': item['model_long'] * item['model_width'] * item['model_height'], - 'length': item['model_long'], - 'width': item['model_width'], - 'height': item['model_height'], - 'volume': item['model_long'] * item['model_width'] * item['model_height'], - 'model_price': item['price'], - 'tracking': 'serial', - 'single_manufacturing': True, + 'length': item['model_long'] + model_type.embryo_tolerance, + 'width': item['model_width'] + model_type.embryo_tolerance, + 'height': item['model_height'] + model_type.embryo_tolerance, + 'volume': (item['model_long'] + model_type.embryo_tolerance) * ( + item['model_width'] + model_type.embryo_tolerance) * ( + item['model_height'] + model_type.embryo_tolerance), + # 'model_price': item['price'], 'list_price': item['price'], 'materials_id': materials_id.id, 'materials_type_id': materials_type_id.id, + # 'route_ids': self._get_routes(route_type), + # 'categ_id': self.env.ref('sf_dlm.product_category_embryo_sf').id, # 'model_surface_process_id': self.env['sf.production.process'].search( # [('process_encode', '=', item['surface_process_code'])]).id, # 'model_process_parameters_id': self.env['sf.processing.technology'].search( # [('process_encode', '=', item['process_parameters_code'])]).id, 'active': True } - logging.info('product_create1:%s' % item) - copy_product_id.sudo().write(vals) - return copy_product_id + # 外协和采购生成的胚料需要根据材料型号绑定供应商 + if route_type == 'subcontract': + no_bom_copy_product_id.purchase_ok = True + no_bom_copy_product_id.seller_ids = [ + (0, 0, {'partner_id': supplier.partner_id.id, 'delay': 1.0, 'is_subcontractor': True})] + logging.info('no_bom_copy_product_id-seller_ids-vals:%s' % no_bom_copy_product_id.seller_ids) + elif route_type == 'purchase': + no_bom_copy_product_id.purchase_ok = True + no_bom_copy_product_id.seller_ids = [ + (0, 0, {'partner_id': supplier.partner_id.id, 'delay': 1.0})] + logging.info('no_bom_copy_product_id-seller_ids-vals:%s' % no_bom_copy_product_id.seller_ids) + no_bom_copy_product_id.write(vals) + logging.info('no_bom_copy_product_id-vals:%s' % vals) + product_id.product_tmpl_id.active = False + return no_bom_copy_product_id - # 根据模型类型默认给模型的长高宽加配置的长度; - @api.onchange('model_type_id') - def add_product_size(self): - if not self.model_type_id: - return - model_type = self.env['sf.model.type'].search( - [('id', '=', self.model_type_id.id), ('embryo_tolerance', '=', True)]) - if model_type: - self.model_long = self.model_long + 1 - self.model_width = self.model_width + 1 - self.model_height = self.model_width + 1 - # for item in self: - # print(item.model_long) - # print(item.model_width) - # print(item.model_height) - # item.model_long = item.model_long + 1 - # item.model_width = item.model_width + 1 - # item.model_height = item.model_width + 1 - else: - return + @api.onchange('upload_model_file') + def onchange_model_file(self): + for item in self: + if len(item.upload_model_file) > 1: + raise ValidationError('只允许上传一个文件') + if item.upload_model_file: + file_attachment_id = item.upload_model_file[0] + item.model_name = file_attachment_id.name + # 附件路径 + report_path = file_attachment_id._full_path(file_attachment_id.store_fname) + shapes = read_step_file(report_path) + output_file = get_resource_path('sf_dlm', 'static/file', 'out.stl') + write_stl_file(shapes, output_file, 'binary', 0.03, 0.5) + # 转化为glb + output_glb_file = get_resource_path('sf_dlm', 'static/file', 'out.glb') + util_path = get_resource_path('sf_dlm', 'static/util') + cmd = 'python %s/stl2gltf.py %s %s -b' % (util_path, output_file, output_glb_file) + os.system(cmd) + # 转base64 + with open(output_glb_file, 'rb') as fileObj: + image_data = fileObj.read() + base64_data = base64.b64encode(image_data) + item.model_file = base64_data class ResMrpBom(models.Model): _inherit = 'mrp.bom' + subcontractor_id = fields.Many2one('res.partner', string='外包商') + + def bom_create_line_has(self, embryo): + vals = { + 'bom_id': self.id, + 'product_id': embryo.id, + 'product_tmpl_id': embryo.product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': 1 + } + logging.info('bom_create_line_has-vals:%s' % vals) + return self.env['mrp.bom.line'].create(vals) + # 业务平台分配工厂后在智能工厂先创建销售订单再创建该产品后再次进行创建bom - def bom_create(self, product, bom_type): + def bom_create(self, product, bom_type, product_type): bom_id = self.env['mrp.bom'].create({ 'product_tmpl_id': product.product_tmpl_id.id, 'type': bom_type, + # 'subcontractor_id': '' or subcontract.partner_id.id, 'product_qty': 1, 'product_uom_id': 1 }) + if bom_type == 'subcontract' and product_type != False: + subcontract = self.get_supplier(product.materials_type_id) + bom_id.subcontractor_id = subcontract.partner_id.id + logging.info('bom_create-vals:%s' % bom_id) return bom_id - # 生成产品BOM匹配胚料,胚料的匹配规则: - # 一、匹配的胚料类别需要带有胚料的标签; - # 二、胚料的材料型号与生成产品的材料型号一致; - # 三、胚料的长宽高均要大于模型的长宽高; - # 四、如果匹配成功多个胚料,则选取体积最小的胚料; - # 创建新的胚料,根据胚料材料型号的获取方式( - # 自加工,外协,采购) 的配置, 选择不同的库存路线,一种材料型号配置一个路线相关的配置: - # 材料型号配置不同的获取方式: (自加工, 外协, 采购); - # 原材料重量KG(公斤)= 胚料的体积(立方米m³) * 材料密度 * 1000 - def bom_create_Line(self, embryo, materials): - bom_line = self.get_raw_bom(embryo, materials) + # 胚料BOM组件:选取当前胚料原材料, + # 然后根据当前的胚料的体积得出需要的原材料重量(立方米m³) *材料密度 * 1000 = 所需原材料重量KG(公斤) + # 胚料所需原材料公式:当前的胚料的体积(立方米m³) *材料密度 * 1000 = 所需原材料重量KG(公斤) + def bom_create_line(self, embryo): + # 选取当前胚料原材料 + bom_line = self.get_raw_bom(embryo) vals = { 'bom_id': self.id, 'product_id': bom_line.id, @@ -185,10 +230,23 @@ class ResMrpBom(models.Model): 'product_qty': bom_line.volume * bom_line.materials_type_id.density * 1000, 'product_uom_id': bom_line.uom_id.id } + logging.info('bom_create_line-vals1:%s' % vals) return self.env['mrp.bom.line'].create(vals) + # 查询材料型号默认排第一的供应商 + def get_supplier(self, materials_type): + seller_id = self.env['sf.supplier.sort'].search( + [('materials_model_id', '=', materials_type.id)], + limit=1, + order='sequence asc') + logging.info('get_supplier-vals:%s' % seller_id) + return seller_id + + # 匹配bom def get_bom(self, product): - embryo = self.env['product.product'].search( + logging.info('get_bom-product:%s' % product) + logging.info('get_bom-product:%s' % product.materials_type_id.id) + embryo_has = self.env['product.product'].search( [('categ_id.type', '=', '胚料'), ('materials_type_id', '=', product.materials_type_id.id), ('length', '>', product.length), ('width', '>', product.width), ('height', '>', product.height) @@ -196,10 +254,11 @@ class ResMrpBom(models.Model): limit=1, order='volume desc' ) - if embryo: - rate_of_waste = ((embryo.volume - product.model_volume) % embryo.volume) * 100 - if rate_of_waste <= 20: - return embryo + logging.info('get_bom-vals:%s' % embryo_has) + if embryo_has: + rate_of_waste = ((embryo_has.volume - product.model_volume) % embryo_has.volume) * 100 + if rate_of_waste >= 20: + return embryo_has else: return @@ -223,5 +282,3 @@ class ResProductCategory(models.Model): # [('type', '=', self.type)]) # if category: # raise ValidationError("该类别已存在,请选择其他类别") - - diff --git a/sf_dlm/static/file/out.glb b/sf_dlm/static/file/out.glb new file mode 100644 index 00000000..c4a6352b Binary files /dev/null and b/sf_dlm/static/file/out.glb differ diff --git a/sf_dlm/static/file/out.stl b/sf_dlm/static/file/out.stl new file mode 100644 index 00000000..4f8b834a Binary files /dev/null and b/sf_dlm/static/file/out.stl differ diff --git a/sf_dlm/static/util/stl2gltf.py b/sf_dlm/static/util/stl2gltf.py new file mode 100644 index 00000000..1bdcc942 --- /dev/null +++ b/sf_dlm/static/util/stl2gltf.py @@ -0,0 +1,277 @@ +import os + +def stl_to_gltf(binary_stl_path, out_path, is_binary): + import struct + + gltf2 = ''' +{ + "scenes" : [ + { + "nodes" : [ 0 ] + } + ], + "nodes" : [ + { + "mesh" : 0 + } + ], + "meshes" : [ + { + "primitives" : [ { + "attributes" : { + "POSITION" : 1 + }, + "indices" : 0 + } ] + } + ], + "buffers" : [ + { + %s + "byteLength" : %d + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteOffset" : 0, + "byteLength" : %d, + "target" : 34963 + }, + { + "buffer" : 0, + "byteOffset" : %d, + "byteLength" : %d, + "target" : 34962 + } + ], + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5125, + "count" : %d, + "type" : "SCALAR", + "max" : [ %d ], + "min" : [ 0 ] + }, + { + "bufferView" : 1, + "byteOffset" : 0, + "componentType" : 5126, + "count" : %d, + "type" : "VEC3", + "min" : [%f, %f, %f], + "max" : [%f, %f, %f] + } + ], + "asset" : { + "version" : "2.0" + } +} +''' + + header_bytes = 80 + unsigned_long_int_bytes = 4 + float_bytes = 4 + vec3_bytes = 4 * 3 + spacer_bytes = 2 + num_vertices_in_face = 3 + + vertices = {} + indices = [] + + if not is_binary: + out_bin = os.path.join(out_path, "out.bin") + out_gltf = os.path.join(out_path, "out.gltf") + else: + out_bin = out_path + + unpack_face = struct.Struct("<12fH").unpack + face_bytes = float_bytes*12 + 2 + + with open(path_to_stl, "rb") as f: + f.seek(header_bytes) # skip 80 bytes headers + + num_faces_bytes = f.read(unsigned_long_int_bytes) + number_faces = struct.unpack(" maxx: maxx = x + if y < miny: miny = y + if y > maxy: maxy = y + if z < minz: minz = z + if z > maxz: maxz = z + + # f.seek(spacer_bytes, 1) # skip the spacer + + number_vertices = len(vertices) + vertices_bytelength = number_vertices * vec3_bytes # each vec3 has 3 floats, each float is 4 bytes + unpadded_indices_bytelength = number_vertices * unsigned_long_int_bytes + + out_number_vertices = len(vertices) + out_number_indices = len(indices) + + unpadded_indices_bytelength = out_number_indices * unsigned_long_int_bytes + indices_bytelength = (unpadded_indices_bytelength + 3) & ~3 + + out_bin_bytelength = vertices_bytelength + indices_bytelength + + if is_binary: + out_bin_uir = "" + else: + out_bin_uir = '"uri": "out.bin",' + + gltf2 = gltf2 % ( out_bin_uir, + #buffer + out_bin_bytelength, + + # bufferViews[0] + indices_bytelength, + + # bufferViews[1] + indices_bytelength, + vertices_bytelength, + + # accessors[0] + out_number_indices, + out_number_vertices - 1, + + # accessors[1] + out_number_vertices, + minx, miny, minz, + maxx, maxy, maxz + ) + + glb_out = bytearray() + if is_binary: + gltf2 = gltf2.replace(" ", "") + gltf2 = gltf2.replace("\n", "") + + scene = bytearray(gltf2.encode()) + + scene_len = len(scene) + padded_scene_len = (scene_len + 3) & ~3 + body_offset = padded_scene_len + 12 + 8 + + file_len = body_offset + out_bin_bytelength + 8 + + # 12-byte header + glb_out.extend(struct.pack(' 3: + is_binary = True + else: + is_binary = False + + if out_path.lower().endswith(".glb"): + print("Use binary mode since output file has glb extension") + is_binary = True + else: + if is_binary: + print("output file should have glb extension but not %s", out_path) + + if not os.path.exists(path_to_stl): + print("stl file does not exists %s" % path_to_stl) + + if not is_binary: + if not os.path.isdir(out_path): + os.mkdir(out_path) + + stl_to_gltf(path_to_stl, out_path, is_binary) \ No newline at end of file diff --git a/sf_dlm/views/product_template_view.xml b/sf_dlm/views/product_template_view.xml index e8c28167..c563f728 100644 --- a/sf_dlm/views/product_template_view.xml +++ b/sf_dlm/views/product_template_view.xml @@ -6,7 +6,13 @@ product.template + + + + + + @@ -37,7 +43,7 @@ - + @@ -48,16 +54,16 @@ - - - - - - - - - - + + product.category.form.inherit.sf + product.category + + + + + + + product.template.stock.property.form.inherit @@ -94,5 +100,17 @@ + + + mrp.bom.form.inherit.sf + mrp.bom + + + + + + + \ No newline at end of file diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py index 419dee46..b3b828db 100644 --- a/sf_manufacturing/models/__init__.py +++ b/sf_manufacturing/models/__init__.py @@ -10,5 +10,3 @@ from . import res_user - - diff --git a/sf_manufacturing/models/model_type.py b/sf_manufacturing/models/model_type.py index bdc5fc78..ecc4b7af 100644 --- a/sf_manufacturing/models/model_type.py +++ b/sf_manufacturing/models/model_type.py @@ -6,7 +6,7 @@ class ModelType(models.Model): _description = '模型类型' name = fields.Char('名称') - embryo_tolerance = fields.Boolean('胚料的容余量', default=False) + embryo_tolerance = fields.Integer('胚料的容余量') routing_tmpl_ids = fields.One2many('sf.model.type.routing.sort', 'model_type_id', '工序模板') diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 55cd0ca4..036bf969 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from odoo import api, fields, models,_ - +from odoo import api, fields, models, _ class MrpProduction(models.Model): @@ -10,6 +9,8 @@ class MrpProduction(models.Model): tray_ids = fields.One2many('sf.tray', 'production_id', string="托盘") maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Number of maintenance requests") request_ids = fields.One2many('maintenance.request', 'production_id') + # model_file = fields.Binary('模型文件', related='product_id.model_file') + # model_information = fields.Char('模型信息', related='product_id.model_information') @api.depends('request_ids') def _compute_maintenance_count(self): @@ -130,6 +131,9 @@ class MrpProduction(models.Model): 'location_src_id': production.location_src_id.id, 'location_dest_id': production.location_dest_id.id, 'bom_id': production.bom_id.id, + 'model_file': base64.b64decode(production.product_id.model_file), + 'model_information': '%s/%s' % ( + production.product_id.model_processing_panel, production.product_id.materials_id.name), 'date_deadline': production.date_deadline, 'date_planned_start': production.date_planned_start, 'date_planned_finished': production.date_planned_finished, diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py index e853ab3f..2dad059b 100644 --- a/sf_manufacturing/models/mrp_workcenter.py +++ b/sf_manufacturing/models/mrp_workcenter.py @@ -8,6 +8,11 @@ class ResWorkcenter(models.Model): machine_tool_id = fields.Many2one('sf.machine_tool', '机床') users_ids = fields.Many2many("res.users", 'users_workcenter') + @api.onchange('machine_tool_id') + def get_machine_tool_is_binding(self): + print('1111111') + for item in self: + item.machine_tool_id.is_binding = False equipment_ids = fields.One2many( 'maintenance.equipment', 'workcenter_id', string="Maintenance Equipment", diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index e2b5033f..512a0030 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -45,9 +45,11 @@ class ResMrpWorkOrder(models.Model): user_permissions = fields.Boolean('用户权限', compute='get_user_permissions') + programming_no = fields.Char('编程单号') + is_programming = fields.Boolean('是否编程', default=False) cnc_worksheet = fields.Binary( '工作指令', readonly=True) - material_center_point = fields.Char(string='配料中心点') + material_center_point = fields.Char(string='胚料中心点') X1_axis = fields.Float(default=0) Y1_axis = fields.Float(default=0) Z1_axis = fields.Float(default=0) @@ -120,7 +122,11 @@ class ResMrpWorkOrder(models.Model): print("(%.2f,%.2f)" % (x, y)) self.material_center_point = ("(%.2f,%.2f,%.2f)" % (x, y, z)) self.X_deviation_angle = jdz - return self.material_center_point + # 将补偿值写入CNC加工工单 + workorder = self.env['mrp.workorder'].browse(self.ids) + work = workorder.production_id.workorder_ids + work.compensation_value_x = eval(self.material_center_point)[0] + work.compensation_value_y = eval(self.material_center_point)[1] def json_workorder_str(self, k, production, route): workorders_values_str = [0, '', { @@ -261,20 +267,21 @@ class ResMrpWorkOrder(models.Model): 'embryo_long': cnc.product_id.bom_ids.bom_line_ids.product_id.length, 'embryo_height': cnc.product_id.bom_ids.bom_line_ids.product_id.height, 'embryo_width': cnc.product_id.bom_ids.bom_line_ids.product_id.width, - 'order_no': cnc.production_id.origin - # 'factory_code': self.env.user.company_id.partner_id. + 'order_no': cnc.production_id.origin, + 'user': self.env.user.name, + 'model_file': base64.b64encode(cnc.product_id.model_file).decode('utf-8') } logging.info('res:%s' % res) configsettings = self.env['res.config.settings'].get_values() config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key']) url = '/api/intelligent_programming/create' config_url = configsettings['sf_url'] + url - res_str = json.dumps(res) - ret = requests.post(config_url, json={"result": res_str}, data=None, headers=config_header) + # res_str = json.dumps(res) + ret = requests.post(config_url, json={}, data=res, headers=config_header) ret = ret.json() result = json.loads(ret['result']) if result['status'] == 1: - return self.write({'state': 'progress'}) + return self.write({'programming_no': result['programming_no'], 'is_programming': True}) def json_workorder_str1(self, k, production, route): workorders_values_str = [0, '', { @@ -296,6 +303,7 @@ class ResMrpWorkOrder(models.Model): def button_start(self): if self.state == 'waiting' or self.state == 'ready': self.ensure_one() + if any(not time.date_end for time in self.time_ids.filtered(lambda t: t.user_id.id == self.env.user.id)): return True # As button_start is automatically called in the new view @@ -357,13 +365,13 @@ class CNCprocessing(models.Model): depth_of_processing_z = fields.Char('加工深度(Z)') cutting_tool_extension_length = fields.Char('刀具伸出长度') cutting_tool_handle_type = fields.Char('刀柄型号') - estimated_processing_time = fields.Datetime('预计加工时间') + estimated_processing_time = fields.Char('预计加工时间') remark = fields.Text('备注') workorder_id = fields.Many2one('mrp.workorder', string="工单") # mrs下发编程单创建CNC加工 def cnc_processing_create(self, obj): - workorder = self.env['mrp.workorder'].search([('production_id.name', '=', obj['manufacturing_order_no']), + workorder = self.env['mrp.workorder'].search([('production_id.name', '=', obj['production_order_no']), ('processing_panel', '=', obj['processing_panel']), ('routing_type', '=', 'CNC加工')]) vals = { @@ -406,26 +414,12 @@ class CNCprocessing(models.Model): if os.path.exists(nc_file_path): with open(nc_file_path, 'rb') as file: data_bytes = file.read() - attachment = self.attachment_create(cnc.program_name + '.NC', data_bytes) - cnc.write({'cnc_id': attachment.id}) - file.close() + attachment = self.attachment_create(cnc.program_name, data_bytes) + cnc.write({'cnc_id': attachment.id}) + file.close() else: return False - # 将nc文件对应的excel清单转为pdf - # def to_pdf(self, excel_path, pdf_path): - # """ - # 需要在linux中下载好libreoffice - # """ - # logging.info('pdf_path:%s' % pdf_path) - # logging.info('pdf_path:%s' % excel_path) - # # 注意cmd中的libreoffice要和linux中安装的一致 - # cmd = 'soffice --headless --convert-to pdf'.split() + [excel_path] + ['--outdir'] + [pdf_path] - # p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=1) - # # p.wait(timeout=30) # 停顿30秒等待转化 - # # stdout, stderr = p.communicate() - # p.communicate() - class SfWorkOrderBarcodes(models.Model): """ diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index cf1433f7..487f69ed 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -74,13 +74,17 @@ class StockRule(models.Model): Procurement = namedtuple('Procurement', ['product_id', 'product_qty', 'product_uom', 'location_id', 'name', 'origin', 'company_id', - 'values']) + 'values', 'model_file', + 'model_information']) s = Procurement(product_id=item[0].product_id, product_qty=1.0, product_uom=item[0].product_uom, location_id=item[0].location_id, name=item[0].name, origin=item[0].origin, company_id=item[0].company_id, values=item[0].values, + model_file=base64.b64decode(item[0].product_id.model_file), + model_information='%s/%s' % (item[0].product_id.model_processing_panel, + item[0].product_id.materials_id.name), ) item1 = list(item) item1[0] = s diff --git a/sf_manufacturing/models/tray.py b/sf_manufacturing/models/tray.py index 08906217..bdea35c4 100644 --- a/sf_manufacturing/models/tray.py +++ b/sf_manufacturing/models/tray.py @@ -3,7 +3,7 @@ import base64 from io import BytesIO from odoo import api, fields, models, SUPERUSER_ID, _ -from pystrich.code128 import Code128Encoder +# from pystrich.code128 import Code128Encoder class Tray(models.Model): diff --git a/sf_manufacturing/views/model_type_view.xml b/sf_manufacturing/views/model_type_view.xml index 58fd70be..58d1bf94 100644 --- a/sf_manufacturing/views/model_type_view.xml +++ b/sf_manufacturing/views/model_type_view.xml @@ -31,7 +31,7 @@
- + diff --git a/sf_manufacturing/views/mrp_workcenter_views.xml b/sf_manufacturing/views/mrp_workcenter_views.xml index 186dce3c..6c6e73e6 100644 --- a/sf_manufacturing/views/mrp_workcenter_views.xml +++ b/sf_manufacturing/views/mrp_workcenter_views.xml @@ -107,7 +107,7 @@ - + diff --git a/sf_mrs_connect/controllers/controllers.py b/sf_mrs_connect/controllers/controllers.py index 94e7e2d1..76fc34c5 100644 --- a/sf_mrs_connect/controllers/controllers.py +++ b/sf_mrs_connect/controllers/controllers.py @@ -32,21 +32,32 @@ class Sf_Mrs_Connect(http.Controller): logging.info('model_code:%s' % model_code) server_dir = cnc.with_user(request.env.ref("base.user_admin")).download_file_tmp(model_code, processing_panel) - cnc_file_path = os.path.join(server_dir, cnc.program_name + '.NC') - logging.info('cnc_file_path:%s' % cnc_file_path) - cnc.with_user(request.env.ref("base.user_admin")).write_file(cnc_file_path, cnc) - # logging.info('get_cnc_processing_create:%s' % '111111111111111') - # for root, dirs, files in os.walk(server_dir): - # for file in files: - # if os.path.splitext(file)[1] == '.xlsx' or os.path.splitext(file)[1] == ".xls": - # pdf_path = os.path.splitext(file)[1] + '.PDF' - # cnc_pdf_path = request.env['sf.cnc.processing'].with_user( - # request.env.ref("base.user_admin")).to_pdf(os.path.join(root, file), pdf_path) - # if pdf_path != False: - # if not cnc.workorder_id.cnc_worksheet: - # cnc.workorder_id.cnc_worksheet = base64.b64encode(open(cnc_pdf_path, 'rb').read()) - # else: - # logging.info('break:%s' % 'break') - # break + # cnc_file_path = os.path.join('/', server_dir, cnc.program_name + '.nc') + # logging.info('cnc_file_path:%s' % cnc_file_path) + # cnc.with_user(request.env.ref("base.user_admin")).write_file(cnc_file_path, cnc) + logging.info('server_dir:%s' % server_dir) + for root, dirs, files in os.walk(server_dir): + for f in files: + logging.info('f:%s' % f) + logging.info('f[0]:%s' % f.split('.')[0]) + if os.path.splitext(f)[1] == ".pdf": + full_path = os.path.join(server_dir, root, f) + logging.info('pdf:%s' % full_path) + if full_path != False: + if not cnc.workorder_id.cnc_worksheet: + cnc.workorder_id.cnc_worksheet = base64.b64encode(open(full_path, 'rb').read()) + else: + logging.info('break:%s' % 'break') + continue + else: + logging.info('cnc.program_name:%s' % cnc.program_name) + if cnc.program_name == f.split('.')[0]: + logging.info('f[0]:%s' % f[0]) + cnc_file_path = os.path.join(server_dir, root, f) + logging.info('cnc_file_path:%s' % cnc_file_path) + cnc.with_user(request.env.ref("base.user_admin")).write_file(cnc_file_path, cnc) + else: + continue + except Exception as e: logging.info('get_cnc_processing_create error:%s' % e) diff --git a/sf_mrs_connect/models/ftp_operate.py b/sf_mrs_connect/models/ftp_operate.py index a71ee83f..655c50e6 100644 --- a/sf_mrs_connect/models/ftp_operate.py +++ b/sf_mrs_connect/models/ftp_operate.py @@ -25,15 +25,16 @@ class FtpController(): # 下载目录下的文件 def download_file_tree(self, target_dir, serverdir): - self.ftp.cwd(target_dir) # 切换工作路径 if not os.path.exists(serverdir): os.makedirs(serverdir) - remotenames = self.ftp.nlst() - for file in remotenames: - server = os.path.join(serverdir, file) - if file.find(".") != -1: - self.download_file(server, file) - return + self.ftp.cwd(target_dir) # 切换工作路径 + remotenames = self.ftp.nlst() + for file in remotenames: + server = os.path.join(serverdir, file) + if file.find(".") != -1: + self.download_file(server, file) + else: + return # 下载指定目录下的指定文件 def download_file(self, serverfile, remotefile): diff --git a/sf_sale/models/sale_order.py b/sf_sale/models/sale_order.py index 2ae0ae49..899e2126 100644 --- a/sf_sale/models/sale_order.py +++ b/sf_sale/models/sale_order.py @@ -15,20 +15,23 @@ class ReSaleOrder(models.Model): # 业务平台分配工厂后在智能工厂先创建销售订单 def sale_order_create(self, company_id, delivery_name, delivery_telephone, delivery_address, - deadline_of_delivery): + deadline_of_delivery,user_id): now_time = datetime.datetime.now() + partner = self.env.user.partner_id + logging.info('partner:%s' % partner) order_id = self.env['sale.order'].sudo().create({ 'company_id': company_id.id, 'date_order': now_time, 'name': self.env['ir.sequence'].next_by_code('sale.order', sequence_date=now_time), - 'partner_id': 1, - 'state': 'sale', - 'user_id': 1, + 'partner_id': partner.id, + 'state': 'draft', + 'user_id': user_id.id, 'person_of_delivery': delivery_name, 'telephone_of_delivery': delivery_telephone, 'address_of_delivery': delivery_address, 'deadline_of_delivery': deadline_of_delivery }) + logging.info('sale_order_create:%s' % order_id) return order_id # 业务平台分配工厂时在创建完产品后再创建销售明细信息 @@ -36,10 +39,12 @@ class ReSaleOrder(models.Model): vals = { 'order_id': self.id, 'product_id': product.id, - 'name': '%s/%s/%s/%s/%s' % ( - item['model_long'], item['model_width'], item['model_height'], item['model_volume'], - product.materials_id.name), - 'price_unit': item['price'], + 'name': '%s/%s/%s/%s/%s/%s' % ( + product.model_long, product.model_width, product.model_height, product.model_volume, + product.model_machining_precision, + product.materials_id.name), + 'price_unit': product.list_price, + 'route_id': product.route_ids, 'product_uom_qty': item['number'] } return self.env['sale.order.line'].create(vals) diff --git a/web_widget_model_viewer/README.rst b/web_widget_model_viewer/README.rst new file mode 100644 index 00000000..82cacf22 --- /dev/null +++ b/web_widget_model_viewer/README.rst @@ -0,0 +1,126 @@ +=================== +Model viewer widget +=================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/14.0/web_widget_model_viewer + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-14-0/web-14-0-web_widget_model_viewer + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/162/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +```` is a web component that makes rendering interactive 3D models - optionally in AR - easy to do, on as many browsers and devices as possible. ```` strives to give you great defaults for rendering quality and performance. + +See `source repository `_ and `documentation `_. + +The model to load is a GLTF 2.0 file format. + +See ``_ and GLTF overview: + +.. figure:: https://raw.githubusercontent.com/OCA/web/14.0/web_widget_model_viewer/static/img/gltfOverview.png + +Many engine developers have already started transitioning to glTF 2.0 to reap performance, portability and quality benefits, including BabylonJS, three.js, Cesium, Sketchfab, and xeogl and instant3Dhub engines. glTF 2.0 is also seeing industry support by companies such as Adobe, Google, Marmoset, Microsoft, NVIDIA, Oculus, UX3D, and more as well as prominent universities such as, University of Pennsylvania and Sapienza University of Rome. + +"example" directory contains the GLB file of a chair, that is rendered in the following way: + +.. figure:: https://raw.githubusercontent.com/OCA/web/14.0/web_widget_model_viewer/static/img/Eames_Lounge_Chair.gif + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Add ``widget="model_viewer"`` to your binary field in form view. Optionally you can set ``style`` and ``max_upload_size`` (in MB) attributes. + +Changelog +========= + +14.0.1.0.0 (2021-10-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIG] v14 + +12.0.2.0.0 (2020-07-14) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [IMP] fullscreen and view redesign + +12.0.1.0.0 (2020-07-10) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Start of the history. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TAKOBI +* Openindustry.it + +Contributors +~~~~~~~~~~~~ + +* Lorenzo Battistini (https://takobi.online) +* Andrea Piovesana (https://openindustry.it) +* Marco Colombo (https://phi.technology) + +Other credits +~~~~~~~~~~~~~ + +Chair © Copyright 2020 Shopify Inc., licensed under CC-BY-4.0. + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-eLBati| image:: https://github.com/eLBati.png?size=40px + :target: https://github.com/eLBati + :alt: eLBati + +Current `maintainer `__: + +|maintainer-eLBati| + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_widget_model_viewer/__init__.py b/web_widget_model_viewer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web_widget_model_viewer/__manifest__.py b/web_widget_model_viewer/__manifest__.py new file mode 100644 index 00000000..342b9538 --- /dev/null +++ b/web_widget_model_viewer/__manifest__.py @@ -0,0 +1,33 @@ +# Copyright 2020 Andrea Piovesana @ Openindustry.it +# Copyright 2020 Lorenzo Battistini @ TAKOBI +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Model viewer widget", + "summary": "Easily display interactive 3D models on the web & in AR", + "version": "14.0.1.0.0", + "development_status": "Beta", + "category": "Web", + "website": "https://github.com/OCA/web", + "author": "TAKOBI, Openindustry.it, Odoo Community Association (OCA)", + "maintainers": ["eLBati"], + "license": "AGPL-3", + "depends": [ + "web", + ], + 'assets': { + 'web.assets_qweb': [ + "/web_widget_model_viewer/static/src/xml/*.xml", + ], + 'web.assets_backend': [ + '/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js', + ], + }, + "data": [ + "views/assets.xml", + ], + "qweb": [ + + ], + "application": False, + "installable": True, +} diff --git a/web_widget_model_viewer/example/Chair.glb b/web_widget_model_viewer/example/Chair.glb new file mode 100644 index 00000000..d1a9bb0a Binary files /dev/null and b/web_widget_model_viewer/example/Chair.glb differ diff --git a/web_widget_model_viewer/example/chair.png b/web_widget_model_viewer/example/chair.png new file mode 100644 index 00000000..e3ddf31c Binary files /dev/null and b/web_widget_model_viewer/example/chair.png differ diff --git a/web_widget_model_viewer/i18n/ca.po b/web_widget_model_viewer/i18n/ca.po new file mode 100644 index 00000000..58fc7e59 --- /dev/null +++ b/web_widget_model_viewer/i18n/ca.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_widget_model_viewer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2021-02-17 13:45+0000\n" +"Last-Translator: claudiagn \n" +"Language-Team: none\n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.3.2\n" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js:59 +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:26 +#, python-format +msgid "3D model" +msgstr "Model 3D" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:8 +#, python-format +msgid "Clear" +msgstr "Clar" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js:59 +#, python-format +msgid "Could not display the selected model." +msgstr "No s'ha pogut mostrar el model seleccionat." + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:7 +#, python-format +msgid "Edit" +msgstr "Editar" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:28 +#, python-format +msgid "Fullscreen" +msgstr "Pantalla completa" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:10 +#, python-format +msgid "Uploading..." +msgstr "Pujant..." + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:28 +#, python-format +msgid "View fullscreen" +msgstr "Veure pantalla completa" diff --git a/web_widget_model_viewer/i18n/es.po b/web_widget_model_viewer/i18n/es.po new file mode 100644 index 00000000..c5a55533 --- /dev/null +++ b/web_widget_model_viewer/i18n/es.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_widget_model_viewer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-09-16 14:00+0000\n" +"Last-Translator: claudiagn \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 3.10\n" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js:59 +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:26 +#, python-format +msgid "3D model" +msgstr "Modelo 3D" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:8 +#, python-format +msgid "Clear" +msgstr "Claro" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js:59 +#, python-format +msgid "Could not display the selected model." +msgstr "No se pudo mostrar el modelo seleccionado." + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:7 +#, python-format +msgid "Edit" +msgstr "Editar" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:28 +#, python-format +msgid "Fullscreen" +msgstr "Pantalla completa" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:10 +#, python-format +msgid "Uploading..." +msgstr "Subiendo..." + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:28 +#, python-format +msgid "View fullscreen" +msgstr "Ver pantalla completa" diff --git a/web_widget_model_viewer/i18n/pt_BR.po b/web_widget_model_viewer/i18n/pt_BR.po new file mode 100644 index 00000000..7a53dde5 --- /dev/null +++ b/web_widget_model_viewer/i18n/pt_BR.po @@ -0,0 +1,65 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_widget_model_viewer +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js:59 +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:26 +#, python-format +msgid "3D model" +msgstr "" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:8 +#, python-format +msgid "Clear" +msgstr "" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js:59 +#, python-format +msgid "Could not display the selected model." +msgstr "" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:7 +#, python-format +msgid "Edit" +msgstr "" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:28 +#, python-format +msgid "Fullscreen" +msgstr "" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:10 +#, python-format +msgid "Uploading..." +msgstr "" + +#. module: web_widget_model_viewer +#. openerp-web +#: code:addons/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml:28 +#, python-format +msgid "View fullscreen" +msgstr "" diff --git a/web_widget_model_viewer/readme/CONTRIBUTORS.rst b/web_widget_model_viewer/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..7d946daf --- /dev/null +++ b/web_widget_model_viewer/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Lorenzo Battistini (https://takobi.online) +* Andrea Piovesana (https://openindustry.it) +* Marco Colombo (https://phi.technology) diff --git a/web_widget_model_viewer/readme/CREDITS.rst b/web_widget_model_viewer/readme/CREDITS.rst new file mode 100644 index 00000000..f7db4618 --- /dev/null +++ b/web_widget_model_viewer/readme/CREDITS.rst @@ -0,0 +1 @@ +Chair © Copyright 2020 Shopify Inc., licensed under CC-BY-4.0. diff --git a/web_widget_model_viewer/readme/DESCRIPTION.rst b/web_widget_model_viewer/readme/DESCRIPTION.rst new file mode 100644 index 00000000..ca410f1a --- /dev/null +++ b/web_widget_model_viewer/readme/DESCRIPTION.rst @@ -0,0 +1,15 @@ +```` is a web component that makes rendering interactive 3D models - optionally in AR - easy to do, on as many browsers and devices as possible. ```` strives to give you great defaults for rendering quality and performance. + +See `source repository `_ and `documentation `_. + +The model to load is a GLTF 2.0 file format. + +See ``_ and GLTF overview: + +.. figure:: ../static/img/gltfOverview.png + +Many engine developers have already started transitioning to glTF 2.0 to reap performance, portability and quality benefits, including BabylonJS, three.js, Cesium, Sketchfab, and xeogl and instant3Dhub engines. glTF 2.0 is also seeing industry support by companies such as Adobe, Google, Marmoset, Microsoft, NVIDIA, Oculus, UX3D, and more as well as prominent universities such as, University of Pennsylvania and Sapienza University of Rome. + +"example" directory contains the GLB file of a chair, that is rendered in the following way: + +.. figure:: ../static/img/Eames_Lounge_Chair.gif diff --git a/web_widget_model_viewer/readme/HISTORY.rst b/web_widget_model_viewer/readme/HISTORY.rst new file mode 100644 index 00000000..8c0d1df2 --- /dev/null +++ b/web_widget_model_viewer/readme/HISTORY.rst @@ -0,0 +1,14 @@ +14.0.1.0.0 (2021-10-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIG] v14 + +12.0.2.0.0 (2020-07-14) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [IMP] fullscreen and view redesign + +12.0.1.0.0 (2020-07-10) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Start of the history. diff --git a/web_widget_model_viewer/readme/USAGE.rst b/web_widget_model_viewer/readme/USAGE.rst new file mode 100644 index 00000000..d249c04c --- /dev/null +++ b/web_widget_model_viewer/readme/USAGE.rst @@ -0,0 +1 @@ +Add ``widget="model_viewer"`` to your binary field in form view. Optionally you can set ``style`` and ``max_upload_size`` (in MB) attributes. diff --git a/web_widget_model_viewer/static/description/icon.png b/web_widget_model_viewer/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/web_widget_model_viewer/static/description/icon.png differ diff --git a/web_widget_model_viewer/static/description/index.html b/web_widget_model_viewer/static/description/index.html new file mode 100644 index 00000000..335cca09 --- /dev/null +++ b/web_widget_model_viewer/static/description/index.html @@ -0,0 +1,472 @@ + + + + + + +Model viewer widget + + + +
+

Model viewer widget

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runbot

+

<model-viewer> is a web component that makes rendering interactive 3D models - optionally in AR - easy to do, on as many browsers and devices as possible. <model-viewer> strives to give you great defaults for rendering quality and performance.

+

See source repository and documentation.

+

The model to load is a GLTF 2.0 file format.

+

See https://www.khronos.org/gltf/ and GLTF overview:

+
+https://raw.githubusercontent.com/OCA/web/14.0/web_widget_model_viewer/static/img/gltfOverview.png +
+

Many engine developers have already started transitioning to glTF 2.0 to reap performance, portability and quality benefits, including BabylonJS, three.js, Cesium, Sketchfab, and xeogl and instant3Dhub engines. glTF 2.0 is also seeing industry support by companies such as Adobe, Google, Marmoset, Microsoft, NVIDIA, Oculus, UX3D, and more as well as prominent universities such as, University of Pennsylvania and Sapienza University of Rome.

+

“example” directory contains the GLB file of a chair, that is rendered in the following way:

+
+https://raw.githubusercontent.com/OCA/web/14.0/web_widget_model_viewer/static/img/Eames_Lounge_Chair.gif +
+

Table of contents

+ +
+

Usage

+

Add widget="model_viewer" to your binary field in form view. Optionally you can set style and max_upload_size (in MB) attributes.

+
+
+

Changelog

+
+

14.0.1.0.0 (2021-10-07)

+
    +
  • [MIG] v14
  • +
+
+
+

12.0.2.0.0 (2020-07-14)

+
    +
  • [IMP] fullscreen and view redesign
  • +
+
+
+

12.0.1.0.0 (2020-07-10)

+
    +
  • Start of the history.
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TAKOBI
  • +
  • Openindustry.it
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

Chair © Copyright 2020 Shopify Inc., licensed under CC-BY-4.0.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

eLBati

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_widget_model_viewer/static/img/Eames_Lounge_Chair.gif b/web_widget_model_viewer/static/img/Eames_Lounge_Chair.gif new file mode 100644 index 00000000..8d049e34 Binary files /dev/null and b/web_widget_model_viewer/static/img/Eames_Lounge_Chair.gif differ diff --git a/web_widget_model_viewer/static/img/gltfOverview.png b/web_widget_model_viewer/static/img/gltfOverview.png new file mode 100644 index 00000000..6a5bb613 Binary files /dev/null and b/web_widget_model_viewer/static/img/gltfOverview.png differ diff --git a/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js b/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js new file mode 100644 index 00000000..356060b6 --- /dev/null +++ b/web_widget_model_viewer/static/src/js/web_widget_model_viewer.js @@ -0,0 +1,114 @@ +// Copyright 2020 Lorenzo Battistini @ TAKOBI +// Copyright 2020 Andrea Piovesana @ Openindustry.it +odoo.define("web_widget_model_viewer.FieldBinaryModelViewer", function (require) { + "use strict"; + + var BasicFields = require("web.basic_fields"); + var core = require("web.core"); + var registry = require("web.field_registry"); + var session = require("web.session"); + var utils = require("web.utils"); + var _t = core._t; + var qweb = core.qweb; + + var FieldBinaryModelViewer = BasicFields.FieldBinaryFile.extend({ + template: "FieldBinaryModelViewer", + events: _.extend({}, BasicFields.FieldBinaryFile.prototype.events, { + click: function () { + if (this.mode === "readonly") { + this.trigger_up("bounce_edit"); + } + }, + "click #model-viewer-fullscreen": "fullscreen", + }), + supportedFieldTypes: ["binary"], + init: function () { + this._super.apply(this, arguments); + var max_upload_size = this.attrs.max_upload_size; + if (max_upload_size) { + this.max_upload_size = parseInt(max_upload_size, 10) * 1024 * 1024; + } else { + // 250M + this.max_upload_size = 250 * 1024 * 1024; + } + }, + _render: function () { + var self = this; + var url = ""; + if (this.value) { + if (utils.is_bin_size(this.value)) { + url = session.url("/web/content", { + model: this.model, + id: JSON.stringify(this.res_id), + field: this.name, + }); + } else { + url = "data:model/gltf-binary;base64," + this.value; + } + } + var $glb = $( + qweb.render("FieldBinaryModelViewer-glb", {widget: this, url: url}) + ); + var style = this.attrs.style; + if (style) { + $glb.attr("style", style); + } + this.$("> model-viewer").remove(); + this.$el.prepend($glb); + $glb.on("error", function () { + self._clearFile(); + $glb.attr("src", ""); + self.do_warn( + _t("3D model"), + _t("Could not display the selected model.") + ); + }); + }, + /* eslint-disable complexity */ + fullscreen: function (ev) { + var isFullscreenAvailable = + document.fullscreenEnabled || + document.mozFullScreenEnabled || + document.webkitFullscreenEnabled || + document.msFullscreenEnabled || + false; + var modelViewerElem = ev.target.parentElement.parentElement.parentElement; + if (isFullscreenAvailable) { + var fullscreenElement = + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement; + if (fullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + /* Firefox */ + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + /* Chrome, Safari and Opera */ + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + /* IE/Edge */ + document.msExitFullscreen(); + } + } else if (modelViewerElem.requestFullscreen) { + modelViewerElem.requestFullscreen(); + } else if (modelViewerElem.mozRequestFullScreen) { + /* Firefox */ + modelViewerElem.mozRequestFullScreen(); + } else if (modelViewerElem.webkitRequestFullscreen) { + /* Chrome, Safari and Opera */ + modelViewerElem.webkitRequestFullscreen(); + } else if (modelViewerElem.msRequestFullscreen) { + /* IE/Edge */ + modelViewerElem.msRequestFullscreen(); + } + } else { + console.error("ERROR : full screen not supported by web browser"); + } + }, + }); + + registry.add("model_viewer", FieldBinaryModelViewer); +}); diff --git a/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml b/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml new file mode 100644 index 00000000..91314103 --- /dev/null +++ b/web_widget_model_viewer/static/src/xml/web_widget_model_viewer.xml @@ -0,0 +1,57 @@ + + + +
+ +
+
+
+
+
+ + +
+ + + +
+ + + + + + +
+
+
diff --git a/web_widget_model_viewer/views/assets.xml b/web_widget_model_viewer/views/assets.xml new file mode 100644 index 00000000..855f4a3e --- /dev/null +++ b/web_widget_model_viewer/views/assets.xml @@ -0,0 +1,27 @@ + + +