summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2026-02-24 13:20:33 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2026-02-24 13:20:33 +0700
commit316b8257845d0df10153fa7e5e294a699ad17c56 (patch)
tree78337836f0cc61e1af8e1549dc22f907a1d2dcee
parent345d45f5fae0f152ca39e9ba491513582a5c6791 (diff)
push
-rwxr-xr-xindoteknik_custom/__manifest__.py3
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/automatic_purchase.py3
-rwxr-xr-xindoteknik_custom/models/purchase_order.py299
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py17
-rw-r--r--indoteknik_custom/models/stock_picking.py1
-rw-r--r--indoteknik_custom/models/token_log.py17
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv1
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml18
-rw-r--r--indoteknik_custom/views/stock_picking.xml1
-rw-r--r--indoteknik_custom/views/token_log.xml33
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>