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')