From 316b8257845d0df10153fa7e5e294a699ad17c56 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 24 Feb 2026 13:20:33 +0700 Subject: push --- indoteknik_custom/__manifest__.py | 3 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/automatic_purchase.py | 3 + indoteknik_custom/models/purchase_order.py | 299 ++++++++++++++++++++++++ indoteknik_custom/models/purchase_order_line.py | 17 ++ indoteknik_custom/models/stock_picking.py | 1 + indoteknik_custom/models/token_log.py | 17 ++ indoteknik_custom/security/ir.model.access.csv | 1 + indoteknik_custom/views/purchase_order.xml | 18 ++ indoteknik_custom/views/stock_picking.xml | 1 + indoteknik_custom/views/token_log.xml | 33 +++ 11 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 indoteknik_custom/models/token_log.py create mode 100644 indoteknik_custom/views/token_log.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 36588967..1ad1e52e 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -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/token_log.xml' ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index a042750b..faea989d 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 token_log diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 0b2f7d1b..3035ceab 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -744,7 +744,10 @@ class SaleNotInMatchPO(models.Model): select distinct coalesce(posm.sale_line_id,0) from purchase_order_sales_match posm join purchase_order po on po.id = posm.purchase_order_id + join purchase_order_line pol on pol.order_id = posm.purchase_order_id and pol.product_id = posm.product_id + join stock_move sm on sm.purchase_line_id = pol.id where po.state not in ('cancel') + and sm.state not in ('cancel') ) ) """ % self._table) \ No newline at end of file diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index b3ecca56..dd0c5cd5 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -8,6 +8,8 @@ import io import base64 from odoo.tools import lazy_property import socket +import requests +import json try: from odoo.tools.misc import xlsxwriter @@ -125,7 +127,304 @@ class PurchaseOrder(models.Model): ) overseas_po = fields.Boolean(string='PO Luar Negeri?', tracking=3, help='Centang jika PO untuk pembelian luar negeri') + order_altama_id = fields.Integer('Req Order Altama', copy=False) + soo_number = fields.Char('SOO Number', copy=False) + soo_price = fields.Float('SOO Price', copy=False) + soo_discount = fields.Float('SOO Discount', copy=False) + soo_tax = fields.Float('SOO Tax', copy=False) + + def _get_altama_token(self, source='auto'): + ICP = self.env['ir.config_parameter'].sudo() + TokenLog = self.env['token.log'].sudo() + + token_url = ICP.get_param('token.adempiere.altama') + client_id = ICP.get_param('client.adempiere.altama') + client_secret = ICP.get_param('secret_key.adempiere.altama') + + active_token = TokenLog.search([ + ('is_active', '=', True), + ('token_from', '=', 'Adempiere Altama'), + ('expires_at', '>', datetime.utcnow()), + ], limit=1, order='id desc') + + if active_token: + return active_token.token + + headers = { + "Authorization": "Basic " + base64.b64encode(f"{client_id}:{client_secret}".encode()).decode(), + "Content-Type": "application/x-www-form-urlencoded", + } + data = {"grant_type": "client_credentials"} + + response = requests.post(token_url, data=data, headers=headers, timeout=15) + if response.status_code == 200: + result = response.json() + token = result.get("access_token") + expires_in = result.get("expires_in", 3600) + expiry_time = datetime.utcnow() + timedelta(seconds=expires_in - 60) + + TokenLog.search([ + ('token_from', '=', 'Adempiere Altama'), + ('is_active', '=', True), + ]).write({'is_active': False}) + + TokenLog.create({ + 'token': token, + 'expires_at': expiry_time, + 'is_active': True, + 'created_by': self.env.user.id if self.env.user else None, + 'source': source, + 'token_from': 'Adempiere Altama', + }) + + return token + + else: + raise Exception(f"Gagal ambil token: {response.status_code} - {response.text}") + + def action_create_order_altama(self): + ICP = self.env['ir.config_parameter'].sudo() + for order in self: + try: + token = self._get_altama_token(source='manual') + url = ICP.get_param('endpoint.create.order.adempiere.altama') + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + payload = { + "date_po": order.date_approve.strftime("%Y%m%d%H%M%S"), + "no_po": order.name, + "details": [ + { + "item_code": line.product_id.default_code or "", + "price": line.price_unit, + "qty": line.product_qty, + } + for line in order.order_line + ], + } + + response = requests.post(url, json=payload, headers=headers, timeout=20) + + try: + result = response.json() + except json.JSONDecodeError: + raise Exception(f"Response bukan JSON valid: {response.text}") + + if response.status_code == 200 and result.get("code") == "00": + contents = result.get("contents", {}) + if isinstance(contents, dict): + order.order_altama_id = contents.get("req_id") + else: + order.order_altama_id = contents.get("req_id") + + elif response.status_code == 404: + raise Exception("URL endpoint gak ditemukan (404). Pastikan path-nya benar di Altama API.") + elif response.status_code == 401: + token = self._get_altama_token(source='auto') + headers["Authorization"] = f"Bearer {token}" + response = requests.post(url, json=payload, headers=headers, timeout=20) + elif response.status_code not in (200, 201): + raise Exception(f"Gagal kirim ke Altama: {response.status_code} - {response.text}") + + self.message_post(body=f"✅ PO berhasil dikirim ke Altama!\nResponse: {json.dumps(result, indent=2)}") + + except Exception as e: + self.message_post(body=f"❌ Gagal kirim ke Altama:
{str(e)}
") + + + def action_get_order_altama(self): + ICP = self.env['ir.config_parameter'].sudo() + for order in self: + try: + # ============================ + # Get Token + # ============================ + token = self._get_altama_token(source='manual') + + url = ICP.get_param('endpoint.get.order.adempiere.altama') + if not url: + raise Exception("Parameter 'endpoint.adempiere.altama' belum diset di System Parameters.") + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + params = { + "orderreq_id": order.order_altama_id or 0, + } + + # ============================ + # Request ke API + # ============================ + response = requests.get(url, headers=headers, params=params, timeout=20) + + if response.status_code == 401: + token = self._get_altama_token(source='auto') + headers["Authorization"] = f"Bearer {token}" + response = requests.get(url, headers=headers, params=params, timeout=20) + + if response.status_code not in (200, 201): + raise Exception(f"Gagal ambil data dari Altama: {response.status_code} - {response.text}") + + data = response.json() + + # ============================ + # Extract Data + # ============================ + contents_root = data.get("contents", {}) + contents_list = contents_root.get("contents", []) + + if not isinstance(contents_list, list): + raise Exception("Format data contents dari Altama tidak sesuai (expected list).") + + order.message_post( + body=f"✅ Berhasil ambil data dari Altama. Ditemukan {len(contents_list)} record." + ) + + # ===================================================== + # LOOP MAIN DATA + # ===================================================== + for item in contents_list: + + req_id = item.get("req_id") + no_po = item.get("no_po") + list_item_po = item.get("list_Item_po", []) + list_soo = item.get("list_soo", []) + + # ============================ + # Isi Data SOO Ke Order + # ============================ + soo_numbers = [s.get("no_soo") for s in list_soo if s.get("no_soo")] + unique_soo = list(set(soo_numbers)) + if len(unique_soo) == 1: + order.soo_number = unique_soo[0] + if not order.picking_ids.number_soo: + order.picking_ids[0].number_soo = unique_soo[0] + elif len(unique_soo) > 1: + order.soo_number = ", ".join(unique_soo) + if not order.picking_ids.number_soo: + order.picking_ids[0].number_soo = ", ".join(unique_soo) + else: + order.soo_number = False + + if list_soo: + first_soo = list_soo[0] + order.soo_price = first_soo.get("totalprice") + order.soo_discount = first_soo.get("diskon") + order.soo_tax = first_soo.get("ppn") + + order.order_altama_id = req_id + + # ============================ + # Update Order Lines + # ============================ + for item_line in list_item_po: + + line = order.order_line.filtered( + lambda l: l.product_id.default_code == item_line.get("item_code") + ) + + if line: + line.write({ + "description": item_line.get("description", ""), + "altama_ordered": item_line.get("qtyordered", 0), + "altama_delivered": item_line.get("qtydelivered", 0), + "altama_invoiced": item_line.get("qtyinvoiced", 0), + "docstatus_altama": item_line.get("docstatus", ""), + }) + + # ===================================================== + # BUILD HTML TABLES FOR CHATTER + # ===================================================== + + # ---- SOO TABLE ---- + soo_rows = "" + for s in list_soo: + soo_rows += f""" + + {s.get('no_soo')} + {s.get('totalprice')} + {s.get('diskon')} + {s.get('ppn')} + + """ + + soo_table = f""" + + + + + + + + + + + {soo_rows or ''} + +
SOO NumberTotal PriceDiskonPPN
Tidak ada data SOO
+ """ + + # ---- ITEM PO TABLE ---- + po_rows = "" + for l in list_item_po: + + desc = l.get("description") or "" + + # Flag: row error kalau description tidak mulai dengan SOO/ + is_error = desc and not desc.startswith("SOO/") + + # Style row merah + row_style = "color:red; font-weight:bold;" if is_error else "" + + po_rows += f""" + + {l.get('item_code')} + {desc} + {l.get('qtyordered')} + {l.get('qtydelivered')} + {l.get('qtyinvoiced')} + {l.get('docstatus')} + + """ + + + po_table = f""" + + + + + + + + + + + + + {po_rows or ''} + +
Item CodeDescriptionOrderedDeliveredInvoicedStatus
Tidak ada item PO
+ """ + + # ---- POST TO CHATTER ---- + order.message_post( + body=f""" + 📦 Data SOO
{soo_table} +

