summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-11-26 10:08:16 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-11-26 10:08:16 +0700
commit25edffb8ebf51e4b133132f4fbd49363b1426664 (patch)
tree903a73bef338d0ddd1bed8577b276b318f37cc56
parent29d10b8de8422a7c2ced1816d7cc7df41c20b73c (diff)
api altama
-rwxr-xr-xfixco_custom/__manifest__.py1
-rwxr-xr-xfixco_custom/models/__init__.py3
-rw-r--r--fixco_custom/models/account_move.py27
-rw-r--r--fixco_custom/models/purchase_order.py317
-rw-r--r--fixco_custom/models/purchase_order_line.py27
-rwxr-xr-xfixco_custom/models/sale.py2
-rwxr-xr-xfixco_custom/models/stock_picking.py11
-rw-r--r--fixco_custom/models/token_log.py17
-rwxr-xr-xfixco_custom/security/ir.model.access.csv1
-rw-r--r--fixco_custom/views/account_move.xml15
-rw-r--r--fixco_custom/views/purchase_order.xml24
-rw-r--r--fixco_custom/views/token_log.xml33
12 files changed, 471 insertions, 7 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py
index ce9c5b8..badd1c6 100755
--- a/fixco_custom/__manifest__.py
+++ b/fixco_custom/__manifest__.py
@@ -46,6 +46,7 @@
'views/vit_kelurahan.xml',
'views/vit_kecamatan.xml',
'views/vit_kota.xml',
+ 'views/token_log.xml',
],
'demo': [],
'css': [],
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:<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:
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
diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv
index 8103ca2..eee7451 100755
--- a/fixco_custom/security/ir.model.access.csv
+++ b/fixco_custom/security/ir.model.access.csv
@@ -38,3 +38,4 @@ access_invoice_reklas,access.invoice.reklas,model_invoice_reklas,,1,1,1,1
access_uangmuka_pembelian,access.uangmuka.pembelian,model_uangmuka_pembelian,,1,1,1,1
access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1
access_report.fixco_custom.report_picking_list_custom,access.report.fixco_custom.report_picking_list_custom,model_report_fixco_custom_report_picking_list_custom,,1,1,1,1
+access_token_log,access.token.log,model_token_log,,1,1,1,1
diff --git a/fixco_custom/views/account_move.xml b/fixco_custom/views/account_move.xml
index 3b122dd..8eb7975 100644
--- a/fixco_custom/views/account_move.xml
+++ b/fixco_custom/views/account_move.xml
@@ -57,6 +57,7 @@
<!-- ✅ Add the new Many2many field after invoice_vendor_bill_id -->
<field name="invoice_vendor_bill_id" position="after">
<field name="purchase_id" invisible="1"/>
+ <field name="need_refund" invisible="1"/>
<label for="purchase_vendor_bill_ids" string="Auto-Complete" class="oe_edit_only"
attrs="{'invisible': ['|', ('state','!=','draft'), ('move_type', '!=', 'in_invoice')]}" />
@@ -116,6 +117,20 @@
</field>
</record>
+ <record id="view_move_tree_inherit_need_refund" model="ir.ui.view">
+ <field name="name">account.move.tree.need.refund</field>
+ <field name="model">account.move</field>
+ <field name="inherit_id" ref="account.view_invoice_tree"/>
+ <field name="arch" type="xml">
+ <xpath expr="//tree" position="attributes">
+ <attribute name="decoration-danger">need_refund</attribute>
+ </xpath>
+ <xpath expr="//tree" position="inside">
+ <field name="need_refund" invisible="1"/>
+ </xpath>
+ </field>
+ </record>
+
<record id="action_export_faktur" model="ir.actions.server">
<field name="name">Export Faktur ke XML</field>
<field name="model_id" ref="account.model_account_move" />
diff --git a/fixco_custom/views/purchase_order.xml b/fixco_custom/views/purchase_order.xml
index 174929c..dc863bc 100644
--- a/fixco_custom/views/purchase_order.xml
+++ b/fixco_custom/views/purchase_order.xml
@@ -19,10 +19,25 @@
<button id="draft_confirm" position="after">
<button name="fixco_custom.action_view_uangmuka_pembelian" string="UangMuka"
type="action" attrs="{'invisible': [('state', '!=', 'purchase')]}"/>
+ <button name="action_create_order_altama"
+ type="object"
+ string="Create Order Altama"
+ class="oe_highlight"
+ icon="fa-cloud-upload"/>
+ <button name="action_get_order_altama"
+ type="object"
+ string="Get Order Altama"
+ class="oe_highlight"
+ icon="fa-cloud-download"/>
</button>
- <field name="currency_id" position="after">
+ <field name="fiscal_position_id" position="after">
+ <field name="soo_number" invisible="1"/>
+ <field name="soo_price" readonly="1"/>
+ <field name="soo_discount" readonly="1"/>
+ <field name="soo_tax" readonly="1"/>
<field name="sale_order_id" readonly="1"/>
<field name="biaya_lain_lain"/>
+ <field name="shipping_cost"/>
<field name="source"/>
<field name="move_entry_id" readonly="1"/>
</field>
@@ -32,6 +47,13 @@
<field name="price_unit" position="after">
<field name="discount"/>
<field name="discount_amount" 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>
</field>
</record>
diff --git a/fixco_custom/views/token_log.xml b/fixco_custom/views/token_log.xml
new file mode 100644
index 0000000..77e6dd4
--- /dev/null
+++ b/fixco_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>