From 25edffb8ebf51e4b133132f4fbd49363b1426664 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 26 Nov 2025 10:08:16 +0700 Subject: api altama --- fixco_custom/models/__init__.py | 3 +- fixco_custom/models/account_move.py | 27 ++- fixco_custom/models/purchase_order.py | 317 +++++++++++++++++++++++++++++ fixco_custom/models/purchase_order_line.py | 27 +++ fixco_custom/models/sale.py | 2 +- fixco_custom/models/stock_picking.py | 11 + fixco_custom/models/token_log.py | 17 ++ 7 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 fixco_custom/models/token_log.py (limited to 'fixco_custom/models') diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py index 27ca4e1..c9c2484 100755 --- a/fixco_custom/models/__init__.py +++ b/fixco_custom/models/__init__.py @@ -30,4 +30,5 @@ from . import reordering_rule from . import update_depreciation_move_wizard from . import invoice_reklas from . import uangmuka_pembelian -from . import coretax_faktur \ No newline at end of file +from . import coretax_faktur +from . import token_log \ No newline at end of file diff --git a/fixco_custom/models/account_move.py b/fixco_custom/models/account_move.py index bb84573..63b3e8e 100644 --- a/fixco_custom/models/account_move.py +++ b/fixco_custom/models/account_move.py @@ -39,8 +39,27 @@ class AccountMove(models.Model): count_reverse = fields.Integer('Count Reverse', compute='_compute_count_reverse') uangmuka = fields.Boolean('Uang Muka?') reklas = fields.Boolean('Reklas?') - reklas_used = fields.Boolean('Reklas Used?', compute='_compute_reklas_used') - reklas_used_by = fields.Many2one('account.move', string='Reklas Used By', compute='_compute_reklas_used') + reklas_used = fields.Boolean('Reklas Used?', compute='_compute_reklas_used', store=True) + reklas_used_by = fields.Many2one('account.move', string='Reklas Used By', compute='_compute_reklas_used', store=True) + need_refund = fields.Boolean( + string="Need Refund", + compute="_compute_need_refund", + help="Flag otomatis kalau invoice sudah paid dan picking terkait di-return." + ) + + def _compute_need_refund(self): + for move in self: + flag = False + if move.move_type == 'out_invoice' and move.payment_state == 'paid' and move.invoice_origin: + refund_exists = bool(self.env['account.move'].search([('reversed_entry_id', '=', move.id), ('payment_state', '=', 'paid')])) + if not refund_exists: + sale_orders = self.env['sale.order'].search([('name', '=', move.invoice_origin)]) + if sale_orders: + pickings = sale_orders.picking_ids.filtered(lambda p: p.state == 'done' and p.is_return) + if pickings: + flag = True + move.need_refund = flag + def export_faktur_to_xml(self): valid_invoices = self @@ -104,9 +123,9 @@ class AccountMove(models.Model): reverses = reverse return { - 'name': 'Payments', + 'name': 'Refund', 'type': 'ir.actions.act_window', - 'res_model': 'account.payment', + 'res_model': 'account.move', 'view_mode': 'tree,form', 'target': 'current', 'domain': [('id', 'in', list(reverses.ids))], 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:
{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_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""" + + {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 = 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)}
" + ) + + + 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: diff --git a/fixco_custom/models/purchase_order_line.py b/fixco_custom/models/purchase_order_line.py index c06ed4f..9d73192 100644 --- a/fixco_custom/models/purchase_order_line.py +++ b/fixco_custom/models/purchase_order_line.py @@ -15,10 +15,37 @@ class PurchaseOrderLine(models.Model): digits='Discount', default=0.0 ) + 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 + ) discount_amount = fields.Float( string='Discount Amount', compute='_compute_discount_amount' ) + original_price_unit = fields.Float(string='Original Unit Price', readonly=True) + original_price_subtotal = fields.Float(string='Original Subtotal', readonly=True) + description = fields.Text(string='Description', readonly=True, copy=False) + docstatus_altama = fields.Text(string='Status Altama', readonly=True, copy=False) + + @api.constrains('product_id', 'price_unit', 'product_qty') + def _store_original_price(self): + for line in self: + # Simpen harga awal cuma sekali + if not line.original_price_unit or line.price_unit != line.original_price_unit: + line.original_price_unit = line.price_unit + line.original_price_subtotal = line.original_price_unit * line.product_qty @api.constrains('product_qty', 'product_id') def constrains_product_qty(self): diff --git a/fixco_custom/models/sale.py b/fixco_custom/models/sale.py index c6e9ccb..9c4ed6b 100755 --- a/fixco_custom/models/sale.py +++ b/fixco_custom/models/sale.py @@ -15,7 +15,7 @@ class SaleOrder(models.Model): count_journals = fields.Integer('Count Payment', compute='_compute_count_journals') - remaining_down_payment = fields.Float('Remaining Down Payment', compute='_compute_remaining_down_payment') + remaining_down_payment = fields.Float('Remaining Down Payment', compute='_compute_remaining_down_payment', store=True) def _compute_remaining_down_payment(self): for order in self: diff --git a/fixco_custom/models/stock_picking.py b/fixco_custom/models/stock_picking.py index 73f4175..5c432b0 100755 --- a/fixco_custom/models/stock_picking.py +++ b/fixco_custom/models/stock_picking.py @@ -44,6 +44,17 @@ class StockPicking(models.Model): store=False ) is_printed = fields.Boolean(string="Sudah Dicetak", default=False) + is_return = fields.Boolean( + string="Is Return", + compute="_compute_is_return", + store=True + ) + + @api.depends('move_lines.origin_returned_move_id') + def _compute_is_return(self): + for picking in self: + # Picking dianggap return kalau ada minimal satu move yang direturn dari move lain + picking.is_return = any(m.origin_returned_move_id for m in picking.move_lines) def action_cancel(self): for picking in self: diff --git a/fixco_custom/models/token_log.py b/fixco_custom/models/token_log.py new file mode 100644 index 0000000..fdc0c03 --- /dev/null +++ b/fixco_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 -- cgit v1.2.3