From 6ba1731e740f7ad1e7aea38f723a2f431fec2c4b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 1 Dec 2025 16:27:14 +0700 Subject: Initial Commit --- indoteknik_custom/models/find_page.py | 69 +++++++++++++++++++++++++++-------- indoteknik_custom/views/find_page.xml | 15 +++++++- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index 467e30d1..461e647d 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -4,8 +4,8 @@ import re import pysolr _logger = logging.getLogger(__name__) -_cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30) -# _cat_brand_solr_dev = pysolr.Solr('http://127.0.0.1:8983/solr/url_category_brand/', always_commit=True, timeout=30) +# _cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30) +_cat_brand_solr_dev = pysolr.Solr('http://localhost:8983/solr/url_category_brand', always_commit=True, timeout=30) class BrandProductCategory(models.Model): @@ -36,7 +36,20 @@ class FindPage(models.Model): brand_id = fields.Many2one('x_manufactures', string='Brand') category_id = fields.Many2one('product.public.category', string='Category', help='Bisa semua level Category') - url = fields.Char(string='Url') + url = fields.Char(string='Url', readonly=True) + keywords = fields.Char(string='Keywords') + + + @api.model + def create(self, vals): + record = super(FindPage, self).create(vals) + record._sync_to_solr() + return record + + def write(self, vals): + res = super(FindPage, self).write(vals) + self._sync_to_solr() + return res def _sync_to_solr(self, limit=10000): urls = self.env['web.find.page'].search([]) @@ -44,18 +57,27 @@ class FindPage(models.Model): catch = {} for url in urls: try: + keyword = self.keywords document = { 'id': url.id, 'category_id_i': url.category_id.id, 'brand_id_i': url.brand_id.id, - 'url_s': url.url + 'url_s': url.url, } + if keyword: + document['keyword_s'] = keyword documents.append(document) catch = document except Exception as e: _logger.error('Failed to add document to Solr URL Category Brand: %s', e) _logger.error('Document Data: %s', catch) - _cat_brand_solr.add(documents) + + try: + _cat_brand_solr_dev.add(documents) + except Exception as e: + _logger.error('Gagal sinkron ke Solr: %s', e) + return True + return True def _get_category_hierarchy(self, category): @@ -66,26 +88,38 @@ class FindPage(models.Model): current_category = current_category.parent_id return categories - def _generate_url(self): - categories = self.env['v.brand.product.category'].search([]) + def _generate_url(self): list_url = [] - for category in categories: - category_hierarchy = self._get_category_hierarchy(category.category_id) + keyword = self.keywords - for level, cat in enumerate(reversed(category_hierarchy), start=1): - list_url.append(self._generate_mod_url(cat, category.brand_id)) - # print(f"Level {level}: {cat.name} {category.brand_id.x_name}") + if keyword: + brands = self.env['x_manufactures'].search([('x_name', 'ilike', keyword)]) + categories = self.env['product.public.category'].search([('name', 'ilike', keyword)]) + for brand in brands: + for category in categories: + list_url.append(self._generate_mod_url(category, brand, keyword)) + else: + # Default: ambil semua kombinasi brand-category dari view + categories = self.env['v.brand.product.category'].search([]) + for category in categories: + category_hierarchy = self._get_category_hierarchy(category.category_id) + for level, cat in enumerate(reversed(category_hierarchy), start=1): + list_url.append(self._generate_mod_url(cat, category.brand_id)) + + # Hapus duplikat URL unique_list = [] for item in list_url: if item not in unique_list: unique_list.append(item) + count = 0 for item in unique_list: self._create_find_page(item['url'], item['category_id'], item['brand_id']) count += 1 - print(f"Total categories processed: {count}") + + _logger.info(f"Total URL generated: {count}") def _create_find_page(self, url, category_id, brand_id): param = { @@ -96,20 +130,25 @@ class FindPage(models.Model): find_page = self.env['web.find.page'].create(param) _logger.info('Created Web Find Page %s' % find_page.id) - def _generate_mod_url(self, category, brand): + def _generate_mod_url(self, category, brand, keyword): # generate_url = 'https://indoteknik.com/shop/find/category-brand' example cleaned_category = re.sub(r'[^\w\s]', '', category.name) cleaned_brand = re.sub(r'[^\w\s]', '', brand.x_name) cleaned_combined = cleaned_category+' '+cleaned_brand cleaned_combined = cleaned_combined.replace(' ', '-') cleaned_combined = cleaned_combined.replace('--', '-') - url = 'https://indoteknik.com/shop/find/'+cleaned_combined + if self.keywords: + url = 'https://indoteknik.com/shop/find/key='+keyword + else: + url = 'https://indoteknik.com/shop/find/'+cleaned_combined url = url.lower() result = { 'url': url, 'category_id': category.id, 'brand_id': brand.id } + if self.keywords: + result['keywords_s'] = keyword # print(url) # param = { # 'brand_id': brand.id, diff --git a/indoteknik_custom/views/find_page.xml b/indoteknik_custom/views/find_page.xml index c752aa98..65b0a570 100644 --- a/indoteknik_custom/views/find_page.xml +++ b/indoteknik_custom/views/find_page.xml @@ -7,6 +7,7 @@ + @@ -25,7 +26,8 @@ - + + @@ -62,9 +64,18 @@ tree,form + + + Sync to solr + + + code + model._sync_to_solr() + + - \ No newline at end of file + -- cgit v1.2.3 From c03a690a935508c2c669a5e7ff6da3c50f42fb53 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 2 Dec 2025 09:15:20 +0700 Subject: change url mod gen --- indoteknik_custom/models/find_page.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index 461e647d..5e530c64 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -130,31 +130,24 @@ class FindPage(models.Model): find_page = self.env['web.find.page'].create(param) _logger.info('Created Web Find Page %s' % find_page.id) - def _generate_mod_url(self, category, brand, keyword): - # generate_url = 'https://indoteknik.com/shop/find/category-brand' example + def _generate_mod_url(self, category, brand, keyword=None): cleaned_category = re.sub(r'[^\w\s]', '', category.name) cleaned_brand = re.sub(r'[^\w\s]', '', brand.x_name) - cleaned_combined = cleaned_category+' '+cleaned_brand - cleaned_combined = cleaned_combined.replace(' ', '-') - cleaned_combined = cleaned_combined.replace('--', '-') - if self.keywords: - url = 'https://indoteknik.com/shop/find/key='+keyword + cleaned_combined = f"{cleaned_category} {cleaned_brand}" + cleaned_combined = re.sub(r'\s+', '-', cleaned_combined.strip()) + + if keyword: + slugified_keyword = re.sub(r'\s+', '-', keyword.strip().lower()) + url = f"https://indoteknik.com/shop/find/key={slugified_keyword}" else: - url = 'https://indoteknik.com/shop/find/'+cleaned_combined - url = url.lower() + url = f"https://indoteknik.com/shop/find/{cleaned_combined.lower()}" + result = { 'url': url, 'category_id': category.id, 'brand_id': brand.id } - if self.keywords: - result['keywords_s'] = keyword - # print(url) - # param = { - # 'brand_id': brand.id, - # 'category_id': category.id, - # 'url':'' - # } - # self.env['web.find.page'].create() - # print(1) + if keyword: + result['keyword_s'] = keyword + return result -- cgit v1.2.3 From 0db7816b9208f9ef72bb4bb02e1d550cffb884ee Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 2 Dec 2025 10:31:41 +0700 Subject: set url not readonly and fix typo --- indoteknik_custom/models/find_page.py | 102 ++++++++++++++-------------------- indoteknik_custom/views/find_page.xml | 2 +- 2 files changed, 43 insertions(+), 61 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index 5e530c64..e50ccd5e 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -5,7 +5,7 @@ import pysolr _logger = logging.getLogger(__name__) # _cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30) -_cat_brand_solr_dev = pysolr.Solr('http://localhost:8983/solr/url_category_brand', always_commit=True, timeout=30) +_cat_brand_solr_dev = pysolr.Solr('http://127.0.0.1:8983/solr/url_category_brand/', always_commit=True, timeout=30) class BrandProductCategory(models.Model): @@ -34,50 +34,32 @@ class FindPage(models.Model): _inherit = ['mail.thread'] _rec_name = 'url' + keywords = fields.Char('Keywords') brand_id = fields.Many2one('x_manufactures', string='Brand') category_id = fields.Many2one('product.public.category', string='Category', help='Bisa semua level Category') - url = fields.Char(string='Url', readonly=True) - keywords = fields.Char(string='Keywords') - - - @api.model - def create(self, vals): - record = super(FindPage, self).create(vals) - record._sync_to_solr() - return record - - def write(self, vals): - res = super(FindPage, self).write(vals) - self._sync_to_solr() - return res + url = fields.Char(string='Url') def _sync_to_solr(self, limit=10000): urls = self.env['web.find.page'].search([]) documents = [] catch = {} + keywords = self.keywords for url in urls: try: - keyword = self.keywords document = { 'id': url.id, 'category_id_i': url.category_id.id, 'brand_id_i': url.brand_id.id, - 'url_s': url.url, + 'url_s': url.url } - if keyword: - document['keyword_s'] = keyword + if url.keywords: + document['keywords_s']= keywords documents.append(document) catch = document except Exception as e: _logger.error('Failed to add document to Solr URL Category Brand: %s', e) _logger.error('Document Data: %s', catch) - - try: - _cat_brand_solr_dev.add(documents) - except Exception as e: - _logger.error('Gagal sinkron ke Solr: %s', e) - return True - + _cat_brand_solr_dev.add(documents) return True def _get_category_hierarchy(self, category): @@ -88,66 +70,66 @@ class FindPage(models.Model): current_category = current_category.parent_id return categories - def _generate_url(self): - list_url = [] - keyword = self.keywords + categories = self.env['v.brand.product.category'].search([]) - if keyword: - brands = self.env['x_manufactures'].search([('x_name', 'ilike', keyword)]) - categories = self.env['product.public.category'].search([('name', 'ilike', keyword)]) + list_url = [] + for category in categories: + category_hierarchy = self._get_category_hierarchy(category.category_id) - for brand in brands: - for category in categories: - list_url.append(self._generate_mod_url(category, brand, keyword)) - else: - # Default: ambil semua kombinasi brand-category dari view - categories = self.env['v.brand.product.category'].search([]) - for category in categories: - category_hierarchy = self._get_category_hierarchy(category.category_id) - for level, cat in enumerate(reversed(category_hierarchy), start=1): - list_url.append(self._generate_mod_url(cat, category.brand_id)) + for level, cat in enumerate(reversed(category_hierarchy), start=1): + list_url.append(self._generate_mod_url(cat, category.brand_id)) + # print(f"Level {level}: {cat.name} {category.brand_id.x_name}") - # Hapus duplikat URL unique_list = [] for item in list_url: if item not in unique_list: unique_list.append(item) - count = 0 for item in unique_list: - self._create_find_page(item['url'], item['category_id'], item['brand_id']) + self._create_find_page(item['url'], item['category_id'], item['brand_id'], item['keywords']) count += 1 + print(f"Total categories processed: {count}") - _logger.info(f"Total URL generated: {count}") - - def _create_find_page(self, url, category_id, brand_id): + def _create_find_page(self, url, category_id, brand_id, keywords=None): param = { 'url': url, 'category_id': category_id, 'brand_id': brand_id, } + if keywords: + param['keywords'] = keywords find_page = self.env['web.find.page'].create(param) _logger.info('Created Web Find Page %s' % find_page.id) - def _generate_mod_url(self, category, brand, keyword=None): + def _generate_mod_url(self, category, brand, keywords=None): + # generate_url = 'https://indoteknik.com/shop/find/category-brand' example cleaned_category = re.sub(r'[^\w\s]', '', category.name) cleaned_brand = re.sub(r'[^\w\s]', '', brand.x_name) - cleaned_combined = f"{cleaned_category} {cleaned_brand}" - cleaned_combined = re.sub(r'\s+', '-', cleaned_combined.strip()) - - if keyword: - slugified_keyword = re.sub(r'\s+', '-', keyword.strip().lower()) - url = f"https://indoteknik.com/shop/find/key={slugified_keyword}" + cleaned_combined = cleaned_category+' '+cleaned_brand + cleaned_combined = cleaned_combined.replace(' ', '-') + cleaned_combined = cleaned_combined.replace('--', '-') + if keywords: + clean_keyword = re.sub(r'\s+', '-', keywords.strip().lower()) + # url = f"https://indoteknik.com/shop/find/key={clean_keyword}" + url = f"http://localhost:2100/shop/find/key={clean_keyword}" else: - url = f"https://indoteknik.com/shop/find/{cleaned_combined.lower()}" - + url = 'http://localhost:2100/shop/find/'+cleaned_combined + # url = 'https://indoteknik.com/shop/find/'+cleaned_combined + url = url.lower() result = { 'url': url, 'category_id': category.id, 'brand_id': brand.id } - if keyword: - result['keyword_s'] = keyword - + if keywords: + result['keywords_s'] = keywords + # print(url) + # param = { + # 'brand_id': brand.id, + # 'category_id': category.id, + # 'url':'' + # } + # self.env['web.find.page'].create() + # print(1) return result diff --git a/indoteknik_custom/views/find_page.xml b/indoteknik_custom/views/find_page.xml index 65b0a570..e333559e 100644 --- a/indoteknik_custom/views/find_page.xml +++ b/indoteknik_custom/views/find_page.xml @@ -27,7 +27,7 @@ - + -- cgit v1.2.3 From f21999829173fe8d2ef935f0564014fc04b8214e Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 2 Dec 2025 10:33:03 +0700 Subject: fix error --- indoteknik_custom/models/find_page.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index e50ccd5e..561b15ee 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -43,7 +43,6 @@ class FindPage(models.Model): urls = self.env['web.find.page'].search([]) documents = [] catch = {} - keywords = self.keywords for url in urls: try: document = { @@ -53,7 +52,7 @@ class FindPage(models.Model): 'url_s': url.url } if url.keywords: - document['keywords_s']= keywords + document['keywords_s']= url.keywords documents.append(document) catch = document except Exception as e: -- cgit v1.2.3 From 9a16b32bb2fdd58739c410e7c4ea376ead340a32 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 2 Dec 2025 10:41:46 +0700 Subject: fix generate url --- indoteknik_custom/models/find_page.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index 561b15ee..46b69ff2 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -70,23 +70,41 @@ class FindPage(models.Model): return categories def _generate_url(self): - categories = self.env['v.brand.product.category'].search([]) + keyword = self.keywords list_url = [] - for category in categories: - category_hierarchy = self._get_category_hierarchy(category.category_id) + if not keyword: + categories = self.env['v.brand.product.category'].search([]) - for level, cat in enumerate(reversed(category_hierarchy), start=1): - list_url.append(self._generate_mod_url(cat, category.brand_id)) - # print(f"Level {level}: {cat.name} {category.brand_id.x_name}") + for category in categories: + category_hierarchy = self._get_category_hierarchy(category.category_id) + for level, cat in enumerate(reversed(category_hierarchy), start=1): + list_url.append( + self._generate_mod_url(cat, category.brand_id) # keywords=None otomatis + ) + else: + brands = self.env['x_manufactures'].search([('x_name', 'ilike', keyword)]) + categories = self.env['product.public.category'].search([('name', 'ilike', keyword)]) + + for brand in brands: + for category in categories: + list_url.append( + self._generate_mod_url(category, brand, keywords=keyword) + ) unique_list = [] for item in list_url: if item not in unique_list: unique_list.append(item) + count = 0 for item in unique_list: - self._create_find_page(item['url'], item['category_id'], item['brand_id'], item['keywords']) + self._create_find_page( + item['url'], + item['category_id'], + item['brand_id'], + item.get('keywords') + ) count += 1 print(f"Total categories processed: {count}") @@ -122,7 +140,7 @@ class FindPage(models.Model): 'brand_id': brand.id } if keywords: - result['keywords_s'] = keywords + result['keywords'] = keywords # print(url) # param = { # 'brand_id': brand.id, -- cgit v1.2.3 From fc226081de0bcf8e573307f94c08dfe4c3769b4d Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 2 Dec 2025 11:53:49 +0700 Subject: add import from excel --- indoteknik_custom/models/find_page.py | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index 46b69ff2..3968caed 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -2,6 +2,11 @@ from odoo import fields, models, api, tools, _ import logging import re import pysolr +from odoo.exceptions import UserError +import base64 +import xlrd, xlwt +import io + _logger = logging.getLogger(__name__) # _cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30) @@ -39,6 +44,78 @@ class FindPage(models.Model): category_id = fields.Many2one('product.public.category', string='Category', help='Bisa semua level Category') url = fields.Char(string='Url') + def action_import_excel(self): + if not self.excel_file: + raise UserError(_("⚠️ Harap upload file Excel terlebih dahulu.")) + + try: + data = base64.b64decode(self.excel_file) + book = xlrd.open_workbook(file_contents=data) + sheet = book.sheet_by_index(0) + except: + raise UserError(_("❌ Format Excel tidak valid atau rusak.")) + + header = [str(sheet.cell(0, col).value).strip() for col in range(sheet.ncols)] + required_headers = [ + 'category', + 'brand', + 'keyword', + 'url', + ] + + for req in required_headers: + if req not in header: + raise UserError(_("❌ Kolom '%s' tidak ditemukan di file Excel.") % req) + + header_map = {h: idx for idx, h in enumerate(header)} + lines_created = 0 + PublicCategory = self.env['product.public.category'] + url = self.url + keywords = self.keywords + brand = self.env['x_manufactures'] + + for row_idx in range(1, sheet.nrows): + row = sheet.row(row_idx) + def val(field): + return str(sheet.cell(row_idx, header_map[field]).value).strip() + + if not val('category'): + continue # skip kosong + + # Relations + brand = brand.search([('name', 'ilike', val('brand'))], limit=1) + + # Many2many: Categories + class_names = val('Categories').split(';') + class_ids = [] + for name in class_names: + name = name.strip() + if name: + pc = PublicCategory.search([('name', 'ilike', name)], limit=1) + if pc: + class_ids.append(pc.id) + + # Build values + vals = { + 'brand_id': val('brand'), + 'keywords': val('keywords'), + 'url': val('url'), + 'category_id': val('category') + } + + lines_created += 1 + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('✅ Import Selesai'), + 'message': _('%s baris berhasil diimport.') % lines_created, + 'type': 'success', + 'sticky': False, + } + } + def _sync_to_solr(self, limit=10000): urls = self.env['web.find.page'].search([]) documents = [] -- cgit v1.2.3 From 860187e158fff35cc8f6bd5169583ff5ccd78ffa Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 2 Dec 2025 17:15:23 +0700 Subject: remove import action --- indoteknik_custom/models/find_page.py | 72 ----------------------------------- 1 file changed, 72 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index 3968caed..c31b8334 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -44,78 +44,6 @@ class FindPage(models.Model): category_id = fields.Many2one('product.public.category', string='Category', help='Bisa semua level Category') url = fields.Char(string='Url') - def action_import_excel(self): - if not self.excel_file: - raise UserError(_("⚠️ Harap upload file Excel terlebih dahulu.")) - - try: - data = base64.b64decode(self.excel_file) - book = xlrd.open_workbook(file_contents=data) - sheet = book.sheet_by_index(0) - except: - raise UserError(_("❌ Format Excel tidak valid atau rusak.")) - - header = [str(sheet.cell(0, col).value).strip() for col in range(sheet.ncols)] - required_headers = [ - 'category', - 'brand', - 'keyword', - 'url', - ] - - for req in required_headers: - if req not in header: - raise UserError(_("❌ Kolom '%s' tidak ditemukan di file Excel.") % req) - - header_map = {h: idx for idx, h in enumerate(header)} - lines_created = 0 - PublicCategory = self.env['product.public.category'] - url = self.url - keywords = self.keywords - brand = self.env['x_manufactures'] - - for row_idx in range(1, sheet.nrows): - row = sheet.row(row_idx) - def val(field): - return str(sheet.cell(row_idx, header_map[field]).value).strip() - - if not val('category'): - continue # skip kosong - - # Relations - brand = brand.search([('name', 'ilike', val('brand'))], limit=1) - - # Many2many: Categories - class_names = val('Categories').split(';') - class_ids = [] - for name in class_names: - name = name.strip() - if name: - pc = PublicCategory.search([('name', 'ilike', name)], limit=1) - if pc: - class_ids.append(pc.id) - - # Build values - vals = { - 'brand_id': val('brand'), - 'keywords': val('keywords'), - 'url': val('url'), - 'category_id': val('category') - } - - lines_created += 1 - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('✅ Import Selesai'), - 'message': _('%s baris berhasil diimport.') % lines_created, - 'type': 'success', - 'sticky': False, - } - } - def _sync_to_solr(self, limit=10000): urls = self.env['web.find.page'].search([]) documents = [] -- cgit v1.2.3 From 28355b77ffedbc87f9e8f211e2fed91c15022e4c Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 5 Dec 2025 14:50:18 +0700 Subject: balikin find page --- indoteknik_custom/models/find_page.py | 62 ++++++++--------------------------- 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py index c31b8334..467e30d1 100644 --- a/indoteknik_custom/models/find_page.py +++ b/indoteknik_custom/models/find_page.py @@ -2,15 +2,10 @@ from odoo import fields, models, api, tools, _ import logging import re import pysolr -from odoo.exceptions import UserError -import base64 -import xlrd, xlwt -import io - _logger = logging.getLogger(__name__) -# _cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30) -_cat_brand_solr_dev = pysolr.Solr('http://127.0.0.1:8983/solr/url_category_brand/', always_commit=True, timeout=30) +_cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30) +# _cat_brand_solr_dev = pysolr.Solr('http://127.0.0.1:8983/solr/url_category_brand/', always_commit=True, timeout=30) class BrandProductCategory(models.Model): @@ -39,7 +34,6 @@ class FindPage(models.Model): _inherit = ['mail.thread'] _rec_name = 'url' - keywords = fields.Char('Keywords') brand_id = fields.Many2one('x_manufactures', string='Brand') category_id = fields.Many2one('product.public.category', string='Category', help='Bisa semua level Category') url = fields.Char(string='Url') @@ -56,14 +50,12 @@ class FindPage(models.Model): 'brand_id_i': url.brand_id.id, 'url_s': url.url } - if url.keywords: - document['keywords_s']= url.keywords documents.append(document) catch = document except Exception as e: _logger.error('Failed to add document to Solr URL Category Brand: %s', e) _logger.error('Document Data: %s', catch) - _cat_brand_solr_dev.add(documents) + _cat_brand_solr.add(documents) return True def _get_category_hierarchy(self, category): @@ -75,77 +67,49 @@ class FindPage(models.Model): return categories def _generate_url(self): - keyword = self.keywords + categories = self.env['v.brand.product.category'].search([]) list_url = [] - if not keyword: - categories = self.env['v.brand.product.category'].search([]) - - for category in categories: - category_hierarchy = self._get_category_hierarchy(category.category_id) + for category in categories: + category_hierarchy = self._get_category_hierarchy(category.category_id) - for level, cat in enumerate(reversed(category_hierarchy), start=1): - list_url.append( - self._generate_mod_url(cat, category.brand_id) # keywords=None otomatis - ) - else: - brands = self.env['x_manufactures'].search([('x_name', 'ilike', keyword)]) - categories = self.env['product.public.category'].search([('name', 'ilike', keyword)]) + for level, cat in enumerate(reversed(category_hierarchy), start=1): + list_url.append(self._generate_mod_url(cat, category.brand_id)) + # print(f"Level {level}: {cat.name} {category.brand_id.x_name}") - for brand in brands: - for category in categories: - list_url.append( - self._generate_mod_url(category, brand, keywords=keyword) - ) unique_list = [] for item in list_url: if item not in unique_list: unique_list.append(item) - count = 0 for item in unique_list: - self._create_find_page( - item['url'], - item['category_id'], - item['brand_id'], - item.get('keywords') - ) + self._create_find_page(item['url'], item['category_id'], item['brand_id']) count += 1 print(f"Total categories processed: {count}") - def _create_find_page(self, url, category_id, brand_id, keywords=None): + def _create_find_page(self, url, category_id, brand_id): param = { 'url': url, 'category_id': category_id, 'brand_id': brand_id, } - if keywords: - param['keywords'] = keywords find_page = self.env['web.find.page'].create(param) _logger.info('Created Web Find Page %s' % find_page.id) - def _generate_mod_url(self, category, brand, keywords=None): + def _generate_mod_url(self, category, brand): # generate_url = 'https://indoteknik.com/shop/find/category-brand' example cleaned_category = re.sub(r'[^\w\s]', '', category.name) cleaned_brand = re.sub(r'[^\w\s]', '', brand.x_name) cleaned_combined = cleaned_category+' '+cleaned_brand cleaned_combined = cleaned_combined.replace(' ', '-') cleaned_combined = cleaned_combined.replace('--', '-') - if keywords: - clean_keyword = re.sub(r'\s+', '-', keywords.strip().lower()) - # url = f"https://indoteknik.com/shop/find/key={clean_keyword}" - url = f"http://localhost:2100/shop/find/key={clean_keyword}" - else: - url = 'http://localhost:2100/shop/find/'+cleaned_combined - # url = 'https://indoteknik.com/shop/find/'+cleaned_combined + url = 'https://indoteknik.com/shop/find/'+cleaned_combined url = url.lower() result = { 'url': url, 'category_id': category.id, 'brand_id': brand.id } - if keywords: - result['keywords'] = keywords # print(url) # param = { # 'brand_id': brand.id, -- cgit v1.2.3 From 8ee7bd9211a75809e1819de32ddf974c3736ebde Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 5 Dec 2025 16:46:24 +0700 Subject: Initial COmmit baru --- indoteknik_custom/__manifest__.py | 5 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/keywords.py | 52 ++++++++ indoteknik_custom/models/product_template.py | 171 +++++++++++++------------ indoteknik_custom/security/ir.model.access.csv | 1 + indoteknik_custom/views/find_page.xml | 2 - indoteknik_custom/views/keywords.xml | 55 ++++++++ 7 files changed, 198 insertions(+), 89 deletions(-) create mode 100644 indoteknik_custom/models/keywords.py create mode 100644 indoteknik_custom/views/keywords.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 66962a24..dcb23344 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -90,7 +90,7 @@ 'views/product_sla.xml', 'views/voucher.xml', 'views/bill_receipt.xml', - 'views/account_financial_report_view.xml', + 'views/account_financial_report_view.xml', 'views/account_report_general_ledger_view.xml', 'views/account_move_multi_update.xml', 'views/airway_bill.xml', @@ -192,7 +192,8 @@ 'views/close_tempo_mail_template.xml', 'views/domain_apo.xml', 'views/uom_uom.xml', - 'views/commission_internal.xml' + 'views/commission_internal.xml', + 'views/keywords.xml' ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a14c766e..328c76b0 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,3 +165,4 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal +from . import keywords diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py new file mode 100644 index 00000000..38b6e2fe --- /dev/null +++ b/indoteknik_custom/models/keywords.py @@ -0,0 +1,52 @@ +from itertools import product +from odoo import fields, models, api, tools, _ +import logging +import re +import pysolr +from odoo.exceptions import UserError +import base64 +import xlrd, xlwt +import io + + +_logger = logging.getLogger(__name__) + +class Keywords(models.Model): + _name = 'keywords' + _order= 'id desc' + + category_id = fields.Many2one('product.public.category', string='Category') + keywords = fields.Char('Keywords') + product_ids = fields.One2many('product.product', 'keyword_id', string='Products') + + @api.constrains('product_ids', 'keywords', 'category_id') + def action_generate_products(self): + for record in self: + if not record.keywords: + continue + + domain = [ + ('name', 'ilike', record.keywords), + ('product_rating', '>=', 8), + ('unpublish', '=', False) + ] + + # if record.category_id: + # domain += [(record.product_ids.id, 'in', record.category_id.id)] + + matched_products = self.env['product.product'].search(domain) + + record.product_ids = [(6, 0, matched_products.ids)] + + _logger.info('Generated %s products for keyword "%s"', len(matched_products), record.keywords) + + @api.model + def create(self, vals): + record = super().create(vals) + record.action_generate_products() + return record + + def write(self, vals): + result = super().write(vals) + self.action_generate_products() + return result diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index ee33a185..400d31e0 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -68,7 +68,7 @@ class ProductTemplate(models.Model): ('sp', 'Spare Part'), ('acc', 'Accessories') ], string='Kind of', copy=False) - sni = fields.Boolean(string='SNI') + sni = fields.Boolean(string='SNI') tkdn = fields.Boolean(string='TKDN') short_spesification = fields.Char(string='Short Spesification') merchandise_ok = fields.Boolean(string='Product Promotion') @@ -85,7 +85,7 @@ class ProductTemplate(models.Model): 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])]) @@ -112,7 +112,7 @@ class ProductTemplate(models.Model): # 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: @@ -125,7 +125,7 @@ class ProductTemplate(models.Model): 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) @@ -138,25 +138,25 @@ class ProductTemplate(models.Model): 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), + ('default_code', '!=', False), ('id', '!=', product.id), - '|', - ('name', 'in', names), + '|', + ('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 '' @@ -165,7 +165,7 @@ class ProductTemplate(models.Model): 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 @@ -204,7 +204,7 @@ class ProductTemplate(models.Model): 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) @@ -235,7 +235,7 @@ class ProductTemplate(models.Model): 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) @@ -327,7 +327,7 @@ class ProductTemplate(models.Model): 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') @@ -338,7 +338,7 @@ class ProductTemplate(models.Model): 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 = { @@ -352,7 +352,7 @@ class ProductTemplate(models.Model): 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: @@ -371,12 +371,12 @@ class ProductTemplate(models.Model): 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)) @@ -392,18 +392,18 @@ class ProductTemplate(models.Model): 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 = { @@ -419,10 +419,10 @@ class ProductTemplate(models.Model): 'date_end': 'End Date', 'min_qty': 'Quantity' } - return field_labels.get(field_name, field_name.replace('_', ' ').title()) + return field_labels.get(field_name, field_name.replace('_', ' ').title()) # ============================== - + def _collect_old_values(self, vals): """Collect old values before write""" return { @@ -433,7 +433,7 @@ class ProductTemplate(models.Model): } for record in self } - + def _prepare_attribute_line_info(self): """Prepare attribute line info for logging and update comparison""" line_info = {} @@ -446,7 +446,7 @@ class ProductTemplate(models.Model): '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 = {} @@ -465,13 +465,13 @@ class ProductTemplate(models.Model): '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 = {} @@ -479,7 +479,7 @@ class ProductTemplate(models.Model): 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 = {} @@ -488,18 +488,18 @@ class ProductTemplate(models.Model): # 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 = { @@ -517,18 +517,18 @@ class ProductTemplate(models.Model): 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}'" @@ -537,7 +537,7 @@ class ProductTemplate(models.Model): 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: @@ -552,10 +552,10 @@ class ProductTemplate(models.Model): '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', '-') @@ -567,7 +567,7 @@ class ProductTemplate(models.Model): # 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']: @@ -579,14 +579,14 @@ class ProductTemplate(models.Model): 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}'") @@ -596,7 +596,7 @@ class ProductTemplate(models.Model): message += "
    ".join(changes) self.message_post(body=message) - elif cmd[0] in (2, 3): # Remove + elif cmd[0] in (2, 3): # Remove # Use info from stored data line_data = stored_info.get(cmd[1]) if line_data: @@ -610,7 +610,7 @@ class ProductTemplate(models.Model): else: attribute_name = 'Attribute' values = f"ID {cmd[1]}" - + message = f"Product Attribute:
    {attribute_name} removed
    " if values: message += f"Values: '{values}'" @@ -618,25 +618,25 @@ class ProductTemplate(models.Model): 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: @@ -653,18 +653,18 @@ class ProductTemplate(models.Model): 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:
      {''.join(details)}
    " 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: @@ -685,10 +685,10 @@ class ProductTemplate(models.Model): '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': @@ -698,9 +698,9 @@ class ProductTemplate(models.Model): 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: @@ -732,14 +732,14 @@ class ProductTemplate(models.Model): # 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: @@ -748,20 +748,20 @@ class ProductTemplate(models.Model): 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"
      {''.join(changes)}
    " 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'): @@ -770,7 +770,7 @@ class ProductTemplate(models.Model): 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"
      {''.join(details)}
    " else: @@ -786,7 +786,7 @@ class ProductTemplate(models.Model): details.append(f"
  • Quantity: {rec.min_qty}
  • ") if rec.product_name: details.append(f"
  • Product Name: {rec.product_name}
  • ") - + if details: detail_str = f"
      {''.join(details)}
    " else: @@ -794,12 +794,12 @@ class ProductTemplate(models.Model): 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'] @@ -891,7 +891,7 @@ class ProductTemplate(models.Model): # # 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( @@ -929,6 +929,7 @@ class ProductProduct(models.Model): 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') def generate_product_sla(self): product_variant_ids = self.env.context.get('active_ids', []) @@ -951,7 +952,7 @@ class ProductProduct(models.Model): 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') @@ -967,7 +968,7 @@ class ProductProduct(models.Model): 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, @@ -1055,11 +1056,11 @@ class ProductProduct(models.Model): 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([ @@ -1070,13 +1071,13 @@ class ProductProduct(models.Model): 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 '' @@ -1085,7 +1086,7 @@ class ProductProduct(models.Model): 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( @@ -1207,11 +1208,11 @@ class ProductProduct(models.Model): 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([ @@ -1237,7 +1238,7 @@ class ProductProduct(models.Model): 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', @@ -1314,7 +1315,7 @@ class ProductProduct(models.Model): 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished', 'image_carousel_lines' ] - + if any(field in vals for field in tracked_fields): old_values = self._collect_old_values(vals) result = super().write(vals) @@ -1343,7 +1344,7 @@ class OutstandingMove(models.Model): 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, + 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, diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index bc8dc2a4..796e5275 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -214,3 +214,4 @@ access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1 access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1 access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 access_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1 +access_keywords,keywords,model_keywords,base.group_user,1,1,1,1 diff --git a/indoteknik_custom/views/find_page.xml b/indoteknik_custom/views/find_page.xml index e333559e..fc9bddbb 100644 --- a/indoteknik_custom/views/find_page.xml +++ b/indoteknik_custom/views/find_page.xml @@ -7,7 +7,6 @@ - @@ -26,7 +25,6 @@ - diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml new file mode 100644 index 00000000..872887a7 --- /dev/null +++ b/indoteknik_custom/views/keywords.xml @@ -0,0 +1,55 @@ + + + + keywords.tree + keywords + + + + + + + + + + + keywords.form + keywords + +
    + + + + + + + +
    +
    +
    + + + keywords.list.select + keywords + + + + + + + + + + + Keywords + ir.actions.act_window + keywords + + tree,form + + +
    -- cgit v1.2.3 From e27101a407ad9df9b341149b2b5ff5120062a464 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 8 Dec 2025 13:03:38 +0700 Subject: done temp --- indoteknik_custom/models/keywords.py | 69 +++++++++++++++++++++++++++--------- indoteknik_custom/views/keywords.xml | 1 + 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 38b6e2fe..6e34642a 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -15,38 +15,73 @@ class Keywords(models.Model): _name = 'keywords' _order= 'id desc' - category_id = fields.Many2one('product.public.category', string='Category') - keywords = fields.Char('Keywords') - product_ids = fields.One2many('product.product', 'keyword_id', string='Products') + category_id = fields.Many2one('product.public.category', string='Category', required=True) + keywords = fields.Char('Keywords', required=True) + product_ids = fields.Many2many( + 'product.product', + 'keywords_product_rel', + 'keyword_id', + 'product_id', + string='Products' + ) + skip = fields.Boolean('Skip Generate Product', default=False) - @api.constrains('product_ids', 'keywords', 'category_id') - def action_generate_products(self): + + def generate_products(self): + """Generate product_ids menggunakan SQL mentah (super cepat).""" for record in self: if not record.keywords: continue - domain = [ - ('name', 'ilike', record.keywords), - ('product_rating', '>=', 8), - ('unpublish', '=', False) - ] + keyword = "%" + record.keywords.strip() + "%" + + # Query dasar + sql = """ + SELECT pp.id + FROM product_product pp + JOIN product_template pt ON pt.id = pp.product_tmpl_id + LEFT JOIN product_public_category_product_template_rel rel + ON rel.product_template_id = pt.id + WHERE ( + pt.name ILIKE %s + OR pt.website_description ILIKE %s + AND pt.unpublished = False + AND pt.product_rating >= 8 + ) + """ + + params = [keyword, keyword] + + # Filter kategori kalau ada + if record.category_id: + sql += " AND rel.product_public_category_id = %s" + params.append(record.category_id.id) - # if record.category_id: - # domain += [(record.product_ids.id, 'in', record.category_id.id)] + # Eksekusi SQL + self.env.cr.execute(sql, params) + rows = self.env.cr.fetchall() - matched_products = self.env['product.product'].search(domain) + product_ids = [r[0] for r in rows] - record.product_ids = [(6, 0, matched_products.ids)] + # Update Many2many + record.with_context(skip_generate=True).write({ + 'product_ids': [(6, 0, product_ids)] + }) - _logger.info('Generated %s products for keyword "%s"', len(matched_products), record.keywords) + _logger.info( + "SQL FAST MODE: Found %s products for keyword '%s'", + len(product_ids), + record.keywords + ) @api.model def create(self, vals): record = super().create(vals) - record.action_generate_products() + record.generate_products() return record def write(self, vals): result = super().write(vals) - self.action_generate_products() + if not self.env.context.get("skip_generate") and self.skip == False: + self.generate_products() return result diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 872887a7..77bb8036 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -22,6 +22,7 @@ +
    -- cgit v1.2.3 From f71156bc5e0ce9ceb22215a4345518b02f5250ed Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 8 Dec 2025 15:19:36 +0700 Subject: solr --- indoteknik_custom/models/keywords.py | 76 ++++++++++++++++++++++++++++++++---- indoteknik_custom/views/keywords.xml | 15 ++++++- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 6e34642a..b1f2642d 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -7,9 +7,12 @@ from odoo.exceptions import UserError import base64 import xlrd, xlwt import io +from bs4 import BeautifulSoup _logger = logging.getLogger(__name__) +# solr = pysolr.Solr('http://10.148.0.5:8983/solr/keywords/', always_commit=True, timeout=30) +solr = pysolr.Solr('http://127.0.0.1:8983/solr/keywords/', always_commit=True, timeout=30) class Keywords(models.Model): _name = 'keywords' @@ -24,18 +27,22 @@ class Keywords(models.Model): 'product_id', string='Products' ) + name = fields.Char('Name', compute="_compute_name") skip = fields.Boolean('Skip Generate Product', default=False) + def _compute_name(self): + for record in self: + if not record.name: + record.name = record.keywords + def generate_products(self): - """Generate product_ids menggunakan SQL mentah (super cepat).""" for record in self: if not record.keywords: continue keyword = "%" + record.keywords.strip() + "%" - # Query dasar sql = """ SELECT pp.id FROM product_product pp @@ -52,28 +59,81 @@ class Keywords(models.Model): params = [keyword, keyword] - # Filter kategori kalau ada if record.category_id: sql += " AND rel.product_public_category_id = %s" params.append(record.category_id.id) - # Eksekusi SQL - self.env.cr.execute(sql, params) + # Exec query rows = self.env.cr.fetchall() product_ids = [r[0] for r in rows] - # Update Many2many record.with_context(skip_generate=True).write({ 'product_ids': [(6, 0, product_ids)] }) _logger.info( - "SQL FAST MODE: Found %s products for keyword '%s'", + "SQL : Found %s products for keyword '%s'", len(product_ids), record.keywords ) + + def sync_solr(self): + solr = self.env['apache.solr'] # pastikan ini model Solr sudah ada + documents = [] + + # Sync Keywords + for keyword in self.search([]): + doc = { + 'id': f'keyword_{keyword.id}', # prefix biar unik di Solr + 'type_s': 'keyword', + 'name_s': keyword.keywords, + 'category_id_i': keyword.category_id.id if keyword.category_id else 0, + 'product_ids_ii': keyword.product_ids.ids, + } + documents.append(doc) + _logger.info('Prepared Solr document for keyword %s', keyword.keywords) + + # Sync Products + products = self.env['product.product'].search([]) + for product in products: + category_ids = [c.id for c in product.public_categ_ids] + doc = { + 'id': f'product_{product.id}', + 'type_s': 'product', + 'name_s': product.name, + 'default_code_s': product.default_code or '', + 'category_ids_ii': category_ids, + 'website_description_t': product.website_description or '', + 'product_rating_f': getattr(product, 'virtual_rating', 0), + 'active_b': product.active, + } + documents.append(doc) + _logger.info('Prepared Solr document for product %s', product.name) + + # Sync Categories + categories = self.env['product.public.category'].search([]) + for cat in categories: + doc = { + 'id': f'category_{cat.id}', + 'type_s': 'category', + 'name_s': cat.name, + 'parent_id_i': cat.parent_id.id if cat.parent_id else 0, + } + documents.append(doc) + _logger.info('Prepared Solr document for category %s', cat.name) + + # Kirim ke Solr + if documents: + try: + solr.add(documents, softCommit=True) + _logger.info('Synced %d documents to Solr', len(documents)) + except Exception as e: + _logger.error('Failed to sync documents to Solr: %s', e) + else: + _logger.warning('No documents to sync to Solr') + @api.model def create(self, vals): record = super().create(vals) @@ -82,6 +142,6 @@ class Keywords(models.Model): def write(self, vals): result = super().write(vals) - if not self.env.context.get("skip_generate") and self.skip == False: + if not self.env.context.get("skip_generate") and not self.skip: self.generate_products() return result diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 77bb8036..2fc44ced 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -18,6 +18,11 @@
    +
    +

    + +

    +
    @@ -41,13 +46,21 @@ - + Keywords ir.actions.act_window keywords tree,form + + + Sync to solr + + + code + model.sync_solr() + Date: Tue, 9 Dec 2025 09:20:20 +0700 Subject: solr & fix cannot fetch data --- indoteknik_custom/models/keywords.py | 91 ++++++++++++++---------------------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index b1f2642d..de7636b7 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -35,6 +35,16 @@ class Keywords(models.Model): if not record.name: record.name = record.keywords + # @api.constrains('keywords') + # def check_already_exist(self): + # model = self.env['keywords'] + # for record in self: + # match = model.search([ + # ('name', 'ilike', record.name) + # ]) + # if match: + # raise UserError("Tidak bisa create/write/duplicate karena keywords sudah dipakai") + def generate_products(self): for record in self: @@ -43,6 +53,7 @@ class Keywords(models.Model): keyword = "%" + record.keywords.strip() + "%" + # Query dasar sql = """ SELECT pp.id FROM product_product pp @@ -59,11 +70,16 @@ class Keywords(models.Model): params = [keyword, keyword] + # Filter kategori ke child if record.category_id: - sql += " AND rel.product_public_category_id = %s" - params.append(record.category_id.id) - - # Exec query + child_categs = self.env['product.public.category'].search([ + ('id', 'child_of', record.category_id.id) + ]) + sql += " AND rel.product_public_category_id = ANY(%s)" + params.append(child_categs.ids) + + # Exec SQL + self.env.cr.execute(sql, params) rows = self.env.cr.fetchall() product_ids = [r[0] for r in rows] @@ -73,66 +89,31 @@ class Keywords(models.Model): }) _logger.info( - "SQL : Found %s products for keyword '%s'", + "Product Found: Found %s products for keyword '%s'", len(product_ids), record.keywords ) - def sync_solr(self): - solr = self.env['apache.solr'] # pastikan ini model Solr sudah ada + # solr_model = self.env['apache.solr'] documents = [] - - # Sync Keywords + data = {} for keyword in self.search([]): - doc = { - 'id': f'keyword_{keyword.id}', # prefix biar unik di Solr - 'type_s': 'keyword', - 'name_s': keyword.keywords, - 'category_id_i': keyword.category_id.id if keyword.category_id else 0, - 'product_ids_ii': keyword.product_ids.ids, - } - documents.append(doc) - _logger.info('Prepared Solr document for keyword %s', keyword.keywords) - - # Sync Products - products = self.env['product.product'].search([]) - for product in products: - category_ids = [c.id for c in product.public_categ_ids] - doc = { - 'id': f'product_{product.id}', - 'type_s': 'product', - 'name_s': product.name, - 'default_code_s': product.default_code or '', - 'category_ids_ii': category_ids, - 'website_description_t': product.website_description or '', - 'product_rating_f': getattr(product, 'virtual_rating', 0), - 'active_b': product.active, - } - documents.append(doc) - _logger.info('Prepared Solr document for product %s', product.name) - - # Sync Categories - categories = self.env['product.public.category'].search([]) - for cat in categories: - doc = { - 'id': f'category_{cat.id}', - 'type_s': 'category', - 'name_s': cat.name, - 'parent_id_i': cat.parent_id.id if cat.parent_id else 0, - } - documents.append(doc) - _logger.info('Prepared Solr document for category %s', cat.name) - - # Kirim ke Solr - if documents: try: - solr.add(documents, softCommit=True) - _logger.info('Synced %d documents to Solr', len(documents)) + doc = { + 'id': f'keyword_{keyword.id}', + 'type_s': 'keyword', + 'name_s': keyword.keywords, + 'category_id_i': keyword.category_id.id if keyword.category_id else 0, + 'product_ids_ii': keyword.product_ids.ids, + } + documents.append(doc) + data = doc except Exception as e: - _logger.error('Failed to sync documents to Solr: %s', e) - else: - _logger.warning('No documents to sync to Solr') + _logger.error('failed %s', e) + _logger.error('doc data: %s', data) + solr.add(documents) + return True @api.model def create(self, vals): -- cgit v1.2.3 From ea7edf0fee4949203e94360882590ea0c9bb3b0b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 9 Dec 2025 10:32:27 +0700 Subject: done solr --- indoteknik_custom/models/keywords.py | 27 +++++++++++++++++++++------ indoteknik_custom/views/keywords.xml | 2 ++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index de7636b7..fd8603c6 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -11,8 +11,8 @@ from bs4 import BeautifulSoup _logger = logging.getLogger(__name__) -# solr = pysolr.Solr('http://10.148.0.5:8983/solr/keywords/', always_commit=True, timeout=30) -solr = pysolr.Solr('http://127.0.0.1:8983/solr/keywords/', always_commit=True, timeout=30) +# solr = pysolr.Solr('http://10.148.0.5:8983/solr/searchkey/', always_commit=True, timeout=30) +solr = pysolr.Solr('http://127.0.0.1:8983/solr/searchkey/', always_commit=True, timeout=30) class Keywords(models.Model): _name = 'keywords' @@ -29,6 +29,18 @@ class Keywords(models.Model): ) name = fields.Char('Name', compute="_compute_name") skip = fields.Boolean('Skip Generate Product', default=False) + url = fields.Char('Website URL', compute="_compute_url") + + @api.depends('keywords') + def _compute_url(self): + prefix = "https://indoteknik.com/searchkey/" + for record in self: + if record.keywords: + slug = re.sub(r'[^a-zA-Z0-9]+', '-', record.keywords.strip().lower()) + slug = slug.strip('-') + record.url = prefix + slug + else: + record.url = False def _compute_name(self): for record in self: @@ -84,6 +96,9 @@ class Keywords(models.Model): product_ids = [r[0] for r in rows] + if not product_ids: + raise UserError("Tidak berhasil menemukan barang") + record.with_context(skip_generate=True).write({ 'product_ids': [(6, 0, product_ids)] }) @@ -101,11 +116,11 @@ class Keywords(models.Model): for keyword in self.search([]): try: doc = { - 'id': f'keyword_{keyword.id}', + 'id': keyword.id, 'type_s': 'keyword', - 'name_s': keyword.keywords, - 'category_id_i': keyword.category_id.id if keyword.category_id else 0, - 'product_ids_ii': keyword.product_ids.ids, + 'keywords_t': keyword.keywords, + 'url_s': keyword.url, + 'product_ids_is': keyword.product_ids.ids, } documents.append(doc) data = doc diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 2fc44ced..9145ec47 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -7,6 +7,7 @@ + @@ -26,6 +27,7 @@ + -- cgit v1.2.3 From c13ed8b1f00d513bec387c614ac785c7c91debf7 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 10 Dec 2025 07:53:30 +0700 Subject: Add count product --- indoteknik_custom/models/keywords.py | 14 +++++++++++--- indoteknik_custom/views/keywords.xml | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index fd8603c6..a8d2fce6 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -30,6 +30,12 @@ class Keywords(models.Model): name = fields.Char('Name', compute="_compute_name") skip = fields.Boolean('Skip Generate Product', default=False) url = fields.Char('Website URL', compute="_compute_url") + sum = fields.Integer('Total Product', compute="_compute_total_product", readonly=True) + + @api.depends('product_ids') + def _compute_total_product(self): + for record in self: + record.sum = len(record.product_ids) @api.depends('keywords') def _compute_url(self): @@ -114,13 +120,15 @@ class Keywords(models.Model): documents = [] data = {} for keyword in self.search([]): + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') try: doc = { 'id': keyword.id, - 'type_s': 'keyword', - 'keywords_t': keyword.keywords, + # 'type_s': 'keyword', + 'keywords_s': searchkey, + # 'searchkey_t': searchkey, 'url_s': keyword.url, - 'product_ids_is': keyword.product_ids.ids, + 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids] } documents.append(doc) data = doc diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 9145ec47..f5374655 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -8,6 +8,7 @@ + @@ -28,6 +29,7 @@ + -- cgit v1.2.3 From 2ae552a716040d65690bd20e6b1854d5201d32bd Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 10 Dec 2025 13:06:25 +0700 Subject: throw category to solr --- indoteknik_custom/models/keywords.py | 1 + 1 file changed, 1 insertion(+) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index a8d2fce6..cee189c4 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -125,6 +125,7 @@ class Keywords(models.Model): doc = { 'id': keyword.id, # 'type_s': 'keyword', + 'category_id_i': keyword.category_id.id, 'keywords_s': searchkey, # 'searchkey_t': searchkey, 'url_s': keyword.url, -- cgit v1.2.3 From 64da5a5b37a7c016e863b2c3d615a21bdb45839a Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 10 Dec 2025 13:25:56 +0700 Subject: remove unused solr field --- indoteknik_custom/models/keywords.py | 1 - 1 file changed, 1 deletion(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index cee189c4..03f5a11d 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -124,7 +124,6 @@ class Keywords(models.Model): try: doc = { 'id': keyword.id, - # 'type_s': 'keyword', 'category_id_i': keyword.category_id.id, 'keywords_s': searchkey, # 'searchkey_t': searchkey, -- cgit v1.2.3 From ed88e91aa769e07ba048fd8b3067442754c06065 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 11 Dec 2025 08:53:01 +0700 Subject: disable query --- indoteknik_custom/models/keywords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 03f5a11d..5faa94a6 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -146,6 +146,6 @@ class Keywords(models.Model): def write(self, vals): result = super().write(vals) - if not self.env.context.get("skip_generate") and not self.skip: - self.generate_products() + # if not self.env.context.get("skip_generate") and not self.skip: + # self.generate_products() return result -- cgit v1.2.3 From 097f53b8cb0b2c39fb14f5f3a665410df2c7ebf3 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 12 Dec 2025 08:55:07 +0700 Subject: done temp add product to keywords --- indoteknik_custom/models/product_template.py | 33 +++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 400d31e0..2cbef168 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -1,3 +1,4 @@ +from this import s from odoo import fields, models, api, tools, _ from datetime import datetime, timedelta, date from odoo.exceptions import UserError @@ -84,6 +85,7 @@ class ProductTemplate(models.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) + self.env['product.product']._add_product_to_keywords(result) return result # def write(self, values): @@ -883,6 +885,12 @@ class ProductTemplate(models.Model): result = super().write(vals) # Log changes self._log_field_changes_product(vals, old_values) + + # Add product to keywords + keyword_trigger = ['name', 'website_description', 'unpublished'] + if any(field in vals for field in keyword_trigger): + for product in self: + self.env['product.product']._add_product_to_keywords(product) return result # def write(self, vals): @@ -931,6 +939,24 @@ class ProductProduct(models.Model): barcode_box = fields.Char("Barcode Box") keyword_id = fields.Many2one('keywords', string='Keyword') + def _add_product_to_keywords(self,product): + keywords_model = self.env['keywords'] + if not product: + return False + + for product in self: + match_keywords = keywords_model.search([ + '|', + ('name', 'ilike', product.name), + ('keywords', 'ilike', product.website_description) + ]) + + for kw in match_keywords.filtered(lambda k: not k.skip): + if not self.unpublished and product.id not in kw.product_ids.ids: + kw.write({'product_ids': [(4, product.id)]}) + + return True + def generate_product_sla(self): product_variant_ids = self.env.context.get('active_ids', []) product_variant = self.search([('id', 'in', product_variant_ids)]) @@ -951,6 +977,7 @@ class ProductProduct(models.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(ProductProduct, self).create(vals) + self._add_product_to_keywords(result) return result # def write(self, values): @@ -1315,7 +1342,11 @@ class ProductProduct(models.Model): 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished', 'image_carousel_lines' ] - + # pake ini kalau mau Cek semua field + # if vals: + trigger_fields = ['name', 'website_description', 'unpublished'] + if any(f in vals for f in trigger_fields): + self._add_product_to_keywords(vals) if any(field in vals for field in tracked_fields): old_values = self._collect_old_values(vals) result = super().write(vals) -- cgit v1.2.3 From c8fadfcb646ec86b73d92f199a98000aeabcbfe8 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 15 Dec 2025 11:06:20 +0700 Subject: (andri) comment create & write --- indoteknik_custom/models/keywords.py | 122 +++++++++++++++++------------------ 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 5faa94a6..28dfa09e 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -64,56 +64,56 @@ class Keywords(models.Model): # raise UserError("Tidak bisa create/write/duplicate karena keywords sudah dipakai") - def generate_products(self): - for record in self: - if not record.keywords: - continue - - keyword = "%" + record.keywords.strip() + "%" - - # Query dasar - sql = """ - SELECT pp.id - FROM product_product pp - JOIN product_template pt ON pt.id = pp.product_tmpl_id - LEFT JOIN product_public_category_product_template_rel rel - ON rel.product_template_id = pt.id - WHERE ( - pt.name ILIKE %s - OR pt.website_description ILIKE %s - AND pt.unpublished = False - AND pt.product_rating >= 8 - ) - """ - - params = [keyword, keyword] - - # Filter kategori ke child - if record.category_id: - child_categs = self.env['product.public.category'].search([ - ('id', 'child_of', record.category_id.id) - ]) - sql += " AND rel.product_public_category_id = ANY(%s)" - params.append(child_categs.ids) - - # Exec SQL - self.env.cr.execute(sql, params) - rows = self.env.cr.fetchall() - - product_ids = [r[0] for r in rows] - - if not product_ids: - raise UserError("Tidak berhasil menemukan barang") - - record.with_context(skip_generate=True).write({ - 'product_ids': [(6, 0, product_ids)] - }) - - _logger.info( - "Product Found: Found %s products for keyword '%s'", - len(product_ids), - record.keywords - ) + # def generate_products(self): + # for record in self: + # if not record.keywords: + # continue + + # keyword = "%" + record.keywords.strip() + "%" + + # # Query dasar + # sql = """ + # SELECT pp.id + # FROM product_product pp + # JOIN product_template pt ON pt.id = pp.product_tmpl_id + # LEFT JOIN product_public_category_product_template_rel rel + # ON rel.product_template_id = pt.id + # WHERE ( + # pt.name ILIKE %s + # OR pt.website_description ILIKE %s + # AND pt.unpublished = False + # AND pt.product_rating >= 8 + # ) + # """ + + # params = [keyword, keyword] + + # # Filter kategori ke child + # if record.category_id: + # child_categs = self.env['product.public.category'].search([ + # ('id', 'child_of', record.category_id.id) + # ]) + # sql += " AND rel.product_public_category_id = ANY(%s)" + # params.append(child_categs.ids) + + # # Exec SQL + # self.env.cr.execute(sql, params) + # rows = self.env.cr.fetchall() + + # product_ids = [r[0] for r in rows] + + # if not product_ids: + # raise UserError("Tidak berhasil menemukan barang") + + # record.with_context(skip_generate=True).write({ + # 'product_ids': [(6, 0, product_ids)] + # }) + + # _logger.info( + # "Product Found: Found %s products for keyword '%s'", + # len(product_ids), + # record.keywords + # ) def sync_solr(self): # solr_model = self.env['apache.solr'] @@ -138,14 +138,14 @@ class Keywords(models.Model): solr.add(documents) return True - @api.model - def create(self, vals): - record = super().create(vals) - record.generate_products() - return record - - def write(self, vals): - result = super().write(vals) - # if not self.env.context.get("skip_generate") and not self.skip: - # self.generate_products() - return result + # @api.model + # def create(self, vals): + # record = super().create(vals) + # # record.generate_products() + # return record + + # def write(self, vals): + # result = super().write(vals) + # # if not self.env.context.get("skip_generate") and not self.skip: + # # self.generate_products() + # return result -- cgit v1.2.3 From 0a81a44ccfdfc7219ee29b2b6f79fa73143e095d Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 15 Dec 2025 13:42:30 +0700 Subject: add button generate product manually --- indoteknik_custom/models/keywords.py | 5 ++++- indoteknik_custom/views/keywords.xml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 5faa94a6..495a42cc 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -1,4 +1,5 @@ from itertools import product +from multiprocessing import Condition from odoo import fields, models, api, tools, _ import logging import re @@ -68,6 +69,8 @@ class Keywords(models.Model): for record in self: if not record.keywords: continue + if record.skip: + continue keyword = "%" + record.keywords.strip() + "%" @@ -141,7 +144,7 @@ class Keywords(models.Model): @api.model def create(self, vals): record = super().create(vals) - record.generate_products() + # record.generate_products() return record def write(self, vals): diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index f5374655..878952f8 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -19,6 +19,9 @@ keywords +
    +

    -- cgit v1.2.3 From 274a067754ae67bc0f7f77c4c079aa2634b8000c Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 15 Dec 2025 15:11:30 +0700 Subject: add clear button --- indoteknik_custom/models/keywords.py | 4 ++++ indoteknik_custom/views/keywords.xml | 1 + 2 files changed, 5 insertions(+) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 495a42cc..3602be15 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -65,6 +65,10 @@ class Keywords(models.Model): # raise UserError("Tidak bisa create/write/duplicate karena keywords sudah dipakai") + def clear_products(self): + for record in self: + record.product_ids = [(5, 0, 0)] + def generate_products(self): for record in self: if not record.keywords: diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 878952f8..6f1a675f 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -21,6 +21,7 @@
    -- cgit v1.2.3 From dcfbed94e24f7bf6fd06bd0404c1424af2a840be Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 16 Dec 2025 09:39:32 +0700 Subject: fix quey --- indoteknik_custom/models/keywords.py | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 3602be15..e0c7705c 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -71,31 +71,31 @@ class Keywords(models.Model): def generate_products(self): for record in self: - if not record.keywords: - continue - if record.skip: + if not record.keywords or record.skip: continue - keyword = "%" + record.keywords.strip() + "%" + keyword = f"%{record.keywords.strip()}%" - # Query dasar sql = """ - SELECT pp.id + SELECT DISTINCT pp.id FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id - LEFT JOIN product_public_category_product_template_rel rel + JOIN product_public_category_product_template_rel rel ON rel.product_template_id = pt.id - WHERE ( - pt.name ILIKE %s - OR pt.website_description ILIKE %s - AND pt.unpublished = False - AND pt.product_rating >= 8 - ) + WHERE + pp.active = true + AND COALESCE(pt.product_rating, 0) >= 8 + AND ( + pt.name ILIKE %s + OR COALESCE(pt.website_description, '') ILIKE %s + ) """ - params = [keyword, keyword] + params = [ + keyword, + keyword, + ] - # Filter kategori ke child if record.category_id: child_categs = self.env['product.public.category'].search([ ('id', 'child_of', record.category_id.id) @@ -103,14 +103,15 @@ class Keywords(models.Model): sql += " AND rel.product_public_category_id = ANY(%s)" params.append(child_categs.ids) - # Exec SQL self.env.cr.execute(sql, params) - rows = self.env.cr.fetchall() + rows = self.env.cr.fetchall() product_ids = [r[0] for r in rows] if not product_ids: - raise UserError("Tidak berhasil menemukan barang") + raise UserError( + f"Tidak berhasil menemukan barang untuk keyword '{record.keywords}'" + ) record.with_context(skip_generate=True).write({ 'product_ids': [(6, 0, product_ids)] @@ -121,7 +122,6 @@ class Keywords(models.Model): len(product_ids), record.keywords ) - def sync_solr(self): # solr_model = self.env['apache.solr'] documents = [] -- cgit v1.2.3 From 33e6fc960fc3dcdbb77eedbff6239b7763ace468 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 18 Dec 2025 10:52:43 +0700 Subject: fix query --- indoteknik_custom/models/keywords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index e0c7705c..69ecdd46 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -83,8 +83,8 @@ class Keywords(models.Model): JOIN product_public_category_product_template_rel rel ON rel.product_template_id = pt.id WHERE - pp.active = true - AND COALESCE(pt.product_rating, 0) >= 8 + COALESCE(pt.product_rating, 0) >= 3 + AND (pt.unpublished IS FALSE OR pt.unpublished IS NULL) AND ( pt.name ILIKE %s OR COALESCE(pt.website_description, '') ILIKE %s -- cgit v1.2.3 From fbb30fff2489546e4c21ab55d504b6b861e70deb Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 18 Dec 2025 15:43:02 +0700 Subject: fix solr sync --- indoteknik_custom/models/keywords.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 69ecdd46..225710ab 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -76,6 +76,7 @@ class Keywords(models.Model): keyword = f"%{record.keywords.strip()}%" + # AND (pt.unpublished IS FALSE OR pt.unpublished IS NULL) sql = """ SELECT DISTINCT pp.id FROM product_product pp @@ -83,11 +84,11 @@ class Keywords(models.Model): JOIN product_public_category_product_template_rel rel ON rel.product_template_id = pt.id WHERE - COALESCE(pt.product_rating, 0) >= 3 - AND (pt.unpublished IS FALSE OR pt.unpublished IS NULL) + pt.product_rating >= 3 + AND pp.active IS TRUE AND ( pt.name ILIKE %s - OR COALESCE(pt.website_description, '') ILIKE %s + OR pt.website_description ILIKE %s ) """ @@ -122,27 +123,34 @@ class Keywords(models.Model): len(product_ids), record.keywords ) + def sync_solr(self): - # solr_model = self.env['apache.solr'] + active_ids = self.env.context.get('active_ids', []) + if not active_ids: + _logger.warning("No active_ids found, nothing to sync") + return True + + keywords = self.browse(active_ids) + documents = [] - data = {} - for keyword in self.search([]): + for keyword in keywords: searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') try: doc = { 'id': keyword.id, 'category_id_i': keyword.category_id.id, 'keywords_s': searchkey, - # 'searchkey_t': searchkey, 'url_s': keyword.url, - 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids] + 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], } documents.append(doc) - data = doc except Exception as e: _logger.error('failed %s', e) - _logger.error('doc data: %s', data) - solr.add(documents) + _logger.error('doc data: %s', doc) + + if documents: + solr.add(documents) + return True @api.model -- cgit v1.2.3 From 9596f3806d8f177c5a701c16be4b10c964be7c07 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 18 Dec 2025 15:43:18 +0700 Subject: fix product rating --- indoteknik_custom/models/keywords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 225710ab..b2ada3f8 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -84,7 +84,7 @@ class Keywords(models.Model): JOIN product_public_category_product_template_rel rel ON rel.product_template_id = pt.id WHERE - pt.product_rating >= 3 + pt.product_rating >= 8 AND pp.active IS TRUE AND ( pt.name ILIKE %s -- cgit v1.2.3 From 41e5e8defc0d0b928e43307f72887b0789ffb589 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 18 Dec 2025 16:00:19 +0700 Subject: fix duplicate keywords functions --- indoteknik_custom/models/keywords.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index b2ada3f8..41deea3e 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -8,7 +8,6 @@ from odoo.exceptions import UserError import base64 import xlrd, xlwt import io -from bs4 import BeautifulSoup _logger = logging.getLogger(__name__) @@ -19,7 +18,7 @@ class Keywords(models.Model): _name = 'keywords' _order= 'id desc' - category_id = fields.Many2one('product.public.category', string='Category', required=True) + category_id = fields.Many2one('product.public.category', string='Category', required=True, help="Category to filter products when generating products for this keyword and to throw to solr") keywords = fields.Char('Keywords', required=True) product_ids = fields.Many2many( 'product.product', @@ -29,9 +28,9 @@ class Keywords(models.Model): string='Products' ) name = fields.Char('Name', compute="_compute_name") - skip = fields.Boolean('Skip Generate Product', default=False) - url = fields.Char('Website URL', compute="_compute_url") - sum = fields.Integer('Total Product', compute="_compute_total_product", readonly=True) + skip = fields.Boolean('Skip Generate Product', default=False, help="If checked, the system will not generate products for this keyword") + url = fields.Char('Website URL', compute="_compute_url", help="Generated website url based on keywords") + sum = fields.Integer('Total Product', compute="_compute_total_product", readonly=True, help="Total products found for this keyword including variants") @api.depends('product_ids') def _compute_total_product(self): @@ -54,15 +53,19 @@ class Keywords(models.Model): if not record.name: record.name = record.keywords - # @api.constrains('keywords') - # def check_already_exist(self): - # model = self.env['keywords'] - # for record in self: - # match = model.search([ - # ('name', 'ilike', record.name) - # ]) - # if match: - # raise UserError("Tidak bisa create/write/duplicate karena keywords sudah dipakai") + @api.constrains('keywords', 'name') + def check_already_exist(self): + model = self.env['keywords'] + for record in self: + match = model.search([ '|', + ('name', '=', record.name), + ('keywords', '=', record.keywords) + ]) + if match: + raise UserError("Tidak bisa create/write/duplicate karena keywords sudah dipakai") + + def copy(self): + raise UserError("Duplicate Record not allowed") def clear_products(self): -- cgit v1.2.3 From 2e3e5853ce9fc3e9568eea3a673c2a154b8de9da Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 15:10:58 +0700 Subject: fix duplicate keywords --- indoteknik_custom/models/keywords.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 41deea3e..fedaa180 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -53,16 +53,15 @@ class Keywords(models.Model): if not record.name: record.name = record.keywords - @api.constrains('keywords', 'name') + @api.depends('keywords') def check_already_exist(self): model = self.env['keywords'] for record in self: - match = model.search([ '|', - ('name', '=', record.name), - ('keywords', '=', record.keywords) + match = model.search([ + ('keywords', 'ilike', record.keywords) ]) if match: - raise UserError("Tidak bisa create/write/duplicate karena keywords sudah dipakai") + raise UserError("Tidak bisa create karena keywords sudah dipakai") def copy(self): raise UserError("Duplicate Record not allowed") @@ -159,11 +158,13 @@ class Keywords(models.Model): @api.model def create(self, vals): record = super().create(vals) + self.check_already_exist() # record.generate_products() return record def write(self, vals): result = super().write(vals) + self.check_already_exist() # if not self.env.context.get("skip_generate") and not self.skip: # self.generate_products() return result -- cgit v1.2.3 From b0c16534780cc69320f16e7d3bbec29a811a6215 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 30 Dec 2025 11:48:16 +0700 Subject: fix check duplicate --- indoteknik_custom/models/keywords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index fedaa180..47546c1f 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -53,12 +53,12 @@ class Keywords(models.Model): if not record.name: record.name = record.keywords - @api.depends('keywords') def check_already_exist(self): model = self.env['keywords'] for record in self: match = model.search([ - ('keywords', 'ilike', record.keywords) + ('keywords', '=', record.keywords), + ('category_id.id', '=', record.category_id.id) ]) if match: raise UserError("Tidak bisa create karena keywords sudah dipakai") -- cgit v1.2.3 From ea309ccb34ed588e91198cde98074c77ed1b2bef Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 30 Dec 2025 16:19:05 +0700 Subject: add brands --- indoteknik_custom/models/keywords.py | 26 ++++--- indoteknik_custom/views/keywords.xml | 140 ++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 79 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 47546c1f..ef3715c9 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -20,6 +20,10 @@ class Keywords(models.Model): category_id = fields.Many2one('product.public.category', string='Category', required=True, help="Category to filter products when generating products for this keyword and to throw to solr") keywords = fields.Char('Keywords', required=True) + brand_id = fields.Many2one( + comodel_name="x_manufactures", + string="Brand" + ) product_ids = fields.Many2many( 'product.product', 'keywords_product_rel', @@ -53,15 +57,15 @@ class Keywords(models.Model): if not record.name: record.name = record.keywords - def check_already_exist(self): - model = self.env['keywords'] - for record in self: - match = model.search([ - ('keywords', '=', record.keywords), - ('category_id.id', '=', record.category_id.id) - ]) - if match: - raise UserError("Tidak bisa create karena keywords sudah dipakai") + # def check_already_exist(self): + # model = self.env['keywords'] + # for record in self: + # match = model.search([ + # ('keywords', '=', record.keywords), + # ('category_id.id', '=', record.category_id.id) + # ]) + # if match: + # raise UserError("Tidak bisa create karena keywords sudah dipakai") def copy(self): raise UserError("Duplicate Record not allowed") @@ -158,13 +162,13 @@ class Keywords(models.Model): @api.model def create(self, vals): record = super().create(vals) - self.check_already_exist() + # self.check_already_exist() # record.generate_products() return record def write(self, vals): result = super().write(vals) - self.check_already_exist() + # self.check_already_exist() # if not self.env.context.get("skip_generate") and not self.skip: # self.generate_products() return result diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 6f1a675f..febd6d39 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -1,77 +1,81 @@ - - keywords.tree - keywords - - - - - - - - - - + + keywords.tree + keywords + + + + + + + + + + + - + keywords.form keywords - -
    -
    - -
    -

    - -

    -
    - - - - - - - - -
    - -
    -
    +
    +
    +
    + +
    +

    + +

    +
    + + + + + + + + + +
    +
    + +
    - - keywords.list.select - keywords - - - - - - - - - - - Keywords - ir.actions.act_window - keywords - - tree,form - + + keywords.list.select + keywords + + + + + + + + + + + + + Keywords + ir.actions.act_window + keywords + + tree,form + - - Sync to solr - - - code - model.sync_solr() - - + + Sync to solr + + + code + model.sync_solr() + +
    -- cgit v1.2.3 From a069b9d6b986e514c17b42df5e807b7076338cda Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 31 Dec 2025 13:29:51 +0700 Subject: add brand & compute url --- indoteknik_custom/models/keywords.py | 92 +++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 22 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index ef3715c9..53cf4225 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -41,16 +41,25 @@ class Keywords(models.Model): for record in self: record.sum = len(record.product_ids) - @api.depends('keywords') + + @api.depends('keywords', 'brand_id') def _compute_url(self): prefix = "https://indoteknik.com/searchkey/" for record in self: - if record.keywords: - slug = re.sub(r'[^a-zA-Z0-9]+', '-', record.keywords.strip().lower()) - slug = slug.strip('-') - record.url = prefix + slug - else: + if not record.keywords: record.url = False + continue + slug = re.sub(r'[^a-zA-Z0-9]+', '-', record.keywords.strip().lower()).strip('-') + + if record.brand_id: + brand_slug = re.sub( + r'[^a-zA-Z0-9]+', + '-', + record.brand_id.x_name.strip().lower() + ).strip('-') + record.url = f"{prefix}{slug}-{brand_slug}" + else: + record.url = f"{prefix}{slug}" def _compute_name(self): for record in self: @@ -75,14 +84,23 @@ class Keywords(models.Model): for record in self: record.product_ids = [(5, 0, 0)] + def generate_products(self): for record in self: - if not record.keywords or record.skip: + if record.skip: continue - keyword = f"%{record.keywords.strip()}%" + if not record.keywords: + raise UserError("Keyword wajib diisi") + + if not record.category_id: + raise UserError( + f"Category wajib diisi untuk keyword '{record.keywords}'" + ) + + keyword_raw = record.keywords.strip() + keyword = f"%{keyword_raw}%" - # AND (pt.unpublished IS FALSE OR pt.unpublished IS NULL) sql = """ SELECT DISTINCT pp.id FROM product_product pp @@ -98,26 +116,55 @@ class Keywords(models.Model): ) """ - params = [ - keyword, - keyword, - ] + params = [keyword, keyword] + + # ====================== + # CATEGORY (WAJIB) + # ====================== + child_categs = self.env['product.public.category'].search([ + ('id', 'child_of', record.category_id.id) + ]) + + sql += " AND rel.product_public_category_id = ANY(%s)" + params.append(child_categs.ids) - if record.category_id: - child_categs = self.env['product.public.category'].search([ - ('id', 'child_of', record.category_id.id) + # ====================== + # BRAND (OPTIONAL) + # ====================== + if record.brand_id: + brand = record.brand_id.x_name.strip() + + brand_kw_1 = f"%{brand} {keyword_raw}%" + brand_kw_2 = f"%{keyword_raw} {brand}%" + + sql += """ + AND ( + pt.name ILIKE %s + OR pt.name ILIKE %s + OR pt.website_description ILIKE %s + OR pt.website_description ILIKE %s + ) + """ + + params.extend([ + brand_kw_1, + brand_kw_2, + brand_kw_1, + brand_kw_2, ]) - sql += " AND rel.product_public_category_id = ANY(%s)" - params.append(child_categs.ids) + # ====================== + # EXECUTE + # ====================== self.env.cr.execute(sql, params) - rows = self.env.cr.fetchall() + product_ids = [r[0] for r in rows] if not product_ids: raise UserError( - f"Tidak berhasil menemukan barang untuk keyword '{record.keywords}'" + f"Tidak menemukan product untuk keyword " + f"'{record.keywords}' di category '{record.category_id.name}'" ) record.with_context(skip_generate=True).write({ @@ -125,9 +172,10 @@ class Keywords(models.Model): }) _logger.info( - "Product Found: Found %s products for keyword '%s'", + "Product Found: %s products | keyword='%s' | category='%s'", len(product_ids), - record.keywords + record.keywords, + record.category_id.name ) def sync_solr(self): -- cgit v1.2.3 From 2d46030560670a0d016012465447ea37b0242581 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 31 Dec 2025 13:57:38 +0700 Subject: cannot sync solr when no products --- indoteknik_custom/models/keywords.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 53cf4225..d7cfc810 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -132,7 +132,7 @@ class Keywords(models.Model): # BRAND (OPTIONAL) # ====================== if record.brand_id: - brand = record.brand_id.x_name.strip() + brand = record.brand_id.x_name brand_kw_1 = f"%{brand} {keyword_raw}%" brand_kw_2 = f"%{keyword_raw} {brand}%" @@ -183,12 +183,16 @@ class Keywords(models.Model): if not active_ids: _logger.warning("No active_ids found, nothing to sync") return True + elif active_ids and self.sum < 1: + raise UserError("Tidak ada product untuk keyword ini, sync ke solr dibatalkan") keywords = self.browse(active_ids) documents = [] for keyword in keywords: searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') + if keyword.brand_id: + searchkey = f"{searchkey}-{keyword.brand_id.x_name.strip().lower().replace(' ', '-')}" try: doc = { 'id': keyword.id, -- cgit v1.2.3 From 604598292fa99546b34422613a8f6399e1a55c61 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 31 Dec 2025 21:54:08 +0700 Subject: fix check duplicate --- indoteknik_custom/models/keywords.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index d7cfc810..f6c1cf6e 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -66,15 +66,18 @@ class Keywords(models.Model): if not record.name: record.name = record.keywords - # def check_already_exist(self): - # model = self.env['keywords'] - # for record in self: - # match = model.search([ - # ('keywords', '=', record.keywords), - # ('category_id.id', '=', record.category_id.id) - # ]) - # if match: - # raise UserError("Tidak bisa create karena keywords sudah dipakai") + @api.constrains('keywords', 'category_id') + def check_already_exist(self): + for record in self: + if not (record.keywords and record.category_id): + continue + match = self.search([ + ('id', '!=', record.id), + ('keywords', '=', record.keywords), + ('category_id', '=', record.category_id.id) + ], limit = 1) + if match: + raise UserError("Tidak bisa create karena keywords sudah dipakai") def copy(self): raise UserError("Duplicate Record not allowed") -- cgit v1.2.3 From 9039bb8de5ea157692120b8b2dd24338a13a0071 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 31 Dec 2025 22:24:35 +0700 Subject: rev query (need improvement) --- indoteknik_custom/models/keywords.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index f6c1cf6e..b5c7ae8c 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -139,6 +139,8 @@ class Keywords(models.Model): brand_kw_1 = f"%{brand} {keyword_raw}%" brand_kw_2 = f"%{keyword_raw} {brand}%" + keyword = f"%{keyword_raw}%" + sql += "AND pt.x_manufacture = %s" sql += """ AND ( @@ -147,13 +149,22 @@ class Keywords(models.Model): OR pt.website_description ILIKE %s OR pt.website_description ILIKE %s ) + OR ( + pt.x_manufacture = %s + AND (pt.name ilike %s + OR pt.website_description ilike %s) + ) """ params.extend([ + record.brand_id.id, brand_kw_1, brand_kw_2, brand_kw_1, brand_kw_2, + record.brand_id.id, + keyword, + keyword ]) # ====================== -- cgit v1.2.3 From d2bc3e1d20f196628edaf6956777ca332b8d5063 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 12 Jan 2026 15:02:52 +0700 Subject: create menu monitoring gudang service --- indoteknik_custom/__manifest__.py | 3 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/gudang_service.py | 37 +++++++++++++++ indoteknik_custom/security/ir.model.access.csv | 2 + indoteknik_custom/views/gudang_service.xml | 65 ++++++++++++++++++++++++++ indoteknik_custom/views/ir_sequence.xml | 9 ++++ 6 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 indoteknik_custom/models/gudang_service.py create mode 100644 indoteknik_custom/views/gudang_service.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 66962a24..ddf80cb1 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -192,7 +192,8 @@ 'views/close_tempo_mail_template.xml', 'views/domain_apo.xml', 'views/uom_uom.xml', - 'views/commission_internal.xml' + 'views/commission_internal.xml', + 'views/gudang_service.xml' ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a14c766e..e6a59246 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,3 +165,4 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal +from . import gudang_service diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py new file mode 100644 index 00000000..a84f5c8a --- /dev/null +++ b/indoteknik_custom/models/gudang_service.py @@ -0,0 +1,37 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +import logging +from datetime import datetime +from collections import defaultdict + + +class GudangService(models.Model): + _name = "gudang.service" + _description = "Gudang Service" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char('Name', readonly=True) + partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) + origin = fields.Char(string='Origin SO') + picking_id = fields.Many2one('stock.picking', string = 'Picking ID') + # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') + date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') + + @api.model + def create(self, vals): + if not vals.get('name') or vals['name'] == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') + return super(GudangService, self).create(vals) + + +class GudangServiceLine(models.Model): + _name = "gudang.service.line" + _description = "Gudang Service Line" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + product_id = fields.Many2one('product.product', string='Product') + quantity = fields.Float(string='Quantity') + picking_id = fields.Many2one('stock.picking', string = 'Nomor Picking') + # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') + gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index d501de1a..84545488 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -215,3 +215,5 @@ access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1 access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1 access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 access_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1 +access_gudang_service,gudang.service,model_gudang_service,base.group_user,1,1,1,1 +access_gudang_service_line,gudang.service.line,model_gudang_service_line,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml new file mode 100644 index 00000000..19ddcdc6 --- /dev/null +++ b/indoteknik_custom/views/gudang_service.xml @@ -0,0 +1,65 @@ + + + + + + gudang.serivice.tree + gudang.service + + + + + + + + + + + + gudang.service.form + gudang.service + +
    + +
    +

    + +

    +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + Monitoring Gudang Service + ir.actions.act_window + gudang.service + tree,form + + + +
    +
    diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index 46148606..246e3a13 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -260,5 +260,14 @@ 1 1 + + + Gudang Service + gudang.service + MGS/%(year)s/%(month)s/ + 3 + 1 + 1 + \ No newline at end of file -- cgit v1.2.3 From f77738e0c20c01a544dc233c12c1233793b45180 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 14 Jan 2026 17:01:56 +0700 Subject: done temp --- indoteknik_custom/models/gudang_service.py | 93 +++++++++++++++++++++++++++--- indoteknik_custom/views/gudang_service.xml | 35 ++++++++--- indoteknik_custom/views/ir_sequence.xml | 2 +- 3 files changed, 113 insertions(+), 17 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index a84f5c8a..d9b32e91 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -12,26 +12,103 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - origin = fields.Char(string='Origin SO') - picking_id = fields.Many2one('stock.picking', string = 'Picking ID') - # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') + origin = fields.Many2one('sale.order', string='Origin SO') + # picking_id = fields.Many2one('stock.picking', string = 'Picking ID') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') + remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') + state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') + + def _compute_remaining_date(self): + if self.state in ['done', 'cancel', 'draft']: + return + for rec in self: + if rec.date: + rec.remaining_date = (datetime.now() - rec.date).days + + def send_odoo_notification(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.message', + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': self.message_ids[-1].id, + } + + def action_submit(self): + self.state = 'onprogress' + # self.send_odoo_notification() + + def action_done(self): + self.state = 'done' + # self.send_odoo_notification() + + def action_draft(self): + """Reset to draft state""" + for record in self: + if record.state == 'cancel': + record.write({'state': 'draft'}) + else: + raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + def action_cancel(self): + self.state = 'cancel' + + # def write(vals, self): + # self.send_odoo_notification() + # return super(GudangService, self).write(vals) + + @api.depends('date') + def _compute_remaining_date(self): + for rec in self: + if rec.date: + rec.remaining_date = (datetime.now() - rec.date).days @api.model def create(self, vals): if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') + + if vals.get('origin') and not vals.get('partner_id'): + so = self.env['sale.order'].browse(vals['origin']) + vals['partner_id'] = so.partner_id.id + return super(GudangService, self).create(vals) + + def write(self, vals): + if vals.get('origin'): + so = self.env['sale.order'].browse(vals['origin']) + vals['partner_id'] = so.partner_id.id + + return super(GudangService, self).write(vals) + + + + @api.onchange('origin') + def _onchange_origin(self): + if not self.origin: + self.gudang_service_lines = [(5, 0, 0)] + return + + self.partner_id = self.origin.partner_id + + lines = [] + for line in self.origin.order_line: + lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'origin_so': self.origin.id, + })) + + # hapus line lama lalu isi baru + self.gudang_service_lines = [(5, 0, 0)] + lines + class GudangServiceLine(models.Model): _name = "gudang.service.line" - _description = "Gudang Service Line" - _inherit = ['mail.thread', 'mail.activity.mixin'] product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Quantity') - picking_id = fields.Many2one('stock.picking', string = 'Nomor Picking') - # origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') - gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file + # picking_id = fields.Many2one('stock.picking', string='Nomor Picking') + origin_so = fields.Many2one('sale.order', string='Origin SO') + gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 19ddcdc6..ada1291b 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -10,7 +10,8 @@ - + + @@ -20,16 +21,34 @@ gudang.service
    +
    +
    -
    -

    - -

    -
    +
    +

    + +

    +
    - + + + + @@ -37,7 +56,7 @@ - + diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index 246e3a13..55e48300 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -265,7 +265,7 @@ Gudang Service gudang.service MGS/%(year)s/%(month)s/ - 3 + 4 1 1 -- cgit v1.2.3 From e1c687df876eaa970cb40393cb4443abcfbd0b77 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 14 Jan 2026 18:55:22 +0700 Subject: add cron notification --- indoteknik_custom/models/gudang_service.py | 50 ++++++++++++++++++++++-------- indoteknik_custom/views/gudang_service.xml | 11 +++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index d9b32e91..6944f169 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -19,22 +19,45 @@ class GudangService(models.Model): remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') - def _compute_remaining_date(self): - if self.state in ['done', 'cancel', 'draft']: + def _send_logistic_notification(self): + logistic_user = self.env.user.has_group('indoteknik_custom.group_role_logistic') + + if not logistic_user: return + for rec in self: - if rec.date: - rec.remaining_date = (datetime.now() - rec.date).days + for user in logistic_user: + self.env['mail.activity'].create({ + 'res_model_id': self.env['ir.model']._get_id('gudang.service'), + 'res_id': rec.id, + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'user_id': user.id, + 'summary': 'Gudang Service On Progress', + 'note': _( + 'Gudang Service %s masih On Progress sejak %s' + ) % (rec.name, rec.date), + 'date_deadline': fields.Date.today(), + }) + + @api.model + def cron_notify_onprogress_gudang_service(self): + records = self.search([ + ('state', '=', 'onprogress') + ]) + + if records: + records._send_logistic_notification() + + + @api.depends('date', 'state') + def _compute_remaining_date(self): + today = fields.Date.today() + for rec in self: + if rec.state in ['done', 'cancel', 'draft'] or not rec.date: + rec.remaining_date = 0 + continue + rec.remaining_date = (today - rec.date.date()).days - def send_odoo_notification(self): - return { - 'type': 'ir.actions.act_window', - 'res_model': 'mail.message', - 'view_mode': 'form', - 'view_type': 'form', - 'res_id': self.message_ids[-1].id, - } - def action_submit(self): self.state = 'onprogress' # self.send_odoo_notification() @@ -50,6 +73,7 @@ class GudangService(models.Model): record.write({'state': 'draft'}) else: raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + def action_cancel(self): self.state = 'cancel' diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index ada1291b..97bb0227 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -81,4 +81,15 @@ action="action_gudang_service" /> + + + Gudang Service Daily Notification + + code + model.cron_notify_onprogress_gudang_service() + 1 + days + -1 + False + -- cgit v1.2.3 From 4e1e2f93b0788c020bd3f1c1f802cf2f53997de5 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 14 Jan 2026 19:19:00 +0700 Subject: fix cron notif & add option to get product from picking --- indoteknik_custom/models/gudang_service.py | 36 +++++++++++++++++++++--------- indoteknik_custom/views/gudang_service.xml | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 6944f169..600febfd 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -13,20 +13,41 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO') - # picking_id = fields.Many2one('stock.picking', string = 'Picking ID') + picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') + @api.onchange('picking_id') + def _onchange_picking_id(self): + if not self.picking_id: + self.gudang_service_lines = [(5, 0, 0)] + return + + lines = [(5, 0, 0)] + for move in self.picking_id.move_ids_without_package: + if move.product_id: + lines.append((0, 0, { + 'product_id': move.product_id.id, + 'quantity': move.product_uom_qty, + 'origin_so': self.origin.id, + })) + + self.gudang_service_lines = lines + + def _send_logistic_notification(self): - logistic_user = self.env.user.has_group('indoteknik_custom.group_role_logistic') + group = self.env.ref('indoteknik_custom.group_role_logistic', raise_if_not_found=False) + if not group: + return - if not logistic_user: + users = group.users + if not users: return for rec in self: - for user in logistic_user: + for user in users: self.env['mail.activity'].create({ 'res_model_id': self.env['ir.model']._get_id('gudang.service'), 'res_id': rec.id, @@ -60,6 +81,7 @@ class GudangService(models.Model): def action_submit(self): self.state = 'onprogress' + self._send_logistic_notification # self.send_odoo_notification() def action_done(self): @@ -81,12 +103,6 @@ class GudangService(models.Model): # self.send_odoo_notification() # return super(GudangService, self).write(vals) - @api.depends('date') - def _compute_remaining_date(self): - for rec in self: - if rec.date: - rec.remaining_date = (datetime.now() - rec.date).days - @api.model def create(self, vals): if not vals.get('name') or vals['name'] == 'New': diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 97bb0227..8c681451 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -45,7 +45,7 @@ - + -- cgit v1.2.3 From 2e1e5b798cc5b859a0c10cbdb4e48cbbe0687112 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 00:12:17 +0700 Subject: add cancel reason when trying to cancel and some decoration --- indoteknik_custom/models/gudang_service.py | 39 +++++++++++++++++------------- indoteknik_custom/views/gudang_service.xml | 8 ++++-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 600febfd..585563ca 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -12,29 +12,30 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - origin = fields.Many2one('sale.order', string='Origin SO') - picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") + origin = fields.Many2one('sale.order', string='Origin SO', required=True) + # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') + cancel_reason = fields.Text('Cancel Reason') - @api.onchange('picking_id') - def _onchange_picking_id(self): - if not self.picking_id: - self.gudang_service_lines = [(5, 0, 0)] - return + # @api.onchange('picking_id') + # def _onchange_picking_id(self): + # if not self.picking_id: + # self.gudang_service_lines = [(5, 0, 0)] + # return - lines = [(5, 0, 0)] - for move in self.picking_id.move_ids_without_package: - if move.product_id: - lines.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': move.product_uom_qty, - 'origin_so': self.origin.id, - })) + # lines = [(5, 0, 0)] + # for move in self.picking_id.move_ids_without_package: + # if move.product_id: + # lines.append((0, 0, { + # 'product_id': move.product_id.id, + # 'quantity': move.product_uom_qty, + # 'origin_so': self.origin.id, + # })) - self.gudang_service_lines = lines + # self.gudang_service_lines = lines def _send_logistic_notification(self): @@ -94,9 +95,13 @@ class GudangService(models.Model): if record.state == 'cancel': record.write({'state': 'draft'}) else: - raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") + raise UserError("Only Canceled Record Can Be Reset To Draft") def action_cancel(self): + if self.state == 'done': + raise UserError("You cannot cancel a done record") + if not self.cancel_reason: + raise UserError("Cancel Reason must be filled") self.state = 'cancel' # def write(vals, self): diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 8c681451..4365ba5d 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -6,11 +6,13 @@ gudang.serivice.tree gudang.service - + + @@ -45,10 +47,12 @@ - + + -- cgit v1.2.3 From b19788761f065be6593698d07f1e4743e151a8c6 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 00:46:47 +0700 Subject: add done date & code improvement --- indoteknik_custom/models/gudang_service.py | 44 ++++++++++++++++++++---------- indoteknik_custom/views/gudang_service.xml | 14 ++++++++-- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 585563ca..2fe239ad 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -9,12 +9,14 @@ class GudangService(models.Model): _name = "gudang.service" _description = "Gudang Service" _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'remaining_date desc, id asc' name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) + done_date = fields.Datetime(string='Date Done', copy=False) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') @@ -71,38 +73,52 @@ class GudangService(models.Model): records._send_logistic_notification() - @api.depends('date', 'state') + @api.depends('date', 'state', 'done_date') def _compute_remaining_date(self): today = fields.Date.today() for rec in self: - if rec.state in ['done', 'cancel', 'draft'] or not rec.date: + if not rec.date: rec.remaining_date = 0 continue + + if rec.state in ['draft', 'cancel']: + rec.remaining_date = 0 + continue + + if rec.state == 'done' and rec.done_date: + rec.remaining_date = (rec.done_date.date() - rec.date.date()).days + continue + rec.remaining_date = (today - rec.date.date()).days + def action_submit(self): - self.state = 'onprogress' + for rec in self: + rec.state = 'onprogress' + rec.date = fields.Datetime.now() self._send_logistic_notification - # self.send_odoo_notification() def action_done(self): - self.state = 'done' - # self.send_odoo_notification() + for rec in self: + rec.state = 'done' + if not rec.done_date: + rec.done_date = fields.Datetime.now() def action_draft(self): """Reset to draft state""" - for record in self: - if record.state == 'cancel': - record.write({'state': 'draft'}) + for rec in self: + if rec.state == 'cancel': + rec.write({'state': 'draft'}) else: raise UserError("Only Canceled Record Can Be Reset To Draft") def action_cancel(self): - if self.state == 'done': - raise UserError("You cannot cancel a done record") - if not self.cancel_reason: - raise UserError("Cancel Reason must be filled") - self.state = 'cancel' + for rec in self: + if rec.state == 'done': + raise UserError("You cannot cancel a done record") + if not rec.cancel_reason: + raise UserError("Cancel Reason must be filled") + rec.state = 'cancel' # def write(vals, self): # self.send_odoo_notification() diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 4365ba5d..a4150452 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -6,10 +6,19 @@ gudang.serivice.tree gudang.service - + + + @@ -50,6 +59,7 @@ + @@ -59,7 +69,7 @@ - + -- cgit v1.2.3 From 4e372aad3010db13453b9e332c6f058fc8375a99 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 00:52:34 +0700 Subject: add tracking --- indoteknik_custom/models/gudang_service.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 2fe239ad..06d08917 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -9,18 +9,18 @@ class GudangService(models.Model): _name = "gudang.service" _description = "Gudang Service" _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'remaining_date desc, id asc' + _order = 'id asc' name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") - date = fields.Datetime('Date', default=fields.Datetime.now, required=True) - done_date = fields.Datetime(string='Date Done', copy=False) + date = fields.Datetime('Date', default=fields.Datetime.now, tracking=True, copy=False) + done_date = fields.Datetime(string='Date Done', copy=False, tracking=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') - state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft') - cancel_reason = fields.Text('Cancel Reason') + state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) + cancel_reason = fields.Text('Cancel Reason', tracking=True) # @api.onchange('picking_id') # def _onchange_picking_id(self): -- cgit v1.2.3 From 3b400df6d9f630c4f20e518126afccf280ff62ac Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 09:36:37 +0700 Subject: fix mail activity nyangkut --- indoteknik_custom/models/gudang_service.py | 14 +++++++++++--- indoteknik_custom/views/gudang_service.xml | 10 +++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 06d08917..2a8b589d 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -86,7 +86,8 @@ class GudangService(models.Model): continue if rec.state == 'done' and rec.done_date: - rec.remaining_date = (rec.done_date.date() - rec.date.date()).days + days = (rec.done_date.date() - rec.date.date()).days + rec.remaining_date = "Since %s days" % days continue rec.remaining_date = (today - rec.date.date()).days @@ -96,10 +97,16 @@ class GudangService(models.Model): for rec in self: rec.state = 'onprogress' rec.date = fields.Datetime.now() - self._send_logistic_notification + self._send_logistic_notification() def action_done(self): for rec in self: + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'gudang.service'), + ('state', '=', 'done') + ]) + activities.unlink() rec.state = 'done' if not rec.done_date: rec.done_date = fields.Datetime.now() @@ -167,9 +174,10 @@ class GudangService(models.Model): class GudangServiceLine(models.Model): _name = "gudang.service.line" + _inherit = ['mail.thread', 'mail.activity.mixin'] product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Quantity') # picking_id = fields.Many2one('stock.picking', string='Nomor Picking') origin_so = fields.Many2one('sale.order', string='Origin SO') - gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') + gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index a4150452..7fc7109b 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -12,7 +12,7 @@ decoration-bf="remaining_date > 7" > --> @@ -20,7 +20,7 @@ - @@ -76,6 +76,10 @@
    +
    + + +
    @@ -90,7 +94,7 @@ -- cgit v1.2.3 From 6a7c7c1ad694ba12f23b9a4cc5c5deca6ff52bd8 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 15 Jan 2026 13:21:44 +0700 Subject: push --- indoteknik_custom/models/stock_picking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 2465fa96..c8f7bb5b 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1685,7 +1685,7 @@ class StockPicking(models.Model): ]) for line in po.order_sales_match_line: - if not line.bu_pick: + if not line.bu_pick and line.hold_outgoing_so: continue line.bu_pick.action_assign() -- cgit v1.2.3 From b2cbd45338fd26fb285f68e4c609284395ba9897 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 16 Jan 2026 15:38:15 +0700 Subject: remove from mail activity when docs canceled --- indoteknik_custom/models/gudang_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 2a8b589d..c65e599d 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -121,6 +121,11 @@ class GudangService(models.Model): def action_cancel(self): for rec in self: + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'gudang.service'), + ]) + activities.unlink() if rec.state == 'done': raise UserError("You cannot cancel a done record") if not rec.cancel_reason: -- cgit v1.2.3 From 8de3619bc698551e7d23b463d4bcf55f9ad8b62c Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 09:45:14 +0700 Subject: send private message --- indoteknik_custom/models/gudang_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 2a8b589d..5aeabb39 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -62,6 +62,11 @@ class GudangService(models.Model): ) % (rec.name, rec.date), 'date_deadline': fields.Date.today(), }) + channel = self.env['mail.channel'].channel_get([self.env.user.partner_id.id, user.partner_id.id]).get('id') + if not channel: + continue + res = self.env['mail.channel'].browse(channel) + res.message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') @api.model def cron_notify_onprogress_gudang_service(self): -- cgit v1.2.3 From b9b6d10b518bd9bd80c85216de7ef33cf194f9ba Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 10:37:24 +0700 Subject: add send with user --- indoteknik_custom/models/gudang_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 5aeabb39..8edf57d0 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -62,11 +62,13 @@ class GudangService(models.Model): ) % (rec.name, rec.date), 'date_deadline': fields.Date.today(), }) + + # kirim ke private message odoo channel = self.env['mail.channel'].channel_get([self.env.user.partner_id.id, user.partner_id.id]).get('id') if not channel: continue res = self.env['mail.channel'].browse(channel) - res.message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') + res.with_user(self.env.user.browse(25)).message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') @api.model def cron_notify_onprogress_gudang_service(self): -- cgit v1.2.3 From 9633f55a90d8e70d754e4611c9bfc84d7643f89a Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 11:10:25 +0700 Subject: add MD and exclude some users --- indoteknik_custom/models/gudang_service.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 1aadea70..93f8805a 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -46,11 +46,20 @@ class GudangService(models.Model): return users = group.users + # Safa + md = self.env['res.users'].browse([3425]) + # send to logistic and safa + users = users | md + if not users: return + excluded_users = [7, 17098, 216, 28, 15710] + for rec in self: for user in users: + if user.id in excluded_users: + continue self.env['mail.activity'].create({ 'res_model_id': self.env['ir.model']._get_id('gudang.service'), 'res_id': rec.id, -- cgit v1.2.3 From b40989dc9dc36056f4e416aa5d2e040be595fd61 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 15:22:22 +0700 Subject: done --- indoteknik_custom/models/gudang_service.py | 9 ++++----- indoteknik_custom/views/gudang_service.xml | 15 +++++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 93f8805a..0cdee5af 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -19,7 +19,7 @@ class GudangService(models.Model): done_date = fields.Datetime(string='Date Done', copy=False, tracking=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') - state = fields.Selection([('draft', 'Draft'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) + state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) # @api.onchange('picking_id') @@ -113,7 +113,6 @@ class GudangService(models.Model): for rec in self: rec.state = 'onprogress' rec.date = fields.Datetime.now() - self._send_logistic_notification() def action_done(self): for rec in self: @@ -130,6 +129,7 @@ class GudangService(models.Model): def action_draft(self): """Reset to draft state""" for rec in self: + rec.cancel_reason = False if rec.state == 'cancel': rec.write({'state': 'draft'}) else: @@ -148,12 +148,11 @@ class GudangService(models.Model): raise UserError("Cancel Reason must be filled") rec.state = 'cancel' - # def write(vals, self): - # self.send_odoo_notification() - # return super(GudangService, self).write(vals) @api.model def create(self, vals): + # Send notification + self._send_logistic_notification() if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 7fc7109b..9818825b 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -7,12 +7,12 @@ gudang.service - @@ -20,8 +20,9 @@ - + @@ -85,15 +86,17 @@ - Monitoring Gudang Service + Monitoring Barang Service ir.actions.act_window gudang.service tree,form + {'group_by': ['state']} + Date: Mon, 19 Jan 2026 22:25:00 +0700 Subject: revisi date --- indoteknik_custom/models/gudang_service.py | 55 +++++++++++++++++++----------- indoteknik_custom/views/gudang_service.xml | 31 +++++++++-------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 0cdee5af..e401a65c 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -15,7 +15,18 @@ class GudangService(models.Model): partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") - date = fields.Datetime('Date', default=fields.Datetime.now, tracking=True, copy=False) + schedule_date = fields.Date( + string="Schedule Date", + required=True, + tracking=True + ) + + start_date = fields.Datetime( + string="Date Processed", + copy=False, + tracking=True + ) + done_date = fields.Datetime(string='Date Done', copy=False, tracking=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') @@ -67,9 +78,9 @@ class GudangService(models.Model): 'user_id': user.id, 'summary': 'Gudang Service On Progress', 'note': _( - 'Gudang Service %s masih On Progress sejak %s' - ) % (rec.name, rec.date), - 'date_deadline': fields.Date.today(), + 'Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s' + ) % (rec.name, rec.schedule_date), + # 'date_deadline': fields.Date.today(), }) # kirim ke private message odoo @@ -77,7 +88,7 @@ class GudangService(models.Model): if not channel: continue res = self.env['mail.channel'].browse(channel) - res.with_user(self.env.user.browse(25)).message_post(body=_('Gudang Service %s masih On Progress sejak %s') % (rec.name, rec.date), message_type='comment', subtype_xmlid='mail.mt_comment') + res.with_user(self.env.user.browse(25)).message_post(body=_('Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s') % (rec.name, rec.schedule_date), message_type='comment', subtype_xmlid='mail.mt_comment') @api.model def cron_notify_onprogress_gudang_service(self): @@ -89,30 +100,32 @@ class GudangService(models.Model): records._send_logistic_notification() - @api.depends('date', 'state', 'done_date') + @api.depends('start_date', 'done_date', 'state') def _compute_remaining_date(self): today = fields.Date.today() + for rec in self: - if not rec.date: - rec.remaining_date = 0 + if not rec.start_date: + rec.remaining_date = "-" continue - if rec.state in ['draft', 'cancel']: - rec.remaining_date = 0 - continue + start = rec.start_date.date() if rec.state == 'done' and rec.done_date: - days = (rec.done_date.date() - rec.date.date()).days - rec.remaining_date = "Since %s days" % days - continue + end = rec.done_date.date() + else: + end = today + + days = (end - start).days + rec.remaining_date = _("Since %s days") % days - rec.remaining_date = (today - rec.date.date()).days def action_submit(self): for rec in self: rec.state = 'onprogress' - rec.date = fields.Datetime.now() + rec.start_date = fields.Datetime.now() + # rec.date = fields.Datetime.now() def action_done(self): for rec in self: @@ -146,13 +159,14 @@ class GudangService(models.Model): raise UserError("You cannot cancel a done record") if not rec.cancel_reason: raise UserError("Cancel Reason must be filled") + rec.start_date = False + rec.done_date = False rec.state = 'cancel' @api.model def create(self, vals): # Send notification - self._send_logistic_notification() if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') @@ -160,7 +174,10 @@ class GudangService(models.Model): so = self.env['sale.order'].browse(vals['origin']) vals['partner_id'] = so.partner_id.id - return super(GudangService, self).create(vals) + res = super(GudangService, self).create(vals) + # Send notification + res._send_logistic_notification() + return res def write(self, vals): if vals.get('origin'): @@ -169,8 +186,6 @@ class GudangService(models.Model): return super(GudangService, self).write(vals) - - @api.onchange('origin') def _onchange_origin(self): if not self.origin: diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 9818825b..42674585 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -18,7 +18,8 @@ - + + @@ -34,7 +35,7 @@
    -

    - + - - + + ir.actions.act_window gudang.service tree,form - {'group_by': ['state']} + - - + + -- cgit v1.2.3 From 7e52c025439a8614a1ec6ae77dfdda934989a09b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 19 Jan 2026 22:26:55 +0700 Subject: change string --- indoteknik_custom/models/gudang_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index e401a65c..d4f7397c 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -29,7 +29,7 @@ class GudangService(models.Model): done_date = fields.Datetime(string='Date Done', copy=False, tracking=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') - remaining_date = fields.Char('Remaining Date', compute='_compute_remaining_date') + remaining_date = fields.Char('Unprocessed Since', compute='_compute_remaining_date') state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -117,7 +117,7 @@ class GudangService(models.Model): end = today days = (end - start).days - rec.remaining_date = _("Since %s days") % days + rec.remaining_date = _("%s days") % days -- cgit v1.2.3 From 2e708e7a98aca710b006fbc249366e7cea151b78 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 20 Jan 2026 08:52:18 +0700 Subject: cleanup code --- indoteknik_custom/models/gudang_service.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index d4f7397c..73dcf0a4 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -14,7 +14,6 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) - # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") schedule_date = fields.Date( string="Schedule Date", required=True, @@ -33,24 +32,6 @@ class GudangService(models.Model): state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) - # @api.onchange('picking_id') - # def _onchange_picking_id(self): - # if not self.picking_id: - # self.gudang_service_lines = [(5, 0, 0)] - # return - - # lines = [(5, 0, 0)] - # for move in self.picking_id.move_ids_without_package: - # if move.product_id: - # lines.append((0, 0, { - # 'product_id': move.product_id.id, - # 'quantity': move.product_uom_qty, - # 'origin_so': self.origin.id, - # })) - - # self.gudang_service_lines = lines - - def _send_logistic_notification(self): group = self.env.ref('indoteknik_custom.group_role_logistic', raise_if_not_found=False) if not group: @@ -99,7 +80,6 @@ class GudangService(models.Model): if records: records._send_logistic_notification() - @api.depends('start_date', 'done_date', 'state') def _compute_remaining_date(self): today = fields.Date.today() @@ -119,8 +99,6 @@ class GudangService(models.Model): days = (end - start).days rec.remaining_date = _("%s days") % days - - def action_submit(self): for rec in self: rec.state = 'onprogress' @@ -163,7 +141,6 @@ class GudangService(models.Model): rec.done_date = False rec.state = 'cancel' - @api.model def create(self, vals): # Send notification @@ -206,13 +183,11 @@ class GudangService(models.Model): self.gudang_service_lines = [(5, 0, 0)] + lines - class GudangServiceLine(models.Model): _name = "gudang.service.line" _inherit = ['mail.thread', 'mail.activity.mixin'] product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Quantity') - # picking_id = fields.Many2one('stock.picking', string='Nomor Picking') origin_so = fields.Many2one('sale.order', string='Origin SO') gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file -- cgit v1.2.3 From 144c65b3bf3e0971c2f99413f321e65f0c2cbac1 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 20 Jan 2026 08:57:11 +0700 Subject: send message when backlog/draft --- indoteknik_custom/models/gudang_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 73dcf0a4..dc13a139 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -74,7 +74,7 @@ class GudangService(models.Model): @api.model def cron_notify_onprogress_gudang_service(self): records = self.search([ - ('state', '=', 'onprogress') + ('state', '=', 'draft') ]) if records: -- cgit v1.2.3 From ecd50c41bc9db2697865dfe1c55bb32b7c9410c0 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 22 Jan 2026 17:04:08 +0700 Subject: add vendor --- indoteknik_custom/models/gudang_service.py | 53 +++++++++++++++++++++++------- indoteknik_custom/views/gudang_service.xml | 6 +++- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index d4f7397c..72a0f00e 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -13,6 +13,7 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) + vendor_id = fields.Many2one('res.partner', string='Vendor', readonly=True, required=True) origin = fields.Many2one('sale.order', string='Origin SO', required=True) # picking_id = fields.Many2one('stock.picking', string = 'Picking ID', domain="[('sale_id', '=', origin)]") schedule_date = fields.Date( @@ -26,10 +27,17 @@ class GudangService(models.Model): copy=False, tracking=True ) - + create_date = fields.Datetime(string='Create Date', copy=False, tracking=True, default=fields.Datetime.now()) done_date = fields.Datetime(string='Date Done', copy=False, tracking=True) gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') - remaining_date = fields.Char('Unprocessed Since', compute='_compute_remaining_date') + # unprocessed_date = fields.Char( + # string='Unprocessed Since', + # compute='_compute_unprocessed_date' + # ) + remaining_date = fields.Char( + compute='_compute_remaining_date', + string='Remaining Date' + ) state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -99,26 +107,47 @@ class GudangService(models.Model): if records: records._send_logistic_notification() + # @api.depends('schedule_date', 'start_date', 'state') + # def _compute_unprocessed_date(self): + # today = fields.Date.today() + + # for rec in self: + # if not rec.schedule_date or rec.state == 'cancel': + # rec.unprocessed_date = "-" + # continue + + # schedule = rec.schedule_date + + # # BELUM DIPROSES + # if not rec.start_date: + # days = (today - schedule).days - @api.depends('start_date', 'done_date', 'state') + # # SUDAH DIPROSES + # else: + # days = (rec.start_date.date() - schedule).days + + # rec.unprocessed_date = _("Unprocessed %s days") % max(days, 0) + + @api.depends('schedule_date', 'create_date') def _compute_remaining_date(self): today = fields.Date.today() for rec in self: - if not rec.start_date: + if not rec.schedule_date: rec.remaining_date = "-" continue - start = rec.start_date.date() + base_date = rec.create_date.date() if rec.create_date else today - if rec.state == 'done' and rec.done_date: - end = rec.done_date.date() - else: - end = today - - days = (end - start).days - rec.remaining_date = _("%s days") % days + schedule = rec.schedule_date + days = (schedule - base_date).days + if days > 0: + rec.remaining_date = _("In %s days") % days + elif days == 0: + rec.remaining_date = _("Today") + else: + rec.remaining_date = _("Overdue %s days") % abs(days) def action_submit(self): diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 42674585..e06e9e7e 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -17,13 +17,16 @@ > + + +
    @@ -58,7 +61,9 @@ + + @@ -91,7 +96,6 @@ ir.actions.act_window gudang.service tree,form - -- cgit v1.2.3 From 9496bae642b86438f0dcef1595255e2f01384040 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 23 Jan 2026 09:40:35 +0700 Subject: fix --- indoteknik_custom/models/purchase_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 35fa79a8..50418fc9 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1086,12 +1086,12 @@ class PurchaseOrder(models.Model): if self.order_sales_match_line: if self.total_percent_margin <= 15.0: raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") - else: - low_margin_match_so = self.order_sales_match_line.filtered( - lambda match: match.so_header_margin <= 15.0 - ) - if low_margin_match_so: - raise UserError("Approval Pimpinan diperlukan jika pada PO Line yang Matches SO item memiliki header margin SO <= 15%") + # else: + # low_margin_match_so = self.order_sales_match_line.filtered( + # lambda match: match.so_header_margin <= 15.0 + # ) + # if low_margin_match_so: + # raise UserError("Approval Pimpinan diperlukan jika pada PO Line yang Matches SO item memiliki header margin SO <= 15%") # else: # is_po_manual = '/A/' not in self.name and '/MO/' not in self.name # if is_po_manual: -- cgit v1.2.3 From e700b4017f5f33564386dec52f3633b84a7e35a8 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 23 Jan 2026 09:43:28 +0700 Subject: fix --- indoteknik_custom/models/purchase_order.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 50418fc9..2154fb8c 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1079,14 +1079,14 @@ class PurchaseOrder(models.Model): ) % order.name) def button_confirm(self): - if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan - if '/PJ/' in self.name: - price_change_detected = any(line.price_unit_before for line in self.order_line) - if price_change_detected: - if self.order_sales_match_line: - if self.total_percent_margin <= 15.0: - raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") - # else: + # if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan + # if '/PJ/' in self.name: + # price_change_detected = any(line.price_unit_before for line in self.order_line) + # if price_change_detected: + # if self.order_sales_match_line: + # if self.total_percent_margin <= 15.0: + # raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") + # # else: # low_margin_match_so = self.order_sales_match_line.filtered( # lambda match: match.so_header_margin <= 15.0 # ) -- cgit v1.2.3 From f2fbd1496a5ef1f86df794f69c8d0430f7caed63 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 23 Jan 2026 09:57:46 +0700 Subject: fix --- indoteknik_custom/models/purchase_order.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 2154fb8c..e16c8d61 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1079,24 +1079,13 @@ class PurchaseOrder(models.Model): ) % order.name) def button_confirm(self): - # if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan - # if '/PJ/' in self.name: - # price_change_detected = any(line.price_unit_before for line in self.order_line) - # if price_change_detected: - # if self.order_sales_match_line: - # if self.total_percent_margin <= 15.0: - # raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") - # # else: - # low_margin_match_so = self.order_sales_match_line.filtered( - # lambda match: match.so_header_margin <= 15.0 - # ) - # if low_margin_match_so: - # raise UserError("Approval Pimpinan diperlukan jika pada PO Line yang Matches SO item memiliki header margin SO <= 15%") - # else: - # is_po_manual = '/A/' not in self.name and '/MO/' not in self.name - # if is_po_manual: - # if not self.order_sales_match_line: - # raise UserError("Tidak ada matches SO, Approval Pimpinan diperlukan.") + if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan + if '/PJ/' in self.name: + price_change_detected = any(line.price_unit_before for line in self.order_line) + if price_change_detected: + if self.order_sales_match_line: + if self.total_percent_margin <= 15.0: + raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") self._check_assets_note() # self._check_payment_term() # check payment term -- cgit v1.2.3 From 7be11b56799d45e044f5af5927f63180c419e36e Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 23 Jan 2026 11:07:11 +0700 Subject: fix hitung qty purchase --- indoteknik_custom/models/automatic_purchase.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 4b0ce325..0b2f7d1b 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -499,7 +499,8 @@ class AutomaticPurchase(models.Model): # _logger.info('test %s' % point.product_id.name) if point.product_id.qty_available_bandengan > point.product_min_qty: continue - qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_available_bandengan + # qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_available_bandengan + qty_purchase = point.product_max_qty - point.product_id.qty_available_bandengan po_line = self.env['purchase.order.line'].search([('product_id', '=', point.product_id.id), ('order_id.state', '=', 'done')], order='id desc', limit=1) if self.vendor_id: -- cgit v1.2.3 From abe9e16dff1d7b65530b258a306a6376b71c655b Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Fri, 23 Jan 2026 13:39:30 +0700 Subject: cashback brand --- indoteknik_custom/models/sale_order_line.py | 17 +++++++++++++++++ indoteknik_custom/models/x_manufactures.py | 2 +- indoteknik_custom/views/sale_order.xml | 1 + indoteknik_custom/views/x_manufactures.xml | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 1df1a058..7b97bd85 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -17,6 +17,7 @@ class SaleOrderLine(models.Model): help="Total % Margin in Sales Order Header") item_percent_margin_before = fields.Float('%Margin Before', compute='_compute_item_percent_margin_before', help="Total % Margin excluding third party in Sales Order Header") + amount_cashback = fields.Float('Cashback Brand', compute='_compute_cashback_brand', help='Cashback from product who has cashback percent in manufacture') initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( 'res.partner', string='Vendor', readonly=True, @@ -212,6 +213,8 @@ class SaleOrderLine(models.Model): sales_price -= line.delivery_amt_line # if line.order_id.fee_third_party > 0: # sales_price -= line.fee_third_party_line + if line.amount_cashback > 0: + sales_price += line.amount_cashback purchase_price = line.purchase_price if line.purchase_tax_id.price_include: @@ -247,6 +250,20 @@ class SaleOrderLine(models.Model): margin_per_item = sales_price - purchase_price line.item_before_margin = margin_per_item + def _compute_cashback_brand(self): + for line in self: + line.amount_cashback = 0 + + if not line.product_id: + continue + + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 + if cashback_percent <= 0: + continue + + price, taxes, vendor_id = self._get_purchase_price(line.product_id) + line.amount_cashback = price * cashback_percent + # @api.onchange('vendor_id') # def onchange_vendor_id(self): # # TODO : need to change this logic @stephan diff --git a/indoteknik_custom/models/x_manufactures.py b/indoteknik_custom/models/x_manufactures.py index 9e214d92..0c3bfa3b 100755 --- a/indoteknik_custom/models/x_manufactures.py +++ b/indoteknik_custom/models/x_manufactures.py @@ -50,7 +50,7 @@ class XManufactures(models.Model): # user_id = fields.Many2one('res.users', string='Responsible', domain="['|'('id', '=', 19), ('id', '=', 6)]", help="Siapa yang bertanggung jawab") user_id = fields.Many2one('res.users', string='Responsible', help="Siapa yang bertanggung jawab") override_vendor_id = fields.Many2one('res.partner', string='Override Vendor') - # cashback_percent = fields.Float(string='Cashback Percent') + cashback_percent = fields.Float(string='Cashback Percent', default=0) def _compute_vendor_ids(self): for manufacture in self: diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 23fbe155..c3df92ec 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -302,6 +302,7 @@ ] } "/> + - + -- cgit v1.2.3 From e52ef1be01707cb049dae84d061e49b316d6c17c Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 26 Jan 2026 08:45:40 +0700 Subject: hide note --- indoteknik_custom/views/purchasing_job.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml index d303e5ea..8e1eb3b6 100644 --- a/indoteknik_custom/views/purchasing_job.xml +++ b/indoteknik_custom/views/purchasing_job.xml @@ -16,7 +16,7 @@ - + @@ -59,7 +59,7 @@ - +
    -- cgit v1.2.3 From d74fa661fad2d83483d23e935836165359c0a1d2 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Tue, 27 Jan 2026 09:47:05 +0700 Subject: cashback brand done --- indoteknik_custom/models/sale_order_line.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 7b97bd85..c9e75fc7 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -213,12 +213,13 @@ class SaleOrderLine(models.Model): sales_price -= line.delivery_amt_line # if line.order_id.fee_third_party > 0: # sales_price -= line.fee_third_party_line - if line.amount_cashback > 0: - sales_price += line.amount_cashback purchase_price = line.purchase_price if line.purchase_tax_id.price_include: - purchase_price = line.purchase_price / 1.11 + purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) + + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price @@ -252,7 +253,7 @@ class SaleOrderLine(models.Model): def _compute_cashback_brand(self): for line in self: - line.amount_cashback = 0 + line.amount_cashback = 0 if not line.product_id: continue @@ -261,8 +262,18 @@ class SaleOrderLine(models.Model): if cashback_percent <= 0: continue - price, taxes, vendor_id = self._get_purchase_price(line.product_id) - line.amount_cashback = price * cashback_percent + price, taxes, vendor = self._get_purchase_price(line.product_id) + + price_tax_excl = price + + if taxes: + tax = self.env['account.tax'].browse(taxes) + if tax.price_include: + price_tax_excl = price / (1 + (tax.amount / 100)) + else: + price_tax_excl = price + + line.amount_cashback = price_tax_excl * cashback_percent # @api.onchange('vendor_id') # def onchange_vendor_id(self): -- cgit v1.2.3 From 51c5332d8c7d8bf64297d78e4a870568a0631d9c Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 27 Jan 2026 13:31:05 +0700 Subject: fix address print po --- indoteknik_custom/report/purchase_report.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/report/purchase_report.xml b/indoteknik_custom/report/purchase_report.xml index a6804ca4..81129dde 100644 --- a/indoteknik_custom/report/purchase_report.xml +++ b/indoteknik_custom/report/purchase_report.xml @@ -67,7 +67,7 @@ ADM. JAKARTA UTARA, DKI JAKARTA - Jl. Bandengan Utara Komp A 8 B RT.
    + Jl. Bandengan Utara Komp A & B RT.
    Penjaringan, Kec. Penjaringan, Jakarta
    (BELAKANG INDOMARET)
    -- cgit v1.2.3 From 4560970a9bdfd9fab483019f24d78cfe78330e32 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Fri, 30 Jan 2026 14:20:18 +0700 Subject: fix cashback --- indoteknik_custom/models/sale_order_line.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index c9e75fc7..d36c03e5 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -165,7 +165,10 @@ class SaleOrderLine(models.Model): purchase_price = line.purchase_price if line.purchase_tax_id.price_include: - purchase_price = line.purchase_price / 1.11 + purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) + + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price @@ -187,7 +190,7 @@ class SaleOrderLine(models.Model): purchase_price = line.purchase_price if line.purchase_tax_id and line.purchase_tax_id.price_include: - purchase_price = line.purchase_price / 1.11 + purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) purchase_price = purchase_price * line.product_uom_qty @@ -245,7 +248,7 @@ class SaleOrderLine(models.Model): purchase_price = line.purchase_price if line.purchase_tax_id.price_include: - purchase_price = line.purchase_price / 1.11 + purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price -- cgit v1.2.3 From 4ac8b06616a0dce80029e1063078b31b6100084e Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Fri, 30 Jan 2026 15:52:21 +0700 Subject: refund kebutuhan BA dan cahsback --- indoteknik_custom/models/refund_sale_order.py | 53 ++++++++++++++++++++------- indoteknik_custom/models/sale_order_line.py | 42 ++++++++++----------- indoteknik_custom/views/sale_order.xml | 2 +- indoteknik_custom/views/x_manufactures.xml | 2 +- 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 1c482619..c3b7a356 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -62,7 +62,8 @@ class RefundSaleOrder(models.Model): ('uang', 'Refund Lebih Bayar'), ('retur_half', 'Refund Retur Sebagian'), ('retur', 'Refund Retur Full'), - ('salah_transfer', 'Salah Transfer') + ('salah_transfer', 'Salah Transfer'), + ('berita_acara', 'Kebutuhan Berita Acara') ], string='Refund Type', required=True) tukar_guling_ids = fields.One2many( @@ -251,7 +252,7 @@ class RefundSaleOrder(models.Model): invoice_ids_data = vals.get('invoice_ids', []) invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else [] invoices = self.env['account.move'].browse(invoice_ids) - if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: + if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'berita_acara']: raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice") if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']: @@ -434,13 +435,17 @@ class RefundSaleOrder(models.Model): total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 vals['total_invoice'] = total_invoice amount_refund = vals.get('amount_refund', 0.0) - can_refund = sisa_uang_masuk - total_invoice - - if amount_refund > can_refund or can_refund == 0.0: - raise ValidationError( - _("Maksimal refund yang bisa dilakukan adalah sebesar %s. " - "Silakan sesuaikan jumlah refund.") % (can_refund) - ) + can_refund = 0.0 + if refund_type == 'berita_acara': + can_refund = sisa_uang_masuk + else: + can_refund = sisa_uang_masuk - total_invoice + if refund_type != 'berita_acara': + if amount_refund > can_refund or can_refund == 0.0: + raise ValidationError( + _("Maksimal refund yang bisa dilakukan adalah sebesar %s. " + "Silakan sesuaikan jumlah refund.") % (can_refund) + ) if amount_refund <= 0.00: raise ValidationError('Total Refund harus lebih dari 0 jika ingin mengajukan refund') @@ -451,7 +456,11 @@ class RefundSaleOrder(models.Model): raise UserError("❌ Refund multi SO hanya bisa 1 kali.") vals['remaining_refundable'] = 0.0 elif so_ids and len(so_ids) == 1 and refund_type != 'salah_transfer': - remaining = vals['uang_masuk'] - amount_refund + remaining = 0.0 + if refund_type == 'berita_acara': + vals['remaining_refundable'] = vals['uang_masuk'] - amount_refund + else: + vals['remaining_refundable'] = remaining if remaining < 0: raise ValidationError("❌ Tidak ada sisa transaksi untuk di-refund di SO ini. Semua dana sudah dikembalikan.") vals['remaining_refundable'] = remaining @@ -548,10 +557,28 @@ class RefundSaleOrder(models.Model): if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids', 'amount_refund']): total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) vals['total_invoice'] = total_invoice - uang_masuk = rec.uang_masuk - can_refund = uang_masuk - total_invoice - + uang_masuk = vals.get('uang_masuk', rec.uang_masuk) amount_refund = vals.get('amount_refund', rec.amount_refund) + can_refund = 0.0 + total_refunded = 0.0 + + if refund_type == 'berita_acara': + can_refund = uang_masuk + remaining = uang_masuk - amount_refund + else: + can_refund = uang_masuk - total_invoice + + existing_refunds = self.search([ + ('sale_order_ids', 'in', so_ids), + ('id', '!=', rec.id) + ]) + total_refunded = sum(existing_refunds.mapped('amount_refund')) + + if existing_refunds: + remaining = uang_masuk - total_refunded + else: + remaining = uang_masuk - amount_refund + if amount_refund > can_refund: raise ValidationError( diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index d36c03e5..55bea22c 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -167,8 +167,8 @@ class SaleOrderLine(models.Model): if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) - if line.amount_cashback > 0: - purchase_price = purchase_price - line.amount_cashback + # if line.amount_cashback > 0: + # purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price @@ -221,8 +221,8 @@ class SaleOrderLine(models.Model): if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) - if line.amount_cashback > 0: - purchase_price = purchase_price - line.amount_cashback + # if line.amount_cashback > 0: + # purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price @@ -254,29 +254,29 @@ class SaleOrderLine(models.Model): margin_per_item = sales_price - purchase_price line.item_before_margin = margin_per_item - def _compute_cashback_brand(self): - for line in self: - line.amount_cashback = 0 + # def _compute_cashback_brand(self): + # for line in self: + # line.amount_cashback = 0 - if not line.product_id: - continue + # if not line.product_id: + # continue - cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 - if cashback_percent <= 0: - continue + # cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 + # if cashback_percent <= 0: + # continue - price, taxes, vendor = self._get_purchase_price(line.product_id) + # price, taxes, vendor = self._get_purchase_price(line.product_id) - price_tax_excl = price + # price_tax_excl = price - if taxes: - tax = self.env['account.tax'].browse(taxes) - if tax.price_include: - price_tax_excl = price / (1 + (tax.amount / 100)) - else: - price_tax_excl = price + # if taxes: + # tax = self.env['account.tax'].browse(taxes) + # if tax.price_include: + # price_tax_excl = price / (1 + (tax.amount / 100)) + # else: + # price_tax_excl = price - line.amount_cashback = price_tax_excl * cashback_percent + # line.amount_cashback = price_tax_excl * cashback_percent # @api.onchange('vendor_id') # def onchange_vendor_id(self): diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index c3df92ec..7a517ca7 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -302,7 +302,7 @@ ] } "/> - + - + -- cgit v1.2.3 From 24b33148858c9827ad0b14e716562c377e1644ca Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 30 Jan 2026 16:05:57 +0700 Subject: Initial integration to ccm --- indoteknik_custom/models/gudang_service.py | 48 ++++++++++++++++++++--- indoteknik_custom/models/tukar_guling.py | 61 ++++++++++++++++++++++++++++++ indoteknik_custom/views/gudang_service.xml | 10 ----- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index ce3b2919..1056c4c8 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -20,7 +20,6 @@ class GudangService(models.Model): required=True, tracking=True ) - start_date = fields.Datetime( string="Date Processed", copy=False, @@ -54,6 +53,7 @@ class GudangService(models.Model): if not users: return + # Logistic users to be excluded excluded_users = [7, 17098, 216, 28, 15710] for rec in self: @@ -175,16 +175,52 @@ class GudangService(models.Model): @api.model def create(self, vals): - # Send notification + # sequence if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') - if vals.get('origin') and not vals.get('partner_id'): - so = self.env['sale.order'].browse(vals['origin']) - vals['partner_id'] = so.partner_id.id + # partner dari SO + so = self.env['sale.order'].browse(vals['origin']) + vals['partner_id'] = so.partner_id.id + + # 1️⃣ Cari BU/OUT dari SO + picking = self.env['stock.picking'].search([ + ('origin', '=', so.name), + ('picking_type_id', '=', 29), # BU/OUT + ('state', '=', 'done'), + ], limit=1) + + if not picking: + raise UserError("BU/OUT (done) tidak ditemukan untuk SO ini.") + + # 2️⃣ Validasi product service ada di picking + picking_products = picking.move_lines.mapped('product_id') + service_lines = vals.get('gudang_service_lines', []) + + tg_lines = [] + for line in service_lines: + product = self.env['product.product'].browse(line[2]['product_id']) + if product not in picking_products: + raise UserError( + f"Produk {product.display_name} tidak ada di BU/OUT {picking.name}" + ) + + tg_lines.append((0, 0, { + 'product_id': product.id, + 'quantity': line[2]['quantity'], + })) + + # 3️⃣ Create Tukar Guling + self.env['tukar.guling'].create({ + 'origin_so': so.id, + 'operation_type': 'service', + 'partner_id': so.partner_id.id, + 'operations': picking.id, + 'line_ids': tg_lines, + }) res = super(GudangService, self).create(vals) - # Send notification + res._send_logistic_notification() return res diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 682c478a..03af984f 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -62,6 +62,7 @@ class TukarGuling(models.Model): notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama + ('service', 'Service'), # -> barang yang sama ('retur_so', 'Retur SO')], required=True, tracking=3, help='Retur SO (ORT-SRT),\n Tukar Guling (ORT-SRT-PICK-OUT)') state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), @@ -931,6 +932,66 @@ class TukarGuling(models.Model): _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}") record.message_post( body=f"📦 {new_pick.name} created by {self.env.user.name} (state: {new_pick.state})") + + if record.return_type == 'service': + GUDANG_SERVICE_LOCATION_ID = 98 + # From STOCK to OUTPUT + done_service = self.env['stock.picking'].create({ + 'group_id': bu_out.group_id.id, + 'tukar_guling_id': record.id, + 'sale_order': record.origin, + 'note': record.notes, + 'picking_type_id': 32, + 'location_id': GUDANG_SERVICE_LOCATION_ID, + 'location_dest_id': BU_STOCK_LOCATION_ID, + 'partner_id': bu_out.partner_id.id, + 'move_ids_without_package': [(0, 0, { + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'name': line.product_id.display_name, + 'location_id': GUDANG_SERVICE_LOCATION_ID, + 'location_dest_id': BU_STOCK_LOCATION_ID, + }) for line in record.line_ids], + }) + if done_service: + done_service.action_confirm() + done_service.action_assign() + else: + raise UserError("Gagal membuat picking service") + + service_to_output = self.env['stock.picking'].create({ + 'group_id': bu_out.group_id.id, + 'tukar_guling_id': record.id, + 'sale_order': record.origin, + 'note': record.notes, + 'picking_type_id': 32, + 'location_id': BU_STOCK_LOCATION_ID, + 'location_dest_id': BU_OUTPUT_LOCATION_ID, + 'partner_id': bu_out.partner_id.id, + 'move_lines': [(0, 0, { + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'name': line.product_id.display_name, + 'location_id':BU_STOCK_LOCATION_ID, + 'location_dest_id': BU_STOCK_LOCATION_ID, + }) for line in record.line_ids], + 'move_ids_without_package': [(0, 0, { + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'name': line.product_id.display_name, + 'location_id': BU_STOCK_LOCATION_ID, + 'location_dest_id': BU_OUTPUT_LOCATION_ID, + }) for line in record.line_ids], + }) + if service_to_output: + service_to_output.action_confirm() + service_to_output.action_assign() + else: + raise UserError("Gagal membuat picking service") + # BU/OUT Baru dari SRT if srt_picking: diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index e06e9e7e..9a8382d3 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -6,11 +6,6 @@ gudang.serivice.tree gudang.service - - - @@ -63,7 +56,6 @@ - @@ -76,8 +68,6 @@ - - -- cgit v1.2.3 From 48e48a61a6fc0addcc1e0c3590ca8582abe66a6a Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Fri, 30 Jan 2026 18:19:30 +0700 Subject: refund kebutuhan BA dan cahsback --- indoteknik_custom/models/refund_sale_order.py | 6 +--- indoteknik_custom/models/sale_order_line.py | 48 +++++++++++++++------------ indoteknik_custom/views/sale_order.xml | 2 +- indoteknik_custom/views/x_manufactures.xml | 2 +- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index c3b7a356..7a219130 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -456,11 +456,7 @@ class RefundSaleOrder(models.Model): raise UserError("❌ Refund multi SO hanya bisa 1 kali.") vals['remaining_refundable'] = 0.0 elif so_ids and len(so_ids) == 1 and refund_type != 'salah_transfer': - remaining = 0.0 - if refund_type == 'berita_acara': - vals['remaining_refundable'] = vals['uang_masuk'] - amount_refund - else: - vals['remaining_refundable'] = remaining + remaining = vals['uang_masuk'] - amount_refund if remaining < 0: raise ValidationError("❌ Tidak ada sisa transaksi untuk di-refund di SO ini. Semua dana sudah dikembalikan.") vals['remaining_refundable'] = remaining diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 55bea22c..270fc842 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -167,8 +167,8 @@ class SaleOrderLine(models.Model): if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) - # if line.amount_cashback > 0: - # purchase_price = purchase_price - line.amount_cashback + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price @@ -192,6 +192,9 @@ class SaleOrderLine(models.Model): if line.purchase_tax_id and line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback + purchase_price = purchase_price * line.product_uom_qty margin_before = sales_price - purchase_price @@ -221,8 +224,8 @@ class SaleOrderLine(models.Model): if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) - # if line.amount_cashback > 0: - # purchase_price = purchase_price - line.amount_cashback + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price @@ -250,33 +253,36 @@ class SaleOrderLine(models.Model): if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback + purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price line.item_before_margin = margin_per_item - # def _compute_cashback_brand(self): - # for line in self: - # line.amount_cashback = 0 + def _compute_cashback_brand(self): + for line in self: + line.amount_cashback = 0 - # if not line.product_id: - # continue + if not line.product_id: + continue - # cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 - # if cashback_percent <= 0: - # continue + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 + if cashback_percent <= 0: + continue - # price, taxes, vendor = self._get_purchase_price(line.product_id) + price, taxes, vendor = self._get_purchase_price(line.product_id) - # price_tax_excl = price + price_tax_excl = price - # if taxes: - # tax = self.env['account.tax'].browse(taxes) - # if tax.price_include: - # price_tax_excl = price / (1 + (tax.amount / 100)) - # else: - # price_tax_excl = price + if taxes: + tax = self.env['account.tax'].browse(taxes) + if tax.price_include: + price_tax_excl = price / (1 + (tax.amount / 100)) + else: + price_tax_excl = price - # line.amount_cashback = price_tax_excl * cashback_percent + line.amount_cashback = price_tax_excl * cashback_percent # @api.onchange('vendor_id') # def onchange_vendor_id(self): diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 7a517ca7..c3df92ec 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -302,7 +302,7 @@ ] } "/> - + - + -- cgit v1.2.3 From 12c5971f46f3527ed9a8e1d865430d2b9fa25b38 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 2 Feb 2026 11:13:40 +0700 Subject: Initial Commit --- indoteknik_custom/models/gudang_service.py | 23 +---------------------- indoteknik_custom/views/gudang_service.xml | 9 --------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index ce3b2919..55e27b65 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -35,7 +35,7 @@ class GudangService(models.Model): # ) remaining_date = fields.Char( compute='_compute_remaining_date', - string='Remaining Date' + string='Date Status' ) state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -88,27 +88,6 @@ class GudangService(models.Model): if records: records._send_logistic_notification() - # @api.depends('schedule_date', 'start_date', 'state') - # def _compute_unprocessed_date(self): - # today = fields.Date.today() - - # for rec in self: - # if not rec.schedule_date or rec.state == 'cancel': - # rec.unprocessed_date = "-" - # continue - - # schedule = rec.schedule_date - - # # BELUM DIPROSES - # if not rec.start_date: - # days = (today - schedule).days - - # # SUDAH DIPROSES - # else: - # days = (rec.start_date.date() - schedule).days - - # rec.unprocessed_date = _("Unprocessed %s days") % max(days, 0) - @api.depends('schedule_date', 'create_date') def _compute_remaining_date(self): today = fields.Date.today() diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index e06e9e7e..8bc787cf 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -6,11 +6,6 @@ gudang.serivice.tree gudang.service - - - @@ -63,7 +56,6 @@ - @@ -77,7 +69,6 @@ - -- cgit v1.2.3 From 04093dbd490ef94a19aa2df69793e8eeb48831c5 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Mon, 2 Feb 2026 11:24:09 +0700 Subject: fix date order cashback start februari --- indoteknik_custom/models/refund_sale_order.py | 2 +- indoteknik_custom/models/sale_order_line.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 7a219130..7ce347a8 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -540,7 +540,7 @@ class RefundSaleOrder(models.Model): else: invoice_ids = rec.invoice_ids.ids - if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur']: + if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur', 'berita_acara']: raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice") if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']: diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 270fc842..dd44f84a 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -261,17 +261,25 @@ class SaleOrderLine(models.Model): line.item_before_margin = margin_per_item def _compute_cashback_brand(self): + start_date = datetime(2026, 2, 1, 0, 0, 0) for line in self: line.amount_cashback = 0 if not line.product_id: continue + if line.order_id.date_order < start_date: + continue + + price, taxes, vendor_id = self._get_purchase_price(line.product_id) + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 if cashback_percent <= 0: continue - price, taxes, vendor = self._get_purchase_price(line.product_id) + + if line.vendor_id.id != 5571: + continue price_tax_excl = price -- cgit v1.2.3 From d43b0c4621421fcfb1afe4724e13d2604570e1e6 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Mon, 2 Feb 2026 13:02:55 +0700 Subject: fix margin po --- indoteknik_custom/models/purchase_order_line.py | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 8c72887d..603a4ca2 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -23,6 +23,9 @@ class PurchaseOrderLine(models.Model): so_item_percent_margin = fields.Float( 'SO Margin%', compute='compute_item_margin', help="Total % Margin in Sales Order Header") + amount_cashback = fields.Float( + 'SO Margin%', compute='_compute_cashback_brand', + help="Total % Margin in Sales Order Header") delivery_amt_line = fields.Float('DeliveryAmtLine', compute='compute_delivery_amt_line') line_no = fields.Integer('No', default=0) qty_available = fields.Float('Qty Available', compute='_compute_qty_stock') @@ -373,6 +376,9 @@ class PurchaseOrderLine(models.Model): purchase_price = line.price_subtotal if order.delivery_amount > 0: purchase_price += line.delivery_amt_line + + if line.amount_cashback > 0: + purchase_price = purchase_price - line.amount_cashback # Hitung margin dan persentase margin real_item_margin = total_sales_price - purchase_price @@ -384,6 +390,46 @@ class PurchaseOrderLine(models.Model): sum_margin += real_item_margin + def _compute_cashback_brand(self): + start_date = datetime(2026, 2, 1, 0, 0, 0) + + for line in self: + line.amount_cashback = 0.0 + + product = line.product_id + order = line.order_id + + if not product or not order: + continue + + if order.partner_id.id != 5571: + continue + + sales_matches = self.env['purchase.order.sales.match'].search([ + ('purchase_order_id', '=', order.id), + ('product_id', '=', product.id) + ]) + + total_cashback = 0.0 + + for match in sales_matches: + so_line = match.sale_line_id + so_order = so_line.order_id + + if not so_order.date_order or so_order.date_order < start_date: + continue + + cashback_percent = brand.cashback_percent or 0.0 + if cashback_percent <= 0: + continue + sales_price = so_line.price_reduce_taxexcl * match.qty_so + + cashback = sales_price * cashback_percent + total_cashback += cashback + + line.amount_cashback = total_cashback + + def compute_delivery_amt_line(self): for line in self: if line.product_id.type == 'product': -- cgit v1.2.3 From fe3fb82fdd879c703d968cf09b09e6411e91100f Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Mon, 2 Feb 2026 13:08:50 +0700 Subject: fix margin po --- indoteknik_custom/models/purchase_order_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 603a4ca2..76dcc09e 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -419,7 +419,7 @@ class PurchaseOrderLine(models.Model): if not so_order.date_order or so_order.date_order < start_date: continue - cashback_percent = brand.cashback_percent or 0.0 + cashback_percent = product.x_manufacture.cashback_percent or 0.0 if cashback_percent <= 0: continue sales_price = so_line.price_reduce_taxexcl * match.qty_so -- cgit v1.2.3 From b7cd9ffe12f0dbae9abba2fdac32417bd400e481 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 2 Feb 2026 13:43:44 +0700 Subject: fix vcm picking wrong receipt --- indoteknik_custom/models/tukar_guling_po.py | 38 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index ae58d509..1ee10679 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -582,7 +582,23 @@ class TukarGulingPO(models.Model): ('group_id', '=', group.id), ('state', '=', 'done') ]) - bu_inputs = po_pickings.filtered(lambda p: p.picking_type_id.id == 28) + + product_ids = set(record.line_ids.mapped("product_id").ids) + + _logger.info("TG product_ids: %s", product_ids) + + def _get_moves(picking): + return picking.move_ids_without_package if picking.move_ids_without_package else picking.move_lines + + bu_inputs = po_pickings.filtered( + lambda p: p.picking_type_id.id == 28 and any( + m.product_id.id in product_ids + for m in _get_moves(p) + ) + ) + + _logger.info("BU INPUT dengan product sama: %s", bu_inputs.mapped("name")) + bu_puts = po_pickings.filtered(lambda p: p.picking_type_id.id == 75) else: raise UserError("Group ID tidak ditemukan pada BU Operations.") @@ -711,12 +727,26 @@ class TukarGulingPO(models.Model): # Ambil pasangannya di BU INPUT (asumsi urutan sejajar) sorted_bu_puts = sorted(bu_puts, key=lambda p: p.name) + # sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name) + + # if bu_put_index >= len(sorted_bu_inputs): + # raise UserError("Tidak ditemukan pasangan BU INPUT untuk BU PUT yang dipilih.") + + # paired = [(sorted_bu_puts[bu_put_index], sorted_bu_inputs[bu_put_index])] sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name) - if bu_put_index >= len(sorted_bu_inputs): - raise UserError("Tidak ditemukan pasangan BU INPUT untuk BU PUT yang dipilih.") + if not sorted_bu_inputs: + raise UserError( + "Tidak ditemukan BU INPUT yang memiliki product TG." + ) - paired = [(sorted_bu_puts[bu_put_index], sorted_bu_inputs[bu_put_index])] + paired = [(record.operations, sorted_bu_inputs[0])] + + _logger.info( + "🔗 Pairing BU PUT %s dengan BU INPUT %s", + record.operations.name, + sorted_bu_inputs[0].name + ) for bu_put, bu_input in paired: vrt = _create_return_from_picking(bu_put, bu_put_qty_map) -- cgit v1.2.3 From 8b28af52afe07363209601a1ad1cb90b7778d1d8 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Mon, 2 Feb 2026 14:48:34 +0700 Subject: fix margin po 2 --- indoteknik_custom/models/purchase_order.py | 13 +++++++++++++ indoteknik_custom/models/refund_sale_order.py | 6 +++--- indoteknik_custom/models/sale_order.py | 2 +- indoteknik_custom/views/purchase_order.xml | 1 + 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 35fa79a8..a1e92e10 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -53,6 +53,9 @@ class PurchaseOrder(models.Model): total_so_percent_margin = fields.Float( 'SO Margin%', compute='compute_total_margin', help="Total % Margin in Sales Order Header") + amount_cashback = fields.Float( + 'Cashback', compute='compute_total_margin', + help="Total Cashback brand Altama") amount_total_without_service = fields.Float('AmtTotalWithoutService', compute='compute_amt_total_without_service') summary_qty_po = fields.Float('Total Qty', compute='_compute_summary_qty') summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty') @@ -1418,6 +1421,14 @@ class PurchaseOrder(models.Model): purchase_price += line.delivery_amt_line if line.order_id.delivery_amt > 0: purchase_price += line.order_id.delivery_amt + + cashback_amount = 0.0 + if self.partner_id.id == 5571: + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 + if cashback_percent > 0: + cashback_amount = purchase_price * cashback_percent + purchase_price -= cashback_amount + real_item_margin = sales_price - purchase_price sum_margin += real_item_margin @@ -1426,11 +1437,13 @@ class PurchaseOrder(models.Model): self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 + self.amount_cashback = cashback_amount else: self.total_margin = 0 self.total_percent_margin = 0 self.total_so_margin = 0 self.total_so_percent_margin = 0 + self.amount_cashback = 0 def compute_total_margin_from_apo(self): sum_so_margin = sum_sales_price = sum_margin = 0 diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 7ce347a8..d6aa1ad2 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -243,7 +243,7 @@ class RefundSaleOrder(models.Model): ) invoices = sale_orders.mapped('invoice_ids').filtered( - lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.payment_state == 'paid' + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted' ) if invoices: vals['invoice_ids'] = [(6, 0, invoices.ids)] @@ -497,7 +497,7 @@ class RefundSaleOrder(models.Model): valid_invoices = sale_orders.mapped('invoice_ids').filtered( - lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.payment_state == 'paid' + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted' ) vals['invoice_ids'] = [(6, 0, valid_invoices.ids)] vals['ongkir'] = sum(so.delivery_amt or 0.0 for so in sale_orders) @@ -733,7 +733,7 @@ class RefundSaleOrder(models.Model): for so in self.sale_order_ids: self.ongkir += so.delivery_amt or 0.0 valid_invoices = so.invoice_ids.filtered( - lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.payment_state == 'paid' + lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted' ) all_invoices |= valid_invoices total_invoice += sum(valid_invoices.mapped('amount_total_signed')) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 469509d4..a4bc2309 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -3438,7 +3438,7 @@ class SaleOrder(models.Model): def button_refund(self): self.ensure_one() - invoice_ids = self.invoice_ids.filtered(lambda inv: inv.payment_state == 'paid') + invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state == 'posted') moves = self.env['account.move'].search([ ('sale_id', '=', self.id), diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 16b8bd44..59e317d2 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -105,6 +105,7 @@ + -- cgit v1.2.3 From 750cdae1141d0039ab2c8d5796c5fb7bb2726bcc Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Mon, 2 Feb 2026 14:54:14 +0700 Subject: (andri) fix compute when PUM is canceled --- indoteknik_custom/models/advance_payment_request.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index ed0b0809..8cadb1b6 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -641,10 +641,16 @@ class AdvancePaymentRequest(models.Model): today = date.today() for rec in self: - current_days = rec.days_remaining or 0 - current_due_date = rec.estimated_return_date or False - if rec.type_request == 'pum': - is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids) + # current_days = rec.days_remaining or 0 + # current_due_date = rec.estimated_return_date or False + current_days = 0 + current_due_date = False + + is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids) + is_pum_canceled = (rec.status == 'cancel') + + if rec.type_request == 'pum' and not is_pum_canceled and not is_settlement_approved: + if not is_settlement_approved: due_date = False -- cgit v1.2.3 From 5118ff0549de5bea4e83b31da2c2347f227c488a Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Mon, 2 Feb 2026 14:55:02 +0700 Subject: pusing margin po --- indoteknik_custom/models/purchase_order.py | 13 ------------- indoteknik_custom/views/purchase_order.xml | 1 - 2 files changed, 14 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index a1e92e10..35fa79a8 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -53,9 +53,6 @@ class PurchaseOrder(models.Model): total_so_percent_margin = fields.Float( 'SO Margin%', compute='compute_total_margin', help="Total % Margin in Sales Order Header") - amount_cashback = fields.Float( - 'Cashback', compute='compute_total_margin', - help="Total Cashback brand Altama") amount_total_without_service = fields.Float('AmtTotalWithoutService', compute='compute_amt_total_without_service') summary_qty_po = fields.Float('Total Qty', compute='_compute_summary_qty') summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty') @@ -1421,14 +1418,6 @@ class PurchaseOrder(models.Model): purchase_price += line.delivery_amt_line if line.order_id.delivery_amt > 0: purchase_price += line.order_id.delivery_amt - - cashback_amount = 0.0 - if self.partner_id.id == 5571: - cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 - if cashback_percent > 0: - cashback_amount = purchase_price * cashback_percent - purchase_price -= cashback_amount - real_item_margin = sales_price - purchase_price sum_margin += real_item_margin @@ -1437,13 +1426,11 @@ class PurchaseOrder(models.Model): self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 - self.amount_cashback = cashback_amount else: self.total_margin = 0 self.total_percent_margin = 0 self.total_so_margin = 0 self.total_so_percent_margin = 0 - self.amount_cashback = 0 def compute_total_margin_from_apo(self): sum_so_margin = sum_sales_price = sum_margin = 0 diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 59e317d2..16b8bd44 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -105,7 +105,6 @@ - -- cgit v1.2.3 From 37ccff02eb47b50ca6d23e4cd027155381c53947 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Mon, 2 Feb 2026 16:59:12 +0700 Subject: coba margin po last --- indoteknik_custom/models/purchase_order.py | 24 +++++++++++++++++++++++- indoteknik_custom/models/refund_sale_order.py | 2 +- indoteknik_custom/views/purchase_order.xml | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 35fa79a8..60802649 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -53,6 +53,9 @@ class PurchaseOrder(models.Model): total_so_percent_margin = fields.Float( 'SO Margin%', compute='compute_total_margin', help="Total % Margin in Sales Order Header") + amount_cashback = fields.Float( + 'Cashback', compute='compute_total_margin', + help="Total Cashback brand Altama") amount_total_without_service = fields.Float('AmtTotalWithoutService', compute='compute_amt_total_without_service') summary_qty_po = fields.Float('Total Qty', compute='_compute_summary_qty') summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty') @@ -1418,19 +1421,29 @@ class PurchaseOrder(models.Model): purchase_price += line.delivery_amt_line if line.order_id.delivery_amt > 0: purchase_price += line.order_id.delivery_amt + + cashback_amount = 0.0 + if self.partner_id.id == 5571: + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 + if cashback_percent > 0: + cashback_amount = purchase_price * cashback_percent + purchase_price -= cashback_amount + real_item_margin = sales_price - purchase_price sum_margin += real_item_margin - if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0: + if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0 and cashback_amount != 0: self.total_so_margin = sum_so_margin self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 + self.amount_cashback = cashback_amount else: self.total_margin = 0 self.total_percent_margin = 0 self.total_so_margin = 0 self.total_so_percent_margin = 0 + self.amount_cashback = 0 def compute_total_margin_from_apo(self): sum_so_margin = sum_sales_price = sum_margin = 0 @@ -1469,6 +1482,13 @@ class PurchaseOrder(models.Model): purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * qty_po if line.purchase_order_id.delivery_amt > 0: purchase_price += line.purchase_order_id.delivery_amt + + cashback_amount = 0.0 + if self.partner_id.id == 5571: + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 + if cashback_percent > 0: + cashback_amount = purchase_price * cashback_percent + purchase_price -= cashback_amount real_item_margin = sales_price - purchase_price sum_margin += real_item_margin @@ -1479,9 +1499,11 @@ class PurchaseOrder(models.Model): self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 + self.amount_cashback = cashback_amount else: self.total_margin = self.total_percent_margin = 0 self.total_so_margin = self.total_so_percent_margin = 0 + self.amount_cashback = 0 def compute_amt_total_without_service(self): diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index d6aa1ad2..1ce53113 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -624,7 +624,7 @@ class RefundSaleOrder(models.Model): for rec in self: move_links = [] - invoice_ids = rec.sale_order_ids.mapped('invoice_ids') + invoice_ids = rec.sale_order_ids.mapped('invoice_ids').filtered(lambda m: m.state == 'posted') moves = self.env['account.move'].search([ ('sale_id', 'in', rec.sale_order_ids.ids), diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 16b8bd44..59e317d2 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -105,6 +105,7 @@ + -- cgit v1.2.3 From 4fa6b57647f7f53573bd83d9dd4f0292ab955e1a Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 3 Feb 2026 11:41:35 +0700 Subject: fix margin PO --- indoteknik_custom/models/purchase_order.py | 51 +++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 6b6e6aa2..820f8091 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -53,9 +53,7 @@ class PurchaseOrder(models.Model): total_so_percent_margin = fields.Float( 'SO Margin%', compute='compute_total_margin', help="Total % Margin in Sales Order Header") - amount_cashback = fields.Float( - 'Cashback', compute='compute_total_margin', - help="Total Cashback brand Altama") + amount_cashback = fields.Float('Cashback', compute = 'compute_total_margin', help = 'Total Cashback brand Altama') amount_total_without_service = fields.Float('AmtTotalWithoutService', compute='compute_amt_total_without_service') summary_qty_po = fields.Float('Total Qty', compute='_compute_summary_qty') summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty') @@ -1086,9 +1084,19 @@ class PurchaseOrder(models.Model): if '/PJ/' in self.name: price_change_detected = any(line.price_unit_before for line in self.order_line) if price_change_detected: - if self.order_sales_match_line: - if self.total_percent_margin <= 15.0: - raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") + if self.total_percent_margin <= 15.0: + raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") + else: + low_margin_match_so = self.order_sales_match_line.filtered( + lambda match: match.so_header_margin <= 15.0 + ) + if low_margin_match_so: + raise UserError("Approval Pimpinan diperlukan jika pada PO Line yang Matches SO item memiliki header margin SO <= 15%") + # else: + # is_po_manual = '/A/' not in self.name and '/MO/' not in self.name + # if is_po_manual: + # if not self.order_sales_match_line: + # raise UserError("Tidak ada matches SO, Approval Pimpinan diperlukan.") self._check_assets_note() # self._check_payment_term() # check payment term @@ -1410,18 +1418,25 @@ class PurchaseOrder(models.Model): purchase_price += line.delivery_amt_line if line.order_id.delivery_amt > 0: purchase_price += line.order_id.delivery_amt + real_item_margin = sales_price - purchase_price + sum_margin += real_item_margin - cashback_amount = 0.0 + cashback_amount = 0 if self.partner_id.id == 5571: cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 if cashback_percent > 0: cashback_amount = purchase_price * cashback_percent purchase_price -= cashback_amount - real_item_margin = sales_price - purchase_price - sum_margin += real_item_margin + # line.amount_cashback = cashback_amount - if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0 and cashback_amount != 0: + if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0: + self.total_so_margin = sum_so_margin + self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 + self.total_margin = sum_margin + self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 + self.amount_cashback = 0 + elif self.partner_id.id == 5571 and sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0 and cashback_amount != 0: self.total_so_margin = sum_so_margin self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin @@ -1435,7 +1450,7 @@ class PurchaseOrder(models.Model): self.amount_cashback = 0 def compute_total_margin_from_apo(self): - sum_so_margin = sum_sales_price = sum_margin = 0 + sum_so_margin = sum_sales_price = sum_margin = cashback_amount = 0 for line in self.order_sales_match_line: po_line = self.env['purchase.order.line'].search([ ('product_id', '=', line.product_id.id), @@ -1471,17 +1486,17 @@ class PurchaseOrder(models.Model): purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * qty_po if line.purchase_order_id.delivery_amt > 0: purchase_price += line.purchase_order_id.delivery_amt - - cashback_amount = 0.0 - if self.partner_id.id == 5571: - cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 - if cashback_percent > 0: - cashback_amount = purchase_price * cashback_percent - purchase_price -= cashback_amount + + if self.partner_id.id == 5571: + cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 + if cashback_percent > 0: + cashback_amount = purchase_price * cashback_percent + purchase_price -= cashback_amount real_item_margin = sales_price - purchase_price sum_margin += real_item_margin + self.amount_cashback = cashback_amount # Akumulasi hasil akhir if sum_sales_price != 0: self.total_so_margin = sum_so_margin -- cgit v1.2.3 From 9da91430c095af5d46e6821de82a93b30ce42a26 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 4 Feb 2026 13:19:37 +0700 Subject: off confirm --- indoteknik_custom/models/purchase_order.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index e16c8d61..cb6e70b1 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1079,13 +1079,13 @@ class PurchaseOrder(models.Model): ) % order.name) def button_confirm(self): - if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan - if '/PJ/' in self.name: - price_change_detected = any(line.price_unit_before for line in self.order_line) - if price_change_detected: - if self.order_sales_match_line: - if self.total_percent_margin <= 15.0: - raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") + # if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan + # if '/PJ/' in self.name: + # price_change_detected = any(line.price_unit_before for line in self.order_line) + # if price_change_detected: + # if self.order_sales_match_line: + # if self.total_percent_margin <= 15.0: + # raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") self._check_assets_note() # self._check_payment_term() # check payment term -- cgit v1.2.3 From 983fa2ec6b13f4005f4e27e7b7860503c4823f8e Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 4 Feb 2026 15:45:56 +0700 Subject: on confirm --- indoteknik_custom/models/purchase_order.py | 14 +++++++------- indoteknik_custom/views/advance_payment_request.xml | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 6c3e4185..a066d90b 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1080,13 +1080,13 @@ class PurchaseOrder(models.Model): ) % order.name) def button_confirm(self): - # if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan - # if '/PJ/' in self.name: - # price_change_detected = any(line.price_unit_before for line in self.order_line) - # if price_change_detected: - # if self.order_sales_match_line: - # if self.total_percent_margin <= 15.0: - # raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") + if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan + if '/PJ/' in self.name: + price_change_detected = any(line.price_unit_before for line in self.order_line) + if price_change_detected: + if self.order_sales_match_line: + if self.total_percent_margin <= 25.0: + raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") self._check_assets_note() # self._check_payment_term() # check payment term diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml index 7f422aa9..340e0caf 100644 --- a/indoteknik_custom/views/advance_payment_request.xml +++ b/indoteknik_custom/views/advance_payment_request.xml @@ -236,6 +236,7 @@ + -- cgit v1.2.3 From 26713fca51335e68f737c171b8de918c8192ea8d Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 4 Feb 2026 15:55:21 +0700 Subject: fix --- indoteknik_custom/models/purchase_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index a066d90b..b3ecca56 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1085,7 +1085,7 @@ class PurchaseOrder(models.Model): price_change_detected = any(line.price_unit_before for line in self.order_line) if price_change_detected: if self.order_sales_match_line: - if self.total_percent_margin <= 25.0: + if self.total_percent_margin <= 15.0: raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") self._check_assets_note() -- cgit v1.2.3 From 250c5d6ba209c80e909d0194218d08422d2daaa6 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 5 Feb 2026 15:18:10 +0700 Subject: fix duplicate --- indoteknik_custom/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a4bc2309..49e36279 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2316,7 +2316,7 @@ class SaleOrder(models.Model): for order in self: for line in order.order_line: search_product = self.env['sale.order.line'].search( - [('product_id', '=', line.product_id.id), ('order_id', '=', order.id)]) + [('product_id', '=', line.product_id.id), ('order_id', '=', order.id), ('display_type', '=', False)]) if len(search_product) > 1: raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name)) -- cgit v1.2.3 From 6ec6a23399f026de17974f39fd3e325d27093199 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 6 Feb 2026 09:31:55 +0700 Subject: add filter search tukar guling --- indoteknik_custom/views/tukar_guling.xml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/indoteknik_custom/views/tukar_guling.xml b/indoteknik_custom/views/tukar_guling.xml index 8cfb5680..7865b43d 100644 --- a/indoteknik_custom/views/tukar_guling.xml +++ b/indoteknik_custom/views/tukar_guling.xml @@ -133,5 +133,28 @@ + + tukar.guling.filter + tukar.guling + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From 9a769c9d9fe2a561c0b3c4bed3e1f9ad6822639e Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 6 Feb 2026 09:36:24 +0700 Subject: fix search tukar guling --- indoteknik_custom/views/tukar_guling.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/views/tukar_guling.xml b/indoteknik_custom/views/tukar_guling.xml index 7865b43d..609dea15 100644 --- a/indoteknik_custom/views/tukar_guling.xml +++ b/indoteknik_custom/views/tukar_guling.xml @@ -141,10 +141,11 @@ + - + -- cgit v1.2.3 From a0988c45dba1451dc5a670eb6a378527de1390ec Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 6 Feb 2026 10:28:11 +0700 Subject: show create date Approval date doc --- indoteknik_custom/views/approval_date_doc.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/indoteknik_custom/views/approval_date_doc.xml b/indoteknik_custom/views/approval_date_doc.xml index 3d597aa8..a3aae3b4 100644 --- a/indoteknik_custom/views/approval_date_doc.xml +++ b/indoteknik_custom/views/approval_date_doc.xml @@ -14,6 +14,7 @@ +
    @@ -46,6 +47,7 @@ + -- cgit v1.2.3 From c37c440f117cbb5f227096c1fceb74b2809349ee Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 08:55:36 +0700 Subject: approval adjust out --- indoteknik_custom/models/stock_inventory.py | 29 +++++++++++++++++++++++++++-- indoteknik_custom/views/stock_inventory.xml | 5 +++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/stock_inventory.py b/indoteknik_custom/models/stock_inventory.py index 84eb5a17..c4ebaeda 100644 --- a/indoteknik_custom/models/stock_inventory.py +++ b/indoteknik_custom/models/stock_inventory.py @@ -16,6 +16,12 @@ class StockInventory(models.Model): ('in', 'Adjusment In'), ('out', 'Adjusment Out'), ], string='Adjusments Type', required=True) + approval_state = fields.Selection([ + ('draft', 'Draft'), + ('logistic', 'Logistic'), + ('accounting', 'Accounting'), + ('approved', 'Approved'), + ], default='draft', tracking=True) def _generate_number_stock_inventory(self): """Men-generate nomor untuk semua stock inventory yang belum memiliki number.""" @@ -53,8 +59,11 @@ class StockInventory(models.Model): return "00001" # Jika belum ada data, mulai dari 00001 def action_start(self): - if self.env.user.id not in [21, 17, 571, 28]: - raise UserError("Hanya Rafly, Denise, Iqmal, dan Stephan yang bisa start inventory") + if self.approval_state != 'approved' and self.adjusment_type == 'out': + raise UserError('Harus melalui proses approval') + if self.adjusment_type == 'in': + if self.env.user.id not in [21, 17, 571, 28]: + raise UserError("Hanya Rafly, Denise, Iqmal, dan Stephan yang bisa start inventory") return super(StockInventory, self).action_start() @api.model @@ -69,6 +78,22 @@ class StockInventory(models.Model): self._assign_number(order) # Generate number setelah save return order + + def action_approve(self): + if self.adjusment_type == 'out': + for rec in self: + if self.approval_state in [False, '', 'draft']: + self.approval_state = 'logistic' + elif self.approval_state == 'logistic': + if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Harus diapprove logistic") + self.approval_state = 'accounting' + elif self.approval_state == 'accounting': + if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Harus diapprove accounting") + self.approval_state = 'approved' + else: + raise UserError("Sudah Approved") def write(self, vals): """Jika adjusment_type diubah, generate ulang nomor.""" diff --git a/indoteknik_custom/views/stock_inventory.xml b/indoteknik_custom/views/stock_inventory.xml index db85f05c..ebbc5bb3 100644 --- a/indoteknik_custom/views/stock_inventory.xml +++ b/indoteknik_custom/views/stock_inventory.xml @@ -6,9 +6,14 @@ stock.inventory +
    +
    +
    -- cgit v1.2.3 From b04fb88af7e868a32af5ffbe4b5f5e97a5da4878 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 09:15:30 +0700 Subject: hide approval state for adjust in --- indoteknik_custom/views/stock_inventory.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/views/stock_inventory.xml b/indoteknik_custom/views/stock_inventory.xml index ebbc5bb3..df747830 100644 --- a/indoteknik_custom/views/stock_inventory.xml +++ b/indoteknik_custom/views/stock_inventory.xml @@ -13,7 +13,7 @@ - +
    -- cgit v1.2.3 From b4c46fd025ce765acdf9efd3e44f475b6e83d747 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 09:50:21 +0700 Subject: rev --- indoteknik_custom/models/gudang_service.py | 48 +++--------- indoteknik_custom/models/tukar_guling.py | 116 ++++++++++++++--------------- indoteknik_custom/views/gudang_service.xml | 2 +- 3 files changed, 68 insertions(+), 98 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 70ff27c0..3f483a56 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -13,8 +13,8 @@ class GudangService(models.Model): name = fields.Char('Name', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - vendor_id = fields.Many2one('res.partner', string='Vendor', readonly=True, required=True) - origin = fields.Many2one('sale.order', string='Origin SO', required=True) + vendor_id = fields.Many2one('res.partner', string='Vendor Service', required=True) + origin = fields.Many2one('sale.order', string='Origin SO', required=True, domain=[('state', 'in', ['done', 'sale'])]) schedule_date = fields.Date( string="Schedule Date", required=True, @@ -36,7 +36,13 @@ class GudangService(models.Model): compute='_compute_remaining_date', string='Date Status' ) - state = fields.Selection([('draft', 'Backlog'), ('onprogress', 'On Progress'),('done', 'Done'), ('cancel', 'Cancel')], default='draft', tracking=True) + state = fields.Selection([ + ('draft', 'Backlog'), + ('received_from_cust', 'Received From Customer'), + ('sent_to_vendor', 'Sent to Service Vendor'), + ('received_from_vendor', 'Received From Service Vendor'), + ('delived_to_cust', 'Delivered to Customer'), + ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) def _send_logistic_notification(self): @@ -162,42 +168,6 @@ class GudangService(models.Model): so = self.env['sale.order'].browse(vals['origin']) vals['partner_id'] = so.partner_id.id - # 1️⃣ Cari BU/OUT dari SO - picking = self.env['stock.picking'].search([ - ('origin', '=', so.name), - ('picking_type_id', '=', 29), # BU/OUT - ('state', '=', 'done'), - ], limit=1) - - if not picking: - raise UserError("BU/OUT (done) tidak ditemukan untuk SO ini.") - - # 2️⃣ Validasi product service ada di picking - picking_products = picking.move_lines.mapped('product_id') - service_lines = vals.get('gudang_service_lines', []) - - tg_lines = [] - for line in service_lines: - product = self.env['product.product'].browse(line[2]['product_id']) - if product not in picking_products: - raise UserError( - f"Produk {product.display_name} tidak ada di BU/OUT {picking.name}" - ) - - tg_lines.append((0, 0, { - 'product_id': product.id, - 'quantity': line[2]['quantity'], - })) - - # 3️⃣ Create Tukar Guling - self.env['tukar.guling'].create({ - 'origin_so': so.id, - 'operation_type': 'service', - 'partner_id': so.partner_id.id, - 'operations': picking.id, - 'line_ids': tg_lines, - }) - res = super(GudangService, self).create(vals) res._send_logistic_notification() diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 03af984f..619e7c99 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -62,7 +62,7 @@ class TukarGuling(models.Model): notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama - ('service', 'Service'), # -> barang yang sama + # ('service', 'Service'), # -> barang yang sama ('retur_so', 'Retur SO')], required=True, tracking=3, help='Retur SO (ORT-SRT),\n Tukar Guling (ORT-SRT-PICK-OUT)') state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), @@ -933,64 +933,64 @@ class TukarGuling(models.Model): record.message_post( body=f"📦 {new_pick.name} created by {self.env.user.name} (state: {new_pick.state})") - if record.return_type == 'service': - GUDANG_SERVICE_LOCATION_ID = 98 - # From STOCK to OUTPUT - done_service = self.env['stock.picking'].create({ - 'group_id': bu_out.group_id.id, - 'tukar_guling_id': record.id, - 'sale_order': record.origin, - 'note': record.notes, - 'picking_type_id': 32, - 'location_id': GUDANG_SERVICE_LOCATION_ID, - 'location_dest_id': BU_STOCK_LOCATION_ID, - 'partner_id': bu_out.partner_id.id, - 'move_ids_without_package': [(0, 0, { - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'name': line.product_id.display_name, - 'location_id': GUDANG_SERVICE_LOCATION_ID, - 'location_dest_id': BU_STOCK_LOCATION_ID, - }) for line in record.line_ids], - }) - if done_service: - done_service.action_confirm() - done_service.action_assign() - else: - raise UserError("Gagal membuat picking service") + # if record.return_type == 'service': + # GUDANG_SERVICE_LOCATION_ID = 98 + # # From STOCK to OUTPUT + # done_service = self.env['stock.picking'].create({ + # 'group_id': bu_out.group_id.id, + # 'tukar_guling_id': record.id, + # 'sale_order': record.origin, + # 'note': record.notes, + # 'picking_type_id': 32, + # 'location_id': GUDANG_SERVICE_LOCATION_ID, + # 'location_dest_id': BU_STOCK_LOCATION_ID, + # 'partner_id': bu_out.partner_id.id, + # 'move_ids_without_package': [(0, 0, { + # 'product_id': line.product_id.id, + # 'product_uom_qty': line.product_uom_qty, + # 'product_uom': line.product_uom.id, + # 'name': line.product_id.display_name, + # 'location_id': GUDANG_SERVICE_LOCATION_ID, + # 'location_dest_id': BU_STOCK_LOCATION_ID, + # }) for line in record.line_ids], + # }) + # if done_service: + # done_service.action_confirm() + # done_service.action_assign() + # else: + # raise UserError("Gagal membuat picking service") - service_to_output = self.env['stock.picking'].create({ - 'group_id': bu_out.group_id.id, - 'tukar_guling_id': record.id, - 'sale_order': record.origin, - 'note': record.notes, - 'picking_type_id': 32, - 'location_id': BU_STOCK_LOCATION_ID, - 'location_dest_id': BU_OUTPUT_LOCATION_ID, - 'partner_id': bu_out.partner_id.id, - 'move_lines': [(0, 0, { - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'name': line.product_id.display_name, - 'location_id':BU_STOCK_LOCATION_ID, - 'location_dest_id': BU_STOCK_LOCATION_ID, - }) for line in record.line_ids], - 'move_ids_without_package': [(0, 0, { - 'product_id': line.product_id.id, - 'product_uom_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'name': line.product_id.display_name, - 'location_id': BU_STOCK_LOCATION_ID, - 'location_dest_id': BU_OUTPUT_LOCATION_ID, - }) for line in record.line_ids], - }) - if service_to_output: - service_to_output.action_confirm() - service_to_output.action_assign() - else: - raise UserError("Gagal membuat picking service") + # service_to_output = self.env['stock.picking'].create({ + # 'group_id': bu_out.group_id.id, + # 'tukar_guling_id': record.id, + # 'sale_order': record.origin, + # 'note': record.notes, + # 'picking_type_id': 32, + # 'location_id': BU_STOCK_LOCATION_ID, + # 'location_dest_id': BU_OUTPUT_LOCATION_ID, + # 'partner_id': bu_out.partner_id.id, + # 'move_lines': [(0, 0, { + # 'product_id': line.product_id.id, + # 'product_uom_qty': line.product_uom_qty, + # 'product_uom': line.product_uom.id, + # 'name': line.product_id.display_name, + # 'location_id':BU_STOCK_LOCATION_ID, + # 'location_dest_id': BU_STOCK_LOCATION_ID, + # }) for line in record.line_ids], + # 'move_ids_without_package': [(0, 0, { + # 'product_id': line.product_id.id, + # 'product_uom_qty': line.product_uom_qty, + # 'product_uom': line.product_uom.id, + # 'name': line.product_id.display_name, + # 'location_id': BU_STOCK_LOCATION_ID, + # 'location_dest_id': BU_OUTPUT_LOCATION_ID, + # }) for line in record.line_ids], + # }) + # if service_to_output: + # service_to_output.action_confirm() + # service_to_output.action_assign() + # else: + # raise UserError("Gagal membuat picking service") # BU/OUT Baru dari SRT diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 8bc787cf..d68b0f08 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -68,7 +68,7 @@ - + -- cgit v1.2.3 From b6d2feb969d38e1991e5afa129813da89e8a51fb Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 09:55:53 +0700 Subject: fix colors --- indoteknik_custom/views/gudang_service.xml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index d68b0f08..68e2428c 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -6,10 +6,8 @@ gudang.serivice.tree gudang.service - + @@ -17,8 +15,8 @@ - + -- cgit v1.2.3 From 7aadac577c7bdacf1fab54c23fe50b04fba08393 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 15:06:17 +0700 Subject: fix state --- indoteknik_custom/models/gudang_service.py | 37 ++++++++++++++++++------------ indoteknik_custom/views/gudang_service.xml | 8 +++---- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 3f483a56..4e3b2c34 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -41,7 +41,7 @@ class GudangService(models.Model): ('received_from_cust', 'Received From Customer'), ('sent_to_vendor', 'Sent to Service Vendor'), ('received_from_vendor', 'Received From Service Vendor'), - ('delived_to_cust', 'Delivered to Customer'), + ('delivered_to_cust', 'Delivered to Customer'), ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) @@ -88,7 +88,7 @@ class GudangService(models.Model): @api.model def cron_notify_onprogress_gudang_service(self): records = self.search([ - ('state', '=', 'draft') + ('state', 'in', ['draft', 'received_from_customer']), ]) if records: @@ -118,21 +118,28 @@ class GudangService(models.Model): def action_submit(self): for rec in self: - rec.state = 'onprogress' - rec.start_date = fields.Datetime.now() - # rec.date = fields.Datetime.now() + if rec.state == 'draft': + rec.state = 'received_from_cust' + elif rec.state == 'received_from_cust': + rec.state = 'sent_to_vendor' + rec.start_date = fields.Datetime.now() + elif rec.state == 'sent_to_vendor': + rec.state = 'received_from_vendor' def action_done(self): - for rec in self: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ('state', '=', 'done') - ]) - activities.unlink() - rec.state = 'done' - if not rec.done_date: - rec.done_date = fields.Datetime.now() + if self.state != 'received_from_vendor': + raise UserError("Only 'Received From Vendor' state can be set to Done") + else: + for rec in self: + activities = self.env['mail.activity'].search([ + ('res_id', '=', rec.id), + ('res_model', '=', 'gudang.service'), + ('state', '=', 'delivered_to_cust'), + ]) + activities.unlink() + rec.state = 'delivered_to_cust' + if not rec.done_date: + rec.done_date = fields.Datetime.now() def action_draft(self): """Reset to draft state""" diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 68e2428c..6abdcc8b 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -31,16 +31,16 @@
    -- cgit v1.2.3 From 2def9515d57eb3128cad31c8b97901055e4e0523 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 15:42:21 +0700 Subject: fix approval flow stock inventory --- indoteknik_custom/models/stock_inventory.py | 13 +++++-------- indoteknik_custom/views/stock_inventory.xml | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/indoteknik_custom/models/stock_inventory.py b/indoteknik_custom/models/stock_inventory.py index c4ebaeda..cb7d3773 100644 --- a/indoteknik_custom/models/stock_inventory.py +++ b/indoteknik_custom/models/stock_inventory.py @@ -17,11 +17,10 @@ class StockInventory(models.Model): ('out', 'Adjusment Out'), ], string='Adjusments Type', required=True) approval_state = fields.Selection([ - ('draft', 'Draft'), ('logistic', 'Logistic'), ('accounting', 'Accounting'), ('approved', 'Approved'), - ], default='draft', tracking=True) + ], default='logistic', tracking=True) def _generate_number_stock_inventory(self): """Men-generate nomor untuk semua stock inventory yang belum memiliki number.""" @@ -82,16 +81,14 @@ class StockInventory(models.Model): def action_approve(self): if self.adjusment_type == 'out': for rec in self: - if self.approval_state in [False, '', 'draft']: - self.approval_state = 'logistic' - elif self.approval_state == 'logistic': + if rec.approval_state == 'logistic': if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Harus diapprove logistic") - self.approval_state = 'accounting' - elif self.approval_state == 'accounting': + rec.approval_state = 'accounting' + elif rec.approval_state == 'accounting': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Harus diapprove accounting") - self.approval_state = 'approved' + rec.approval_state = 'approved' else: raise UserError("Sudah Approved") diff --git a/indoteknik_custom/views/stock_inventory.xml b/indoteknik_custom/views/stock_inventory.xml index df747830..89c058ea 100644 --- a/indoteknik_custom/views/stock_inventory.xml +++ b/indoteknik_custom/views/stock_inventory.xml @@ -26,6 +26,7 @@ + -- cgit v1.2.3 From 30ccd81fe087eec277da6875b1df9d9a3579ab77 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 10 Feb 2026 16:35:53 +0700 Subject: (andri) add report internal PO --- indoteknik_custom/__manifest__.py | 1 + .../report/purchase_report_internal.xml | 201 +++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 indoteknik_custom/report/purchase_report_internal.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 66962a24..61eef35b 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -169,6 +169,7 @@ 'report/report_surat_piutang.xml', 'report/report_tutup_tempo.xml', 'report/purchase_report.xml', + 'report/purchase_report_internal.xml', 'views/vendor_sla.xml', 'views/coretax_faktur.xml', 'views/public_holiday.xml', diff --git a/indoteknik_custom/report/purchase_report_internal.xml b/indoteknik_custom/report/purchase_report_internal.xml new file mode 100644 index 00000000..09fb81f3 --- /dev/null +++ b/indoteknik_custom/report/purchase_report_internal.xml @@ -0,0 +1,201 @@ + + + + + + Purchase Order (Internal) + purchase.order + qweb-pdf + indoteknik_custom.report_purchaseorder_internal + indoteknik_custom.report_purchaseorder_internal + + ('%s - %s' % (object.name, object.partner_id.name)) + + + report + + + + + + + + + + + -- cgit v1.2.3 From 9879c06b35da5402d1df545c352bd2729722ca24 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 10 Feb 2026 16:38:26 +0700 Subject: foix --- indoteknik_custom/report/purchase_report_internal.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/report/purchase_report_internal.xml b/indoteknik_custom/report/purchase_report_internal.xml index 09fb81f3..7df847de 100644 --- a/indoteknik_custom/report/purchase_report_internal.xml +++ b/indoteknik_custom/report/purchase_report_internal.xml @@ -172,7 +172,7 @@ - Margin % + Margin PO % -- cgit v1.2.3 From dc2baa117413f3140adf99c8a398da3d7637c3d3 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 10 Feb 2026 18:26:52 +0700 Subject: push --- indoteknik_custom/models/gudang_service.py | 2 +- indoteknik_custom/views/gudang_service.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 4e3b2c34..4900113f 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -157,7 +157,7 @@ class GudangService(models.Model): ('res_model', '=', 'gudang.service'), ]) activities.unlink() - if rec.state == 'done': + if rec.state == 'delivered_to_cust': raise UserError("You cannot cancel a done record") if not rec.cancel_reason: raise UserError("Cancel Reason must be filled") diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index 6abdcc8b..e5cc94c4 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -56,10 +56,10 @@ - + + attrs="{'invisible': [('state', 'in', ['delivered_to_cust', 'draft'])]}"/> -- cgit v1.2.3 From 506f561cbebcc4677b37aaba52493eb174bcfd30 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 10 Feb 2026 19:50:16 +0700 Subject: (andri) add automatic send efaktur for special cust --- indoteknik_custom/models/account_move.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index e1360cfa..42467b78 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -587,6 +587,10 @@ class AccountMove(models.Model): records = self.search([('id', 'in', self.ids)]) template = self.env.ref('indoteknik_custom.mail_template_efaktur_document') + ICP = self.env['ir.config_parameter'].sudo() + special_partner_ids = set( + int(x) for x in (ICP.get_param('efaktur.special_partner_ids') or '').split(',') if x + ) for record in records: if record.invoice_payment_term_id.id == 26: @@ -595,6 +599,18 @@ class AccountMove(models.Model): 'attachment_ids': [(4, attachment.id)] } template.send_mail(record.id, email_values=email_values, force_send=True) + + elif record.partner_id.id in special_partner_ids: + attachment = self.generate_attachment(record) + email_list = [record.partner_id.email] if record.partner_id.email else [] + if record.real_invoice_id and record.real_invoice_id.email: + email_list.append(record.real_invoice_id.email) + + email_values = { + 'email_to': ",".join(set(email_list)), + 'attachment_ids': [(4, attachment.id)] + } + template.send_mail(record.id, email_values=email_values, force_send=True) # @api.model # def create(self, vals): -- cgit v1.2.3 From aded9892296ef3b4fd76b743081206eb89167857 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 11 Feb 2026 12:00:56 +0700 Subject: req sgr bg denise --- indoteknik_custom/models/shipment_group.py | 24 +++++++++++++++++++++--- indoteknik_custom/models/stock_picking.py | 10 ++++++++++ indoteknik_custom/views/shipment_group.xml | 6 +++++- indoteknik_custom/views/stock_picking.xml | 1 + 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index 7203b566..ce4a9fdd 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -2,6 +2,7 @@ from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError from datetime import timedelta, date import logging +from markupsafe import escape as html_escape _logger = logging.getLogger(__name__) @@ -16,7 +17,21 @@ class ShipmentGroup(models.Model): partner_id = fields.Many2one('res.partner', string='Customer') carrier_id = fields.Many2one('delivery.carrier', string='Ekspedisi') total_colly_line = fields.Float(string='Total Colly', compute='_compute_total_colly_line') + is_multi_partner = fields.Boolean(string='Is Multi Partner', compute='_compute_is_multi_partner') + partner_ids = fields.Many2many('res.partner', string='Customers', compute='_compute_partner_ids') + driver = fields.Many2one('res.users', string='Driver') + @api.depends('shipment_line.partner_id') + def _compute_partner_ids(self): + for rec in self: + rec.partner_ids = rec.shipment_line.mapped('partner_id').ids + + @api.depends('shipment_line.partner_id') + def _compute_is_multi_partner(self): + for rec in self: + partners = rec.shipment_line.mapped('partner_id') + rec.is_multi_partner = len(partners) > 1 + def sync_api_shipping(self): for rec in self.shipment_line: picking_names = [lines.picking_id.name for lines in self.shipment_line] @@ -97,14 +112,14 @@ class ShipmentGroupLine(models.Model): @api.onchange('picking_id') def onchange_picking_id(self): if self.picking_id: - picking = self.env['stock.picking'].browse(self.picking_id.id) + picking = self.picking_id if self.shipment_id.carrier_id and self.shipment_id.carrier_id != picking.carrier_id: raise UserError('carrier must be same as shipment group') - + if picking.total_mapping_koli == 0: raise UserError(f'Picking {picking.name} tidak memiliki mapping koli') - + self.partner_id = picking.partner_id self.shipping_paid_by = picking.sale_id.shipping_paid_by self.carrier_id = picking.carrier_id.id @@ -115,6 +130,9 @@ class ShipmentGroupLine(models.Model): self.sale_id = picking.sale_id + if self.shipment_id: + self.shipment_id._compute_is_multi_partner() + @api.model def create(self, vals): record = super(ShipmentGroupLine, self).create(vals) diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 2465fa96..af3841b2 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -203,6 +203,16 @@ class StockPicking(models.Model): is_so_fiktif = fields.Boolean('SO Fiktif?', compute='_compute_is_so_fiktif', tracking=3) payment_term = fields.Char('Payment Term', compute='_get_partner_payment_term') is_rev_tg = fields.Boolean('Administrasi') + shipment_group_id = fields.Many2one('shipment.group', string='Shipment Group', compute='_compute_shipment_group_id') + + @api.depends('id', 'shipment_group_id') + def _compute_shipment_group_id(self): + for record in self: + shipment_line = self.env['shipment.group.line'].search([('picking_id', '=', record.id)], limit=1) + if shipment_line: + record.shipment_group_id = shipment_line.shipment_id.id + else: + record.shipment_group_id = False @api.depends('sale_id.payment_term_id') def _get_partner_payment_term(self): diff --git a/indoteknik_custom/views/shipment_group.xml b/indoteknik_custom/views/shipment_group.xml index c3f79bda..e348867c 100644 --- a/indoteknik_custom/views/shipment_group.xml +++ b/indoteknik_custom/views/shipment_group.xml @@ -7,6 +7,7 @@ + @@ -42,9 +43,12 @@ + - + + + diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 9aa0581c..9cd63e25 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -245,6 +245,7 @@ + -- cgit v1.2.3 From bb3129b395d5399e08378ca0c2b9fd9dc5dbdb56 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 11 Feb 2026 12:02:47 +0700 Subject: push --- indoteknik_custom/models/stock_picking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index af3841b2..ab6d2966 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -205,7 +205,7 @@ class StockPicking(models.Model): is_rev_tg = fields.Boolean('Administrasi') shipment_group_id = fields.Many2one('shipment.group', string='Shipment Group', compute='_compute_shipment_group_id') - @api.depends('id', 'shipment_group_id') + @api.depends('shipment_group_id') def _compute_shipment_group_id(self): for record in self: shipment_line = self.env['shipment.group.line'].search([('picking_id', '=', record.id)], limit=1) -- cgit v1.2.3 From 6c749fd5c8deee7e38cc123b77e06b938bdcc3b3 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 11 Feb 2026 17:25:00 +0700 Subject: try fix cannot validate ort --- indoteknik_custom/models/stock_picking.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index ab6d2966..065b1484 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1397,7 +1397,8 @@ class StockPicking(models.Model): ]) if quant: - return quant.quantity + return sum(quant.mapped('quantity')) + # return quant.quantity return 0 -- cgit v1.2.3 From 387334dc0929087ac51cc09a90d2db15d79413f1 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 12 Feb 2026 14:18:25 +0700 Subject: automatic reconcile by GL and automatic penyusutan asset --- indoteknik_custom/__manifest__.py | 3 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/account_move_line.py | 65 +++++++++++++++++++++- .../models/update_depreciation_move_wizard.py | 48 ++++++++++++++++ indoteknik_custom/security/ir.model.access.csv | 1 + indoteknik_custom/views/account_move_line.xml | 12 ++++ .../views/update_depreciation_move_wizard_view.xml | 35 ++++++++++++ 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 indoteknik_custom/models/update_depreciation_move_wizard.py create mode 100644 indoteknik_custom/views/update_depreciation_move_wizard_view.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 61eef35b..51cfa592 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -193,7 +193,8 @@ 'views/close_tempo_mail_template.xml', 'views/domain_apo.xml', 'views/uom_uom.xml', - 'views/commission_internal.xml' + 'views/commission_internal.xml', + 'views/update_depreciation_move_wizard_view.xml' ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a14c766e..19a96bee 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,3 +165,4 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal +from . import update_depreciation_move_wizard \ No newline at end of file diff --git a/indoteknik_custom/models/account_move_line.py b/indoteknik_custom/models/account_move_line.py index 7c95d4ef..5edea25f 100644 --- a/indoteknik_custom/models/account_move_line.py +++ b/indoteknik_custom/models/account_move_line.py @@ -1,5 +1,5 @@ from odoo import models, api, fields - +from odoo.exceptions import AccessError, UserError, ValidationError class AccountMoveLine(models.Model): _inherit = "account.move.line" @@ -9,6 +9,69 @@ class AccountMoveLine(models.Model): analytic_account_ids = fields.Many2many('account.analytic.account', string='Analytic Account') line_no = fields.Integer('No', default=0) + def action_gl_reconcile(self): + lines = self + + journal = self.env['account.journal'].search([ + ('suspense_account_id', '=', lines[0].account_id.id) + ], limit=1) + + if not journal: + raise UserError('Journal dengan suspense account ini tidak ditemukan!') + + statement = self.env['account.bank.statement'].create({ + 'journal_id': journal.id, + 'name': f'REKONSIL {journal.name} {lines[0].date.strftime("%d-%m-%Y")}', + 'company_id': self.env.company.id, + 'date': lines[0].date, + }) + + widget_vals = [] + st_line_ids = [] + + for line in lines: + amount = line.debit - line.credit + + st_line = self.env['account.bank.statement.line'].create({ + 'statement_id': statement.id, + 'date': line.date or fields.Date.today(), + 'payment_ref': line.name, + 'partner_id': line.partner_id.id, + 'amount': amount, + 'ref': line.name, + }) + + st_line_ids.append(st_line.id) + + widget_vals.append({ + 'partner_id': st_line.partner_id.id, + 'counterpart_aml_dicts': [{ + 'counterpart_aml_id': line.id, + 'debit': abs(amount) if amount < 0 else 0, + 'credit': abs(amount) if amount > 0 else 0, + 'name': line.name or '/', + }], + 'payment_aml_ids': [], + 'new_aml_dicts': [], + 'to_check': False, + }) + + statement.button_post() + + self.env['account.reconciliation.widget'].process_bank_statement_line( + st_line_ids, + widget_vals + ) + # statement.button_validate_or_action() + + return { + 'effect': { + 'fadeout': 'slow', + 'message': 'Statement + Auto Reconcile sukses besar 😎🔥', + 'type': 'rainbow_man', + } + } + @api.onchange('account_id') def _onchange_account_id(self): for account in self: diff --git a/indoteknik_custom/models/update_depreciation_move_wizard.py b/indoteknik_custom/models/update_depreciation_move_wizard.py new file mode 100644 index 00000000..7d465f1d --- /dev/null +++ b/indoteknik_custom/models/update_depreciation_move_wizard.py @@ -0,0 +1,48 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError + +class UpdateDepreciationMoveWizard(models.TransientModel): + _name = 'update.depreciation.move.wizard' + _description = 'Wizard untuk Update Move Check Depreciation Line' + + target_date = fields.Date(string="Tanggal Depresiasi", required=True) + + # def action_update_move_check(self): + # lines = self.env['account.asset.depreciation.line'].search([ + # ('depreciation_date', '=', self.target_date), + # ]) + # if not lines: + # raise UserError("Tidak ada baris depresiasi dengan tanggal tersebut.") + + # updated_count = 0 + # for line in lines: + # if not line.move_check: + # line.move_check = True + # line.move_posted_check = True + # updated_count += 1 + + # return { + # 'type': 'ir.actions.client', + # 'tag': 'display_notification', + # 'params': { + # 'title': 'Update Selesai', + # 'message': f'{updated_count} baris berhasil di-update.', + # 'type': 'success', + # 'sticky': False, + # } + # } + + def action_update_move_check(self): + assets = self.env['account.asset.asset'] + assets.compute_generated_entries(self.target_date) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Update Selesai', + 'message': 'Depresiasi berhasil di-update.', + 'type': 'success', + 'sticky': False, + } + } \ No newline at end of file diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index d501de1a..bc290370 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -215,3 +215,4 @@ access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1 access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1 access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 access_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1 +access_update_depreciation_move_wizard,access.update.depreciation.move.wizard,model_update_depreciation_move_wizard,,1,1,1,1 \ No newline at end of file diff --git a/indoteknik_custom/views/account_move_line.xml b/indoteknik_custom/views/account_move_line.xml index cb24a0f0..838596c8 100644 --- a/indoteknik_custom/views/account_move_line.xml +++ b/indoteknik_custom/views/account_move_line.xml @@ -22,4 +22,16 @@ + + + Reconcile Selected + + + list + code + + action = records.action_gl_reconcile() + + + diff --git a/indoteknik_custom/views/update_depreciation_move_wizard_view.xml b/indoteknik_custom/views/update_depreciation_move_wizard_view.xml new file mode 100644 index 00000000..ff128a71 --- /dev/null +++ b/indoteknik_custom/views/update_depreciation_move_wizard_view.xml @@ -0,0 +1,35 @@ + + + + update.depreciation.move.wizard.form + update.depreciation.move.wizard + +
    + + + +
    +
    +
    +
    +
    + + + Update Depreciation Asset + ir.actions.act_window + update.depreciation.move.wizard + form + new + + + +
    + \ No newline at end of file -- cgit v1.2.3 From 55a8c1e5b204bd19733f3197b253293bc00fdf32 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 13 Feb 2026 09:42:10 +0700 Subject: fix conflict --- indoteknik_custom/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index b2e94858..6287aace 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -194,7 +194,7 @@ 'views/domain_apo.xml', 'views/uom_uom.xml', 'views/commission_internal.xml', - 'views/gudang_service.xml' + 'views/gudang_service.xml', 'views/update_depreciation_move_wizard_view.xml' ], 'demo': [], -- cgit v1.2.3 From 37e0beac646ee2e676ff935e8289cf3189b3c21b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 13 Feb 2026 09:42:53 +0700 Subject: check archived uom and product in sale order confirm --- indoteknik_custom/models/sale_order.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 49e36279..2548c7b3 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2598,6 +2598,18 @@ class SaleOrder(models.Model): else: return False + def check_archived_product(self): + for order in self: + for line in order.order_line: + if line.product_id.active == False: + raise UserError("Terdapat Product yang sudah di Archive pada Product: {}".format(line.product_id.display_name)) + + def check_archived_uom(self): + for order in self: + for line in order.order_line: + if line.product_uom.active == False: + raise UserError("Terdapat UoM yang sudah di Archive pada UoM {} di Product {}".format(line.product_uom.name, line.product_id.display_name)) + def action_confirm(self): for order in self: order._validate_delivery_amt() @@ -2614,6 +2626,8 @@ class SaleOrder(models.Model): order._validate_order() order._validate_npwp() order.order_line.validate_line() + order.check_archived_product() + order.check_archived_uom() main_parent = order.partner_id.get_main_parent() SYSTEM_UID = 25 -- cgit v1.2.3 From 30e7aae41d50528a4e61e6f55f38e3dd647e41c6 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 14 Feb 2026 11:14:22 +0700 Subject: merge --- indoteknik_custom/models/__init__.py | 5 +---- indoteknik_custom/security/ir.model.access.csv | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 2b1851ce..3b3dcd9d 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -165,8 +165,5 @@ from . import partial_delivery from . import domain_apo from . import uom_uom from . import commission_internal -<<<<<<< HEAD from . import gudang_service -======= -from . import update_depreciation_move_wizard ->>>>>>> 37e0beac646ee2e676ff935e8289cf3189b3c21b +from . import update_depreciation_move_wizard \ No newline at end of file diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 50e2b382..9859b127 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -217,4 +217,4 @@ access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 access_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1 access_gudang_service,gudang.service,model_gudang_service,base.group_user,1,1,1,1 access_gudang_service_line,gudang.service.line,model_gudang_service_line,base.group_user,1,1,1,1 -access_update_depreciation_move_wizard,access.update.depreciation.move.wizard,model_update_depreciation_move_wizard,,1,1,1,1 +access_update_depreciation_move_wizard,access.update.depreciation.move.wizard,model_update_depreciation_move_wizard,,1,1,1,1 \ No newline at end of file -- cgit v1.2.3 From 57221d2537684168a4761ce2eb0b2ede5586325f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 14 Feb 2026 12:02:15 +0700 Subject: fix flow approval state adjust out --- indoteknik_custom/models/stock_inventory.py | 44 +++++++++++++++++------------ indoteknik_custom/views/stock_inventory.xml | 7 ++--- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/indoteknik_custom/models/stock_inventory.py b/indoteknik_custom/models/stock_inventory.py index cb7d3773..b3580a4c 100644 --- a/indoteknik_custom/models/stock_inventory.py +++ b/indoteknik_custom/models/stock_inventory.py @@ -16,11 +16,35 @@ class StockInventory(models.Model): ('in', 'Adjusment In'), ('out', 'Adjusment Out'), ], string='Adjusments Type', required=True) + approval_state = fields.Selection([ ('logistic', 'Logistic'), ('accounting', 'Accounting'), ('approved', 'Approved'), - ], default='logistic', tracking=True) + ], default='logistic', tracking=True, readonly=True) + + def action_validate(self): + if self.adjusment_type == 'out': + + if self.approval_state != 'approved': + + if self.approval_state == 'logistic': + if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Adjustment Out harus dilakukan oleh Logistic") + self.approval_state = 'accounting' + return True + + elif self.approval_state == 'accounting': + if not self.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Adjustment Out harus dilakukan oleh Accounting") + self.approval_state = 'approved' + return True + + else: + raise UserError("Adjustment Out harus melalui approval terlebih dahulu.") + + return super(StockInventory, self).action_validate() + def _generate_number_stock_inventory(self): """Men-generate nomor untuk semua stock inventory yang belum memiliki number.""" @@ -58,10 +82,7 @@ class StockInventory(models.Model): return "00001" # Jika belum ada data, mulai dari 00001 def action_start(self): - if self.approval_state != 'approved' and self.adjusment_type == 'out': - raise UserError('Harus melalui proses approval') - if self.adjusment_type == 'in': - if self.env.user.id not in [21, 17, 571, 28]: + if self.env.user.id not in [21, 17, 571, 28, 25]: raise UserError("Hanya Rafly, Denise, Iqmal, dan Stephan yang bisa start inventory") return super(StockInventory, self).action_start() @@ -78,19 +99,6 @@ class StockInventory(models.Model): return order - def action_approve(self): - if self.adjusment_type == 'out': - for rec in self: - if rec.approval_state == 'logistic': - if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Harus diapprove logistic") - rec.approval_state = 'accounting' - elif rec.approval_state == 'accounting': - if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): - raise UserError("Harus diapprove accounting") - rec.approval_state = 'approved' - else: - raise UserError("Sudah Approved") def write(self, vals): """Jika adjusment_type diubah, generate ulang nomor.""" diff --git a/indoteknik_custom/views/stock_inventory.xml b/indoteknik_custom/views/stock_inventory.xml index 89c058ea..ab1b6eec 100644 --- a/indoteknik_custom/views/stock_inventory.xml +++ b/indoteknik_custom/views/stock_inventory.xml @@ -6,14 +6,10 @@ stock.inventory -
    -
    - +
    @@ -26,6 +22,7 @@ + -- cgit v1.2.3 From d951091bcb1bcab9d5aedf8060bbc1cd8905326c Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sun, 15 Feb 2026 00:05:12 +0700 Subject: fix state typo & code improvement --- indoteknik_custom/models/gudang_service.py | 65 ++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 4900113f..5d89cfad 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -45,15 +45,49 @@ class GudangService(models.Model): ('cancel', 'Cancel')], default='draft', tracking=True) cancel_reason = fields.Text('Cancel Reason', tracking=True) + @api.constrains('gudang_service_lines') + def _check_qty(self): + for rec in self: + if not rec.origin: + continue + + so_qty_map = { + line.product_id.id: line.product_uom_qty + for line in rec.origin.order_line + } + + for line in rec.gudang_service_lines: + if line.quantity <= 0: + raise ValidationError( + f"Quantity for product {line.product_id.display_name} cannot be 0 or negative." + ) + + so_qty = so_qty_map.get(line.product_id.id, 0) + + if line.quantity > so_qty: + raise ValidationError( + f"Quantity for product {line.product_id.display_name} " + f"cannot exceed SO quantity ({so_qty})." + ) + + + @api.constrains('state', 'schedule_date') + def _check_edit_after_sent(self): + for rec in self: + if rec.state in ['sent_to_vendor', 'received_from_vendor', 'delivered_to_cust']: + if rec._origin.schedule_date != rec.schedule_date: + raise ValidationError("Schedule cannot be modified after sent to vendor") + + def _send_logistic_notification(self): group = self.env.ref('indoteknik_custom.group_role_logistic', raise_if_not_found=False) if not group: return users = group.users - # Safa - md = self.env['res.users'].browse([3425]) - # send to logistic and safa + # MD + md = self.env['res.users'].browse([3425, 4801, 1036]) + # send to logistic and MD users = users | md if not users: @@ -88,7 +122,7 @@ class GudangService(models.Model): @api.model def cron_notify_onprogress_gudang_service(self): records = self.search([ - ('state', 'in', ['draft', 'received_from_customer']), + ('state', 'in', ['draft', 'received_from_cust']), ]) if records: @@ -127,19 +161,16 @@ class GudangService(models.Model): rec.state = 'received_from_vendor' def action_done(self): - if self.state != 'received_from_vendor': - raise UserError("Only 'Received From Vendor' state can be set to Done") - else: - for rec in self: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ('state', '=', 'delivered_to_cust'), - ]) - activities.unlink() - rec.state = 'delivered_to_cust' - if not rec.done_date: - rec.done_date = fields.Datetime.now() + for rec in self: + if rec.state != 'received_from_vendor': + raise UserError("Only 'Received From Vendor' state can be set to Done") + + rec.activity_ids.unlink() + + rec.write({ + 'state': 'delivered_to_cust', + 'done_date': fields.Datetime.now() + }) def action_draft(self): """Reset to draft state""" -- cgit v1.2.3 From c08d16f53c4e2c97e74f33018e00efabd08664b3 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sun, 15 Feb 2026 17:36:26 +0700 Subject: push --- indoteknik_custom/views/gudang_service.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/views/gudang_service.xml b/indoteknik_custom/views/gudang_service.xml index e5cc94c4..769664c5 100644 --- a/indoteknik_custom/views/gudang_service.xml +++ b/indoteknik_custom/views/gudang_service.xml @@ -54,7 +54,7 @@ - + -- cgit v1.2.3 From 9e10c2db3c49bbca718061dbd852b8db513bed0f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sun, 15 Feb 2026 17:37:04 +0700 Subject: fix flow adjust out --- indoteknik_custom/models/stock_inventory.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/stock_inventory.py b/indoteknik_custom/models/stock_inventory.py index b3580a4c..efd52e5c 100644 --- a/indoteknik_custom/models/stock_inventory.py +++ b/indoteknik_custom/models/stock_inventory.py @@ -21,7 +21,7 @@ class StockInventory(models.Model): ('logistic', 'Logistic'), ('accounting', 'Accounting'), ('approved', 'Approved'), - ], default='logistic', tracking=True, readonly=True) + ], tracking=True, readonly=True) def action_validate(self): if self.adjusment_type == 'out': @@ -38,7 +38,7 @@ class StockInventory(models.Model): if not self.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Adjustment Out harus dilakukan oleh Accounting") self.approval_state = 'approved' - return True + return super(StockInventory, self).action_validate() else: raise UserError("Adjustment Out harus melalui approval terlebih dahulu.") @@ -89,6 +89,12 @@ class StockInventory(models.Model): @api.model def create(self, vals): """Pastikan nomor hanya dibuat saat penyimpanan.""" + + if vals.get('adjusment_type') == 'in': + vals['approval_state'] = False + elif vals.get('adjusment_type') == 'out': + vals['approval_state'] = 'logistic' + if 'adjusment_type' in vals and not vals.get('number'): vals['number'] = False # Jangan buat number otomatis dulu @@ -105,6 +111,10 @@ class StockInventory(models.Model): res = super(StockInventory, self).write(vals) if 'adjusment_type' in vals: for record in self: + if record.adjusment_type == 'in': + record.approval_state = False + elif record.adjusment_type == 'out' and record.approval_state == False: + record.approval_state = 'logistic' self._assign_number(record) return res -- cgit v1.2.3 From 63ef16f9cef869a7bd855e1568272e035ef55b34 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Sun, 15 Feb 2026 19:37:50 +0700 Subject: hide pos & tiki biteship --- indoteknik_custom/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 49e36279..b7a44702 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -647,7 +647,7 @@ class SaleOrder(models.Model): def _get_biteship_courier_codes(self): return [ - 'gojek','grab','deliveree','lalamove','jne','tiki','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','pos','anteraja','sap','paxel','borzo' + 'gojek','grab','deliveree','lalamove','jne','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','anteraja','sap','paxel','borzo' ] @api.onchange('carrier_id') -- cgit v1.2.3 From 2cf62d4c23e8fbd0770ba05cb4d1f65032bccdf0 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 16 Feb 2026 13:33:02 +0700 Subject: check duplicated mgs docs --- indoteknik_custom/models/gudang_service.py | 239 ++++++++++++++++++----------- 1 file changed, 150 insertions(+), 89 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index 4900113f..b7ccc44f 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -8,51 +8,81 @@ from collections import defaultdict class GudangService(models.Model): _name = "gudang.service" _description = "Gudang Service" - _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'id asc' - - name = fields.Char('Name', readonly=True) - partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - vendor_id = fields.Many2one('res.partner', string='Vendor Service', required=True) - origin = fields.Many2one('sale.order', string='Origin SO', required=True, domain=[('state', 'in', ['done', 'sale'])]) - schedule_date = fields.Date( - string="Schedule Date", + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "id asc" + + name = fields.Char("Name", readonly=True) + partner_id = fields.Many2one("res.partner", string="Customer", readonly=True) + vendor_id = fields.Many2one("res.partner", string="Vendor Service", required=True) + origin = fields.Many2one( + "sale.order", + string="Origin SO", required=True, - tracking=True + domain=[("state", "in", ["done", "sale"])], ) - start_date = fields.Datetime( - string="Date Processed", - copy=False, - tracking=True + schedule_date = fields.Date(string="Schedule Date", required=True, tracking=True) + start_date = fields.Datetime(string="Date Processed", copy=False, tracking=True) + create_date = fields.Datetime( + string="Create Date", copy=False, tracking=True, default=fields.Datetime.now() + ) + done_date = fields.Datetime(string="Date Done", copy=False, tracking=True) + gudang_service_lines = fields.One2many( + "gudang.service.line", "gudang_service_id", string="Gudang Service Lines" ) - create_date = fields.Datetime(string='Create Date', copy=False, tracking=True, default=fields.Datetime.now()) - done_date = fields.Datetime(string='Date Done', copy=False, tracking=True) - gudang_service_lines = fields.One2many('gudang.service.line', 'gudang_service_id', string='Gudang Service Lines') # unprocessed_date = fields.Char( # string='Unprocessed Since', # compute='_compute_unprocessed_date' # ) remaining_date = fields.Char( - compute='_compute_remaining_date', - string='Date Status' + compute="_compute_remaining_date", string="Date Status" + ) + state = fields.Selection( + [ + ("draft", "Backlog"), + ("received_from_cust", "Received From Customer"), + ("sent_to_vendor", "Sent to Service Vendor"), + ("received_from_vendor", "Received From Service Vendor"), + ("delivered_to_cust", "Delivered to Customer"), + ("cancel", "Cancel"), + ], + default="draft", + tracking=True, ) - state = fields.Selection([ - ('draft', 'Backlog'), - ('received_from_cust', 'Received From Customer'), - ('sent_to_vendor', 'Sent to Service Vendor'), - ('received_from_vendor', 'Received From Service Vendor'), - ('delivered_to_cust', 'Delivered to Customer'), - ('cancel', 'Cancel')], default='draft', tracking=True) - cancel_reason = fields.Text('Cancel Reason', tracking=True) + cancel_reason = fields.Text("Cancel Reason", tracking=True) + + def check_duplicate_docs(self): + for rec in self: + found = self.env["gudang.service"].search( + [ + ("id", "!=", self.id), + ("origin.id", "=", self.origin.id), + ("partner_id.id", "=", rec.partner_id.id), + ( + "gudang_service_lines.product_id.name", + "=", + rec.gudang_service_lines.product_id.name, + ), + ( + "gudang_service_lines.quantity", + "=", + rec.gudang_service_lines.quantity, + ), + ("vendor_id.id", "=", rec.vendor_id.id), + ] + ) + if found: + raise UserError("This Document has duplicate with %s" % found.name) def _send_logistic_notification(self): - group = self.env.ref('indoteknik_custom.group_role_logistic', raise_if_not_found=False) + group = self.env.ref( + "indoteknik_custom.group_role_logistic", raise_if_not_found=False + ) if not group: return users = group.users # Safa - md = self.env['res.users'].browse([3425]) + md = self.env["res.users"].browse([3425]) # send to logistic and safa users = users | md @@ -66,35 +96,53 @@ class GudangService(models.Model): for user in users: if user.id in excluded_users: continue - self.env['mail.activity'].create({ - 'res_model_id': self.env['ir.model']._get_id('gudang.service'), - 'res_id': rec.id, - 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, - 'user_id': user.id, - 'summary': 'Gudang Service On Progress', - 'note': _( - 'Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s' - ) % (rec.name, rec.schedule_date), - # 'date_deadline': fields.Date.today(), - }) + self.env["mail.activity"].create( + { + "res_model_id": self.env["ir.model"]._get_id("gudang.service"), + "res_id": rec.id, + "activity_type_id": self.env.ref( + "mail.mail_activity_data_todo" + ).id, + "user_id": user.id, + "summary": "Gudang Service On Progress", + "note": _( + "Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s" + ) + % (rec.name, rec.schedule_date), + # 'date_deadline': fields.Date.today(), + } + ) # kirim ke private message odoo - channel = self.env['mail.channel'].channel_get([self.env.user.partner_id.id, user.partner_id.id]).get('id') + channel = ( + self.env["mail.channel"] + .channel_get([self.env.user.partner_id.id, user.partner_id.id]) + .get("id") + ) if not channel: continue - res = self.env['mail.channel'].browse(channel) - res.with_user(self.env.user.browse(25)).message_post(body=_('Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s') % (rec.name, rec.schedule_date), message_type='comment', subtype_xmlid='mail.mt_comment') + res = self.env["mail.channel"].browse(channel) + res.with_user(self.env.user.browse(25)).message_post( + body=_( + "Ada Jadwal Service Barang di Document %s Jadwal Service 📅 %s" + ) + % (rec.name, rec.schedule_date), + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) @api.model def cron_notify_onprogress_gudang_service(self): - records = self.search([ - ('state', 'in', ['draft', 'received_from_customer']), - ]) + records = self.search( + [ + ("state", "in", ["draft", "received_from_customer"]), + ] + ) if records: records._send_logistic_notification() - @api.depends('schedule_date', 'create_date') + @api.depends("schedule_date", "create_date") def _compute_remaining_date(self): today = fields.Date.today() @@ -115,29 +163,32 @@ class GudangService(models.Model): else: rec.remaining_date = _("Overdue %s days") % abs(days) - def action_submit(self): + self.ensure_one() + self.check_duplicate_docs() for rec in self: - if rec.state == 'draft': - rec.state = 'received_from_cust' - elif rec.state == 'received_from_cust': - rec.state = 'sent_to_vendor' + if rec.state == "draft": + rec.state = "received_from_cust" + elif rec.state == "received_from_cust": + rec.state = "sent_to_vendor" rec.start_date = fields.Datetime.now() - elif rec.state == 'sent_to_vendor': - rec.state = 'received_from_vendor' + elif rec.state == "sent_to_vendor": + rec.state = "received_from_vendor" def action_done(self): - if self.state != 'received_from_vendor': + if self.state != "received_from_vendor": raise UserError("Only 'Received From Vendor' state can be set to Done") else: for rec in self: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ('state', '=', 'delivered_to_cust'), - ]) + activities = self.env["mail.activity"].search( + [ + ("res_id", "=", rec.id), + ("res_model", "=", "gudang.service"), + ("state", "=", "delivered_to_cust"), + ] + ) activities.unlink() - rec.state = 'delivered_to_cust' + rec.state = "delivered_to_cust" if not rec.done_date: rec.done_date = fields.Datetime.now() @@ -145,63 +196,73 @@ class GudangService(models.Model): """Reset to draft state""" for rec in self: rec.cancel_reason = False - if rec.state == 'cancel': - rec.write({'state': 'draft'}) + if rec.state == "cancel": + rec.write({"state": "draft"}) else: raise UserError("Only Canceled Record Can Be Reset To Draft") def action_cancel(self): for rec in self: - activities = self.env['mail.activity'].search([ - ('res_id', '=', rec.id), - ('res_model', '=', 'gudang.service'), - ]) + activities = self.env["mail.activity"].search( + [ + ("res_id", "=", rec.id), + ("res_model", "=", "gudang.service"), + ] + ) activities.unlink() - if rec.state == 'delivered_to_cust': + if rec.state == "delivered_to_cust": raise UserError("You cannot cancel a done record") if not rec.cancel_reason: raise UserError("Cancel Reason must be filled") rec.start_date = False rec.done_date = False - rec.state = 'cancel' + rec.state = "cancel" @api.model def create(self, vals): # sequence - if not vals.get('name') or vals['name'] == 'New': - vals['name'] = self.env['ir.sequence'].next_by_code('gudang.service') + if not vals.get("name") or vals["name"] == "New": + vals["name"] = self.env["ir.sequence"].next_by_code("gudang.service") # partner dari SO - so = self.env['sale.order'].browse(vals['origin']) - vals['partner_id'] = so.partner_id.id + so = self.env["sale.order"].browse(vals["origin"]) + vals["partner_id"] = so.partner_id.id res = super(GudangService, self).create(vals) + res.check_duplicate_docs() res._send_logistic_notification() return res - + def write(self, vals): - if vals.get('origin'): - so = self.env['sale.order'].browse(vals['origin']) - vals['partner_id'] = so.partner_id.id + if vals.get("origin"): + so = self.env["sale.order"].browse(vals["origin"]) + vals["partner_id"] = so.partner_id.id + vals.check_duplicate_docs() return super(GudangService, self).write(vals) - @api.onchange('origin') + @api.onchange("origin") def _onchange_origin(self): if not self.origin: self.gudang_service_lines = [(5, 0, 0)] return - + self.partner_id = self.origin.partner_id lines = [] for line in self.origin.order_line: - lines.append((0, 0, { - 'product_id': line.product_id.id, - 'quantity': line.product_uom_qty, - 'origin_so': self.origin.id, - })) + lines.append( + ( + 0, + 0, + { + "product_id": line.product_id.id, + "quantity": line.product_uom_qty, + "origin_so": self.origin.id, + }, + ) + ) # hapus line lama lalu isi baru self.gudang_service_lines = [(5, 0, 0)] + lines @@ -209,9 +270,9 @@ class GudangService(models.Model): class GudangServiceLine(models.Model): _name = "gudang.service.line" - _inherit = ['mail.thread', 'mail.activity.mixin'] + _inherit = ["mail.thread", "mail.activity.mixin"] - product_id = fields.Many2one('product.product', string='Product') - quantity = fields.Float(string='Quantity') - origin_so = fields.Many2one('sale.order', string='Origin SO') - gudang_service_id = fields.Many2one('gudang.service', string='Gudang Service ID') \ No newline at end of file + product_id = fields.Many2one("product.product", string="Product") + quantity = fields.Float(string="Quantity") + origin_so = fields.Many2one("sale.order", string="Origin SO") + gudang_service_id = fields.Many2one("gudang.service", string="Gudang Service ID") -- cgit v1.2.3 From 3b0686c3cd83dde114359e5a441d2d7b2c0ebc3f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 16 Feb 2026 14:39:52 +0700 Subject: add more validation in duplicate docs --- indoteknik_custom/models/gudang_service.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/indoteknik_custom/models/gudang_service.py b/indoteknik_custom/models/gudang_service.py index bd8ec8bd..d699ccf4 100644 --- a/indoteknik_custom/models/gudang_service.py +++ b/indoteknik_custom/models/gudang_service.py @@ -57,6 +57,8 @@ class GudangService(models.Model): ("id", "!=", self.id), ("origin.id", "=", self.origin.id), ("partner_id.id", "=", rec.partner_id.id), + ("vendor_id.id", "=", rec.vendor_id.id), + ("schedule_date", "=", rec.schedule_date), ( "gudang_service_lines.product_id.name", "=", @@ -67,11 +69,10 @@ class GudangService(models.Model): "=", rec.gudang_service_lines.quantity, ), - ("vendor_id.id", "=", rec.vendor_id.id), ] ) if found: - raise UserError("This Document has duplicate with %s" % found.name) + raise UserError("This Document has duplicate with %s" % found.name) def _send_logistic_notification(self): group = self.env.ref( @@ -82,7 +83,7 @@ class GudangService(models.Model): users = group.users # MD - md = self.env['res.users'].browse([3425, 4801, 1036]) + md = self.env["res.users"].browse([3425, 4801, 1036]) # send to logistic and MD users = users | md @@ -177,15 +178,14 @@ class GudangService(models.Model): def action_done(self): for rec in self: - if rec.state != 'received_from_vendor': + if rec.state != "received_from_vendor": raise UserError("Only 'Received From Vendor' state can be set to Done") rec.activity_ids.unlink() - rec.write({ - 'state': 'delivered_to_cust', - 'done_date': fields.Datetime.now() - }) + rec.write( + {"state": "delivered_to_cust", "done_date": fields.Datetime.now()} + ) def action_draft(self): """Reset to draft state""" -- cgit v1.2.3 From a461efcda1637c267b3c4195eb0f7d649f10f145 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 16 Feb 2026 15:22:10 +0700 Subject: change driver to text --- indoteknik_custom/models/shipment_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py index ce4a9fdd..5f5f2d79 100644 --- a/indoteknik_custom/models/shipment_group.py +++ b/indoteknik_custom/models/shipment_group.py @@ -19,7 +19,7 @@ class ShipmentGroup(models.Model): total_colly_line = fields.Float(string='Total Colly', compute='_compute_total_colly_line') is_multi_partner = fields.Boolean(string='Is Multi Partner', compute='_compute_is_multi_partner') partner_ids = fields.Many2many('res.partner', string='Customers', compute='_compute_partner_ids') - driver = fields.Many2one('res.users', string='Driver') + driver = fields.Text(string='Driver') @api.depends('shipment_line.partner_id') def _compute_partner_ids(self): -- cgit v1.2.3 From 6062eff60486c37144e2672922ad7a5d8333d5f4 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 17 Feb 2026 16:42:40 +0700 Subject: fix merge --- indoteknik_custom/__manifest__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 262096e4..ca23995c 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -195,7 +195,6 @@ 'views/uom_uom.xml', 'views/update_depreciation_move_wizard_view.xml', 'views/commission_internal.xml', - 'views/update_depreciation_move_wizard_view.xml', 'views/keywords.xml' ], 'demo': [], -- cgit v1.2.3 From 2571d2d4d14c843f65e552fa93726b1f0810f42d Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 17 Feb 2026 16:44:37 +0700 Subject: remove unused views --- indoteknik_custom/__manifest__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index ca23995c..36588967 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -178,17 +178,13 @@ 'views/refund_sale_order.xml', 'views/advance_payment_request.xml', 'views/advance_payment_settlement.xml', - # 'views/refund_sale_order.xml', 'views/tukar_guling.xml', - # 'views/tukar_guling_return_views.xml' 'views/tukar_guling_po.xml', - # 'views/refund_sale_order.xml', 'views/update_date_planned_po_wizard_view.xml', 'views/unpaid_invoice_view.xml', 'views/letter_receivable.xml', 'views/letter_receivable_mail_template.xml', 'views/mail_template_pum.xml', - # 'views/reimburse.xml', 'views/sj_tele.xml', 'views/close_tempo_mail_template.xml', 'views/domain_apo.xml', -- cgit v1.2.3 From 48d95b4e9f11eac11931a4679b0cc5cc3249e69d Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 13:06:30 +0700 Subject: remove brand --- indoteknik_custom/models/keywords.py | 118 ++++++++--------------------------- 1 file changed, 27 insertions(+), 91 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index b5c7ae8c..f4774bbd 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -41,25 +41,16 @@ class Keywords(models.Model): for record in self: record.sum = len(record.product_ids) - - @api.depends('keywords', 'brand_id') + @api.depends('keywords') def _compute_url(self): prefix = "https://indoteknik.com/searchkey/" for record in self: - if not record.keywords: - record.url = False - continue - slug = re.sub(r'[^a-zA-Z0-9]+', '-', record.keywords.strip().lower()).strip('-') - - if record.brand_id: - brand_slug = re.sub( - r'[^a-zA-Z0-9]+', - '-', - record.brand_id.x_name.strip().lower() - ).strip('-') - record.url = f"{prefix}{slug}-{brand_slug}" + if record.keywords: + slug = re.sub(r'[^a-zA-Z0-9]+', '-', record.keywords.strip().lower()) + slug = slug.strip('-') + record.url = prefix + slug else: - record.url = f"{prefix}{slug}" + record.url = False def _compute_name(self): for record in self: @@ -68,14 +59,13 @@ class Keywords(models.Model): @api.constrains('keywords', 'category_id') def check_already_exist(self): + model = self.env['keywords'] for record in self: - if not (record.keywords and record.category_id): - continue - match = self.search([ - ('id', '!=', record.id), + match = model.search([ ('keywords', '=', record.keywords), - ('category_id', '=', record.category_id.id) - ], limit = 1) + ('category_id.id', '=', record.category_id.id), + ('id', '!=', record.id) + ]) if match: raise UserError("Tidak bisa create karena keywords sudah dipakai") @@ -87,23 +77,14 @@ class Keywords(models.Model): for record in self: record.product_ids = [(5, 0, 0)] - def generate_products(self): for record in self: - if record.skip: + if not record.keywords or record.skip: continue - if not record.keywords: - raise UserError("Keyword wajib diisi") - - if not record.category_id: - raise UserError( - f"Category wajib diisi untuk keyword '{record.keywords}'" - ) - - keyword_raw = record.keywords.strip() - keyword = f"%{keyword_raw}%" + keyword = f"%{record.keywords.strip()}%" + # AND (pt.unpublished IS FALSE OR pt.unpublished IS NULL) sql = """ SELECT DISTINCT pp.id FROM product_product pp @@ -119,66 +100,26 @@ class Keywords(models.Model): ) """ - params = [keyword, keyword] - - # ====================== - # CATEGORY (WAJIB) - # ====================== - child_categs = self.env['product.public.category'].search([ - ('id', 'child_of', record.category_id.id) - ]) - - sql += " AND rel.product_public_category_id = ANY(%s)" - params.append(child_categs.ids) - - # ====================== - # BRAND (OPTIONAL) - # ====================== - if record.brand_id: - brand = record.brand_id.x_name + params = [ + keyword, + keyword, + ] - brand_kw_1 = f"%{brand} {keyword_raw}%" - brand_kw_2 = f"%{keyword_raw} {brand}%" - keyword = f"%{keyword_raw}%" - sql += "AND pt.x_manufacture = %s" - - sql += """ - AND ( - pt.name ILIKE %s - OR pt.name ILIKE %s - OR pt.website_description ILIKE %s - OR pt.website_description ILIKE %s - ) - OR ( - pt.x_manufacture = %s - AND (pt.name ilike %s - OR pt.website_description ilike %s) - ) - """ - - params.extend([ - record.brand_id.id, - brand_kw_1, - brand_kw_2, - brand_kw_1, - brand_kw_2, - record.brand_id.id, - keyword, - keyword + if record.category_id: + child_categs = self.env['product.public.category'].search([ + ('id', 'child_of', record.category_id.id) ]) + sql += " AND rel.product_public_category_id = ANY(%s)" + params.append(child_categs.ids) - # ====================== - # EXECUTE - # ====================== self.env.cr.execute(sql, params) - rows = self.env.cr.fetchall() + rows = self.env.cr.fetchall() product_ids = [r[0] for r in rows] if not product_ids: raise UserError( - f"Tidak menemukan product untuk keyword " - f"'{record.keywords}' di category '{record.category_id.name}'" + f"Tidak berhasil menemukan barang untuk keyword '{record.keywords}'" ) record.with_context(skip_generate=True).write({ @@ -186,10 +127,9 @@ class Keywords(models.Model): }) _logger.info( - "Product Found: %s products | keyword='%s' | category='%s'", + "Product Found: Found %s products for keyword '%s'", len(product_ids), - record.keywords, - record.category_id.name + record.keywords ) def sync_solr(self): @@ -197,16 +137,12 @@ class Keywords(models.Model): if not active_ids: _logger.warning("No active_ids found, nothing to sync") return True - elif active_ids and self.sum < 1: - raise UserError("Tidak ada product untuk keyword ini, sync ke solr dibatalkan") keywords = self.browse(active_ids) documents = [] for keyword in keywords: searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') - if keyword.brand_id: - searchkey = f"{searchkey}-{keyword.brand_id.x_name.strip().lower().replace(' ', '-')}" try: doc = { 'id': keyword.id, -- cgit v1.2.3 From e419a0e52ac9d01ac0b8900bfb2bebff1db522c9 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 14:45:39 +0700 Subject: remove brand_Id --- indoteknik_custom/models/keywords.py | 4 ---- indoteknik_custom/views/keywords.xml | 3 --- 2 files changed, 7 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index f4774bbd..20a18726 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -20,10 +20,6 @@ class Keywords(models.Model): category_id = fields.Many2one('product.public.category', string='Category', required=True, help="Category to filter products when generating products for this keyword and to throw to solr") keywords = fields.Char('Keywords', required=True) - brand_id = fields.Many2one( - comodel_name="x_manufactures", - string="Brand" - ) product_ids = fields.Many2many( 'product.product', 'keywords_product_rel', diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index febd6d39..9faa7112 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -6,7 +6,6 @@ - @@ -32,7 +31,6 @@ - @@ -51,7 +49,6 @@ - -- cgit v1.2.3 From 2f9d0c4be0a9b8602b745c4faba73ff132419a77 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 17:29:11 +0700 Subject: remove vals BOM --- indoteknik_custom/models/mrp_production.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py index 02679458..3d2507dc 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -52,8 +52,8 @@ class MrpProduction(models.Model): result = super(MrpProduction, self).button_mark_done() for record in self: - if len(record.check_bom_product_lines) < 1: - raise UserError("Check Product Tidak Boleh Kosong") + # if len(record.check_bom_product_lines) < 1: + # raise UserError("Check Product Tidak Boleh Kosong") if not record.sale_order: raise UserError("Sale Order Tidak Boleh Kosong") if record.sale_order and record.state == 'confirmed': -- cgit v1.2.3 From bae2823177d413ecdbdcac8b50fec75dc1c358dd Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 17:37:15 +0700 Subject: balikin --- indoteknik_custom/models/mrp_production.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py index 3d2507dc..02679458 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -52,8 +52,8 @@ class MrpProduction(models.Model): result = super(MrpProduction, self).button_mark_done() for record in self: - # if len(record.check_bom_product_lines) < 1: - # raise UserError("Check Product Tidak Boleh Kosong") + if len(record.check_bom_product_lines) < 1: + raise UserError("Check Product Tidak Boleh Kosong") if not record.sale_order: raise UserError("Sale Order Tidak Boleh Kosong") if record.sale_order and record.state == 'confirmed': -- cgit v1.2.3 From 7456c70b70749acbbe3f6545ee911a754a59f89b Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 19 Feb 2026 10:28:45 +0700 Subject: (andri) add estimate biaya pum --- .../models/advance_payment_request.py | 34 +++++++++++++++++++++- indoteknik_custom/security/ir.model.access.csv | 1 + .../views/advance_payment_request.xml | 24 +++++++++++++-- .../views/advance_payment_settlement.xml | 11 +++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index 8cadb1b6..18953c7c 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -155,6 +155,16 @@ class AdvancePaymentRequest(models.Model): compute='_compute_is_current_user_ap' ) + estimate_line_ids = fields.One2many('advance.payment.request.estimate.line', 'request_id', string='Rincian Estimasi') + + @api.constrains('nominal', 'estimate_line_ids') + def _check_nominal_vs_estimate_total(self): + for rec in self: + if rec.type_request == 'pum' and rec.estimate_line_ids: + total_estimate = sum(line.nominal for line in rec.estimate_line_ids) + if round(total_estimate, 2) != round(rec.nominal, 2): + raise UserError("Total estimasi harus sama dengan nominal PUM. Silakan sesuaikan rincian estimasi atau nominal PUM.") + @api.onchange('grand_total_reimburse', 'type_request') def _onchange_reimburse_line_update_nominal(self): if self.type_request == 'reimburse': @@ -1144,6 +1154,11 @@ class AdvancePaymentSettlement(models.Model): string="Is Current User AP", compute='_compute_is_current_user_ap' ) + pum_estimate_line_ids = fields.One2many( + related='pum_id.estimate_line_ids', + string='Rincian Estimasi PUM', + readonly=True + ) def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] @@ -1612,4 +1627,21 @@ class CreateReimburseCabWizard(models.TransientModel): 'type': 'ir.actions.act_window', 'res_id': move.id, 'target': 'current', - } \ No newline at end of file + } + +class AdvancePaymentRequestEstimateLine(models.Model): + _name = 'advance.payment.request.estimate.line' + _description = 'Advance Payment Request Estimate Line' + + request_id = fields.Many2one('advance.payment.request', string='Request') + category_estimate = fields.Selection([ + ('parkir', 'Parkir'), + ('tol', 'Tol'), + ('bbm', 'BBM'), + ('kuli', 'Kuli'), + ('konsumsi', 'Konsumsi'), + ('lain_lain', 'Lain-lain'), + ], string='Kategori Estimasi', required=True) + description = fields.Text(string='keTambahan', help='Deskripsi tambahan untuk estimasi, misalnya tujuan parkir atau rute tol.') + nominal = fields.Float(string='Nominal Estimasi', required=True, help='Masukkan nominal estimasi untuk kategori ini (tidak mesti akurat, hanya untuk gambaran umum).') + currency_id = fields.Many2one(related='request_id.currency_id') diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index bc290370..1294970c 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -200,6 +200,7 @@ access_advance_payment_usage_line,access.advance.payment.usage.line,model_advanc access_advance_payment_create_bill,access.advance.payment.create.bill,model_advance_payment_create_bill,,1,1,1,1 access_create_reimburse_cab_wizard_user,create.reimburse.cab.wizard user,model_create_reimburse_cab_wizard,,1,1,1,1 access_advance_payment_cancel_wizard,advance.payment.cancel.wizard,model_advance_payment_cancel_wizard,,1,1,1,1 +access_advance_payment_request_estimate_line,advance.payment.request.estimate.line,model_advance_payment_request_estimate_line,,1,1,1,1 access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1 access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1 diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml index 340e0caf..4faf905e 100644 --- a/indoteknik_custom/views/advance_payment_request.xml +++ b/indoteknik_custom/views/advance_payment_request.xml @@ -134,8 +134,28 @@
    - - + + +

    *Masukkan estimasi alokasi biaya sebagai gambaran rencana penggunaan dana, tidak harus diisi dengan nominal yang akurat

    + + + + + + + +
    + + + + + + + +
    +
    +
    + diff --git a/indoteknik_custom/views/advance_payment_settlement.xml b/indoteknik_custom/views/advance_payment_settlement.xml index a8bf1de7..258f469f 100644 --- a/indoteknik_custom/views/advance_payment_settlement.xml +++ b/indoteknik_custom/views/advance_payment_settlement.xml @@ -124,6 +124,17 @@ + +

    *Rincian estimasi PUM ini hanya sebagai gambaran umum untuk realisasi yang dilakukan, tidak harus diisi dengan nominal yang akurat.

    + + + + + + + + +
    -- cgit v1.2.3 From d098ff60225cd6d7770a08bdd1c06aafdd74f1d9 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 19 Feb 2026 15:44:17 +0700 Subject: sync to solr --- indoteknik_custom/models/keywords.py | 66 ++++++++++++++++++++++++++++++++++-- indoteknik_custom/views/keywords.xml | 18 ++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 20a18726..ca5fc8d0 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -31,6 +31,7 @@ class Keywords(models.Model): skip = fields.Boolean('Skip Generate Product', default=False, help="If checked, the system will not generate products for this keyword") url = fields.Char('Website URL', compute="_compute_url", help="Generated website url based on keywords") sum = fields.Integer('Total Product', compute="_compute_total_product", readonly=True, help="Total products found for this keyword including variants") + solr_flag = fields.Integer(string='Solr Flag', default=0, help="0=no sync needed, 2=needs sync, 1=queued") @api.depends('product_ids') def _compute_total_product(self): @@ -128,7 +129,62 @@ class Keywords(models.Model): record.keywords ) + @api.onchange('keywords', 'category_id', 'product_ids') + def _onchange_solr_flag(self): + """Set solr_flag=2 when tracked fields change to trigger queue sync""" + for record in self: + if not record.skip: + record.solr_flag = 2 + + def solr_flag_to_queue(self, limit=500): + """Find keywords with solr_flag=2 and create apache.solr.queue entries""" + keywords = self.search([('solr_flag', '=', 2)], limit=limit) + + for keyword in keywords: + # Create unique queue entry + queue_obj = self.env['apache.solr.queue'] + queue_obj.create_unique({ + 'res_model': 'keywords', + 'res_id': keyword.id, + 'function_name': '_sync_keywords_queue_callback' + }) + + # Set flag to indicate queued + keyword.solr_flag = 1 + + if keywords: + _logger.info( + 'Queued %s keywords for Solr synchronization', + len(keywords) + ) + + return True + + def _sync_keywords_queue_callback(self): + """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" + documents = [] + for keyword in self: + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') + try: + doc = { + 'id': keyword.id, + 'category_id_i': keyword.category_id.id, + 'keywords_s': searchkey, + 'url_s': keyword.url, + 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], + } + documents.append(doc) + except Exception as e: + _logger.error('failed %s', e) + _logger.error('doc data: %s', doc) + + if documents: + solr.add(documents) + + return True + def sync_solr(self): + """Manual sync method for active_ids context (backward compatibility)""" active_ids = self.env.context.get('active_ids', []) if not active_ids: _logger.warning("No active_ids found, nothing to sync") @@ -166,7 +222,11 @@ class Keywords(models.Model): def write(self, vals): result = super().write(vals) - # self.check_already_exist() - # if not self.env.context.get("skip_generate") and not self.skip: - # self.generate_products() + + tracked_fields = ['keywords', 'category_id', 'product_ids'] + neded_sync = any(field in vals for field in tracked_fields) + if neded_sync and self.skip == False: + for record in self: + record.solr_flag = 2 + return result diff --git a/indoteknik_custom/views/keywords.xml b/indoteknik_custom/views/keywords.xml index 9faa7112..a33edae0 100644 --- a/indoteknik_custom/views/keywords.xml +++ b/indoteknik_custom/views/keywords.xml @@ -9,6 +9,7 @@ + @@ -36,6 +37,7 @@ + @@ -51,6 +53,7 @@ + @@ -75,4 +78,19 @@ parent="website_sale.menu_orders" action="action_keywords" sequence="100"/> + + + + Sync Keywords To Solr: Queue Solr Flag 2 + 1 + hours + -1 + + + model.solr_flag_to_queue(limit=500) + code + 55 + False + + -- cgit v1.2.3 From 605241e33856c45660fce9ba16102557160c235a Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 19 Feb 2026 15:44:47 +0700 Subject: sync solr to production --- indoteknik_custom/models/keywords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index ca5fc8d0..4ab649dc 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -11,8 +11,8 @@ import io _logger = logging.getLogger(__name__) -# solr = pysolr.Solr('http://10.148.0.5:8983/solr/searchkey/', always_commit=True, timeout=30) -solr = pysolr.Solr('http://127.0.0.1:8983/solr/searchkey/', always_commit=True, timeout=30) +solr = pysolr.Solr('http://10.148.0.5:8983/solr/searchkey/', always_commit=True, timeout=30) +# solr = pysolr.Solr('http://127.0.0.1:8983/solr/searchkey/', always_commit=True, timeout=30) class Keywords(models.Model): _name = 'keywords' -- cgit v1.2.3 From 10525d407bbc34d79146ff22955a874509f5b204 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Thu, 19 Feb 2026 15:55:27 +0700 Subject: (andri) compute estimate dengan realisasi --- .../models/advance_payment_request.py | 60 +++++++++++++++++++++- .../views/advance_payment_settlement.xml | 3 ++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index 18953c7c..ac9d7c2d 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -921,6 +921,35 @@ class AdvancePaymentUsageLine(models.Model): compute='_compute_is_current_user_ap' ) + category_usage = fields.Selection([ + ('parkir', 'Parkir'), + ('tol', 'Tol'), + ('bbm', 'BBM'), + ('kuli', 'Kuli'), + ('konsumsi', 'Konsumsi'), + ('lain_lain', 'Lain-lain'), + ], string='Kategori System', compute='_compute_category_usage') + + @api.depends('account_id') + def _compute_category_usage(self): + for rec in self: + if not rec.account_id: + rec.category_usage = False + continue + name = rec.account_id.name.lower() + if 'bbm' in name or 'bahan bakar' in name: + rec.category_usage = 'bbm' + elif 'tol' in name: + rec.category_usage = 'tol' + elif 'parkir' in name: + rec.category_usage = 'parkir' + elif 'kuli' in name or 'bongkar' in name: + rec.category_usage = 'kuli' + elif 'konsumsi' in name or 'makan' in name or 'minum' in name: + rec.category_usage = 'konsumsi' + else: + rec.category_usage = 'lain_lain' + def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] is_ap = self.env.user.id in ap_user_ids @@ -1642,6 +1671,35 @@ class AdvancePaymentRequestEstimateLine(models.Model): ('konsumsi', 'Konsumsi'), ('lain_lain', 'Lain-lain'), ], string='Kategori Estimasi', required=True) - description = fields.Text(string='keTambahan', help='Deskripsi tambahan untuk estimasi, misalnya tujuan parkir atau rute tol.') + description = fields.Text(string='Description', help='Deskripsi tambahan untuk estimasi biaya yang diperlukan.') nominal = fields.Float(string='Nominal Estimasi', required=True, help='Masukkan nominal estimasi untuk kategori ini (tidak mesti akurat, hanya untuk gambaran umum).') currency_id = fields.Many2one(related='request_id.currency_id') + + total_actual = fields.Float(string='Nominal Realisasi', compute='_compute_actual_data') + frequency = fields.Integer(string='Qty Realisasi', compute='_compute_actual_data') + + @api.depends('request_id.settlement_ids.penggunaan_line_ids.nominal', + 'request_id.settlement_ids.penggunaan_line_ids.account_id') + def _compute_actual_data(self): + for rec in self: + total_act = 0 + freq = 0 + if rec.request_id and rec.request_id.settlement_ids: + all_usage_lines = rec.request_id.settlement_ids.mapped('penggunaan_line_ids') + valid_lines = all_usage_lines.filtered(lambda l: l.account_id and l.category_usage) + planned_category = rec.request_id.estimate_line_ids.mapped('category_estimate') + if rec.category_estimate == 'lain_lain': + matched_lines = valid_lines.filtered( + lambda l: l.category_usage == 'lain_lain' or \ + l.category_usage not in planned_category + ) + else: + matched_lines = valid_lines.filtered( + lambda l: l.category_usage == rec.category_estimate + ) + + total_act = sum(matched_lines.mapped('nominal')) + freq = len(matched_lines) + rec.total_actual = total_act + rec.frequency = freq + diff --git a/indoteknik_custom/views/advance_payment_settlement.xml b/indoteknik_custom/views/advance_payment_settlement.xml index 258f469f..352c5b96 100644 --- a/indoteknik_custom/views/advance_payment_settlement.xml +++ b/indoteknik_custom/views/advance_payment_settlement.xml @@ -118,6 +118,7 @@ + @@ -131,6 +132,8 @@ + + -- cgit v1.2.3 From 7394cd71c05d56ebfd5a5f95518a849289a782e0 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 20 Feb 2026 10:17:13 +0700 Subject: fix not sync to solr when there is no products --- indoteknik_custom/models/keywords.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 4ab649dc..9c8e2cfd 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -133,7 +133,7 @@ class Keywords(models.Model): def _onchange_solr_flag(self): """Set solr_flag=2 when tracked fields change to trigger queue sync""" for record in self: - if not record.skip: + if not record.skip and len(record.product_ids) > 0: record.solr_flag = 2 def solr_flag_to_queue(self, limit=500): @@ -164,6 +164,11 @@ class Keywords(models.Model): """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" documents = [] for keyword in self: + # Skip syncing if product count is 0 + if len(keyword.product_ids) == 0: + _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords) + continue + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') try: doc = { @@ -194,6 +199,11 @@ class Keywords(models.Model): documents = [] for keyword in keywords: + # Skip syncing if product count is 0 + if len(keyword.product_ids) == 0: + _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords) + continue + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') try: doc = { @@ -227,6 +237,8 @@ class Keywords(models.Model): neded_sync = any(field in vals for field in tracked_fields) if neded_sync and self.skip == False: for record in self: - record.solr_flag = 2 + # Only flag for sync if there are products + if len(record.product_ids) > 0: + record.solr_flag = 2 return result -- cgit v1.2.3 From 62c366e5cf58567b61700d2922b864e348e2a44b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 20 Feb 2026 11:14:23 +0700 Subject: remove add to keywords when creating a product --- indoteknik_custom/models/product_template.py | 31 +--------------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 397bd06d..e10b4de2 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -94,7 +94,6 @@ class ProductTemplate(models.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) - self.env['product.product']._add_product_to_keywords(result) return result # def write(self, values): @@ -895,11 +894,6 @@ class ProductTemplate(models.Model): # Log changes self._log_field_changes_product(vals, old_values) - # Add product to keywords - keyword_trigger = ['name', 'website_description', 'unpublished'] - if any(field in vals for field in keyword_trigger): - for product in self: - self.env['product.product']._add_product_to_keywords(product) return result # def write(self, vals): @@ -946,26 +940,7 @@ class ProductProduct(models.Model): 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') - - def _add_product_to_keywords(self,product): - keywords_model = self.env['keywords'] - if not product: - return False - - for product in self: - match_keywords = keywords_model.search([ - '|', - ('name', 'ilike', product.name), - ('keywords', 'ilike', product.website_description) - ]) - - for kw in match_keywords.filtered(lambda k: not k.skip): - if not self.unpublished and product.id not in kw.product_ids.ids: - kw.write({'product_ids': [(4, product.id)]}) - - return True - + # keyword_id = fields.Many2one('keywords', string='Keyword') has_magento = fields.Boolean(string='Has Magento?', default=False, readonly=True) def generate_product_sla(self): @@ -988,7 +963,6 @@ class ProductProduct(models.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(ProductProduct, self).create(vals) - self._add_product_to_keywords(result) return result # def write(self, values): @@ -1355,9 +1329,6 @@ class ProductProduct(models.Model): ] # pake ini kalau mau Cek semua field # if vals: - trigger_fields = ['name', 'website_description', 'unpublished'] - if any(f in vals for f in trigger_fields): - self._add_product_to_keywords(vals) if any(field in vals for field in tracked_fields): old_values = self._collect_old_values(vals) result = super().write(vals) -- cgit v1.2.3 From a3dc6bc79215e6223c801f49d1997292b8f4c317 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 20 Feb 2026 15:36:56 +0700 Subject: (andri) fix --- indoteknik_custom/models/advance_payment_request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py index ac9d7c2d..039d18a5 100644 --- a/indoteknik_custom/models/advance_payment_request.py +++ b/indoteknik_custom/models/advance_payment_request.py @@ -160,7 +160,9 @@ class AdvancePaymentRequest(models.Model): @api.constrains('nominal', 'estimate_line_ids') def _check_nominal_vs_estimate_total(self): for rec in self: - if rec.type_request == 'pum' and rec.estimate_line_ids: + if rec.type_request == 'pum': + if not rec.estimate_line_ids: + raise UserError("Rincian estimasi wajib diisi untuk PUM. Silakan tambahkan rincian estimasi.") total_estimate = sum(line.nominal for line in rec.estimate_line_ids) if round(total_estimate, 2) != round(rec.nominal, 2): raise UserError("Total estimasi harus sama dengan nominal PUM. Silakan sesuaikan rincian estimasi atau nominal PUM.") @@ -761,7 +763,7 @@ class AdvancePaymentRequest(models.Model): pum_ids = self.search([ ('user_id', '=', user.id), - ('status', '!=', 'reject'), + ('status', '!=', 'cancel'), ('type_request', '=', 'pum') ]) -- cgit v1.2.3 From 52e196b5aceb5b2cd91795a08f4be1517b123583 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 20 Feb 2026 15:57:34 +0700 Subject: hide pos --- indoteknik_custom/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 90cd5fa2..e6fc4732 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -647,7 +647,7 @@ class SaleOrder(models.Model): def _get_biteship_courier_codes(self): return [ - 'gojek','grab','deliveree','lalamove','jne','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','anteraja','sap','paxel','borzo' + 'gojek','grab','deliveree','lalamove','jne','ninja','lion','rara','sicepat','jnt','idexpress','rpx','wahana','jdl','anteraja','sap','paxel','borzo' ] @api.onchange('carrier_id') -- cgit v1.2.3 From 2c89059ab4abc70f6e7f811b7995bc394d6514f8 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 20 Feb 2026 16:21:28 +0700 Subject: fix price not same as calculated on excel --- indoteknik_custom/models/price_group.py | 20 ++++++++++---------- indoteknik_custom/models/purchase_pricelist.py | 7 ++++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/indoteknik_custom/models/price_group.py b/indoteknik_custom/models/price_group.py index fce78fff..1a4f1bd6 100644 --- a/indoteknik_custom/models/price_group.py +++ b/indoteknik_custom/models/price_group.py @@ -10,16 +10,16 @@ class PriceGroup(models.Model): name = fields.Char(string='Name') pricelist_id = fields.Many2one('product.pricelist', string='Price List') - group1 = fields.Float(string='Kelompok 1 (%)') - group2 = fields.Float(string='Kelompok 2 (%)') - group3 = fields.Float(string='Kelompok 3 (%)') - group4 = fields.Float(string='Kelompok 4 (%)') - group5 = fields.Float(string='Kelompok 5 (%)') - group6 = fields.Float(string='Kelompok 6 (%)') - group7 = fields.Float(string='Kelompok 7 (%)') - group8 = fields.Float(string='Kelompok 8 (%)') - group9 = fields.Float(string='Kelompok 9 (%)') - group10 = fields.Float(string='Kelompok 10 (%)') + group1 = fields.Float(string='Kelompok 1 (%)', digits=(16, 12)) + group2 = fields.Float(string='Kelompok 2 (%)', digits=(16, 12)) + group3 = fields.Float(string='Kelompok 3 (%)', digits=(16, 12)) + group4 = fields.Float(string='Kelompok 4 (%)', digits=(16, 12)) + group5 = fields.Float(string='Kelompok 5 (%)', digits=(16, 12)) + group6 = fields.Float(string='Kelompok 6 (%)', digits=(16, 12)) + group7 = fields.Float(string='Kelompok 7 (%)', digits=(16, 12)) + group8 = fields.Float(string='Kelompok 8 (%)', digits=(16, 12)) + group9 = fields.Float(string='Kelompok 9 (%)', digits=(16, 12)) + group10 = fields.Float(string='Kelompok 10 (%)', digits=(16, 12)) def collect_price_group(self): PRICE_GROUP_ID = { diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py index b3a473b6..83e06f55 100755 --- a/indoteknik_custom/models/purchase_pricelist.py +++ b/indoteknik_custom/models/purchase_pricelist.py @@ -118,15 +118,16 @@ class PurchasePricelist(models.Model): product_domain = [('product_id', '=', rec.product_id.id)] markup_pricelist = price_group['markup'].pricelist_id - base_price = price_incl + (price_incl * markup_percentage / 100) + # base_price = price_incl + (price_incl * markup_percentage / 100) + base_price = round(price_incl + (price_incl * markup_percentage / 100), 12) base_prod_pricelist = self.env['product.pricelist.item'].search(product_domain + [('pricelist_id', '=', markup_pricelist.id)], limit=1) base_prod_pricelist.fixed_price = base_price tier_percentages = [price_group[f'tier_{i}'][product_group] for i in range(1, 6)] for i, tier_percentage in enumerate(tier_percentages): tier_pricelist = price_group[f'tier_{i + 1}'].pricelist_id - tier_price = price_incl + (price_incl * tier_percentage / 100) - tier_perc = (base_price - tier_price) / base_price * 100 + tier_price = round(price_incl + (price_incl * tier_percentage / 100), 12) + tier_perc = round((base_price - tier_price) / base_price * 100, 12) tier_prod_pricelist = self.env['product.pricelist.item'].search(product_domain + [('pricelist_id', '=', tier_pricelist.id)], limit=1) tier_prod_pricelist.price_discount = tier_perc -- cgit v1.2.3 From abccff65f5eb91eafe08025ebc86d5e220243854 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 21 Feb 2026 08:15:45 +0700 Subject: fix logic sync solr --- indoteknik_custom/models/keywords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 9c8e2cfd..487e58e5 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -133,7 +133,7 @@ class Keywords(models.Model): def _onchange_solr_flag(self): """Set solr_flag=2 when tracked fields change to trigger queue sync""" for record in self: - if not record.skip and len(record.product_ids) > 0: + if len(record.product_ids) > 0: record.solr_flag = 2 def solr_flag_to_queue(self, limit=500): @@ -235,7 +235,7 @@ class Keywords(models.Model): tracked_fields = ['keywords', 'category_id', 'product_ids'] neded_sync = any(field in vals for field in tracked_fields) - if neded_sync and self.skip == False: + if neded_sync: for record in self: # Only flag for sync if there are products if len(record.product_ids) > 0: -- cgit v1.2.3 From 345d45f5fae0f152ca39e9ba491513582a5c6791 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Sat, 21 Feb 2026 09:41:57 +0700 Subject: fix solr queue keywords --- indoteknik_custom/models/keywords.py | 64 +++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 487e58e5..2ee217f7 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -160,16 +160,50 @@ class Keywords(models.Model): return True + # Old + # def _sync_keywords_queue_callback(self): + # """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" + # documents = [] + # for keyword in self: + # # Skip syncing if product count is 0 + # if len(keyword.product_ids) == 0: + # _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords) + # continue + + # searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') + # try: + # doc = { + # 'id': keyword.id, + # 'category_id_i': keyword.category_id.id, + # 'keywords_s': searchkey, + # 'url_s': keyword.url, + # 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], + # } + # documents.append(doc) + # except Exception as e: + # _logger.error('failed %s', e) + # _logger.error('doc data: %s', doc) + + # if documents: + # solr.add(documents) + + # self.write({'solr_flag': 0}) + + # return True + def _sync_keywords_queue_callback(self): - """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" - documents = [] + success_keywords = self.browse() + for keyword in self: - # Skip syncing if product count is 0 - if len(keyword.product_ids) == 0: - _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords) + if not keyword.product_ids: + _logger.info( + 'Skipping Solr sync for keyword "%s" - no products found', + keyword.keywords + ) continue - + searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') + try: doc = { 'id': keyword.id, @@ -178,13 +212,19 @@ class Keywords(models.Model): 'url_s': keyword.url, 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], } - documents.append(doc) + + solr.add([doc]) + + success_keywords |= keyword + except Exception as e: - _logger.error('failed %s', e) - _logger.error('doc data: %s', doc) + _logger.error( + "Solr sync failed for keyword ID %s: %s", + keyword.id, e + ) - if documents: - solr.add(documents) + if success_keywords: + success_keywords.write({'solr_flag': 0}) return True @@ -233,7 +273,7 @@ class Keywords(models.Model): def write(self, vals): result = super().write(vals) - tracked_fields = ['keywords', 'category_id', 'product_ids'] + tracked_fields = ['keywords', 'category_id', 'product_ids', 'skip', 'name'] neded_sync = any(field in vals for field in tracked_fields) if neded_sync: for record in self: -- cgit v1.2.3 From 18bdbf118d2f582ac0c520a9b4cc5b61fe88488f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Mon, 23 Feb 2026 10:39:48 +0700 Subject: show date maturity in line & remove old keywords code --- indoteknik_custom/models/keywords.py | 31 --------------------------- indoteknik_custom/views/account_move_line.xml | 3 +++ 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 2ee217f7..3fa9dd72 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -160,37 +160,6 @@ class Keywords(models.Model): return True - # Old - # def _sync_keywords_queue_callback(self): - # """Callback method executed by apache.solr.queue - syncs keyword data to Solr""" - # documents = [] - # for keyword in self: - # # Skip syncing if product count is 0 - # if len(keyword.product_ids) == 0: - # _logger.info('Skipping Solr sync for keyword "%s" - no products found', keyword.keywords) - # continue - - # searchkey = (keyword.keywords or '').strip().lower().replace(' ', '-') - # try: - # doc = { - # 'id': keyword.id, - # 'category_id_i': keyword.category_id.id, - # 'keywords_s': searchkey, - # 'url_s': keyword.url, - # 'product_ids_is': [p.product_tmpl_id.id for p in keyword.product_ids], - # } - # documents.append(doc) - # except Exception as e: - # _logger.error('failed %s', e) - # _logger.error('doc data: %s', doc) - - # if documents: - # solr.add(documents) - - # self.write({'solr_flag': 0}) - - # return True - def _sync_keywords_queue_callback(self): success_keywords = self.browse() diff --git a/indoteknik_custom/views/account_move_line.xml b/indoteknik_custom/views/account_move_line.xml index 838596c8..346494f3 100644 --- a/indoteknik_custom/views/account_move_line.xml +++ b/indoteknik_custom/views/account_move_line.xml @@ -9,6 +9,9 @@ + + + -- cgit v1.2.3 From 79471b69ffc683c3c860ecd6456f638750a8c81f Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 24 Feb 2026 09:56:19 +0700 Subject: kartu stock --- indoteknik_custom/__manifest__.py | 5 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/kartu_stock.py | 186 ++++++++++++++++++++++++++++++++ indoteknik_custom/models/keywords.py | 2 +- indoteknik_custom/views/kartu_stock.xml | 17 +++ 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 indoteknik_custom/models/kartu_stock.py create mode 100644 indoteknik_custom/views/kartu_stock.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 36588967..3e39942b 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -8,7 +8,7 @@ 'author': 'Rafi Zadanly', 'website': '', 'images': ['assets/favicon.ico'], - 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur'], + 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur', 'proweb_kartu_stok'], 'data': [ 'views/assets.xml', 'security/ir.model.access.csv', @@ -191,7 +191,8 @@ 'views/uom_uom.xml', 'views/update_depreciation_move_wizard_view.xml', 'views/commission_internal.xml', - 'views/keywords.xml' + 'views/keywords.xml', + 'views/kartu_stock.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a042750b..840796f8 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -167,3 +167,4 @@ from . import uom_uom from . import commission_internal from . import update_depreciation_move_wizard from . import keywords +from . import kartu_stock diff --git a/indoteknik_custom/models/kartu_stock.py b/indoteknik_custom/models/kartu_stock.py new file mode 100644 index 00000000..22f90df0 --- /dev/null +++ b/indoteknik_custom/models/kartu_stock.py @@ -0,0 +1,186 @@ +from odoo import models +from io import BytesIO +import datetime +from base64 import encodebytes +import xlsxwriter + + +class KartuStokWizardInherit(models.TransientModel): + _inherit = 'kartu.stok.wizard' + + + def action_kartu_stok_excel_single_sheet(self): + + active_ids_tmp = self.env.context.get('active_ids') + active_model = self.env.context.get('active_model') + + if active_model == 'product.template': + active_ids = self.env['product.product'].search( + [('product_tmpl_id', 'in', active_ids_tmp), + ('active', '=', True)]).ids + else: + active_ids = active_ids_tmp + + data = { + 'location_id': self.location_id.id, + 'day_date': self.day_date, + 'previous_number_days': self.previous_number_days, + 'date_from': self.date_from, + 'date_to': self.date_to, + 'ids': active_ids, + 'context': {'active_model': active_model} + } + + file_io = BytesIO() + workbook = xlsxwriter.Workbook(file_io) + + self.generate_xlsx_single_sheet(workbook, data) + + workbook.close() + + fout = encodebytes(file_io.getvalue()) + datetime_string = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f'Kartu_Stok_Single_{datetime_string}.xlsx' + + self.write({ + 'fileout': fout, + 'fileout_filename': filename + }) + + file_io.close() + + return { + 'type': 'ir.actions.act_url', + 'target': 'new', + 'url': 'web/content/?model=' + self._name + + '&id=' + str(self.id) + + '&field=fileout&download=true&filename=' + filename, + } + + def generate_xlsx_single_sheet(self, workbook, data): + + bold = workbook.add_format({'bold': True}) + border_date_right = workbook.add_format({'border':1, 'num_format': 'DD-MM', 'bg_color': '#dddddd', 'align': 'right'}) + border_int_right = workbook.add_format({'border':1, 'num_format': '#,##0', 'bg_color': '#dddddd', 'align': 'right'}) + border_int_right_bold = workbook.add_format({'border':1, 'num_format': '#,##0', 'bg_color': '#dddddd', 'align': 'right', 'bold': True}) + border_text_center = workbook.add_format({'border':1, 'bg_color': '#dddddd', 'align': 'center'}) + header_format = workbook.add_format({'bold': True, 'border':1, 'bg_color': '#808080', 'align': 'center'}) + + sheet = workbook.add_worksheet('Kartu Stok') + + docs = self.env['product.product'].browse(data['ids']) + location = self.env['stock.location'].browse(data['location_id']) + location_name = location.display_name.split('/')[0] + + row = 0 + + for doc in docs: + + # ========================= + # HEADER PRODUCT + # ========================= + sheet.write(row, 0, doc.display_name, bold) + row += 1 + sheet.write(row, 0, location_name, bold) + row += 2 + + # ========================= + # TABLE HEADER + # ========================= + sheet.write(row, 0, 'Date', header_format) + sheet.write(row, 1, 'In', header_format) + sheet.write(row, 2, 'Out', header_format) + sheet.write(row, 3, 'Stock', header_format) + sheet.write(row, 4, 'Distributor', header_format) + sheet.write(row, 5, 'Buyer', header_format) + sheet.write(row, 6, 'Document', header_format) + sheet.write(row, 7, 'Source Document', header_format) + row += 1 + + stock_total = 0 + stock_show_initial = False + + # ========================= + # MOVE LOOP (SAMA LOGIC ASLI) + # ========================= + for move in doc.stock_move_ids.sorted(key=lambda sm: sm.date): + for line in move.move_line_ids: + + if line.state != 'done': + continue + + if line.location_id.id != data['location_id'] and \ + line.location_dest_id.id != data['location_id']: + continue + + if not stock_show_initial: + sheet.write(row, 3, stock_total, border_int_right_bold) + sheet.write(row, 4, 'Initial Stock', border_text_center) + stock_show_initial = True + row += 1 + + qty_in = 0 + qty_out = 0 + + if line.location_dest_id.id == data['location_id']: + qty_in = line.qty_done + stock_total += qty_in + + if line.location_id.id == data['location_id']: + qty_out = line.qty_done + stock_total -= qty_out + + sheet.write(row, 0, line.date, border_date_right) + sheet.write(row, 1, qty_in, border_int_right) + sheet.write(row, 2, qty_out, border_int_right) + sheet.write(row, 3, stock_total, border_int_right) + # Distributor + col = 4 + if line.location_dest_id.id == data['location_id']: + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.partner_id.display_name, border_text_center) + else: + if line.location_id: + if line.location_id.name == 'Inventory adjustment': + sheet.write(row, col, 'Adjust *', border_text_center) + else: + sheet.write(row, col, line.location_id.location_id.name + ' *', border_text_center) + else: + sheet.write(row, col, doc.seller_ids[0].name + ' *' if doc.seller_ids else '', border_text_center) + else: + sheet.write(row, col, '', border_text_center) + # Buyer + col = 5 + if line.location_id.id == data['location_id']: + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.partner_id.display_name, border_text_center) + else: + if line.location_dest_id: + if line.location_dest_id.name == 'Inventory adjustment': + sheet.write(row, col, 'Adjust *', border_text_center) + else: + sheet.write(row, col, line.location_dest_id.location_id.name + ' *', border_text_center) + else: + sheet.write(row, col, doc.seller_ids[0].name + ' *' if doc.seller_ids else '', border_text_center) + else: + sheet.write(row, col, '', border_text_center) + # Document + col = 6 + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.name, border_text_center) + else: + sheet.write(row, col, line.reference or '', border_text_center) + # Source Document + col = 7 + if line.picking_id and line.picking_id.origin: + sheet.write(row, col, line.picking_id.origin, border_text_center) + else: + sheet.write(row, col, line.reference or '', border_text_center) + + row += 1 + + row += 3 # jarak antar product + + + + diff --git a/indoteknik_custom/models/keywords.py b/indoteknik_custom/models/keywords.py index 4ab649dc..5b7da705 100644 --- a/indoteknik_custom/models/keywords.py +++ b/indoteknik_custom/models/keywords.py @@ -225,7 +225,7 @@ class Keywords(models.Model): tracked_fields = ['keywords', 'category_id', 'product_ids'] neded_sync = any(field in vals for field in tracked_fields) - if neded_sync and self.skip == False: + if neded_sync and self.skip == False and len(self.product_ids) > 0: for record in self: record.solr_flag = 2 diff --git a/indoteknik_custom/views/kartu_stock.xml b/indoteknik_custom/views/kartu_stock.xml new file mode 100644 index 00000000..705d86a2 --- /dev/null +++ b/indoteknik_custom/views/kartu_stock.xml @@ -0,0 +1,17 @@ + + + + + kartu.stok.wizard.form.inherit.single.excel + kartu.stok.wizard + + + + +