diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2026-02-24 13:20:33 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2026-02-24 13:20:33 +0700 |
| commit | 316b8257845d0df10153fa7e5e294a699ad17c56 (patch) | |
| tree | 78337836f0cc61e1af8e1549dc22f907a1d2dcee | |
| parent | 345d45f5fae0f152ca39e9ba491513582a5c6791 (diff) | |
push
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 3 | ||||
| -rwxr-xr-x | indoteknik_custom/models/__init__.py | 1 | ||||
| -rw-r--r-- | indoteknik_custom/models/automatic_purchase.py | 3 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 299 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order_line.py | 17 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 1 | ||||
| -rw-r--r-- | indoteknik_custom/models/token_log.py | 17 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/views/purchase_order.xml | 18 | ||||
| -rw-r--r-- | indoteknik_custom/views/stock_picking.xml | 1 | ||||
| -rw-r--r-- | indoteknik_custom/views/token_log.xml | 33 |
11 files changed, 393 insertions, 1 deletions
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:<br/><pre>{str(e)}</pre>") + + + 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""" + <tr> + <td>{s.get('no_soo')}</td> + <td>{s.get('totalprice')}</td> + <td>{s.get('diskon')}</td> + <td>{s.get('ppn')}</td> + </tr> + """ + + soo_table = f""" + <table style="width:100%; border-collapse: collapse; margin-top: 10px;"> + <thead> + <tr style="background:#f1f1f1;"> + <th style="border:1px solid #ccc; padding:6px;">SOO Number</th> + <th style="border:1px solid #ccc; padding:6px;">Total Price</th> + <th style="border:1px solid #ccc; padding:6px;">Diskon</th> + <th style="border:1px solid #ccc; padding:6px;">PPN</th> + </tr> + </thead> + <tbody> + {soo_rows or '<tr><td colspan="4">Tidak ada data SOO</td></tr>'} + </tbody> + </table> + """ + + # ---- 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""" + <tr style="{row_style}"> + <td>{l.get('item_code')}</td> + <td>{desc}</td> + <td>{l.get('qtyordered')}</td> + <td>{l.get('qtydelivered')}</td> + <td>{l.get('qtyinvoiced')}</td> + <td>{l.get('docstatus')}</td> + </tr> + """ + + + po_table = f""" + <table style="width:100%; border-collapse: collapse; margin-top: 10px;"> + <thead> + <tr style="background:#f1f1f1;"> + <th style="border:1px solid #ccc; padding:6px;">Item Code</th> + <th style="border:1px solid #ccc; padding:6px;">Description</th> + <th style="border:1px solid #ccc; padding:6px;">Ordered</th> + <th style="border:1px solid #ccc; padding:6px;">Delivered</th> + <th style="border:1px solid #ccc; padding:6px;">Invoiced</th> + <th style="border:1px solid #ccc; padding:6px;">Status</th> + </tr> + </thead> + <tbody> + {po_rows or '<tr><td colspan="6">Tidak ada item PO</td></tr>'} + </tbody> + </table> + """ + + # ---- POST TO CHATTER ---- + order.message_post( + body=f""" + <b>📦 Data SOO</b><br/>{soo_table} + <br/><br/> + <b>📦 Data Item PO</b><br/>{po_table} + """ + ) + + except Exception as e: + order.message_post( + body=f"❌ Gagal ambil data dari Altama:<br/><pre>{str(e)}</pre>" + ) @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 @@ </button> </xpath> <button id="draft_confirm" position="after"> + <button name="action_create_order_altama" + type="object" + string="Create Order Altama" + class="oe_highlight" + icon="fa-cloud-upload" + attrs="{'invisible': [('partner_id', '!=', 5571)]}" + /> + <button name="action_get_order_altama" + type="object" + string="Get Order Altama" + class="oe_highlight" + attrs="{'invisible': [('partner_id', '!=', 5571)]}" + icon="fa-cloud-download"/> <button name="po_approve" string="Ask Approval" type="object" @@ -153,8 +166,13 @@ </field> <field name="price_unit" position="after"> <field name="price_vendor" attrs="{'readonly': 1}" optional="hide"/> + <field name="description" optional="hide"/> + <field name="docstatus_altama" optional="hide"/> </field> <field name="price_subtotal" position="after"> + <field name="altama_ordered" optional="hide" readonly="1"/> + <field name="altama_delivered" optional="hide" readonly="1"/> + <field name="altama_invoiced" optional="hide" readonly="1"/> <field name="so_item_margin" attrs="{'readonly': 1}" optional="hide"/> <field name="so_item_percent_margin" attrs="{'readonly': 1}" optional="hide"/> <field name="item_margin" attrs="{'readonly': 1}" optional="hide"/> diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 9cd63e25..29a95626 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -133,6 +133,7 @@ <field name="total_mapping_koli" attrs="{'invisible': [('location_id', '!=', 60)]}"/> <field name="total_koli_display" readonly="1" attrs="{'invisible': [('location_id', '!=', 60)]}"/> <field name="linked_out_picking_id" readonly="1" attrs="{'invisible': [('location_id', '=', 60)]}"/> + <field name="number_soo" attrs="{'invisible': [('picking_type_id', 'in', [29,30])]}"/> </field> <field name="weight_uom_name" position="after"> <group> diff --git a/indoteknik_custom/views/token_log.xml b/indoteknik_custom/views/token_log.xml new file mode 100644 index 00000000..77e6dd48 --- /dev/null +++ b/indoteknik_custom/views/token_log.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <record id="token_log_tree" model="ir.ui.view"> + <field name="name">token.log.tree</field> + <field name="model">token.log</field> + <field name="arch" type="xml"> + <tree editable="top" default_order="create_date desc"> + <field name="token"/> + <field name="expires_at"/> + <field name="token_from"/> + <field name="created_at"/> + <field name="created_by"/> + <field name="source"/> + <field name="is_active" widget="boolean_toggle"/> + </tree> + </field> + </record> + + <record id="token_log_action" model="ir.actions.act_window"> + <field name="name">Token Log</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">token.log</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem + action="token_log_action" + id="token_log" + parent="base.menu_users" + name="Token Log" + sequence="1" + /> +</odoo> |
