from this import s from odoo import fields, models, api, tools, _ from datetime import datetime, timedelta, date from odoo.exceptions import UserError import logging import requests import json import re import qrcode, base64 from bs4 import BeautifulSoup from io import BytesIO _logger = logging.getLogger(__name__) class ProductTemplate(models.Model): _inherit = "product.template" x_attribute_set_id = fields.Integer( string="Magento Attribute Set ID", help="Attribute Set ID dari Magento", readonly=True ) x_attribute_set_name = fields.Char( string="Magento Attribute Set Name", help="Attribute Set Name dari Magento", readonly=True ) image_carousel_lines = fields.One2many( comodel_name="image.carousel", inverse_name="product_id", string="Image Carousel", auto_join=True, copy=False ) x_studio_field_tGhJR = fields.Many2many('x_product_tags', string="Product Tags") x_manufacture = fields.Many2one( comodel_name="x_manufactures", string="Manufactures" ) x_model_product = fields.Char(string="Model Produk") x_product_manufacture = fields.Many2one( comodel_name="x_manufactures", string="Manufacture" ) x_lazada = fields.Text(string="Lazada") x_tokopedia = fields.Text(string="Tokopedia") web_tax_id = fields.Many2one('account.tax', string='Website Tax') web_price = fields.Float( 'Web Price', compute='_compute_web_price', digits='Product Price', inverse='_set_product_lst_price', help="Web Price with pricelist_id = 1") qty_stock_vendor = fields.Float('QTY Stock Vendor', compute='_compute_qty_stock_vendor') have_promotion_program = fields.Boolean('Have Promotion Program', compute='_have_promotion_program', help="Punya promotion program gak?") product_rating = fields.Float('Product Rating', help="Digunakan untuk sorting product di website", default=0.0) virtual_rating = fields.Float('Virtual Rating', compute='_compute_virtual_rating', help="Column Virtual untuk product rating, digunakan oleh Solr", default=0.0) last_calculate_rating = fields.Datetime("Last Calculate Rating") web_price_sorting = fields.Float('Web Price Sorting', help='Hanya digunakan untuk sorting di web, harga tidak berlaku', default=0.0) virtual_qty = fields.Float(string='Virtual Qty', default=0) solr_flag = fields.Integer(string='Solr Flag', default=0) search_rank = fields.Integer(string='Search Rank', default=0) search_rank_weekly = fields.Integer(string='Search Rank Weekly', default=0) supplier_url = fields.Char(string='Vendor URL') # custom field for support Trusco products maker_code = fields.Char(string='Maker Code') maker_name = fields.Char(string='Maker Name') origin = fields.Char(string='Origin') features = fields.Char(string='Features') usage = fields.Char(string='Usage') specification = fields.Char(string='Specification') material = fields.Char(string='Material') is_new_product = fields.Boolean(string='Produk Baru', help='Centang jika ingin ditammpilkan di website sebagai segment Produk Baru') seq_new_product = fields.Integer(string='Seq New Product', help='Urutan Sequence New Product') is_edited = fields.Boolean(string='Is Edited') qty_sold = fields.Float(string='Sold Quantity', compute='_get_qty_sold') kind_of = fields.Selection([ ('sp', 'Spare Part'), ('acc', 'Accessories') ], string='Kind of', copy=False) sni = fields.Boolean(string='SNI') tkdn = fields.Boolean(string='TKDN') short_spesification = fields.Char(string='Short Spesification') merchandise_ok = fields.Boolean(string='Product Promotion') print_barcode = fields.Boolean(string='Print Barcode', default=True) # qr_code = fields.Binary("QR Code", compute='_compute_qr_code') @api.model def create(self, vals): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) active_model = self.env.context.get('active_model') if self.env.user.id not in users_in_group.mapped('id') and active_model == None: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductTemplate, self).create(vals) return result # def write(self, values): # group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id # users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) # active_model = self.env.context.get('active_model') # if self.env.user.id not in users_in_group.mapped('id') and active_model == None: # raise UserError('Hanya MD yang bisa mengedit Product') # result = super(ProductTemplate, self).write(values) # return result # def _compute_qr_code(self): # for rec in self.product_variant_ids: # qr = qrcode.QRCode( # version=1, # error_correction=qrcode.constants.ERROR_CORRECT_L, # box_size=5, # border=4, # ) # qr.add_data(rec.display_name) # qr.make(fit=True) # img = qr.make_image(fill_color="black", back_color="white") # buffer = BytesIO() # img.save(buffer, format="PNG") # qr_code_img = base64.b64encode(buffer.getvalue()).decode() # rec.qr_code = qr_code_img @api.constrains('name', 'internal_reference', 'x_manufacture') def required_public_categ_ids(self): for rec in self: if not rec.public_categ_ids and rec.type == 'product': raise UserError('Field Categories harus diisi') def _get_qty_sold(self): for rec in self: rec.qty_sold = sum(x.qty_sold for x in rec.product_variant_ids) def day_product_to_edit(self): day_products = [] for product in self: day_product = (product.write_date - product.create_date).days day_products.append(day_product) return day_products @api.constrains('name', 'default_code') def _check_duplicate_product(self): for product in self: variants = product.product_variant_ids names = [x.name for x in variants] if variants else [product.name] default_codes = [x.default_code for x in variants] if variants else [product.default_code] domain = [ ('default_code', '!=', False), ('id', '!=', product.id), '|', ('name', 'in', names), ('default_code', 'in', default_codes) ] product_exist = self.search(domain, limit=1) if len(product_exist) > 0: raise UserError('Name atau Internal Reference sudah digunakan pada produk lain') if self.env.user.is_purchasing_manager or self.env.user.is_editor_product or self.env.user.id in [1, 25]: continue if sum(product.day_product_to_edit()) > 0: raise UserError('Produk ini tidak dapat diubah') @api.constrains('name') def _validate_name(self): rule_regex = self.env['ir.config_parameter'].sudo().get_param('product.product.rule_name_regex') or '' pattern = rf'^{rule_regex}$' if not re.match(pattern, self.name): pattern_suggest = rf"{rule_regex}" suggest = ''.join(re.findall(pattern_suggest, self.name)) raise UserError(f'Contoh yang benar adalah {suggest}') # def write(self, vals): # if 'solr_flag' not in vals and self.solr_flag == 1: # vals['solr_flag'] = 2 # return super().write(vals) def _compute_virtual_rating(self): for product in self: rate = 0 if product.web_price: rate += 8 if product.qty_sold > 0: rate += 5 if product.image_128: rate += 7 if product.website_description: rate += 7 if product.product_variant_id.qty_onhand_bandengan > 0: rate += 5 if product.product_variant_id._is_have_flashsale(): rate += 10 product.virtual_rating = rate # if product.web_price: # rate += 4 # if product.qty_sold > 0: # rate += 3 # if product.have_promotion_program: #have discount from pricelist # rate += 5 # if product.image_128: # rate += 3 # if product.website_description: # rate += 1 # if product.product_variant_id.qty_stock_vendor > 0: # rate += 2 # product.virtual_rating = rate def unlink(self): if self._name == 'product.template': raise UserError('Maaf anda tidak bisa delete product') def update_new_product(self): current_time = datetime.now() delta_time = current_time - timedelta(days=30) delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') products = self.env['product.template'].search([ ('type', '=', 'product'), ('active', '=', True), ('product_rating', '>', 3), ('create_date', '>=', delta_time), ], limit=100) seq = 0 for product in products: seq += 1 product.is_new_product = True product.seq_new_product = seq _logger.info('Updated New Product %s' % product.name) def update_internal_reference(self, limit=100): templates = self.env['product.template'].search([ ('default_code', '=', False), ('product_variant_ids.default_code', '=', False), ('type', '=', 'product'), ('active', '=', True) ], limit=limit, order='write_date desc') for template in templates: if not template.default_code: template.default_code = 'IT.'+str(template.id) for variant in template.product_variant_ids: if not variant.default_code: variant.default_code = 'ITV.%s' % str(variant.id) _logger.info('Updated Template %s' % template.name) # templates_with_variant = self.env['product.product'].search([ # ('default_code', '=', False), # ('type', '=', 'product'), # ('active', '=', True), # ('product_tmpl_id', '!=', False), # ], limit=limit, order='write_date desc') # for template_with_variant in templates_with_variant: # for product in template_with_variant.product_variant_ids: # if product.default_code: # continue # product.default_code = 'ITV.'+str(product.id) # _logger.info('Updated Variant %s' % product.name) @api.onchange('name','default_code','x_manufacture','product_rating','website_description','image_1920','weight','public_categ_ids','image_carousel_lines') def update_solr_flag(self): for tmpl in self: if tmpl.solr_flag == 1: tmpl.solr_flag = 2 def _compute_qty_stock_vendor(self): for product_template in self: product_template.qty_stock_vendor = 0 for product_variant in product_template.product_variant_ids: product_template.qty_stock_vendor += int(product_variant.qty_stock_vendor) def _compute_web_price(self): for template in self: template.web_price = template.product_variant_id.web_price def _have_promotion_program(self): for template in self: # product = self.env['product.product'].search([('product_tmpl_id', '=', template.id)], limit=1) product_pricelist_item = self.env['product.pricelist.item'].search([ ('pricelist_id', '=', 4), ('product_id', '=', template.product_variant_id.id)], limit=1) discount = product_pricelist_item.price_discount if discount: template.have_promotion_program = True else: template.have_promotion_program = False def _get_active_flash_sale(self): current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') variant_ids = [x.id for x in self.product_variant_ids] pricelist = self.env['product.pricelist'].search([ ('is_flash_sale', '=', True), ('item_ids.product_id', 'in', variant_ids), ('start_date', '<=', current_time), ('end_date', '>=', current_time) ], limit=1) return pricelist @api.model def _calculate_rating_product(self, limit=1000, expiry_days=30, ids=False): current_time = datetime.now() current_time_str = current_time.strftime('%Y-%m-%d %H:%M:%S') delta_time = current_time - timedelta(days=expiry_days) delta_time_str = delta_time.strftime('%Y-%m-%d %H:%M:%S') query = [ '&','&', ('type', '=', 'product'), ('active', '=', True) ] if not ids: query += [ '|', ('last_calculate_rating', '=', False), ('last_calculate_rating', '<', delta_time_str) ] else: query += [('id', 'in', ids)] products = self.env['product.template'].search(query, limit=limit) for product in products: _logger.info("Calculate Rating Product %s" % product.id) product.product_rating = product.virtual_rating product.last_calculate_rating = current_time_str def _get_stock_website(self): qty = self._get_stock_altama() print(qty) def get_stock_altama(self, item_code): current_time = datetime.now() current_time = current_time.strftime('%Y-%m-%d %H:%M:%S') query = [('source', '=', 'altama'), ('expired_date', '>', current_time)] token_data = self.env['token.storage'].search(query, order='expired_date desc',limit=1) if not token_data: token_data = self._get_new_token_altama() token = token_data['access_token'] else: token = token_data.access_token url = "https://erpapi.altama.co.id/erp/api/stock/buffer/btob" auth = "Bearer "+token headers = { 'Content-Type': 'application/json', 'Authorization': auth, } json_data = { 'type_search': 'Item_code', 'search_key':[item_code], } response = requests.post(url, headers=headers, json=json_data) if response.status_code != 200: return 0 datas = json.loads(response.text)['data'] qty = 0 for data in datas: availability = float(data['availability']) # Mengonversi ke tipe data int qty += availability # Mengakumulasi qty dari setiap data return qty def _get_new_token_altama(self): url = "https://kc.altama.co.id/realms/altama/protocol/openid-connect/token" auth = 'Basic SW5kb3Rla25pa19DbGllbnQ6Vm1iZExER1ZUS3RuVlRQdkU1MXRvRzdiTW51TE1WRVI=' headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': auth, } data = { 'grant_type': 'client_credentials', } response = requests.post(url, headers=headers, data=data).json() lookup_json = json.dumps(response, indent=4, sort_keys=True) token = json.loads(lookup_json)['access_token'] expires_in = json.loads(lookup_json)['expires_in'] current_time = datetime.now() delta_time = current_time + timedelta(seconds=int(expires_in)) current_time = current_time.strftime('%Y-%m-%d %H:%M:%S') delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') values = { 'source': 'altama', 'access_token': token, 'expires_in': expires_in, 'expired_date': delta_time, } self.env['token.storage'].create([values]) return values # ============================== def get_vendor_name(self, rec): """Get formatted name for vendor/supplier""" return rec.name.name if rec.name else f"ID {rec.id}" def get_attribute_line_name(self, rec): """Get formatted name for attribute line""" if rec.attribute_id and rec.value_ids: values = ", ".join(rec.value_ids.mapped('name')) return f"{rec.attribute_id.name}: {values}" return f"ID {rec.id}" def _get_vendor_field_label(self, field_name): """Get human-readable label for vendor fields""" field_labels = { 'name': 'Vendor', 'currency_id': 'Currency', 'product_uom': 'Unit of Measure', 'price': 'Price', 'delay': 'Delivery Lead Time', 'product_id': 'Product Variant', 'product_name': 'Vendor Product Name', 'product_code': 'Vendor Product Code', 'date_start': 'Start Date', 'date_end': 'End Date', 'min_qty': 'Quantity' } return field_labels.get(field_name, field_name.replace('_', ' ').title()) # ============================== def _collect_old_values(self, vals): """Collect old values before write""" return { record.id: { field_name: record[field_name] for field_name in vals.keys() if field_name in record._fields } for record in self } def _prepare_attribute_line_info(self): """Prepare attribute line info for logging and update comparison""" line_info = {} for line in self.attribute_line_ids: line_info[line.id] = { 'name': self.get_attribute_line_name(line), 'attribute_id': line.attribute_id.id if line.attribute_id else None, 'attribute_name': line.attribute_id.name if line.attribute_id else None, 'value_ids': [(v.id, v.name) for v in line.value_ids], 'value_names': ", ".join(line.value_ids.mapped('name')) } return line_info def _prepare_vendor_info(self): """Prepare vendor info for logging before they are deleted""" vendor_info = {} for seller in self.seller_ids: vendor_info[seller.id] = { 'name': self.get_vendor_name(seller), 'price': seller.price, 'min_qty': seller.min_qty, 'delay': seller.delay, 'product_name': seller.product_name, 'product_code': seller.product_code, 'currency_id': seller.currency_id.id if seller.currency_id else None, 'product_uom': seller.product_uom.id if seller.product_uom else None, 'product_id': seller.product_id.id if seller.product_id else None, 'date_start': seller.date_start, 'date_end': seller.date_end, } return vendor_info # ========================== def _get_context_with_all_info(self, vals): """Get context with all necessary info (attributes and vendors)""" context = dict(self.env.context) # Check for attribute line changes if 'attribute_line_ids' in vals: attribute_line_info = {} for product in self: product_line_info = product._prepare_attribute_line_info() attribute_line_info.update(product_line_info) context['attribute_line_info'] = attribute_line_info # Check for vendor changes - store both for deletion and for comparing old values if 'seller_ids' in vals: vendor_info = {} vendor_old_values = {} for product in self: # For deletion logging product_vendor_info = product._prepare_vendor_info() vendor_info.update(product_vendor_info) # For update comparison product_vendor_old = product._prepare_vendor_info() vendor_old_values.update(product_vendor_old) context['vendor_info'] = vendor_info context['vendor_old_values'] = vendor_old_values return context # ======================== def _log_image_changes(self, field_name, old_val, new_val): """Log image field changes""" label_map = { 'image_1920': 'Main Image', 'image_carousel_lines': 'Carousel Images', 'product_template_image_ids': 'Extra Product Media', } label = label_map.get(field_name, field_name) if old_val == 'None' and new_val != 'None': return f"
  • {label}: image added
  • " elif old_val != 'None' and new_val == 'None': return f"
  • {label}: image removed
  • " elif old_val != new_val: return f"
  • {label}: image updated
  • " return None def _log_attribute_line_changes(self, commands): """Log changes to attribute lines with complete information""" # Get stored info from context stored_info = self.env.context.get('attribute_line_info', {}) for cmd in commands: if cmd[0] == 0: # Add new = self.env['product.template.attribute.line'].new(cmd[2]) attribute_name = new.attribute_id.name if new.attribute_id else 'Attribute' values = ", ".join(new.value_ids.mapped('name')) if new.value_ids else '' message = f"Product Attribute:
    {attribute_name} added
    " if values: message += f"Values: '{values}'" self.message_post(body=message) elif cmd[0] == 1: # Update rec_id = cmd[1] vals = cmd[2] # Get old values from context old_data = stored_info.get(rec_id, {}) if not old_data: # Fallback: get current record rec = self.env['product.template.attribute.line'].browse(rec_id) if not rec.exists(): continue old_data = { 'name': self.get_attribute_line_name(rec), 'attribute_id': rec.attribute_id.id if rec.attribute_id else None, 'attribute_name': rec.attribute_id.name if rec.attribute_id else None, 'value_ids': [(v.id, v.name) for v in rec.value_ids], 'value_names': ", ".join(rec.value_ids.mapped('name')) } changes = [] attribute_name = old_data.get('attribute_name', 'Attribute') # Check for attribute change if 'attribute_id' in vals: old_attr = old_data.get('attribute_name', '-') new_attr = self.env['product.attribute'].browse(vals['attribute_id']).name if old_attr != new_attr: attribute_name = new_attr # Update attribute name for display changes.append(f"Attribute changed from '{old_attr}' to '{new_attr}'") # Check for value changes if 'value_ids' in vals: old_vals = old_data.get('value_names', '') # Parse the command for value_ids new_value_ids = [] for value_cmd in vals['value_ids']: if isinstance(value_cmd, (list, tuple)): if value_cmd[0] == 6: # Replace all new_value_ids = value_cmd[2] elif value_cmd[0] == 4: # Add new_value_ids.append(value_cmd[1]) elif value_cmd[0] == 3: # Remove # This is more complex, would need current state pass # Get new value names if new_value_ids: new_values = self.env['product.attribute.value'].browse(new_value_ids) new_vals = ", ".join(new_values.mapped('name')) else: new_vals = "" if old_vals != new_vals: changes.append(f"Values: '{old_vals}' → '{new_vals}'") if changes: # Format with attribute name message = f"Product Attribute:
    {attribute_name} updated
    " message += "
    ".join(changes) self.message_post(body=message) elif cmd[0] in (2, 3): # Remove # Use info from stored data line_data = stored_info.get(cmd[1]) if line_data: attribute_name = line_data.get('attribute_name', 'Attribute') values = line_data.get('value_names', '') else: rec = self.env['product.template.attribute.line'].browse(cmd[1]) if rec.exists(): attribute_name = rec.attribute_id.name if rec.attribute_id else 'Attribute' values = ", ".join(rec.value_ids.mapped('name')) if rec.value_ids else '' else: attribute_name = 'Attribute' values = f"ID {cmd[1]}" message = f"Product Attribute:
    {attribute_name} removed
    " if values: message += f"Values: '{values}'" self.message_post(body=message) elif cmd[0] == 5: # Clear all self.message_post(body=f"Product Attribute:
    All attributes removed") def _log_vendor_pricelist_changes(self, commands): """Log changes to vendor pricelist with complete information""" # Get stored info from context stored_info = self.env.context.get('vendor_info', {}) old_values_info = self.env.context.get('vendor_old_values', {}) for cmd in commands: if cmd[0] == 0: # Add vals = cmd[2] # Create temporary record to get proper display values temp_values = vals.copy() temp_values['product_tmpl_id'] = self.id new = self.env['product.supplierinfo'].new(temp_values) name = self.get_vendor_name(new) details = [] if 'price' in vals and vals['price'] is not None: details.append(f"
  • Price: {vals['price']}
  • ") if 'min_qty' in vals and vals['min_qty'] is not None: details.append(f"
  • Quantity: {vals['min_qty']}
  • ") if 'delay' in vals and vals['delay'] is not None: details.append(f"
  • Delivery Lead Time: {vals['delay']}
  • ") if 'product_name' in vals and vals['product_name']: details.append(f"
  • Vendor Product Name: {vals['product_name']}
  • ") if 'product_code' in vals and vals['product_code']: details.append(f"
  • Vendor Product Code: {vals['product_code']}
  • ") if 'currency_id' in vals and vals['currency_id']: currency = self.env['res.currency'].browse(vals['currency_id']) details.append(f"
  • Currency: {currency.name}
  • ") if 'product_uom' in vals and vals['product_uom']: uom = self.env['uom.uom'].browse(vals['product_uom']) details.append(f"
  • Unit of Measure: {uom.name}
  • ") if details: detail_str = f" with:" else: detail_str = "" self.message_post(body=f"Vendor Pricelist: added '{name}'{detail_str}") elif cmd[0] == 1: # Update rec_id = cmd[1] vals = cmd[2] # Get old values from context old_data = old_values_info.get(rec_id, {}) if not old_data: # Fallback: get current record rec = self.env['product.supplierinfo'].browse(rec_id) if not rec.exists(): continue old_data = { 'name': self.get_vendor_name(rec), 'price': rec.price, 'min_qty': rec.min_qty, 'delay': rec.delay, 'product_name': rec.product_name, 'product_code': rec.product_code, 'currency_id': rec.currency_id.id if rec.currency_id else None, 'product_uom': rec.product_uom.id if rec.product_uom else None, 'product_id': rec.product_id.id if rec.product_id else None, 'date_start': rec.date_start, 'date_end': rec.date_end, } name = old_data.get('name', f'ID {rec_id}') changes = [] # Check each field in vals for changes for field, new_value in vals.items(): if field == 'name': # Special handling for vendor name change if new_value != old_data.get('name'): old_name = old_data.get('name', 'None') new_name = self.env['res.partner'].browse(new_value).name if new_value else 'None' changes.append(f"
  • Vendor: {old_name} → {new_name}
  • ") continue old_value = old_data.get(field) # Format values based on field type if field == 'currency_id': if old_value != new_value: old_str = self.env['res.currency'].browse(old_value).name if old_value else 'None' new_str = self.env['res.currency'].browse(new_value).name if new_value else 'None' else: continue elif field == 'product_uom': if old_value != new_value: old_str = self.env['uom.uom'].browse(old_value).name if old_value else 'None' new_str = self.env['uom.uom'].browse(new_value).name if new_value else 'None' else: continue elif field == 'product_id': if old_value != new_value: old_str = self.env['product.product'].browse(old_value).display_name if old_value else 'None' new_str = self.env['product.product'].browse(new_value).display_name if new_value else 'None' else: continue elif field in ['date_start', 'date_end']: if str(old_value) != str(new_value): old_str = old_value.strftime('%Y-%m-%d') if old_value else 'None' new_str = new_value if new_value else 'None' else: continue else: # For numeric and other fields if field in ['price', 'min_qty', 'delay']: # Compare numeric values properly old_num = float(old_value) if old_value is not None else 0.0 new_num = float(new_value) if new_value is not None else 0.0 if field == 'delay': # Integer field old_num = int(old_num) new_num = int(new_num) if old_num == new_num: continue old_str = str(old_value) if old_value is not None else 'None' new_str = str(new_value) if new_value is not None else 'None' else: # String and other types if str(old_value) == str(new_value): continue old_str = str(old_value) if old_value is not None else 'None' new_str = str(new_value) if new_value is not None else 'None' label = self._get_vendor_field_label(field) changes.append(f"
  • {label}: {old_str} → {new_str}
  • ") if changes: changes_str = f"" self.message_post(body=f"Vendor Pricelist: updated '{name}':{changes_str}") elif cmd[0] in (2, 3): # Remove vendor_data = stored_info.get(cmd[1]) if vendor_data: name = vendor_data['name'] details = [] if vendor_data.get('price'): details.append(f"
  • Price: {vendor_data['price']}
  • ") if vendor_data.get('min_qty'): details.append(f"
  • Quantity: {vendor_data['min_qty']}
  • ") if vendor_data.get('product_name'): details.append(f"
  • Product Name: {vendor_data['product_name']}
  • ") if vendor_data.get('delay'): details.append(f"
  • Delivery Lead Time: {vendor_data['delay']}
  • ") if details: detail_str = f"" else: detail_str = "" else: rec = self.env['product.supplierinfo'].browse(cmd[1]) if rec.exists(): name = self.get_vendor_name(rec) details = [] if rec.price: details.append(f"
  • Price: {rec.price}
  • ") if rec.min_qty: details.append(f"
  • Quantity: {rec.min_qty}
  • ") if rec.product_name: details.append(f"
  • Product Name: {rec.product_name}
  • ") if details: detail_str = f"" else: detail_str = "" else: name = f"ID {cmd[1]}" detail_str = "" self.message_post(body=f"Vendor Pricelist: removed '{name}'{detail_str}") elif cmd[0] == 5: # Clear all self.message_post(body=f"Vendor Pricelist: all removed") def _log_field_changes_product(self, vals, old_values): """Revised - Log general field changes for product template without posting to variants""" exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited'] image_fields = ['image_1920', 'image_carousel_lines', 'product_template_image_ids'] for record in self: changes = [] for field_name in vals: if field_name not in record._fields or field_name in exclude_fields: continue field = record._fields[field_name] # Handle image fields specially if field_name in image_fields: old_val = 'None' if not old_values.get(record.id, {}).get(field_name) else 'Yes' new_val = 'None' if not record[field_name] else 'Yes' image_msg = record._log_image_changes(field_name, old_val, new_val) if image_msg: changes.append(image_msg) continue # Handle vendor fields if field_name == 'seller_ids': commands = vals[field_name] if isinstance(commands, list): record._log_vendor_pricelist_changes(commands) continue # Handle attribute lines if field_name == 'attribute_line_ids': commands = vals[field_name] if isinstance(commands, list): record._log_attribute_line_changes(commands) continue # Handle other fields def stringify(val): if val in [None, False]: return 'None' if isinstance(field, fields.Selection): selection = field.selection(record) if callable(field.selection) else field.selection return dict(selection).get(val, str(val)) if isinstance(field, fields.Boolean): return 'Yes' if val else 'No' if isinstance(field, fields.Many2one): if isinstance(val, int): rec = record.env[field.comodel_name].browse(val) return rec.display_name if rec.exists() else str(val) elif isinstance(val, models.BaseModel): return val.display_name return str(val) if isinstance(field, fields.Many2many): records = val if isinstance(val, models.BaseModel) else record[field.name] if not records: return 'None' for attr in ['name', 'x_name', 'display_name']: if hasattr(records[0], attr): return ", ".join(records.mapped(attr)) return ", ".join(str(r.id) for r in records) return str(val) old_val_str = stringify(old_values.get(record.id, {}).get(field_name)) new_val_str = stringify(record[field_name]) if old_val_str != new_val_str: field_label = field.string or field_name changes.append(f"
  • {field_label}: '{old_val_str}' → '{new_val_str}'
  • ") if changes: # PERBAIKAN: Hanya post ke template, HAPUS bagian log ke variants record.message_post(body=f"Updated:") # simpan data lama dan log perubahan field def write(self, vals): context = self._get_context_with_all_info(vals) if context != self.env.context: self = self.with_context(**context) old_values = self._collect_old_values(vals) result = super().write(vals) # Log changes self._log_field_changes_product(vals, old_values) return result # def write(self, vals): # # for rec in self: # # if rec.id == 224484: # # raise UserError('Tidak dapat mengubah produk sementara') # self._log_field_changes(vals) # return super(ProductTemplate, self).write(vals) class ProductProduct(models.Model): _inherit = "product.product" web_price = fields.Float( 'Web Price', compute='_compute_web_price', digits='Product Price', inverse='_set_product_lst_price', help="Web Price with pricelist_id = 1") qty_stock_vendor = fields.Float( 'Qty Stock Vendor', compute='_compute_stock_vendor', help="Stock Vendor") solr_flag = fields.Integer(string='Solr Flag', default=0) # custom field for support Trusco products maker_code = fields.Char(string='Maker Code') maker_name = fields.Char(string='Maker Name') origin = fields.Char(string='Origin') features = fields.Char(string='Features') usage = fields.Char(string='Usage') specification = fields.Char(string='Specification') material = fields.Char(string='Material') qty_onhand_bandengan = fields.Float(string='Onhand BU', compute='_get_qty_onhand_bandengan') clean_website_description = fields.Char(string='Clean Website Description', compute='_get_clean_website_description') qty_incoming_bandengan = fields.Float(string='Incoming BU', compute='_get_qty_incoming_bandengan') qty_outgoing_bandengan = fields.Float(string='Outgoing BU', compute='_get_qty_outgoing_bandengan', help='only outgoing from sales order bandengan') qty_outgoing_mo_bandengan = fields.Float(string='Outgoing MO BU', compute='_get_qty_outgoing_mo_bandengan', help='only outgoing from manufacturing order bandengan') qty_available_bandengan = fields.Float(string='Available BU', compute='_get_qty_available_bandengan') qty_free_bandengan = fields.Float(string='Free BU', compute='_get_qty_free_bandengan') qty_upcoming = fields.Float(string='Qty Upcoming', compute='_get_qty_upcoming') sla_version = fields.Integer(string="SLA Version", default=0) is_edited = fields.Boolean(string='Is Edited') qty_sold = fields.Float(string='Sold Quantity', compute='_get_qty_sold') short_spesification = fields.Char(string='Short Spesification') max_qty_reorder = fields.Float(string='Max Qty Reorder', compute='_get_max_qty_reordering_rule') qty_rpo = fields.Float(string='Qty RPO', compute='_get_qty_rpo') plafon_qty = fields.Float(string='Max Plafon', compute='_get_plafon_qty_product') merchandise_ok = fields.Boolean(string='Product Promotion') qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant') qty_pcs_box = fields.Float("Pcs Box") barcode_box = fields.Char("Barcode Box") # keyword_id = fields.Many2one('keywords', string='Keyword') has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True) def generate_product_sla(self): product_variant_ids = self.env.context.get('active_ids', []) product_variant = self.search([('id', 'in', product_variant_ids)]) sla_record = self.env['product.sla'].search([('product_variant_id', '=', product_variant.id)], limit=1) if sla_record: sla_record.generate_product_sla() else: new_sla_record = self.env['product.sla'].create({ 'product_variant_id': product_variant.id, }) new_sla_record.generate_product_sla() @api.model def create(self, vals): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id active_model = self.env.context.get('active_model') users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) if self.env.user.id not in users_in_group.mapped('id') and active_model == None: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductProduct, self).create(vals) return result # def write(self, values): # group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id # active_model = self.env.context.get('active_model') # users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) # if self.env.user.id not in users_in_group.mapped('id') and active_model == None: # raise UserError('Hanya MD yang bisa mengedit Product') # result = super(ProductProduct, self).write(values) # return result def _compute_qr_code_variant(self): for rec in self: # Skip inactive variants if not rec.active: rec.qr_code_variant = False # Clear the QR Code for archived variants continue qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=4, ) qr.add_data(rec.barcode if rec.barcode else rec.default_code) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffer = BytesIO() img.save(buffer, format="PNG") qr_code_img = base64.b64encode(buffer.getvalue()).decode() rec.qr_code_variant = qr_code_img def _get_clean_website_description(self): for rec in self: cleaned_desc = BeautifulSoup(self.website_description or '', "html.parser").get_text() rec.clean_website_description = cleaned_desc @api.constrains('name', 'internal_reference', 'x_manufacture') def required_public_categ_ids(self): for rec in self: if not rec.public_categ_ids and rec.type == 'product': raise UserError('Field Categories harus diisi') @api.constrains('active') def archive_product(self): for product in self: if self.env.context.get('skip_unpublished_constraint'): continue # Mencegah looping saat dipanggil dari metode lain product_template = product.product_tmpl_id variants = product_template.product_variant_ids if len(variants) == 1: # Jika hanya ada satu varian, atur status `unpublished` berdasarkan `active` product_template.with_context(skip_unpublished_constraint=True).unpublished = not product.active product.with_context(skip_unpublished_constraint=True).unpublished = not product.active else: if product.active: product.with_context(skip_unpublished_constraint=True).unpublished = False product_template.with_context(skip_unpublished_constraint=True).unpublished = any(variant.active for variant in variants) else: product.with_context(skip_unpublished_constraint=True).unpublished = True all_inactive = all(not variant.active for variant in variants) product_template.with_context(skip_unpublished_constraint=True).unpublished = all_inactive @api.constrains('unpublished') def archive_product_unpublished(self): for product in self: if self.env.context.get('skip_active_constraint'): continue # Mencegah looping saat dipanggil dari metode lain product_template = product.product_tmpl_id variants = product_template.product_variant_ids if len(variants) == 1: # Jika hanya ada satu varian, atur status `unpublished` pada template, tetapi biarkan `active` tetap True product_template.with_context(skip_active_constraint=True).unpublished = product.unpublished else: if not product.unpublished: # Jika `unpublished` adalah False, pastikan `active` tetap True product.with_context(skip_active_constraint=True).active = True product_template.with_context(skip_active_constraint=True).active = any(not variant.unpublished for variant in variants) else: # Jika `unpublished` adalah True, atur template hanya jika semua varian di-unpublished all_unpublished = all(variant.unpublished for variant in variants) product_template.with_context(skip_active_constraint=True).active = not all_unpublished def update_internal_reference_variants(self, limit=100): variants = self.env['product.product'].search([ ('default_code', '=', False), ('type', '=', 'product'), ('active', '=', True) ], limit=limit, order='write_date desc') for variant in variants: if not variant.default_code: variant.default_code = 'ITV.'+str(variant.id) _logger.info('Updated variant %s' % variant.name) def _get_po_suggest(self, qty_purchase): if self.qty_available_bandengan < qty_purchase: return 'harus beli' return 'masih cukup' def _get_qty_upcoming(self): for product in self: product.qty_upcoming = product.incoming_qty + product.qty_available def _get_qty_sold(self): for product in self: order_line = self.env['sale.order.line'].search([ ('order_id.state', 'in', ['done', 'sale']), ('product_id', '=', product.id) ]) product.qty_sold = sum(x.product_uom_qty for x in order_line) def day_product_to_edit(self): day_products = [] for product in self: day_product = (product.write_date - product.create_date).days day_products.append(day_product) return day_products @api.constrains('name') def _validate_name(self): rule_regex = self.env['ir.config_parameter'].sudo().get_param('product.product.rule_name_regex') or '' pattern = rf'^{rule_regex}$' if not re.match(pattern, self.name): pattern_suggest = rf"{rule_regex}" suggest = ''.join(re.findall(pattern_suggest, self.name)) raise UserError(f'Contoh yang benar adalah {suggest}') def _get_qty_incoming_bandengan(self): for product in self: qty = self.env['v.move.outstanding'].read_group( domain=[ ('product_id', '=', product.id), ('location_dest_id', 'in', [57, 83]), ], fields=['qty_need'], groupby=[] )[0].get('qty_need', 0.0) product.qty_incoming_bandengan = qty def _get_qty_incoming_bandengan_with_exclude(self): for product in self: qty = self.env['v.move.outstanding'].read_group( domain=[ ('product_id', '=', product.id), ('location_dest_id', 'in', [57, 83]), ], fields=['qty_need'], groupby=[] )[0].get('qty_need', 0.0) product.qty_incoming_bandengan = qty def _get_qty_outgoing_bandengan(self): for product in self: qty = self.env['v.move.outstanding'].read_group( domain=[ ('product_id', '=', product.id), ('location_id', 'in', [57, 83]), ('mo_id', '=', False), ('hold_outgoing', '=', False) ], fields=['qty_need'], groupby=[] )[0].get('qty_need', 0.0) product.qty_outgoing_bandengan = qty def _get_qty_outgoing_mo_bandengan(self): for product in self: records = self.env['v.move.outstanding'].search([ ('product_id.id', '=', product.id), ('location_id.id', 'in', [57, 83]), ('mo_id.id', '>', 0) ]) qty = sum(records.mapped('qty_need') or [0.0]) product.qty_outgoing_mo_bandengan = qty def _get_qty_onhand_bandengan(self): for product in self: qty_onhand = self.env['stock.quant'].search([ ('product_id', '=', product.id), ('location_id', 'in', [57, 83]) ]) qty = sum(qty_onhand.mapped('quantity')) product.qty_onhand_bandengan = qty def _get_qty_available_bandengan(self): for product in self: qty_available = product.qty_incoming_bandengan + product.qty_onhand_bandengan - product.qty_outgoing_bandengan - product.qty_outgoing_mo_bandengan product.qty_available_bandengan = qty_available or 0 def _get_qty_free_bandengan(self): for product in self: qty_free = product.qty_onhand_bandengan - product.qty_outgoing_bandengan - product.qty_outgoing_mo_bandengan product.qty_free_bandengan = qty_free def _get_max_qty_reordering_rule(self): for product in self: reordering = self.env['stock.warehouse.orderpoint'].search([ ('product_id', '=', product.id) ], limit=1) if not reordering: product.max_qty_reorder = 0 else: product.max_qty_reorder = reordering.product_max_qty def _get_qty_rpo(self): for product in self: rpo = self.env['v.requisition.match.po'].search([ ('product_id', '=', product.id) ], limit=1) if not rpo: product.qty_rpo = 0 else: product.qty_rpo = rpo.qty_rpo def _get_plafon_qty_product(self): for product in self: qty_available = product.qty_available_bandengan max_qty = product.max_qty_reorder qty_rpo = product.qty_rpo product.plafon_qty = max_qty - qty_available + qty_rpo # def write(self, vals): # if 'solr_flag' not in vals: # for variant in self: # if variant.solr_flag == 1: # variant.product_tmpl_id.solr_flag = 2 # vals['solr_flag'] = 2 # return super().write(vals) def _compute_web_price(self): for product in self: pricelist_id = self.env['ir.config_parameter'].sudo().get_param('product.pricelist.default_price_id_v2') domain = [('pricelist_id.id', '=', pricelist_id or 17022), ('product_id.id', '=', product.id)] product_pricelist_item = self.env['product.pricelist.item'].search(domain, limit=1) if product_pricelist_item.base_pricelist_id: base_pricelist_id = product_pricelist_item.base_pricelist_id.id domain = [('pricelist_id', '=', base_pricelist_id), ('product_id', '=', product.id)] product_pricelist_item = self.env['product.pricelist.item'].search(domain, limit=1) product.web_price = product_pricelist_item.fixed_price def _compute_stock_vendor(self): for product in self: stock_vendor = self.env['stock.vendor'].search([('product_variant_id', '=', product.id)], limit=1) product.qty_stock_vendor = stock_vendor.quantity + product.qty_available def unlink(self): if self._name == 'product.product': raise UserError('Maaf anda tidak bisa delete product') def _get_active_flash_sale(self): current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') pricelist = self.env['product.pricelist'].search([ ('is_flash_sale', '=', True), ('item_ids.product_id', '=', self.id), ('start_date', '<=', current_time), ('end_date', '>=', current_time) ], limit=1) return pricelist # simpan data lama def _collect_old_values(self, vals): return { record.id: { field_name: record[field_name] for field_name in vals.keys() if field_name in record._fields } for record in self } # log perubahan field def _log_field_changes_product_variants(self, vals, old_values): """Revised - Log field changes for variants without posting to template""" exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited'] # Custom labels for image fields custom_labels = { 'image_1920': 'Main Image', 'image_carousel_lines': 'Carousel Images', 'product_template_image_ids': 'Extra Product Media', } for record in self: changes = [] for field_name in vals: if field_name not in record._fields or field_name in exclude_fields: continue field = record._fields[field_name] field_label = custom_labels.get(field_name, field.string or field_name) old_value = old_values.get(record.id, {}).get(field_name) new_value = record[field_name] def stringify(val, field, record): if val in [None, False]: return 'None' if isinstance(field, fields.Selection): selection = field.selection if callable(selection): selection = selection(record) return dict(selection).get(val, str(val)) if isinstance(field, fields.Boolean): return 'Yes' if val else 'No' if isinstance(field, fields.Many2one): if isinstance(val, int): rec = record.env[field.comodel_name].browse(val) return rec.display_name if rec.exists() else str(val) elif isinstance(val, models.BaseModel): return val.display_name return str(val) if isinstance(field, fields.Many2many): records = val if isinstance(val, models.BaseModel) else record[field.name] if not records: return 'None' for attr in ['name', 'x_name', 'display_name']: if hasattr(records[0], attr): return ", ".join(records.mapped(attr)) return ", ".join(str(r.id) for r in records) if isinstance(field, fields.One2many): records = val if isinstance(val, models.BaseModel) else record[field.name] if not records: return 'None' return f"{field.comodel_name}({', '.join(str(r.id) for r in records)})" return str(val) old_val_str = stringify(old_value, field, record) new_val_str = stringify(new_value, field, record) if old_val_str != new_val_str: if field_name in custom_labels: # handle image field if old_val_str == 'None' and new_val_str != 'None': changes.append(f"
  • {field_label}: image added
  • ") elif old_val_str != 'None' and new_val_str == 'None': changes.append(f"
  • {field_label}: image removed
  • ") else: changes.append(f"
  • {field_label}: image updated
  • ") else: changes.append(f"
  • {field_label}: '{old_val_str}' → '{new_val_str}'
  • ") if changes: # PERBAIKAN: Hanya post message ke variant, HAPUS bagian template_changes variant_message = "Updated:" % "".join(changes) record.message_post(body=variant_message) # simpan data lama dan log perubahan field def write(self, vals): tracked_fields = [ 'default_code', 'name', 'weight', 'x_manufacture', 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished', 'image_carousel_lines' ] # pake ini kalau mau Cek semua field # if vals: if any(field in vals for field in tracked_fields): old_values = self._collect_old_values(vals) result = super().write(vals) self._log_field_changes_product_variants(vals, old_values) return result else: return super().write(vals) class OutstandingMove(models.Model): _name = 'v.move.outstanding' _auto = False _rec_name = 'id' id = fields.Integer(string='ID') product_id = fields.Many2one('product.product', string='Product') reference = fields.Char(string='Reference', help='Nomor Dokumen terkait') qty_need = fields.Float(string='Qty Need', help='Qty yang akan outgoing / incoming') location_id = fields.Many2one('stock.location', string='Location', help='Lokasi asal') location_dest_id = fields.Many2one('stock.location', string='Location To', help='Lokasi tujuan') mo_id = fields.Many2one('mrp.production', string='Manufacturing Order') hold_outgoing = fields.Boolean(string='Hold Outgoing') def init(self): # where clause 'state in' follow the origin of outgoing and incoming odoo tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(""" CREATE OR REPLACE VIEW %s AS select sm.id, sm.reference, sm.product_id, sm.product_uom_qty as qty_need, sm.location_id, sm.location_dest_id, sm.raw_material_production_id as mo_id, so.hold_outgoing from stock_move sm left join procurement_group pg on pg.id = sm.group_id left join sale_order so on so.id = pg.sale_id where 1=1 and sm.state in( 'waiting', 'confirmed', 'assigned', 'partially_available' ) """ % self._table) class ImageCarousel(models.Model): _name = 'image.carousel' _description = 'Image Carousel' _order = 'product_id, id' product_id = fields.Many2one('product.template', string='Product', required=True, ondelete='cascade', index=True, copy=False) sequence = fields.Integer("Sequence", default=10) image = fields.Binary(string='Image')