+ 📦 Data Item PO
{po_table} + """ + ) + + except Exception as e: + order.message_post( + body=f"❌ Gagal ambil data dari Altama:
{str(e)}
" + ) @staticmethod def is_local_env(): hostname = socket.gethostname().lower() diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 76dcc09e..c6a49481 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -55,6 +55,23 @@ class PurchaseOrderLine(models.Model): ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt') show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True) price_unit_before = fields.Float(string='Unit Price Before', help="Harga awal yang sebelumnya telah diinputkan") + altama_ordered = fields.Float( + string='Altama Ordered', + default=0.0, + copy=False + ) + altama_delivered = fields.Float( + string='Altama Delivered', + default=0.0, + copy=False + ) + altama_invoiced = fields.Float( + string='Altama Invoiced', + default=0.0, + copy=False + ) + description = fields.Text(string='Description', readonly=True, copy=False) + docstatus_altama = fields.Text(string='Status Altama', readonly=True, copy=False) @api.onchange('price_unit') def _onchange_price_unit_before(self): diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 065b1484..ab366fd6 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -459,6 +459,7 @@ class StockPicking(models.Model): ('urgent', 'Urgent Delivery'), ], string='Reason Change Date Planned', tracking=True) delivery_date = fields.Datetime(string='Delivery Date', copy=False) + number_soo = fields.Char(string='Number SOO Altama') def _get_kgx_awb_number(self): """Menggabungkan name dan origin untuk membuat AWB Number""" diff --git a/indoteknik_custom/models/token_log.py b/indoteknik_custom/models/token_log.py new file mode 100644 index 00000000..fdc0c03e --- /dev/null +++ b/indoteknik_custom/models/token_log.py @@ -0,0 +1,17 @@ +from odoo import models, fields, api + +class FixcoTokenLog(models.Model): + _name = 'token.log' + _description = 'Log Token Fixco' + _order = 'create_date desc' + + token = fields.Text(string="Access Token", readonly=True) + expires_at = fields.Datetime(string="Expires At", readonly=True) + created_at = fields.Datetime(string="Created At", default=lambda self: fields.Datetime.now(), readonly=True) + created_by = fields.Many2one('res.users', string="Generated By", readonly=True) + source = fields.Selection([ + ('manual', 'Manual Request'), + ('auto', 'Auto Refresh'), + ], string="Token Source", default='auto', readonly=True) + token_from = fields.Char(string="From", readonly=True) + is_active = fields.Boolean("Active", default=True) \ 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 b6583ed5..c1935c36 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -218,3 +218,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_update_depreciation_move_wizard,access.update.depreciation.move.wizard,model_update_depreciation_move_wizard,,1,1,1,1 access_keywords,keywords,model_keywords,base.group_user,1,1,1,1 +access_token_log,access.token.log,model_token_log,,1,1,1,1 diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 59e317d2..e7741abd 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -46,6 +46,19 @@