diff options
Diffstat (limited to 'fixco_custom/models/purchase_order.py')
| -rw-r--r-- | fixco_custom/models/purchase_order.py | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/fixco_custom/models/purchase_order.py b/fixco_custom/models/purchase_order.py index 8c84215..2393618 100644 --- a/fixco_custom/models/purchase_order.py +++ b/fixco_custom/models/purchase_order.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta import logging from pytz import timezone, utc import io +import requests +import json import base64 try: from odoo.tools.misc import xlsxwriter @@ -13,6 +15,7 @@ except ImportError: _logger = logging.getLogger(__name__) + class PurchaseOrder(models.Model): _inherit = 'purchase.order' @@ -46,6 +49,320 @@ class PurchaseOrder(models.Model): ('manual', 'Manual') ], string='Source', default='manual', copy=False) count_journals = fields.Integer('Count Payment', compute='_compute_count_journals') + shipping_cost = fields.Float(string='Shipping Cost', help='Total shipping cost for this PO') + 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_fixco_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_fixco_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_fixco_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_fixco_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_fixco_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 + # ============================ + for soo in list_soo: + order.soo_number = soo.get("no_soo") + order.soo_price = soo.get("totalprice") + order.soo_discount = soo.get("diskon") + order.soo_tax = 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 = 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>" + ) + + + def button_confirm(self): + res = super(PurchaseOrder, self).button_confirm() + self.action_create_order_altama() + return res + + @api.onchange('shipping_cost') + def _onchange_shipping_cost(self): + for order in self: + if not order.order_line: + continue + + total_subtotal = sum(order.order_line.mapped('original_price_unit')) + if total_subtotal == 0: + continue + + if order.shipping_cost and order.shipping_cost > 0: + # Distribusi ongkos kirim prorata + for line in order.order_line: + proportion = (line.original_price_subtotal / total_subtotal) + allocated_cost = order.shipping_cost * proportion + extra_cost_per_unit = allocated_cost / (line.product_qty or 1) + line.price_unit = line.original_price_unit + extra_cost_per_unit + else: + # Balikin harga ke semula kalau shipping_cost = 0 + for line in order.order_line: + line.price_unit = line.original_price_unit def _compute_count_journals(self): for order in self: |
