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"
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)
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
# 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(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] # nilai setelah write
def stringify(val):
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):
return val.name if hasattr(val, 'name') else str(val)
if isinstance(field, fields.Many2many):
records = val if isinstance(val, models.Model) else val
names = []
if records:
for attr in ['name', 'x_name', 'display_name']:
if hasattr(records[0], attr):
names = records.mapped(attr)
break
if not names:
names = [str(r.id) for r in records]
return ", ".join(names) if names else 'None'
return str(val)
old_val_str = stringify(old_value)
new_val_str = stringify(new_value)
if old_val_str != new_val_str:
changes.append(f"
{field_label}: '{old_val_str}' → '{new_val_str}'")
if changes:
message = "Updated:" % "".join(changes)
record.message_post(body=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(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')
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")
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]),
],
fields=['qty_need'],
groupby=[]
)[0].get('qty_need', 0.0)
product.qty_outgoing_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_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_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(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):
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 = self.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 = record[field_name]
names = []
if records:
for attr in ['name', 'x_name', 'display_name']:
if hasattr(records[0], attr):
names = records.mapped(attr)
break
if not names:
names = [str(r.id) for r in records]
return ", ".join(names) if names else 'None'
return str(val)
old_val_str = stringify(old_value)
new_val_str = stringify(new_value)
if old_val_str != new_val_str:
changes.append(f"{field_label}: '{old_val_str}' → '{new_val_str}'")
if changes:
message = "Updated:" % "".join(changes)
record.message_post(body=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(vals, old_values)
return result
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')
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
from stock_move sm
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)
image = fields.Binary(string='Image')