summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-07-18 09:25:57 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-07-18 09:25:57 +0700
commit10e171ea5389873fea3fb13c90242b322a37a990 (patch)
tree9288e4c9693cb5ea1a3d18e66e0bb723f9be5133
parent66b6b5863a15377c91300079026b11e7f81d60e4 (diff)
On change product otomatis terisi di requisition
Create bills by search PO & SKU Create bills by upload
-rwxr-xr-xfixco_custom/__manifest__.py1
-rwxr-xr-xfixco_custom/models/__init__.py3
-rw-r--r--fixco_custom/models/account_move.py38
-rw-r--r--fixco_custom/models/account_move_line.py2
-rw-r--r--fixco_custom/models/automatic_purchase.py33
-rw-r--r--fixco_custom/models/manage_stock.py1
-rw-r--r--fixco_custom/models/purchasing_job.py81
-rw-r--r--fixco_custom/models/upload_bills.py195
-rwxr-xr-xfixco_custom/security/ir.model.access.csv4
-rw-r--r--fixco_custom/views/account_move.xml11
-rw-r--r--fixco_custom/views/automatic_purchase.xml2
-rw-r--r--fixco_custom/views/ir_sequence.xml11
-rw-r--r--fixco_custom/views/upload_bills.xml60
13 files changed, 388 insertions, 54 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py
index 6c6c0df..12e15ca 100755
--- a/fixco_custom/__manifest__.py
+++ b/fixco_custom/__manifest__.py
@@ -36,6 +36,7 @@
'views/manage_stock.xml',
'views/automatic_purchase.xml',
'views/purchasing_job.xml',
+ 'views/upload_bills.xml',
],
'demo': [],
'css': [],
diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py
index bc53263..f345625 100755
--- a/fixco_custom/models/__init__.py
+++ b/fixco_custom/models/__init__.py
@@ -22,4 +22,5 @@ from . import account_move_line
from . import manage_stock
from . import automatic_purchase
from . import purchasing_job
-from . import stock_move \ No newline at end of file
+from . import stock_move
+from . import upload_bills \ No newline at end of file
diff --git a/fixco_custom/models/account_move.py b/fixco_custom/models/account_move.py
index 0a417cd..f491bc4 100644
--- a/fixco_custom/models/account_move.py
+++ b/fixco_custom/models/account_move.py
@@ -31,12 +31,45 @@ class AccountMove(models.Model):
states={'draft': [('readonly', False)]},
help="Auto-complete from multiple past bills / purchase orders.",
)
+ faktur_pajak = fields.Char('Faktur Pajak')
+ count_payment = fields.Integer('Count Payment', compute='_compute_count_payment')
+ def _compute_count_payment(self):
+ for move in self:
+ accountPayment = self.env['account.payment']
+
+ payment = accountPayment.search([]).filtered(
+ lambda p: move.id in p.reconciled_bill_ids.ids
+ )
+
+
+ move.count_payment = len(payment)
+
+ def action_view_related_payment(self):
+ self.ensure_one()
+
+ accountPayment = self.env['account.payment']
+
+ payment = accountPayment.search([]).filtered(
+ lambda p: self.id in p.reconciled_bill_ids.ids
+ )
+
+ payments = payment
+
+ return {
+ 'name': 'Payments',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'account.payment',
+ 'view_mode': 'tree,form',
+ 'target': 'current',
+ 'domain': [('id', 'in', list(payments.ids))],
+ }
def action_post(self):
res = super(AccountMove, self).action_post()
for entry in self:
- entry.invoice_date = entry.picking_id.date_done
+ if entry.move_type == 'out_invoice':
+ entry.invoice_date = entry.picking_id.date_done
return res
@@ -59,7 +92,7 @@ class AccountMove(models.Model):
invoice_vals.pop('ref', None)
self.update(invoice_vals)
- po_lines = po.order_line - self.line_ids.mapped('purchase_line_id')
+ po_lines = po.order_line.filtered(lambda l: l.qty_received != l.qty_invoiced and l.qty_invoiced <= l.qty_received) - self.line_ids.mapped('purchase_line_id')
new_lines = self.env['account.move.line']
sequence = max(self.line_ids.mapped('sequence')) + 1 if self.line_ids else 10
@@ -73,6 +106,7 @@ class AccountMove(models.Model):
new_lines += new_line
new_lines._onchange_mark_recompute_taxes()
+
# Compute invoice_origin
origins = set(self.line_ids.mapped('purchase_line_id.order_id.name'))
diff --git a/fixco_custom/models/account_move_line.py b/fixco_custom/models/account_move_line.py
index cf4d5d7..0db9c33 100644
--- a/fixco_custom/models/account_move_line.py
+++ b/fixco_custom/models/account_move_line.py
@@ -15,5 +15,5 @@ class AccountMoveLine(models.Model):
@api.onchange('quantity')
def _onchange_quantity(self):
for line in self:
- if line.quantity > line.qty_outstanding:
+ if line and line.quantity > line.qty_outstanding:
raise UserError(_("Quantity Tidak Boleh Melebihi Qty Outstanding"))
diff --git a/fixco_custom/models/automatic_purchase.py b/fixco_custom/models/automatic_purchase.py
index f3f650d..1ef12ca 100644
--- a/fixco_custom/models/automatic_purchase.py
+++ b/fixco_custom/models/automatic_purchase.py
@@ -77,7 +77,6 @@ class AutomaticPurchase(models.Model):
'partner_id': vendor.id if vendor else False,
'taxes_id': vendor.tax_id.id if vendor else False,
'price': price,
- 'subtotal': subtotal,
})
self.env['automatic.purchase.line'].create(lines)
@@ -248,7 +247,6 @@ class AutomaticPurchase(models.Model):
'partner_id': stock.vendor_id.id,
'taxes_id': stock.vendor_id.tax_id.id,
'price': price,
- 'subtotal': subtotal,
})
else:
_logger.info(
@@ -290,14 +288,35 @@ class AutomaticPurchaseLine(models.Model):
qty_outgoing = fields.Float(string='Qty Outgoing', compute='compute_qty_outgoing')
partner_id = fields.Many2one('res.partner', string='Vendor')
price = fields.Float(string='Price')
- subtotal = fields.Float(string='Subtotal')
- last_order_id = fields.Many2one('purchase.order', string='Last Order')
- last_orderline_id = fields.Many2one('purchase.order.line', string='Last Order Line')
+ subtotal = fields.Float(string='Subtotal', compute='compute_subtotal')
is_po = fields.Boolean(String='Is PO')
- current_po_id = fields.Many2one('purchase.order', string='Current')
- current_po_line_id = fields.Many2one('purchase.order.line', string='Current Line')
taxes_id = fields.Many2one('account.tax', string='Taxes')
+ def compute_subtotal(self):
+ for line in self:
+ line.subtotal = line.qty_purchase * line.price
+
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ if self.product_id:
+ manage_stock = self.env['manage.stock'].search([
+ ('product_id', '=', self.product_id.id)
+ ], limit=1)
+
+ if manage_stock:
+ self.qty_min = manage_stock.min_stock
+ self.qty_buffer = manage_stock.buffer_stock
+ self.taxes_id = manage_stock.vendor_id.tax_id.id
+ self.partner_id = manage_stock.vendor_id.id
+
+ pricelist = self.env['purchase.pricelist'].search([
+ ('product_id', '=', self.product_id.id),
+ ('vendor_id', '=', manage_stock.vendor_id.id)
+ ], limit=1)
+
+ if pricelist:
+ self.price = pricelist.price
+
def compute_qty_available(self):
for line in self:
if line.product_id:
diff --git a/fixco_custom/models/manage_stock.py b/fixco_custom/models/manage_stock.py
index e654b4e..29d482f 100644
--- a/fixco_custom/models/manage_stock.py
+++ b/fixco_custom/models/manage_stock.py
@@ -78,7 +78,6 @@ class ManageStock(models.Model):
'partner_id': stock.vendor_id.id,
'taxes_id': stock.vendor_id.tax_id.id if stock.vendor_id.tax_id else False,
'price': price,
- 'subtotal': subtotal,
})
self.env['automatic.purchase.line'].create(lines_to_create)
diff --git a/fixco_custom/models/purchasing_job.py b/fixco_custom/models/purchasing_job.py
index 4f301f9..1af47b7 100644
--- a/fixco_custom/models/purchasing_job.py
+++ b/fixco_custom/models/purchasing_job.py
@@ -54,7 +54,6 @@ class PurchasingJob(models.Model):
'partner_id': stock.vendor_id.id,
'taxes_id': stock.vendor_id.tax_id.id if stock.vendor_id.tax_id else False,
'price': price,
- 'subtotal': subtotal,
})
self.env['automatic.purchase.line'].create(lines_to_create)
@@ -71,43 +70,49 @@ class PurchasingJob(models.Model):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
CREATE OR REPLACE VIEW %s AS
- SELECT
- row_number() OVER () AS id,
- a.item_code,
- a.product,
- a.onhand,
- a.incoming,
- a.outgoing,
- CASE
- WHEN (a.incoming + a.onhand) < a.outgoing THEN 'kurang'
- ELSE 'cukup'
- END AS action,
- a.product_id,
- pp2.vendor_id
- FROM (
- SELECT
- COALESCE(pp.default_code, pt.default_code) AS item_code,
- pt.name AS product,
- get_qty_onhand(pp.id::numeric) AS onhand,
- get_qty_incoming(pp.id::numeric) AS incoming,
- get_qty_outgoing(pp.id::numeric) AS outgoing,
- pp.id AS product_id
- FROM stock_move sm
- JOIN stock_picking sp ON sp.id = sm.picking_id
- JOIN product_product pp ON pp.id = sm.product_id
- JOIN product_template pt ON pt.id = pp.product_tmpl_id
- WHERE sp.state IN ('draft', 'waiting', 'confirmed', 'assigned')
- AND sp.name LIKE '%%OUT%%'
- AND sm.location_id = 55
- GROUP BY pp.id, pp.default_code, pt.default_code, pt.name
- ) a
- LEFT JOIN LATERAL (
- SELECT vendor_id
- FROM purchase_pricelist
- WHERE product_id = a.product_id
- ORDER BY id ASC
- LIMIT 1
- ) pp2 ON true
+ SELECT * FROM (
+ SELECT
+ row_number() OVER () AS id,
+ a.item_code,
+ a.product,
+ a.onhand,
+ a.incoming,
+ a.outgoing,
+ CASE
+ WHEN (a.incoming + a.onhand) < a.outgoing THEN 'kurang'
+ ELSE 'cukup'
+ END AS action,
+ a.product_id,
+ pp2.vendor_id
+ FROM (
+ SELECT
+ COALESCE(pp.default_code, pt.default_code) AS item_code,
+ pt.name AS product,
+ get_qty_onhand(pp.id::numeric) AS onhand,
+ get_qty_incoming(pp.id::numeric) AS incoming,
+ get_qty_outgoing(pp.id::numeric) AS outgoing,
+ pp.id AS product_id
+ FROM stock_move sm
+ JOIN stock_picking sp ON sp.id = sm.picking_id
+ JOIN product_product pp ON pp.id = sm.product_id
+ JOIN product_template pt ON pt.id = pp.product_tmpl_id
+ WHERE sp.state IN ('draft', 'waiting', 'confirmed', 'assigned')
+ AND sp.name LIKE '%%OUT%%'
+ AND sm.location_id = 55
+ GROUP BY pp.id, pp.default_code, pt.default_code, pt.name
+ ) a
+ LEFT JOIN LATERAL (
+ SELECT vendor_id
+ FROM purchase_pricelist
+ WHERE product_id = a.product_id
+ ORDER BY id ASC
+ LIMIT 1
+ ) pp2 ON true
+ ) final
+ WHERE final.action = 'kurang'
""" % self._table)
+ super(PurchasingJob, self).init()
+
+
diff --git a/fixco_custom/models/upload_bills.py b/fixco_custom/models/upload_bills.py
new file mode 100644
index 0000000..0c037f3
--- /dev/null
+++ b/fixco_custom/models/upload_bills.py
@@ -0,0 +1,195 @@
+from odoo import models, fields, api, _
+from datetime import datetime
+import base64
+import xlrd
+from odoo.exceptions import ValidationError, UserError
+import requests
+import json
+import hmac
+from hashlib import sha256
+
+class UploadBills(models.Model):
+ _name = "upload.bills"
+ _description = "Upload Bills"
+ _order = "create_date desc"
+ _rec_name = "number"
+
+ bills_lines = fields.One2many('upload.bills.line', 'upload_bills_id',
+ string='Lines', copy=False, auto_join=True)
+ number = fields.Char('Number', copy=False)
+ date_upload = fields.Datetime('Upload Date', copy=False)
+ user_id = fields.Many2one('res.users', 'Created By', default=lambda self: self.env.user.id)
+ excel_file = fields.Binary('Excel File', attachment=True)
+ filename = fields.Char('File Name')
+
+ @api.model
+ def create(self, vals):
+ vals['number'] = self.env['ir.sequence'].next_by_code('upload.bills') or '/'
+ return super().create(vals)
+
+ def action_import_excel(self):
+ self.ensure_one()
+ if not self.excel_file:
+ raise ValidationError(_("Please upload an Excel file first."))
+
+ try:
+ file_content = base64.b64decode(self.excel_file)
+ workbook = xlrd.open_workbook(file_contents=file_content)
+ sheet = workbook.sheet_by_index(0)
+ except Exception:
+ raise ValidationError(_("Invalid Excel file format."))
+
+ if sheet.ncols < 3:
+ raise ValidationError(_("Excel format tidak valid. Minimal ada 3 kolom: No BU IN, Tanggal, dan Faktur Pajak."))
+
+ rows_data = []
+ for row_idx in range(sheet.nrows):
+ try:
+ no_bu = str(sheet.cell(row_idx, 0).value).strip()
+ tanggal_raw = sheet.cell(row_idx, 1).value
+ faktur_pajak = str(sheet.cell(row_idx, 2).value).strip()
+
+ if isinstance(tanggal_raw, float):
+ tanggal = datetime(*xlrd.xldate_as_tuple(tanggal_raw, workbook.datemode)).date()
+ else:
+ tanggal = datetime.strptime(tanggal_raw, '%Y-%m-%d').date()
+
+ rows_data.append({
+ 'row_num': row_idx + 1,
+ 'no_bu': no_bu,
+ 'date': tanggal,
+ 'faktur_pajak': faktur_pajak,
+ })
+ except Exception:
+ continue
+
+ faktur_list = [row['faktur_pajak'] for row in rows_data]
+ existing_faktur = set()
+ no_bu_list = [row['no_bu'] for row in rows_data]
+ existing_no_bu = set()
+
+ chunk_size = 500
+ for i in range(0, len(faktur_list), chunk_size):
+ chunk_faktur = faktur_list[i:i + chunk_size]
+ chunk_no_bu = no_bu_list[i:i + chunk_size]
+
+ existing_lines = self.env['upload.bills.line'].search([
+ '|',
+ ('faktur_pajak', 'in', chunk_faktur),
+ ('no_bu', 'in', chunk_no_bu)
+ ])
+
+ existing_faktur.update(existing_lines.mapped('faktur_pajak'))
+ existing_no_bu.update(existing_lines.mapped('no_bu'))
+
+ duplicate_rows = []
+ for row in rows_data:
+ if row['faktur_pajak'] in existing_faktur or row['no_bu'] in existing_no_bu:
+ duplicate_rows.append(str(row['row_num']))
+
+ if duplicate_rows:
+ raise ValidationError(_("Data duplikat ditemukan di baris: %s\nPeriksa 'No BU IN' atau 'Faktur Pajak' yang sudah pernah diupload.") % ", ".join(duplicate_rows))
+
+ line_vals_list = []
+ for row in rows_data:
+ line_vals = {
+ 'no_bu': row['no_bu'],
+ 'date': row['date'],
+ 'faktur_pajak': row['faktur_pajak'],
+ 'upload_bills_id': self.id,
+ }
+ line_vals_list.append((0, 0, line_vals))
+
+ self.bills_lines.unlink()
+ self.write({'bills_lines': line_vals_list})
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Success'),
+ 'message': _('Berhasil mengimpor %s baris dari Excel.') % len(line_vals_list),
+ 'sticky': False,
+ 'next': {'type': 'ir.actions.act_window_close'},
+ }
+ }
+
+
+ def _show_notification(self, message):
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Success'),
+ 'message': message,
+ 'sticky': False,
+ }
+ }
+
+ def action_create_bills(self):
+ all_invoice_ids = []
+
+ for line in self.bills_lines:
+ bu_in = self.env['stock.picking'].search([
+ ('name', '=', line.no_bu),
+ ('picking_type_code', '=', 'incoming')
+ ], limit=1)
+
+ if not bu_in:
+ raise UserError(f"BU IN '{line.no_bu}' tidak ditemukan.")
+
+ # Panggil pembuatan invoice
+ created_invoices = bu_in.action_create_invoice_from_mr()
+
+ # Sinkron faktur dan tanggal ke account.move
+ if isinstance(created_invoices, dict) and 'res_id' in created_invoices:
+ # Satu invoice
+ invoice = self.env['account.move'].browse(created_invoices['res_id'])
+ invoice.write({
+ 'faktur_pajak': line.faktur_pajak,
+ 'invoice_date': line.date
+ })
+ invoice.action_post()
+ all_invoice_ids.append(invoice.id)
+
+ elif isinstance(created_invoices, dict) and 'domain' in created_invoices:
+ # Banyak invoice
+ invoice_ids = created_invoices.get('domain', [])[0][2] or []
+ invoices = self.env['account.move'].browse(invoice_ids)
+ invoices.write({
+ 'faktur_pajak': line.faktur_pajak,
+ 'invoice_date': line.date
+ })
+ invoices.action_post()
+ all_invoice_ids.extend(invoices.ids)
+
+ else:
+ raise UserError("Gagal menemukan invoice yang baru dibuat.")
+
+ # Tampilkan invoice(s) ke user
+ if not all_invoice_ids:
+ return {'type': 'ir.actions.act_window_close'}
+
+ action = self.env.ref('account.action_move_in_invoice_type').read()[0]
+ if len(all_invoice_ids) == 1:
+ action.update({
+ 'view_mode': 'tree,form',
+ 'res_id': all_invoice_ids,
+ })
+ else:
+ action.update({
+ 'domain': [('id', 'in', all_invoice_ids)],
+ 'view_mode': 'tree,form',
+ })
+ return action
+
+
+class UploadBillsLine(models.Model):
+ _name = "upload.bills.line"
+ _description = "Upload Bills Line"
+ _inherit = ['mail.thread']
+
+ upload_bills_id = fields.Many2one('upload.bills', string='Upload')
+ no_bu = fields.Char('No BU IN')
+ date = fields.Date('Date')
+ faktur_pajak = fields.Char('Faktur Pajak')
diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv
index 3c4541b..a3f7ac4 100755
--- a/fixco_custom/security/ir.model.access.csv
+++ b/fixco_custom/security/ir.model.access.csv
@@ -26,4 +26,6 @@ access_product_shipment_line,access.product.shipment.line,model_product_shipment
access_manage_stock,access.manage.stock,model_manage_stock,,1,1,1,1
access_automatic_purchase_line,access.automatic.purchase.line,model_automatic_purchase_line,,1,1,1,1
access_automatic_purchase,access.automatic.purchase,model_automatic_purchase,,1,1,1,1
-access_purchasing_job,access.purchasing.job,model_purchasing_job,,1,1,1,1 \ No newline at end of file
+access_purchasing_job,access.purchasing.job,model_purchasing_job,,1,1,1,1
+access_upload_bills,access.upload.bills,model_upload_bills,,1,1,1,1
+access_upload_bills_line,access.upload.bills.line,model_upload_bills_line,,1,1,1,1 \ No newline at end of file
diff --git a/fixco_custom/views/account_move.xml b/fixco_custom/views/account_move.xml
index 1c55831..3615fb7 100644
--- a/fixco_custom/views/account_move.xml
+++ b/fixco_custom/views/account_move.xml
@@ -7,11 +7,20 @@
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
+ <button name="open_reconcile_view" position="after">
+ <button type="object" name="action_view_related_payment"
+ class="oe_stat_button"
+ icon="fa-pencil-square-o">
+ <field name="count_payment" widget="statinfo" string="Payments"/>
+ </button>
+ </button>
+
<field name="payment_reference" position="after">
<field name="invoice_marketplace" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/>
+ <field name="faktur_pajak" readonly="1" attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/>
<field name="transaction_type" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/>
</field>
-
+
<field name="partner_id" position="after">
<field name="address" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/>
</field>
diff --git a/fixco_custom/views/automatic_purchase.xml b/fixco_custom/views/automatic_purchase.xml
index d303b24..f985a54 100644
--- a/fixco_custom/views/automatic_purchase.xml
+++ b/fixco_custom/views/automatic_purchase.xml
@@ -31,8 +31,6 @@
<field name="qty_outgoing" optional="hide"/>
<field name="price"/>
<field name="subtotal"/>
- <field name="last_order_id" readonly="1" optional="hide"/>
- <field name="current_po_line_id" readonly="1" optional="hide"/>
</tree>
</field>
</record>
diff --git a/fixco_custom/views/ir_sequence.xml b/fixco_custom/views/ir_sequence.xml
index 6e0e42a..06e11cb 100644
--- a/fixco_custom/views/ir_sequence.xml
+++ b/fixco_custom/views/ir_sequence.xml
@@ -55,5 +55,16 @@
<field name="number_increment">1</field>
<field name="company_id">4</field>
</record>
+
+ <record id="sequence_upload_bills" model="ir.sequence">
+ <field name="name">Upload Bills</field>
+ <field name="code">upload.bills</field>
+ <field name="active">TRUE</field>
+ <field name="prefix">UB/%(year)s/</field>
+ <field name="padding">5</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ <field name="company_id">4</field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/fixco_custom/views/upload_bills.xml b/fixco_custom/views/upload_bills.xml
new file mode 100644
index 0000000..a90546f
--- /dev/null
+++ b/fixco_custom/views/upload_bills.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <record id="upload_bills_tree" model="ir.ui.view">
+ <field name="name">upload.bills.tree</field>
+ <field name="model">upload.bills</field>
+ <field name="arch" type="xml">
+ <tree default_order="create_date desc">
+ <field name="number"/>
+ <field name="date_upload"/>
+ <field name="user_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_upload_bills_form" model="ir.ui.view">
+ <field name="name">upload.bills.form</field>
+ <field name="model">upload.bills</field>
+ <field name="arch" type="xml">
+ <form string="Upload Bills">
+ <header>
+ <button name="action_import_excel" string="Import Excel" type="object" class="oe_highlight" attrs="{'invisible': [('number', '=', False)]}"/>
+ <button name="action_create_bills" string="Create Bills" type="object" class="oe_highlight" attrs="{'invisible': [('number', '=', False)]}"/>
+ <field name="number" widget="field_no_edit" options="{'no_open': True}"/>
+ <field name="date_upload"/>
+ <field name="user_id" widget="field_no_edit" options="{'no_open': True}"/>
+ </header>
+ <sheet>
+ <group>
+ <field name="excel_file" filename="filename"/>
+ <field name="filename" invisible="1"/>
+ </group>
+ <field name="bills_lines">
+ <tree editable="bottom">
+ <field name="no_bu"/>
+ <field name="date"/>
+ <field name="faktur_pajak"/>
+ </tree>
+ </field>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="upload_bills_action" model="ir.actions.act_window">
+ <field name="name">Upload Bills</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">upload.bills</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem
+ id="menu_upload_bills"
+ name="Upload Bills"
+ parent="sale.menu_sale_report"
+ sequence="4"
+ action="upload_bills_action"
+ />
+ </data>
+</odoo>