summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2025-05-14 04:10:19 +0000
committerIT Fixcomart <it@fixcomart.co.id>2025-05-14 04:10:19 +0000
commitdceb69d104510bff6930ad0fe373dcf198417503 (patch)
tree3b081a8434634119c850c7f2da7ec30f1bfea825
parentf9240997e9796a1149a1f8bddc48fa1ebbbbf873 (diff)
parentbf79c492c047b8b9e0aa7657959a6f94263765dd (diff)
Merged in po-lock-and-logproduct (pull request #303)
Po lock and logproduct
-rw-r--r--indoteknik_custom/models/product_pricelist.py6
-rwxr-xr-xindoteknik_custom/models/product_template.py622
-rwxr-xr-xindoteknik_custom/models/purchase_order.py125
-rwxr-xr-xindoteknik_custom/models/purchase_pricelist.py80
-rwxr-xr-xindoteknik_custom/views/purchase_pricelist.xml7
5 files changed, 827 insertions, 13 deletions
diff --git a/indoteknik_custom/models/product_pricelist.py b/indoteknik_custom/models/product_pricelist.py
index ea3ee6cf..94a9b239 100644
--- a/indoteknik_custom/models/product_pricelist.py
+++ b/indoteknik_custom/models/product_pricelist.py
@@ -3,8 +3,8 @@ from datetime import datetime, timedelta
class ProductPricelist(models.Model):
- _inherit = 'product.pricelist'
-
+ _inherit = 'product.pricelist'
+
is_flash_sale = fields.Boolean(string='Flash Sale', default=False)
is_show_program = fields.Boolean(string='Show Program', default=False)
banner = fields.Binary(string='Banner')
@@ -56,7 +56,7 @@ class ProductPricelist(models.Model):
return tier_name
class ProductPricelistItem(models.Model):
- _inherit = 'product.pricelist.item'
+ _inherit = 'product.pricelist.item'
manufacture_id = fields.Many2one('x_manufactures', string='Manufacture')
computed_price = fields.Float(string='Computed Price') \ No newline at end of file
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index a09570f4..2679fbfd 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -388,12 +388,509 @@ class ProductTemplate(models.Model):
self.env['token.storage'].create([values])
return values
- def write(self, vals):
- # for rec in self:
- # if rec.id == 224484:
- # raise UserError('Tidak dapat mengubah produk sementara')
+ # ==============================
+ 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 super(ProductTemplate, self).write(vals)
+ 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"<li><b>{label}</b>: image added</li>"
+ elif old_val != 'None' and new_val == 'None':
+ return f"<li><b>{label}</b>: image removed</li>"
+ elif old_val != new_val:
+ return f"<li><b>{label}</b>: image updated</li>"
+ 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"<b>Product Attribute</b>:<br/>{attribute_name} added<br/>"
+ 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"<b>Product Attribute</b>:<br/>{attribute_name} updated<br/>"
+ message += "<br/>".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"<b>Product Attribute</b>:<br/>{attribute_name} removed<br/>"
+ if values:
+ message += f"Values: '{values}'"
+ self.message_post(body=message)
+
+ elif cmd[0] == 5: # Clear all
+ self.message_post(body=f"<b>Product Attribute</b>:<br/>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"<li>Price: {vals['price']}</li>")
+ if 'min_qty' in vals and vals['min_qty'] is not None:
+ details.append(f"<li>Quantity: {vals['min_qty']}</li>")
+ if 'delay' in vals and vals['delay'] is not None:
+ details.append(f"<li>Delivery Lead Time: {vals['delay']}</li>")
+ if 'product_name' in vals and vals['product_name']:
+ details.append(f"<li>Vendor Product Name: {vals['product_name']}</li>")
+ if 'product_code' in vals and vals['product_code']:
+ details.append(f"<li>Vendor Product Code: {vals['product_code']}</li>")
+ if 'currency_id' in vals and vals['currency_id']:
+ currency = self.env['res.currency'].browse(vals['currency_id'])
+ details.append(f"<li>Currency: {currency.name}</li>")
+ if 'product_uom' in vals and vals['product_uom']:
+ uom = self.env['uom.uom'].browse(vals['product_uom'])
+ details.append(f"<li>Unit of Measure: {uom.name}</li>")
+
+ if details:
+ detail_str = f" with:<ul>{''.join(details)}</ul>"
+ else:
+ detail_str = ""
+
+ self.message_post(body=f"<b>Vendor Pricelist</b>: 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"<li>Vendor: {old_name} → {new_name}</li>")
+ 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"<li>{label}: {old_str} → {new_str}</li>")
+
+ if changes:
+ changes_str = f"<ul>{''.join(changes)}</ul>"
+ self.message_post(body=f"<b>Vendor Pricelist</b>: 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"<li>Price: {vendor_data['price']}</li>")
+ if vendor_data.get('min_qty'):
+ details.append(f"<li>Quantity: {vendor_data['min_qty']}</li>")
+ if vendor_data.get('product_name'):
+ details.append(f"<li>Product Name: {vendor_data['product_name']}</li>")
+ if vendor_data.get('delay'):
+ details.append(f"<li>Delivery Lead Time: {vendor_data['delay']}</li>")
+
+ if details:
+ detail_str = f"<ul>{''.join(details)}</ul>"
+ 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"<li>Price: {rec.price}</li>")
+ if rec.min_qty:
+ details.append(f"<li>Quantity: {rec.min_qty}</li>")
+ if rec.product_name:
+ details.append(f"<li>Product Name: {rec.product_name}</li>")
+
+ if details:
+ detail_str = f"<ul>{''.join(details)}</ul>"
+ else:
+ detail_str = ""
+ else:
+ name = f"ID {cmd[1]}"
+ detail_str = ""
+
+ self.message_post(body=f"<b>Vendor Pricelist</b>: removed '{name}'{detail_str}")
+
+ elif cmd[0] == 5: # Clear all
+ self.message_post(body=f"<b>Vendor Pricelist</b>: all removed")
+
+ def _log_field_changes_product(self, vals, old_values):
+ """Log general field changes for product template"""
+ 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"<li><b>{field_label}</b>: '{old_val_str}' → '{new_val_str}'</li>")
+
+ if changes:
+ record.message_post(body=f"<b>Updated:</b><ul>{''.join(changes)}</ul>")
+
+ # log changes to product variants
+ variant_message = f"<b>Updated:</b><ul>{''.join(changes)}</ul>"
+ for variant in record.product_variant_ids:
+ variant.message_post(body=variant_message)
+
+ # 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"
@@ -712,6 +1209,121 @@ class ProductProduct(models.Model):
], 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):
+ exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
+ # for image fields, use custom labels
+ custom_labels = {
+ 'image_1920': 'Main Image',
+ 'image_carousel_lines': 'Carousel Images',
+ 'product_template_image_ids': 'Extra Product Media',
+ }
+
+ template_changes = {}
+
+ 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] # nilai setelah write
+
+ 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"<li><b>{field_label}</b>: image added</li>")
+ elif old_val_str != 'None' and new_val_str == 'None':
+ changes.append(f"<li><b>{field_label}</b>: image removed</li>")
+ else:
+ changes.append(f"<li><b>{field_label}</b>: image updated</li>")
+ else:
+ changes.append(f"<li><b>{field_label}</b>: '{old_val_str}' → '{new_val_str}'</li>")
+
+ if changes:
+ # Post message to variant
+ variant_message = "<b>Updated:</b><ul>%s</ul>" % "".join(changes)
+ record.message_post(body=variant_message)
+
+ # Group changes by template for posting to template
+ template_id = record.product_tmpl_id.id
+ if template_id not in template_changes:
+ template_changes[template_id] = {}
+
+ # Store variant information including ID for creating clickable link
+ template_changes[template_id][record.id] = {
+ 'name': record.display_name or f"Variant {record.id}",
+ 'changes': changes
+ }
+
+ # Post grouped messages to templates with clickable links using your format
+ for template_id, variants_data in template_changes.items():
+ template = self.env['product.template'].browse(template_id)
+ if template.exists():
+ template_message = "<b>Variant Updates:</b><br/>"
+
+ for variant_id, variant_data in variants_data.items():
+ # Create clickable link using your format
+ variant_link = f"<a href=\"#id={variant_id}&model=product.product&view_type=form\">{variant_data['name']}</a><br/>"
+ template_message += f"{variant_link}<ul>{''.join(variant_data['changes'])}</ul><br/>"
+
+ template.message_post(body=template_message)
+
+ # simpan data lama dan log perubahan field
+ def write(self, vals):
+ old_values = self._collect_old_values(vals)
+ result = super().write(vals)
+ self._log_field_changes_product_variants(vals, old_values)
+ return result
class OutstandingMove(models.Model):
_name = 'v.move.outstanding'
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 98b367d0..c5ba5792 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -562,7 +562,9 @@ class PurchaseOrder(models.Model):
i = 0
for line in self.order_line:
i += 1
- current_time = datetime.utcnow()
+
+ utc_time = fields.Datetime.now()
+ current_time = utc_time.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
# print(i, len(self.order_line))
price_unit = line.price_unit
@@ -580,10 +582,11 @@ class PurchaseOrder(models.Model):
purchase_pricelist = self.env['purchase.pricelist'].search([
('product_id', '=', line.product_id.id),
('vendor_id', '=', line.order_id.partner_id.id)
- ])
- purchase_pricelist = purchase_pricelist.with_context(update_by='system')
+ ])
+
if not purchase_pricelist:
- purchase_pricelist.create([{
+ # Buat pricelist baru dengan context
+ new_pricelist = self.env['purchase.pricelist'].with_context(update_by='system').create([{
'vendor_id': line.order_id.partner_id.id,
'product_id': line.product_id.id,
'product_price': 0,
@@ -591,12 +594,51 @@ class PurchaseOrder(models.Model):
'system_price': price_unit,
'system_last_update': current_time
}])
+
+ # Buat lognote untuk pricelist baru
+ message = f"""
+ <b>New Purchase Pricelist Created from PO</b><br/>
+ <b>PO:</b> <a href="#id={line.order_id.id}&model=purchase.order&view_type=form">{line.order_id.name}</a><br/>
+ <b>System Price:</b> {price_unit:,.2f}<br/>
+ <b>System Tax:</b> {taxes.name if taxes else 'No Tax'}<br/>
+ <b>System Update:</b> {current_time}<br/>
+ """
+ new_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
else:
+ # Simpan nilai lama untuk logging
+ old_values = {
+ 'system_price': purchase_pricelist.system_price,
+ 'taxes_system_id': purchase_pricelist.taxes_system_id,
+ }
+
+ # Update dengan context
+ purchase_pricelist = purchase_pricelist.with_context(update_by='system')
purchase_pricelist.write({
'system_last_update': current_time,
'taxes_system_id': taxes.id,
'system_price': price_unit
})
+
+ # Buat lognote jika ada perubahan
+ changes = []
+ if old_values['system_price'] != price_unit:
+ changes.append(f"<li><b>System Price</b>: {old_values['system_price']:,.2f} → {price_unit:,.2f}</li>")
+ if old_values['taxes_system_id'] != taxes:
+ old_tax_name = old_values['taxes_system_id'].name if old_values['taxes_system_id'] else 'No Tax'
+ new_tax_name = taxes.name if taxes else 'No Tax'
+ changes.append(f"<li><b>System Tax</b>: {old_tax_name} → {new_tax_name}</li>")
+
+ if changes:
+ message = f"""
+ <b>System Fields Updated from PO</b><br/>
+ <b>PO:</b> <a href="#id={line.order_id.id}&model=purchase.order&view_type=form">{line.order_id.name}</a><br/>
+ <b>Changes:</b>
+ <ul>
+ {"".join(changes)}
+ <li><b>System Update</b>: {current_time}</li>
+ </ul>
+ """
+ purchase_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
def _compute_date_planned(self):
for order in self:
@@ -1198,8 +1240,83 @@ class PurchaseOrder(models.Model):
if not self.env.context.get('skip_check_payment'):
self.with_context(skip_check_payment=True)._check_payment_term()
self.with_context(notify_tax=True)._check_tax_rule()
+ # Tambahkan pemanggilan method untuk handle pricelist system update
+ self._handle_pricelist_system_update(vals)
return res
+ def _handle_pricelist_system_update(self, vals):
+ if 'order_line' in vals or any(key in vals for key in ['state', 'approval_status']):
+ for order in self:
+ # Hanya proses jika PO sudah approved
+ if order.state in ['purchase', 'done'] and order.approval_status == 'approved':
+ self._process_pricelist_update(order)
+
+ def _process_pricelist_update(self, order):
+ for line in order.order_line:
+ pricelist = self._get_related_pricelist(line.product_id, order.partner_id)
+
+ if pricelist:
+ # Simpan nilai lama
+ old_values = self._get_pricelist_old_values(pricelist)
+
+ # Update dan cek perubahan
+ self._update_and_log_pricelist(pricelist, line, old_values)
+
+ def _get_related_pricelist(self, product_id, vendor_id):
+ return self.env['purchase.pricelist'].search([
+ ('product_id', '=', product_id.id),
+ ('vendor_id', '=', vendor_id.id)
+ ], limit=1)
+
+ def _get_pricelist_old_values(self, pricelist):
+ return {
+ 'system_price': pricelist.system_price,
+ 'taxes_system_id': pricelist.taxes_system_id,
+ 'system_last_update': pricelist.system_last_update
+ }
+
+ def _update_and_log_pricelist(self, pricelist, po_line, old_values):
+ changes = []
+ current_time = fields.Datetime.now()
+
+ # Cek perubahan System Price
+ if pricelist.system_price != po_line.price_unit:
+ if old_values['system_price'] != po_line.price_unit:
+ changes.append(f"<li><b>System Price</b>: {old_values['system_price']:,.2f} → {po_line.price_unit:,.2f}</li>")
+
+ # Cek perubahan System Tax
+ if pricelist.taxes_system_id != po_line.taxes_id:
+ old_tax = old_values['taxes_system_id']
+ old_tax_name = old_tax.name if old_tax else 'No Tax'
+ new_tax_name = po_line.taxes_id.name if po_line.taxes_id else 'No Tax'
+ if old_tax != po_line.taxes_id:
+ changes.append(f"<li><b>System Tax</b>: {old_tax_name} → {new_tax_name}</li>")
+
+ # Update fields jika ada perubahan
+ if changes:
+ pricelist.with_context(update_by='system').write({
+ 'system_price': po_line.price_unit,
+ 'taxes_system_id': po_line.taxes_id.id if po_line.taxes_id else False,
+ 'system_last_update': current_time
+ })
+
+ # Buat lognote
+ self._create_pricelist_lognote(pricelist, po_line, changes, current_time)
+
+ def _create_pricelist_lognote(self, pricelist, po_line, changes, timestamp):
+ message = f"""
+ <b>System Fields Updated from PO</b><br/>
+ <b>PO:</b> <a href="#id={po_line.order_id.id}&model=purchase.order&view_type=form">{po_line.order_id.name}</a><br/>
+ <b>Changes:</b>
+ <ul>
+ {"".join(changes)}
+ <li><b>System Update</b>: {timestamp}</li>
+ </ul>
+ <b>Updated By:</b> {self.env.user.name}
+ """
+
+ pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
+
class PurchaseOrderUnlockWizard(models.TransientModel):
_name = 'purchase.order.unlock.wizard'
_description = 'Wizard untuk memberikan alasan unlock PO'
diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py
index dd680f4d..b3a473b6 100755
--- a/indoteknik_custom/models/purchase_pricelist.py
+++ b/indoteknik_custom/models/purchase_pricelist.py
@@ -6,6 +6,7 @@ from pytz import timezone
class PurchasePricelist(models.Model):
_name = 'purchase.pricelist'
_rec_name = 'product_id'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Name', compute="_compute_name")
product_id = fields.Many2one('product.product', string="Product", required=True)
@@ -131,4 +132,81 @@ class PurchasePricelist(models.Model):
rec.sync_pricelist_tier()
rec.product_id.product_tmpl_id._create_solr_queue('_sync_price_to_solr')
- \ No newline at end of file
+
+ 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
+ }
+
+ def _log_field_changes(self, vals, old_values):
+ exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
+
+ 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 = 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'
+ # Handle Selection
+ if isinstance(field, fields.Selection):
+ selection = field.selection
+ if callable(selection):
+ selection = selection(record)
+ return dict(selection).get(val, str(val))
+ # Handle Boolean
+ if isinstance(field, fields.Boolean):
+ return 'Yes' if val else 'No'
+ # Handle Many2one
+ 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)
+ # Handle Many2many
+ 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)
+ # Handle One2many
+ 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)})"
+ # Default case (Char, Float, Integer, etc)
+ 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:
+ changes.append(f"<li><b>{field_label}</b>: '{old_val_str}' → '{new_val_str}'</li>")
+
+ if changes:
+ message = "<b>Updated:</b><ul>%s</ul>" % "".join(changes)
+ record.message_post(body=message)
+
+ def write(self, vals):
+ old_values = self._collect_old_values(vals)
+ result = super(PurchasePricelist, self).write(vals)
+ self._log_field_changes(vals, old_values)
+ return result \ No newline at end of file
diff --git a/indoteknik_custom/views/purchase_pricelist.xml b/indoteknik_custom/views/purchase_pricelist.xml
index ca5cd416..409e3b6a 100755
--- a/indoteknik_custom/views/purchase_pricelist.xml
+++ b/indoteknik_custom/views/purchase_pricelist.xml
@@ -20,6 +20,8 @@
<field name="count_brand_vendor" optional="hide"/>
<field name="product_categ_ids" string="Product Category" optional="hide"/>
<field name="is_winner" string="Winner" optional="hide"/>
+ <field name="message_follower_ids" widget="mail_followers" optional="hide"/>
+ <field name="activity_ids" widget="mail_activity" optional="hide"/>
</tree>
</field>
</record>
@@ -52,6 +54,11 @@
</group>
</group>
</sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
</form>
</field>
</record>