summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-11-19 14:49:01 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-11-19 14:49:01 +0700
commitbb2be920076aabc49b4f9fdd896d14e096e633eb (patch)
tree588196ba53fb7e41d96a61272bdb74b4821fe661
parent9c4f131ffaf37ca47a78b320a68f7de4e846ecfb (diff)
parent58623e9509789381dbe334969de647b4ad0302a4 (diff)
Merge branch 'odoo-backup' into locatorlocator
# Conflicts: # indoteknik_custom/models/__init__.py # indoteknik_custom/models/stock_move.py # indoteknik_custom/security/ir.model.access.csv # indoteknik_custom/views/stock_picking.xml
-rw-r--r--indoteknik_api/controllers/api_v1/product.py87
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py85
-rwxr-xr-xindoteknik_custom/__manifest__.py6
-rwxr-xr-xindoteknik_custom/models/__init__.py3
-rw-r--r--indoteknik_custom/models/account_asset.py4
-rw-r--r--indoteknik_custom/models/account_move.py53
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py8
-rw-r--r--indoteknik_custom/models/account_payment.py2
-rw-r--r--indoteknik_custom/models/advance_payment_request.py1553
-rw-r--r--indoteknik_custom/models/automatic_purchase.py6
-rw-r--r--indoteknik_custom/models/commision.py4
-rw-r--r--indoteknik_custom/models/commission_internal.py392
-rw-r--r--indoteknik_custom/models/coretax_fatur.py4
-rw-r--r--indoteknik_custom/models/dunning_run.py1
-rw-r--r--indoteknik_custom/models/mrp_production.py3
-rw-r--r--indoteknik_custom/models/partial_delivery.py14
-rwxr-xr-xindoteknik_custom/models/purchase_order.py17
-rw-r--r--indoteknik_custom/models/refund_sale_order.py115
-rw-r--r--indoteknik_custom/models/res_partner.py4
-rwxr-xr-xindoteknik_custom/models/sale_order.py98
-rw-r--r--indoteknik_custom/models/sj_tele.py20
-rw-r--r--indoteknik_custom/models/solr/apache_solr_queue.py19
-rw-r--r--indoteknik_custom/models/solr/product_product.py4
-rw-r--r--indoteknik_custom/models/solr/product_template.py2
-rw-r--r--indoteknik_custom/models/stock_move.py114
-rw-r--r--indoteknik_custom/models/stock_picking.py223
-rw-r--r--indoteknik_custom/models/tukar_guling.py36
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py4
-rw-r--r--indoteknik_custom/models/uom_uom.py6
-rw-r--r--indoteknik_custom/report/purchase_report.xml5
-rw-r--r--indoteknik_custom/report/report_tutup_tempo.xml69
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv13
-rw-r--r--indoteknik_custom/views/account_asset_views.xml3
-rw-r--r--indoteknik_custom/views/account_move.xml4
-rw-r--r--indoteknik_custom/views/advance_payment_request.xml305
-rw-r--r--indoteknik_custom/views/advance_payment_settlement.xml186
-rw-r--r--indoteknik_custom/views/apache_solr_queue.xml3
-rw-r--r--indoteknik_custom/views/commission_internal.xml160
-rw-r--r--indoteknik_custom/views/dunning_run.xml1
-rw-r--r--indoteknik_custom/views/ir_sequence.xml23
-rw-r--r--indoteknik_custom/views/mail_template_pum.xml76
-rw-r--r--indoteknik_custom/views/refund_sale_order.xml4
-rw-r--r--indoteknik_custom/views/res_partner.xml1
-rwxr-xr-xindoteknik_custom/views/sale_order.xml15
-rw-r--r--indoteknik_custom/views/stock_picking.xml52
-rw-r--r--indoteknik_custom/views/uom_uom.xml14
46 files changed, 3582 insertions, 239 deletions
diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py
index e97a7ff8..2f546078 100644
--- a/indoteknik_api/controllers/api_v1/product.py
+++ b/indoteknik_api/controllers/api_v1/product.py
@@ -103,17 +103,21 @@ class Product(controller.Controller):
@controller.Controller.must_authorized()
def get_product_template_stock_by_id(self, **kw):
id = int(kw.get('id'))
- date_7_days_ago = datetime.now() - timedelta(days=7)
+ # date_7_days_ago = datetime.now() - timedelta(days=7)
+ product = request.env['product.product'].search([('id', '=', id)], limit=1)
+ if not product:
+ return self.response({'qty': 0, 'sla_date': 'N/A'}, headers=[('Cache-Control', 'max-age=600, private')])
+
product_pruchase = request.env['purchase.pricelist'].search([
('product_id', '=', id),
('is_winner', '=', True)
- ])
- stock_vendor = request.env['stock.vendor'].search([
- ('product_variant_id', '=', id),
- ('write_date', '>=', date_7_days_ago.strftime("%Y-%m-%d %H:%M:%S"))
], limit=1)
+ # stock_vendor = request.env['stock.vendor'].search([
+ # ('product_variant_id', '=', id),
+ # ('write_date', '>=', date_7_days_ago.strftime("%Y-%m-%d %H:%M:%S"))
+ # ], limit=1)
- product = product_pruchase.product_id
+ # product = product_pruchase.product_id
vendor_sla = request.env['vendor.sla'].search([('id_vendor', '=', product_pruchase.vendor_id.id)], limit=1)
slatime = 15
@@ -132,42 +136,45 @@ class Product(controller.Controller):
if qty_available < 1 :
qty_available = 0
- qty = 0
+ # qty = 0
+ qty = qty_available
sla_date = f'{slatime} Hari'
+ if qty_available > 0:
+ sla_date = '1 Hari'
# Qty Stock Vendor
- qty_vendor = stock_vendor.quantity
- qty_vendor -= int(qty_vendor * 0.1)
- qty_vendor = math.ceil(float(qty_vendor))
- total_excell = qty_vendor
-
- is_altama_product = product.x_manufacture.id in [10, 122, 89]
- if is_altama_product:
- try:
- # Qty Altama
- qty_altama = request.env['product.template'].get_stock_altama(
- product.default_code)
- qty_altama -= int(qty_altama * 0.1)
- qty_altama = math.ceil(float(qty_altama))
- total_adem = qty_altama
-
- if qty_available > 0:
- qty = qty_available + total_adem + total_excell
- sla_date = '1 Hari'
- elif qty_altama > 0 or qty_vendor > 0:
- qty = total_adem if qty_altama > 0 else total_excell
- sla_date = f'{slatime} Hari'
- else:
- sla_date = f'{slatime} Hari'
- except:
- print('error')
- else:
- if qty_available > 0:
- qty = qty_available
- sla_date = f'1 Hari'
- elif qty_vendor > 0:
- qty = total_excell
- sla_date = f'{slatime} Hari'
+ # qty_vendor = stock_vendor.quantity
+ # qty_vendor -= int(qty_vendor * 0.1)
+ # qty_vendor = math.ceil(float(qty_vendor))
+ # total_excell = qty_vendor
+
+ # is_altama_product = product.x_manufacture.id in [10, 122, 89]
+ # if is_altama_product:
+ # try:
+ # # Qty Altama
+ # qty_altama = request.env['product.template'].get_stock_altama(
+ # product.default_code)
+ # qty_altama -= int(qty_altama * 0.1)
+ # qty_altama = math.ceil(float(qty_altama))
+ # total_adem = qty_altama
+
+ # if qty_available > 0:
+ # qty = qty_available + total_adem + total_excell
+ # sla_date = '1 Hari'
+ # elif qty_altama > 0 or qty_vendor > 0:
+ # qty = total_adem if qty_altama > 0 else total_excell
+ # sla_date = f'{slatime} Hari'
+ # else:
+ # sla_date = f'{slatime} Hari'
+ # except:
+ # print('error')
+ # else:
+ # if qty_available > 0:
+ # qty = qty_available
+ # sla_date = f'1 Hari'
+ # elif qty_vendor > 0:
+ # qty = total_excell
+ # sla_date = f'{slatime} Hari'
data = {
'qty': qty,
@@ -245,6 +252,8 @@ class Product(controller.Controller):
[('id', '=', id)], limit=1)
qty_available = product.qty_free_bandengan
+ if qty_available < 1:
+ qty_available = 0
data = {
'qty': qty_available,
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index ddbb89ad..94d0035f 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -1,5 +1,5 @@
from .. import controller
-from odoo import http
+from odoo import http, fields
from odoo.http import request, Response
from pytz import timezone
from datetime import datetime
@@ -8,7 +8,6 @@ import logging
_logger = logging.getLogger(__name__)
-_logger = logging.getLogger(__name__)
class StockPicking(controller.Controller):
prefix = '/api/v1/'
@@ -125,33 +124,87 @@ class StockPicking(controller.Controller):
@http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
def write_partner_stock_picking_documentation(self, scanid, **kw):
- sj_document = kw.get('sj_document') if 'sj_document' in kw else None
paket_document = kw.get('paket_document') if 'paket_document' in kw else None
dispatch_document = kw.get('dispatch_document') if 'dispatch_document' in kw else None
+ self_pu = kw.get('self_pu') if 'self_pu' in kw else None
# ===== Cari picking by id / picking_code =====
- picking_data = False
+ picking = False
if scanid.isdigit() and int(scanid) < 2147483646:
- picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=0)
+ picking = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=0)
+ if not picking:
+ picking = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=0)
+ if not picking:
+ return self.response(code=403, description='picking not found')
- if not picking_data:
- picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=0)
+ # ===== Ambil MULTIPLE SJ dari form: sj_documentations=...&sj_documentations=... =====
+ form = request.httprequest.form or {}
+ sj_list = form.getlist('sj_documentations') # list of base64 strings
- if not picking_data:
- return self.response(code=403, description='picking not found')
+ # fallback: kalau FE kirim single dengan nama yang sama (bukan list)
+ if not sj_list and 'sj_documentations' in kw and kw.get('sj_documentations'):
+ sj_list = [kw.get('sj_documentations')]
- params = {
- 'driver_arrival_date': datetime.utcnow(),
- }
- if sj_document:
- params['sj_documentation'] = sj_document
+ params = {}
if paket_document:
params['paket_documentation'] = paket_document
+ params['driver_arrival_date'] = datetime.utcnow()
if dispatch_document:
params['dispatch_documentation'] = dispatch_document
+ if sj_list and self_pu:
+ params['driver_arrival_date'] = datetime.utcnow()
+
+ if params:
+ picking.write(params)
+
+ if sj_list:
+ Child = request.env['stock.picking.sj.document'].sudo()
+ seq = (picking.sj_documentations[:1].sequence or 10) if picking.sj_documentations else 10
+ for b64 in sj_list:
+ if not b64:
+ continue
+ Child.create({
+ 'picking_id': picking.id,
+ 'sequence': seq,
+ 'image': b64,
+ })
+ seq += 10
+
+ return self.response({'name': picking.name})
+
+ # @http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
+ # @controller.Controller.must_authorized()
+ # def write_partner_stock_picking_documentation(self, scanid, **kw):
+ # sj_document = kw.get('sj_document') if 'sj_document' in kw else None
+ # paket_document = kw.get('paket_document') if 'paket_document' in kw else None
+ # dispatch_document = kw.get('dispatch_document') if 'dispatch_document' in kw else None
+ # self_pu= kw.get('self_pu') if 'self_pu' in kw else None
+ #
+ # # ===== Cari picking by id / picking_code =====
+ # picking_data = False
+ # if scanid.isdigit() and int(scanid) < 2147483646:
+ # picking_data = request.env['stock.picking'].search([('id', '=', int(scanid))], limit=0)
+ #
+ # if not picking_data:
+ # picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=0)
+ #
+ # if not picking_data:
+ # return self.response(code=403, description='picking not found')
+ #
+ # params = {}
+ # if sj_document:
+ # params['sj_documentation'] = sj_document
+ # if self_pu:
+ # params['driver_arrival_date'] = datetime.utcnow()
+ # if paket_document:
+ # params['paket_documentation'] = paket_document
+ # params['driver_arrival_date'] = datetime.utcnow()
+ # if dispatch_document:
+ # params['dispatch_documentation'] = dispatch_document
+ #
+ # picking_data.write(params)
+ # return self.response({'name': picking_data.name})
- picking_data.write(params)
- return self.response({'name': picking_data.name})
@http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False)
def update_status_from_biteship(self, **kw):
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index d1229ffe..66962a24 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -175,6 +175,9 @@
'views/stock_inventory.xml',
'views/sale_order_delay.xml',
'views/refund_sale_order.xml',
+ 'views/advance_payment_request.xml',
+ 'views/advance_payment_settlement.xml',
+ # 'views/refund_sale_order.xml',
'views/tukar_guling.xml',
# 'views/tukar_guling_return_views.xml'
'views/tukar_guling_po.xml',
@@ -183,10 +186,13 @@
'views/unpaid_invoice_view.xml',
'views/letter_receivable.xml',
'views/letter_receivable_mail_template.xml',
+ 'views/mail_template_pum.xml',
# 'views/reimburse.xml',
'views/sj_tele.xml',
'views/close_tempo_mail_template.xml',
'views/domain_apo.xml',
+ 'views/uom_uom.xml',
+ 'views/commission_internal.xml'
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index c6a85b75..adc80b20 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -154,6 +154,7 @@ from . import approval_invoice_date
from . import approval_payment_term
from . import refund_sale_order
# from . import patch
+from . import advance_payment_request
from . import tukar_guling
from . import tukar_guling_po
from . import update_date_planned_po_wizard
@@ -164,3 +165,5 @@ from . import partial_delivery
from . import domain_apo
from . import stock_location
from . import stock_quant
+from . import uom_uom
+from . import commission_internal
diff --git a/indoteknik_custom/models/account_asset.py b/indoteknik_custom/models/account_asset.py
index bd5f9adb..211ab229 100644
--- a/indoteknik_custom/models/account_asset.py
+++ b/indoteknik_custom/models/account_asset.py
@@ -4,6 +4,10 @@ from odoo.exceptions import AccessError, UserError, ValidationError
class AccountAsset(models.Model):
_inherit = 'account.asset.asset'
+ asset_type = fields.Selection(string='Tipe Aset', selection=[
+ ('aset_gudang', ' Aset Gudang'),
+ ('aset_kantor', 'Aset Kantor'),
+ ], tracking=True )
def action_close_asset(self):
for asset in self:
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 44b3cb76..6212664e 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -3,7 +3,7 @@ from odoo.exceptions import AccessError, UserError, ValidationError
from markupsafe import escape as html_escape
from datetime import timedelta, date, datetime
from pytz import timezone, utc
-import logging
+import logging, json
import base64
import PyPDF2
import os
@@ -11,6 +11,7 @@ import re
from terbilang import Terbilang
from collections import defaultdict
from odoo.tools.misc import formatLang
+import socket
_logger = logging.getLogger(__name__)
@@ -32,7 +33,6 @@ class AccountMove(models.Model):
new_due_date = fields.Date(string='New Due')
counter = fields.Integer(string="Counter", default=0)
cost_centre_id = fields.Many2one('cost.centre', string='Cost Centre')
- analytic_account_ids = fields.Many2many('account.analytic.account', string='Analytic Account')
due_line = fields.One2many('due.extension.line', 'invoice_id', compute='_compute_due_line', string='Due Extension Lines')
no_faktur_pajak = fields.Char(string='No Faktur Pajak')
date_completed = fields.Datetime(string='Date Completed')
@@ -106,6 +106,32 @@ class AccountMove(models.Model):
help="Tanggal janji bayar dari customer setelah reminder dikirim.",
tracking=True
)
+ internal_notes_contact = fields.Text(related='partner_id.comment', string="Internal Notes", readonly=True, help="Internal Notes dari contact utama customer.")
+
+ payment_info = fields.Text(
+ string="Payment Info",
+ compute='_compute_payment_info',
+ store=False,
+ help="Informasi pembayaran yang diambil dari payment yang sudah direkonsiliasi ke invoice ini."
+ )
+
+ def _compute_payment_info(self):
+ for rec in self:
+ summary = ""
+ try:
+ widget_data = rec.invoice_payments_widget
+ if widget_data:
+ data = json.loads(widget_data)
+ lines = []
+ for item in data.get('content', []):
+ amount = item.get('amount', 0.0)
+ date = item.get('date') or item.get('payment_date') or ''
+ formatted_amount = formatLang(self.env, amount, currency_obj=rec.currency_id)
+ lines.append(f"<li><i>Paid on {date}</i> - {formatted_amount}</li>")
+ summary = f"<ul>{''.join(lines)}</ul>" if lines else (data.get('title', '') or "")
+ except Exception:
+ summary = ""
+ rec.payment_info = summary
# def _check_and_lock_cbd(self):
# cbd_term = self.env['account.payment.term'].browse(26)
@@ -160,7 +186,8 @@ class AccountMove(models.Model):
def action_sync_promise_date(self):
self.ensure_one()
finance_user_ids = [688]
- if self.env.user.id not in finance_user_ids:
+ is_it = self.env.user.has_group('indoteknik_custom.group_role_it')
+ if self.env.user.id not in finance_user_ids and not is_it:
raise UserError('Hanya Finance (Widya) yang dapat menggunakan fitur ini.')
if not self.customer_promise_date:
raise UserError("Isi Janji Bayar terlebih dahulu sebelum melakukan sinkronisasi.")
@@ -168,10 +195,11 @@ class AccountMove(models.Model):
other_invoices = self.env['account.move'].search([
('id', '!=', self.id),
('partner_id', '=', self.partner_id.id),
- ('invoice_date_due', '=', self.invoice_date_due),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
- ('date_terima_tukar_faktur', '!=', False)
+ ('date_terima_tukar_faktur', '!=', False),
+ ('invoice_payment_term_id.name', 'ilike', 'tempo')
])
lines = []
for inv in other_invoices:
@@ -191,7 +219,16 @@ class AccountMove(models.Model):
'target': 'new',
}
+ @staticmethod
+ def is_local_env():
+ hostname = socket.gethostname().lower()
+ keywords = ['andri', 'miqdad', 'fin', 'stephan', 'hafid', 'nathan']
+ return any(keyword in hostname for keyword in keywords)
+
def send_due_invoice_reminder(self):
+ if self.is_local_env():
+ _logger.warning("📪 Local environment detected — skip sending email reminders.")
+ return
today = fields.Date.today()
target_dates = [
today + timedelta(days=7),
@@ -213,6 +250,9 @@ class AccountMove(models.Model):
self._send_invoice_reminders(invoices, mode='due')
def send_overdue_invoice_reminder(self):
+ if self.is_local_env():
+ _logger.warning("📪 Local environment detected — skip sending email reminders.")
+ return
today = fields.Date.today()
invoices = self.env['account.move'].search([
('move_type', '=', 'out_invoice'),
@@ -228,6 +268,9 @@ class AccountMove(models.Model):
self._send_invoice_reminders(invoices, mode='overdue')
def _send_invoice_reminders(self, invoices, mode):
+ if self.is_local_env():
+ _logger.warning("📪 Local environment detected — skip sending email reminders.")
+ return
today = fields.Date.today()
template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index 55fc6c65..352200e0 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -115,6 +115,11 @@ class DueExtension(models.Model):
self.order_id.check_credit_limit()
self.order_id.approval_status = 'pengajuan1'
return self.order_id._create_approval_notification('Sales Manager')
+
+ if self.order_id._requires_approval_team_sales():
+ self.order_id.check_credit_limit()
+ self.order_id.approval_status = 'pengajuan1'
+ return self.order_id._create_approval_notification('Team Sales')
sales = self.env['sale.order'].browse(self.order_id.id)
@@ -122,6 +127,9 @@ class DueExtension(models.Model):
self.order_id.due_id = self.id
self.approve_by = self.env.user.id
self.date_approve = datetime.utcnow()
+
+ # self.order_id.message_post("Due Extension telah di approve")
+ self.order_id.message_post(f"Due Extension {self.number} telah di approve")
template = self.env.ref('indoteknik_custom.mail_template_due_extension_approve')
template.send_mail(self.id, force_send=True)
return {
diff --git a/indoteknik_custom/models/account_payment.py b/indoteknik_custom/models/account_payment.py
index 11c664eb..d2d3d175 100644
--- a/indoteknik_custom/models/account_payment.py
+++ b/indoteknik_custom/models/account_payment.py
@@ -42,7 +42,7 @@ class AccountPayment(models.Model):
def allocate_invoices(self):
for payment in self:
- if self.
+ # if self.
for line in payment.payment_line:
invoice = line.account_move_id
move_lines = payment.line_ids.filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py
new file mode 100644
index 00000000..ec23de63
--- /dev/null
+++ b/indoteknik_custom/models/advance_payment_request.py
@@ -0,0 +1,1553 @@
+from odoo import models, api, fields, _
+from odoo.exceptions import UserError, ValidationError
+from datetime import date, datetime, timedelta
+# import datetime
+import logging
+_logger = logging.getLogger(__name__)
+from terbilang import Terbilang
+import pytz
+from pytz import timezone
+import base64
+
+
+class AdvancePaymentRequest(models.Model):
+ _name = 'advance.payment.request'
+ _description = 'Advance Payment Request or Reimburse'
+ _rec_name = 'number'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = 'create_date desc'
+
+ user_id = fields.Many2one('res.users', string='Diajukan Oleh', default=lambda self: self.env.user, tracking=3)
+ partner_id = fields.Many2one('res.partner', string='Partner', related='user_id.partner_id', readonly=True)
+
+ number = fields.Char(string='No. Dokumen', default='New Draft', tracking=3)
+
+ applicant_name = fields.Many2one('res.users', string='Nama Pemohon', default=lambda self: self.env.user, required=True, tracking=3, domain="[('groups_id', 'in', [1])]")
+ # applicant_name = fields.One2many(string='Nama Pemohon', related='res.users')
+ nominal = fields.Float(string='Nominal', tracking=3, required=True)
+
+ bank_name = fields.Char(string='Bank', tracking=3, required=True)
+ account_name = fields.Many2one('res.users', string='Nama Account', default=lambda self: self.env.user, required=True, tracking=3, domain="[('groups_id', 'in', [1])]")
+ # account_name = fields.Char(string='Nama Account', tracking=3, required=True)
+ bank_account = fields.Char(string='No. Rekening', tracking=3, required=True)
+ detail_note = fields.Text(string='Keterangan Penggunaan Rinci', tracking=3)
+
+ date_back_to_office = fields.Date(
+ string='Tanggal Kembali ke Kantor',
+ tracking=3
+ )
+
+ estimated_return_date = fields.Date(
+ string='Batas Pengajuan',
+ help='Tanggal batas maksimal durasi pengajuan realisasi'
+ )
+
+ days_remaining = fields.Integer(
+ string='Sisa Hari Realisasi',
+ compute='_compute_days_remaining',
+ help='Sisa hari batas maksimal pengajuan realisasi setelah kembali ke kantor. '
+ '7 hari setelah tanggal kembali.'
+ )
+
+ status = fields.Selection([
+ ('draft', 'Draft'),
+ ('pengajuan1', 'Menunggu Approval Departement'),
+ ('pengajuan2', 'Menunggu Approval AP'),
+ ('pengajuan3', 'Menunggu Approval Pimpinan'),
+ ('approved', 'Approved'),
+ ], string='Status', default='draft', tracking=3, index=True, track_visibility='onchange')
+
+
+ status_pay_down_payment = fields.Selection([
+ ('pending', 'Pending'),
+ ('payment', 'Payment'),
+ ], string='Status Pembayaran', default='pending', tracking=3)
+
+ name_approval_departement = fields.Char(string='Approval Departement')
+ name_approval_ap = fields.Char(string='Approval AP')
+ email_ap = fields.Char(string = 'Email AP')
+ name_approval_pimpinan = fields.Char(string='Approval Pimpinan')
+
+ date_approved_department = fields.Datetime(string="Date Approved Department")
+ date_approved_ap = fields.Datetime(string="Date Approved AP")
+ date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan")
+
+ position_department = fields.Char(string='Position Departement')
+ position_ap = fields.Char(string='Position AP')
+ position_pimpinan = fields.Char(string='Position Pimpinan')
+
+ approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always')
+
+ departement_type = fields.Selection([
+ ('sales', 'Sales'),
+ ('merchandiser', 'Merchandiser'),
+ ('marketing', 'Marketing'),
+ ('logistic', 'Logistic'),
+ ('procurement', 'Procurement'),
+ ('fat', 'FAT'),
+ ('it', 'IT'),
+ ('hr_ga', 'HR & GA'),
+ ('pimpinan', 'Pimpinan')
+ ], string='Departement Type', tracking=3, required=True)
+
+ attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image')
+ attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf')
+ attachment_filename_image = fields.Char(string='Filename Image')
+ attachment_filename_pdf = fields.Char(string='Filename PDF')
+
+ attachment_type = fields.Selection([
+ ('pdf', 'PDF'),
+ ('image', 'Image'),
+ ], string="Attachment Type")
+
+ move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')])
+ is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible')
+
+
+ currency_id = fields.Many2one(
+ 'res.currency', string='Currency',
+ default=lambda self: self.env.company.currency_id
+ )
+
+ type_request = fields.Selection([
+ ('pum', 'PUM'),
+ ('reimburse', 'Reimburse')], string='Tipe Pengajuan', tracking=3)
+
+ position_type = fields.Selection([
+ ('staff', 'Staff'),
+ ('manager', 'Manager'),
+ ('pimpinan', 'Pimpinan')], string='Jabatan')
+
+ settlement_type = fields.Selection([
+ ('no_settlement', 'Belum Realisasi'),
+ ('settlement', 'Realisasi')
+ ])
+
+ is_represented = fields.Boolean(string='Nama Pemohon Berbeda?', default=False)
+
+ apr_perjalanan = fields.Boolean(string = "PUM Perjalanan?", default = False)
+ reimburse_line_ids = fields.One2many('reimburse.line', 'request_id', string='Rincian Reimburse')
+ upload_attachment_date = fields.Datetime(string='Upload Attachment Date', tracking=3)
+ settlement_ids = fields.One2many(
+ 'advance.payment.settlement',
+ 'pum_id',
+ string='Realisasi'
+ )
+ has_settlement = fields.Boolean(
+ string='Has Settlement',
+ compute='_compute_has_settlement'
+ )
+ settlement_name = fields.Char(
+ string="Nama Realisasi",
+ compute='_compute_settlement_name'
+ )
+
+ grand_total_reimburse = fields.Monetary(
+ string='Total Reimburse',
+ compute='_compute_grand_total_reimburse',
+ currency_field='currency_id'
+ )
+
+ is_current_user_ap = fields.Boolean(
+ string="Is Current User AP",
+ compute='_compute_is_current_user_ap'
+ )
+
+ @api.onchange('grand_total_reimburse', 'type_request')
+ def _onchange_reimburse_line_update_nominal(self):
+ if self.type_request == 'reimburse':
+ self.nominal = self.grand_total_reimburse
+
+ def _compute_is_current_user_ap(self):
+ ap_user_ids = [23, 9468, 16729]
+ is_ap = self.env.user.id in ap_user_ids
+ for line in self:
+ line.is_current_user_ap = is_ap
+
+ @api.depends('reimburse_line_ids.total')
+ def _compute_grand_total_reimburse(self):
+ for request in self:
+ request.grand_total_reimburse = sum(request.reimburse_line_ids.mapped('total'))
+
+ def action_open_create_reimburse_cab(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Buat Jurnal Reimburse',
+ 'res_model': 'create.reimburse.cab.wizard',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'default_request_id': self.id,
+ 'default_total_reimburse': self.grand_total_reimburse,
+ }
+ }
+
+ @api.depends('settlement_ids')
+ def _compute_has_settlement(self):
+ for rec in self:
+ rec.has_settlement = bool(rec.settlement_ids)
+
+ @api.depends('settlement_ids', 'settlement_ids.name')
+ def _compute_settlement_name(self):
+ for request in self:
+ if request.settlement_ids:
+ request.settlement_name = request.settlement_ids[0].name
+ else:
+ request.settlement_name = False
+
+ @api.onchange('is_represented')
+ def _onchange_is_represented(self):
+ if self.is_represented:
+ self.account_name = False
+ self.applicant_name = False
+ else:
+ self.account_name = self.env.user.id
+ self.applicant_name = self.env.user.id
+
+ @api.onchange('nominal')
+ def _onchange_nominal_no_minus(self):
+ if self.nominal and self.nominal < 0:
+ self.nominal = 0
+ return {
+ 'warning': {
+ 'title': _('Nominal Tidak Valid'),
+ 'message': _(
+ "Nominal tidak boleh diisi minus.\n"
+ "Nilai di set menjadi nol."
+ )
+ }
+ }
+
+ def _get_jasper_attachment(self):
+ self.ensure_one()
+ report = self.env['ir.actions.report'].browse(1134) # ID Downpayment Report
+ if not report:
+ raise UserError("Report Jasper tidak ditemukan.")
+
+ data = report.render_jasper(self.ids, data={})[0]
+ filename = f"{self.number}.pdf"
+ return {
+ 'name': filename,
+ 'datas': base64.b64encode(data),
+ 'type': 'binary',
+ 'mimetype': 'application/pdf',
+ 'filename': filename,
+ }
+
+ # def action_send_pum_reminder(self):
+ # """
+ # Kirim email reminder PUM otomatis.
+ # - PUM Perjalanan:
+ # - Hari H kembali ke kantor = template 'mail_template_pum_reminder_today'
+ # - H-2 dari due date = template 'mail_template_pum_reminder_h_2'
+ # - PUM Non-Perjalanan:
+ # - H-2 dari due date = template 'mail_template_pum_reminder_h_2'
+ # """
+ # today = date.today()
+
+ # # Penyesuaian 1: Cari semua PUM yang sudah disetujui (bukan draft/reject)
+ # # Kita tidak filter 'date_back_to_office' di sini lagi.
+ # pum_ids = self.search([
+ # ('status', 'not in', ['draft', 'reject']),
+ # ('type_request', '=', 'pum')
+ # ])
+
+ # template_today = self.env.ref('indoteknik_custom.mail_template_pum_reminder_today', raise_if_not_found=False)
+ # template_h2 = self.env.ref('indoteknik_custom.mail_template_pum_reminder_h_2', raise_if_not_found=False)
+
+ # if not template_today or not template_h2:
+ # _logger.warning("Salah satu template email PUM (today/h2) tidak ditemukan.")
+ # return
+
+ # for pum in pum_ids:
+ # _logger.info(f"[REMINDER] Memproses PUM {pum.number}")
+
+ # # Penyesuaian 2: Logika ini sudah benar (sesuai update kita sebelumnya)
+ # # Jika realisasi sudah dibuat, PUM tidak aktif lagi, lewati.
+ # realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1)
+ # if realization:
+ # _logger.info(f"[REMINDER] Lewati PUM {pum.number}, realisasi sudah dibuat.")
+ # continue
+
+ # if not pum.email_ap or not pum.user_id.partner_id.email:
+ # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} karena email_ap atau email user kosong.")
+ # continue
+
+ # # Penyesuaian 3: Logika penentuan Due Date (Wajib)
+ # due_date = False
+ # base_date_for_today_check = False # Khusus PUM Perjalanan
+
+ # if pum.apr_perjalanan:
+ # if pum.date_back_to_office:
+ # due_date = pum.date_back_to_office + timedelta(days=7)
+ # base_date_for_today_check = pum.date_back_to_office
+ # else:
+ # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} (perjalanan) karena tgl kembali kosong.")
+ # continue
+ # else:
+ # # Ini adalah PUM Non-Perjalanan
+ # if not pum.create_date:
+ # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} (non-perjalanan) karena create_date kosong.")
+ # continue
+ # base_date = pum.create_date.date()
+ # due_date = base_date + timedelta(days=7)
+
+ # # Hitung sisa hari
+ # days_remaining = (due_date - today).days
+
+ # # Penyesuaian 4: Tentukan template berdasarkan sisa hari
+ # template = False
+ # if pum.apr_perjalanan and base_date_for_today_check == today:
+ # # Hari H kembali ke kantor (HANYA PUM Perjalanan)
+ # template = template_today
+ # elif days_remaining == 2:
+ # # H-2 due date (Untuk SEMUA jenis PUM)
+ # template = template_h2
+ # else:
+ # _logger.info(f"[REMINDER] Lewati PUM {pum.number}, hari ini bukan tgl pengingat (Sisa hari: {days_remaining}).")
+ # continue
+
+ # # --- Sisanya (Generate attachment & kirim email) sudah aman ---
+
+ # # Generate attachment
+ # try:
+ # attachment_vals = pum._get_jasper_attachment()
+ # attachment = self.env['ir.attachment'].create({
+ # 'name': attachment_vals['name'],
+ # 'type': 'binary',
+ # 'datas': attachment_vals['datas'],
+ # 'res_model': 'advance.payment.request',
+ # 'res_id': pum.id,
+ # 'mimetype': 'application/pdf',
+ # })
+ # except Exception as e:
+ # _logger.error(f"[REMINDER] Gagal membuat attachment untuk PUM {pum.number}: {str(e)}")
+ # continue
+
+ # email_values = {
+ # # 'email_to': pum.user_id.partner_id.email,
+ # 'email_to': 'andrifebriyadiputra@gmail.com', # Masih hardcode
+ # 'email_from': pum.email_ap,
+ # 'email_from': 'finance@indoteknik.co.id',
+ # 'attachment_ids': [(6, 0, [attachment.id])]
+ # }
+
+ # _logger.info(f"[REMINDER] Mengirim email PUM {pum.number} ke {email_values['email_to']} dari {email_values['email_from']}")
+
+ # try:
+ # body_html = template._render_field('body_html', [pum.id])[pum.id]
+ # template.send_mail(pum.id, force_send=True, email_values=email_values)
+ # _logger.info(f"[REMINDER] Email berhasil dikirim untuk PUM {pum.number}")
+
+ # # Post info sederhana
+ # pum.message_post(
+ # body="Email Reminder Berhasil dikirimkan",
+ # message_type="comment",
+ # subtype_xmlid="mail.mt_note",
+ # )
+
+ # user_system = self.env['res.users'].browse(25)
+ # system_id = user_system.partner_id.id if user_system else False
+
+ # # Post isi email ke chatter
+ # pum.message_post(
+ # body=body_html,
+ # message_type="comment",
+ # subtype_xmlid="mail.mt_note",
+ # author_id=system_id,
+ # )
+ # except Exception as e:
+ # _logger.error(f"[REMINDER] Gagal mengirim email untuk PUM {pum.number}: {str(e)}")
+
+ # return True
+
+
+ @api.depends('move_id.state')
+ def _compute_is_cab_visible(self):
+ for rec in self:
+ move = rec.move_id
+ rec.is_cab_visible = bool(move and move.state == 'posted')
+
+ def action_view_journal_uangmuka(self):
+ self.ensure_one()
+
+ ap_user_ids = [23, 9468, 16729]
+ is_it = self.env.user.has_group('indoteknik_custom.group_role_it')
+ if self.env.user.id not in ap_user_ids and not is_it:
+ raise UserError('Hanya User AP yang dapat menggunakan fitur ini.')
+
+ if not self.move_id:
+ raise UserError("Journal Uang Muka belum tersedia.")
+
+ return {
+ 'name': 'Journal Entry',
+ 'view_mode': 'form',
+ 'res_model': 'account.move',
+ 'type': 'ir.actions.act_window',
+ 'res_id': self.move_id.id,
+ 'target': 'current',
+ }
+
+ @api.onchange('attachment_type')
+ def _onchange_attachment_type(self):
+ self.attachment_file_image = False
+ self.attachment_filename_image = False
+ self.attachment_file_pdf = False
+ self.attachment_filename_pdf = False
+
+ # Sales & MD : Darren ID 19
+ # Marketing : Iwan ID 216
+ # Logistic & Procurement : Rafly H ID 21
+ # FAT & IT : Stephan ID 28
+ # HR & GA : Akbar ID 7 / Pimpinan
+ # ---------------------------------------
+ # AP : Manzila (Finance) ID 23
+
+ def _get_approver_mapping(self):
+ return {
+ 'sales': 19,
+ 'merchandiser': 19,
+ 'marketing': 216,
+ 'logistic': 21,
+ 'procurement': 21,
+ 'fat': 28,
+ 'it': 28,
+ 'hr_ga': 7,
+ }
+
+ def _get_departement_approver(self):
+ mapping = self._get_approver_mapping()
+ return mapping.get(self.departement_type)
+
+ @api.constrains('apr_perjalanan', 'date_back_to_office')
+ def _check_date_back_to_office(self):
+ if self.apr_perjalanan and not self.date_back_to_office:
+ raise ValidationError("Tanggal Kembali ke Kantor wajib diisi jika PUM Perjalanan dicentang.")
+
+ @api.onchange('applicant_name')
+ def _onchange_applicant_name_set_position(self):
+ if self.applicant_name:
+ user_id = self.applicant_name.id
+ mapping = self._get_approver_mapping()
+ manager_ids = set(mapping.values())
+ pimpinan_id = 7
+ if user_id == pimpinan_id:
+ self.position_type = 'pimpinan'
+ elif user_id in manager_ids:
+ self.position_type = 'manager'
+ else:
+ self.position_type = 'staff'
+ else:
+ self.position_type = False
+
+ # @api.model
+ # def default_get(self, fields_list):
+ # defaults = super(AdvancePaymentRequest, self).default_get(fields_list)
+ # user_id = defaults.get('user_id', self.env.uid)
+ # mapping = self._get_approver_mapping()
+ # manager_ids = set(mapping.values())
+ # pimpinan_id = 7
+
+ # position = 'staff'
+ # if user_id == pimpinan_id:
+ # position = 'pimpinan'
+ # elif user_id in manager_ids:
+ # position = 'manager'
+
+ # defaults['position_type'] = position
+ # return defaults
+
+ def action_realisasi_pum(self):
+ self.ensure_one()
+
+ realization = self.env['advance.payment.settlement'].search([('pum_id', '=', self.id)], limit=1)
+
+ if realization:
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Realisasi PUM',
+ 'res_model': 'advance.payment.settlement',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'res_id': realization.id,
+ }
+ else:
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Realisasi PUM',
+ 'res_model': 'advance.payment.settlement',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': {
+ 'default_pum_id': self.id,
+ # 'default_value_down_payment': self.nominal,
+ 'default_name': f'Realisasi - {self.number or ""}',
+ # 'default_pemberian_line_ids': [
+ # (0, 0, {
+ # 'date': self.create_date.date() if self.create_date else fields.Date.today(),
+ # 'description': 'Uang Muka',
+ # 'value': self.nominal
+ # })
+ # ]
+ }
+ }
+
+
+ def action_confirm_payment(self):
+ # jakarta_tz = pytz.timezone('Asia/Jakarta')
+ # now = datetime.now(jakarta_tz).replace(tzinfo=None)
+
+ ap_user_ids = [23, 9468, 16729]
+ if self.env.user.id not in ap_user_ids:
+ raise UserError('Hanya User AP yang dapat menggunakan fitur ini.')
+
+ for rec in self:
+ if not rec.attachment_file_image and not rec.attachment_file_pdf:
+ raise UserError(
+ f'Tidak bisa konfirmasi pembayaran {rec.number or ""} '
+ f'karena belum ada bukti attachment (PDF/Image).'
+ )
+
+ rec.status_pay_down_payment = 'payment'
+ rec.upload_attachment_date = datetime.utcnow()
+
+ rec.message_post(
+ body="Bukti transfer telah di upload oleh <b>Finance AP</b>.",
+ message_type="comment",
+ subtype_xmlid="mail.mt_note",
+ )
+
+
+
+ # def action_approval_check(self):
+ # for record in self:
+ # # user = record.user_id
+ # user = self.env['res.users'].browse(3401)
+ # roles = sorted(set(
+ # f"{group
+ # .name} (Category: {group.category_id.name})"
+ # for group in user.groups_id
+ # if group.category_id.name == 'Roles'
+ # ))
+ # _logger.info(f"[ROLE CHECK] User: {user.name} (Login: {user.login}) Roles: {roles}")
+ # return
+
+ def action_approval_check(self):
+ jakarta_tz = pytz.timezone('Asia/Jakarta')
+ now = datetime.now(jakarta_tz).replace(tzinfo=None)
+ formatted_date = now.strftime('%d %B %Y %H:%M')
+
+ for rec in self:
+ if not rec.departement_type:
+ raise UserError("Field 'departement_type' wajib diisi sebelum approval.")
+
+ approver_id = rec._get_departement_approver()
+
+ if rec.status == 'pengajuan1':
+ if self.env.user.id != approver_id:
+ raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.")
+ rec.name_approval_departement = self.env.user.name
+ rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement
+ rec.date_approved_department = now
+
+ # Mapping posisi berdasarkan departement_type
+ department_titles = {
+ 'sales': 'Sales Manager',
+ 'merchandiser': 'Merchandiser Manager',
+ 'marketing': 'Marketing Manager',
+ 'logistic': 'Logistic Manager',
+ 'procurement': 'Procurement Manager',
+ 'fat': 'Finance & Accounting Manager',
+ 'it': 'IT Manager',
+ 'hr_ga': 'HR & GA Manager',
+ 'pimpinan': 'Pimpinan',
+ }
+ rec.position_department = department_titles.get(rec.departement_type, 'Departement Manager')
+
+ rec.status = 'pengajuan2'
+
+ rec.message_post(
+ body=f"Approval <b>Departement</b> oleh <b>{self.env.user.name}</b> "
+ f"pada <i>{formatted_date}</i>."
+ )
+
+ elif rec.status == 'pengajuan2':
+ ap_user_ids = [23, 9468, 16729] # List user ID yang boleh approve sebagai Finance AP
+ if self.env.user.id not in ap_user_ids:
+ raise UserError("Hanya AP yang berhak menyetujui tahap ini.")
+ rec.name_approval_ap = self.env.user.name
+ rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap
+ rec.email_ap = self.env.user.email
+ rec.date_approved_ap = now
+ rec.position_ap = 'Finance AP'
+ if rec.position_type == 'pimpinan':
+ rec.status = 'approved'
+ else:
+ rec.status = 'pengajuan3'
+
+ rec.message_post(
+ body=f"Approval <b>AP</b> oleh <b>{self.env.user.name}</b> "
+ f"pada <i>{formatted_date}</i>."
+ )
+
+ elif rec.status == 'pengajuan3':
+ if self.env.user.id != 7: # ID user Pimpinan
+ raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.")
+ rec.name_approval_pimpinan = self.env.user.name
+ rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan
+ rec.date_approved_pimpinan = now
+ rec.position_pimpinan = 'Pimpinan'
+ rec.status = 'approved'
+
+ rec.message_post(
+ body=f"Approval <b>Pimpinan</b> oleh <b>{self.env.user.name}</b> "
+ f"pada <i>{formatted_date}</i>."
+ )
+
+ else:
+ raise UserError("Status saat ini tidak bisa di-approve lagi.")
+
+ # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap <b>{rec.status}</b>.")
+
+
+
+ def action_ap_only(self):
+ self.ensure_one()
+
+ ap_user_ids = [23, 9468, 16729] # Ganti sesuai kebutuhan
+ if self.env.user.id not in ap_user_ids:
+ raise UserError('Hanya User AP yang dapat menggunakan fitur ini.')
+
+ if self.move_id:
+ raise UserError('CAB / Jurnal sudah pernah dibuat untuk PUM ini.')
+
+ return {
+ 'name': 'Create CAB AP Only',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'advance.payment.create.bill',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'default_nominal': self.nominal,
+ 'default_apr_id': self.id,
+ }
+ }
+
+
+ @api.depends('date_back_to_office', 'status', 'apr_perjalanan', 'create_date', 'settlement_ids.status', 'type_request')
+ def _compute_days_remaining(self):
+ today = date.today()
+ for rec in self:
+
+ current_days = rec.days_remaining or 0
+ current_due_date = rec.estimated_return_date or False
+ if rec.type_request == 'pum':
+ is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids)
+ if not is_settlement_approved:
+ due_date = False
+
+ if rec.apr_perjalanan:
+ # Alur PUM Perjalanan
+ if rec.date_back_to_office:
+ due_date = rec.date_back_to_office + timedelta(days=7)
+ effective_today = max(today, rec.date_back_to_office)
+
+ current_due_date = due_date
+ current_days = (due_date - effective_today).days
+ else:
+
+ current_due_date = False
+ current_days = 0
+ else:
+ # Alur PUM Non-Perjalanan
+ if rec.create_date:
+ base_date = rec.create_date.date()
+ due_date = base_date + timedelta(days=7)
+
+ current_due_date = due_date
+ current_days = (due_date - today).days
+ else:
+ current_due_date = False
+ current_days = 0
+ else:
+ current_due_date = False
+ current_days = 0
+
+ rec.days_remaining = current_days
+ rec.estimated_return_date = current_due_date
+
+ @api.onchange('date_back_to_office')
+ def _onchange_date_back_to_office(self):
+ if self.date_back_to_office and self.date_back_to_office < date.today():
+ return {
+ 'warning': {
+ 'title': _('Tanggal Tidak Valid'),
+ 'message': _('Tanggal kembali ke kantor tidak boleh lebih awal dari hari ini.')
+ }
+ }
+
+ @api.onchange('applicant_name')
+ def _onchange_applicant_name(self):
+ if self.applicant_name:
+ self.account_name = self.applicant_name.id
+
+ @api.onchange('account_name')
+ def _onchange_account_name(self):
+ if self.account_name:
+ self.applicant_name = self.account_name.id
+
+ @api.onchange('user_id')
+ def _onchange_user_id_limit_check(self):
+ if self.type_request != 'pum':
+ return
+ if not self.user_id:
+ return
+
+ pum_ids = self.search([
+ ('user_id', '=', self.user_id.id),
+ ('status', '!=', 'reject'),
+ ('type_request', '=', 'pum')
+ ])
+
+ active_pum_count = 0
+ for pum in pum_ids:
+ realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1)
+ if not realization:
+ active_pum_count += 1
+
+ if active_pum_count >= 2:
+ return {
+ 'warning': {
+ 'title': 'Batas Pengajuan Tercapai',
+ 'message': 'User ini sudah memiliki 2 PUM aktif. Tidak dapat mengajukan lagi sampai salah satu direalisasi.',
+ }
+ }
+
+ def _get_department_titles_mapping(self):
+ return {
+ 'sales': 'Sales Manager',
+ 'merchandiser': 'Merchandiser Manager',
+ 'marketing': 'Marketing Manager',
+ 'logistic': 'Logistic Manager',
+ 'procurement': 'Procurement Manager',
+ 'fat': 'Finance & Accounting Manager',
+ 'it': 'IT Manager',
+ 'hr_ga': 'HR & GA Manager',
+ }
+
+ @api.model
+ def create(self, vals):
+ jakarta_tz = pytz.timezone('Asia/Jakarta')
+ now = datetime.now(jakarta_tz).replace(tzinfo=None)
+ user = self.env.user
+
+ pum_ids = self.search([
+ ('user_id', '=', user.id),
+ ('status', '!=', 'reject'),
+ ('type_request', '=', 'pum')
+ ])
+
+ active_pum_count = 0
+ for pum in pum_ids:
+ realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1)
+ if not realization:
+ active_pum_count += 1
+
+ if active_pum_count >= 2 and vals.get('type_request') == 'pum':
+ raise UserError("Anda hanya dapat mengajukan maksimal 2 PUM aktif. Silakan realisasikan salah satunya terlebih dahulu.")
+
+ if not vals.get('apr_perjalanan'):
+ if 'estimated_return_date' not in vals:
+ today = date.today()
+ due_date = today + timedelta(days=7)
+ vals['estimated_return_date'] = due_date
+
+ initial_status = ''
+ position = vals.get('position_type')
+ department = vals.get('departement_type')
+ if department == 'hr_ga' or position in ('manager', 'pimpinan'):
+ initial_status = 'pengajuan2'
+ else:
+ initial_status = 'pengajuan1'
+
+ vals['status'] = initial_status
+
+ if initial_status == 'pengajuan2' and department != 'hr_ga':
+ applicant_name = vals.get('applicant_name')
+ vals['name_approval_departement'] = self.env['res.users'].browse(applicant_name).name or ''
+ vals['date_approved_department'] = now
+ department_type = vals.get('departement_type')
+ department_titles = self._get_department_titles_mapping()
+ vals['position_department'] = department_titles.get(department_type, 'Departement Manager')
+
+ if position == 'pimpinan' and department != 'hr_ga':
+ vals['name_approval_pimpinan'] = self.env['res.users'].browse(vals.get('applicant_name')).name or ''
+ vals['position_pimpinan'] = 'Pimpinan'
+ vals['date_approved_pimpinan'] = now
+ # if position == 'staff':
+ # initial_status = 'pengajuan1'
+ # elif position == 'manager':
+ # initial_status = 'pengajuan2'
+ # applicant_name = vals.get('applicant_name')
+ # vals['name_approval_departement'] = self.env['res.users'].browse(applicant_name).name or ''
+ # vals['date_approved_department'] = now
+ # department_type = vals.get('departement_type')
+ # department_titles = self._get_department_titles_mapping()
+ # vals['position_department'] = department_titles.get(department_type, 'Departement Manager')
+ # elif position == 'pimpinan':
+ # initial_status = 'pengajuan2'
+ # applicant_name = vals.get('applicant_name')
+ # vals['name_approval_pimpinan'] = self.env['res.users'].browse(applicant_name).name or ''
+ # vals['position_pimpinan'] = 'Pimpinan'
+ # vals['date_approved_pimpinan'] = now
+
+ # vals['status'] = initial_status
+
+ if not vals.get('number') or vals['number'] == 'New Draft':
+ if vals.get('type_request') == 'reimburse':
+ vals['number'] = self.env['ir.sequence'].next_by_code('reimburse.request') or 'New Draft'
+ else:
+ vals['number'] = self.env['ir.sequence'].next_by_code('advance.payment.request') or 'New Draft'
+
+ # vals['status'] = 'pengajuan1'
+ # return super(AdvancePaymentRequest, self).create(vals)
+ rec = super(AdvancePaymentRequest, self).create(vals)
+ if rec.type_request == 'reimburse':
+ rec._compute_grand_total_reimburse()
+ rec.nominal = rec.grand_total_reimburse
+ return rec
+
+
+class AdvancePaymentUsageLine(models.Model):
+ _name = 'advance.payment.usage.line'
+ _description = 'Advance Payment Usage Line'
+
+ realization_id = fields.Many2one('advance.payment.settlement', string='Realization')
+ date = fields.Date(string='Tanggal', required=True, default=fields.Date.today)
+ description = fields.Text(string='Description', required=True)
+ nominal = fields.Float(string='Nominal', required=True)
+ done_attachment = fields.Boolean(string='Checked', default=False)
+
+ lot_of_attachment = fields.Selection(
+ related='realization_id.lot_of_attachment',
+ string='Lot of Attachment (Related)',
+ store=False
+ )
+
+ attachment_type = fields.Selection([
+ ('pdf', 'PDF'),
+ ('image', 'Image'),
+ ], string="Attachment Type", default='pdf')
+
+ attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image')
+ attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf')
+ attachment_filename_image = fields.Char(string='Filename Image')
+ attachment_filename_pdf = fields.Char(string='Filename PDF')
+
+ account_id = fields.Many2one(
+ 'account.account', string='Jenis Biaya', tracking=3 # ID Jenis Biaya yang dibutuhkan
+ )
+ # domain="[('id', 'in', [484, 486, 488, 506, 507, 625, 471, 519, 527, 528, 529, 530, 565])]" # ID Jenis Biaya yang dibutuhkan
+
+ is_current_user_ap = fields.Boolean(
+ string="Is Current User AP",
+ compute='_compute_is_current_user_ap'
+ )
+
+ def _compute_is_current_user_ap(self):
+ ap_user_ids = [23, 9468, 16729]
+ is_ap = self.env.user.id in ap_user_ids
+ for line in self:
+ line.is_current_user_ap = is_ap
+
+ # @api.onchange('account_id')
+ # def _onchange_account_id(self):
+ # for rec in self:
+ # if rec.account_id:
+ # rec.description = rec.account_id.name + " - "
+
+ @api.onchange('attachment_type')
+ def _onchange_attachment_type(self):
+ self.attachment_file_image = False
+ self.attachment_filename_image = False
+ self.attachment_file_pdf = False
+ self.attachment_filename_pdf = False
+
+ @api.onchange('done_attachment')
+ def _onchange_done_attachment(self):
+ ap_user_ids = [23, 9468, 16729] # List user ID yang boleh approve sebagai Finance AP
+
+ if self.done_attachment and self.env.user.id not in ap_user_ids:
+ self.done_attachment = False
+ return {
+ 'warning': {
+ 'title': _('Tidak Diizinkan'),
+ 'message': _('Hanya user AP yang bisa mencentang Done Attachment.')
+ }
+ }
+
+ @api.onchange('nominal')
+ def _onchange_nominal_no_minus(self):
+ if self.nominal and self.nominal < 0:
+ self.nominal = 0
+ return {
+ 'warning': {
+ 'title': _('Nominal Tidak Valid'),
+ 'message': _(
+ "Nominal penggunaan PUM tidak boleh diisi minus.\n"
+ "Nilai di Set menjadi nol."
+ )
+ }
+ }
+
+class ReimburseLine(models.Model):
+ _name = 'reimburse.line'
+ _description = 'Reimburse Line'
+
+ request_id = fields.Many2one('advance.payment.request', string='Request')
+ date = fields.Date(string='Tanggal', required=True, default=fields.Date.today)
+ account_id = fields.Many2one(
+ 'account.account',
+ string='Jenis Biaya', tracking=3
+ )
+ description = fields.Text(string='Description', required=True, tracking=3)
+ distance_departure = fields.Float(string='Pergi (Km)', tracking=3)
+ distance_return = fields.Float(string='Pulang (Km)', tracking=3)
+ quantity = fields.Float(string='Quantity', tracking=3, default=1)
+ price_unit = fields.Float(string='Price', tracking=3)
+ total = fields.Float(string='Total', tracking=3, compute='_compute_total')
+ # total = fields.Float(string='Total', tracking=3)
+ currency_id = fields.Many2one(related='request_id.currency_id')
+
+ is_vehicle = fields.Boolean(string='Berkendara?')
+ vehicle_type = fields.Selection([
+ ('motor', 'Motor'),
+ ('car', 'Mobil'),
+ ], string='Tipe Kendaraan', tracking=3)
+
+ attachment_image = fields.Binary(string='Image', attachment_filename='attachment_name_image')
+ attachment_pdf = fields.Binary(string='PDF', attachment_filename='attachment_name_pdf')
+ attachment_name_image = fields.Char(string='Filename Image')
+ attachment_name_pdf = fields.Char(string='Filename PDF')
+
+ attachment_type = fields.Selection([
+ ('pdf', 'PDF'),
+ ('image', 'Image'),
+ ], string="Attachment Type")
+
+ is_checked = fields.Boolean(string='Checked', default=False)
+
+ is_current_user_ap = fields.Boolean(
+ string="Is Current User AP",
+ compute='_compute_is_current_user_ap'
+ )
+
+ def _compute_is_current_user_ap(self):
+ ap_user_ids = [23, 9468, 16729]
+ is_ap = self.env.user.id in ap_user_ids
+ for line in self:
+ line.is_current_user_ap = is_ap
+
+ @api.depends('quantity', 'price_unit', 'is_vehicle')
+ def _compute_total(self):
+ for line in self:
+ line.total = line.quantity * line.price_unit
+
+ @api.onchange('is_vehicle', 'vehicle_type', 'distance_departure', 'distance_return')
+ def _onchange_vehicle_data(self):
+ if not self.is_vehicle:
+ self.vehicle_type = False
+ self.distance_departure = 0
+ self.distance_return = 0
+ self.price_unit = 0
+ return
+
+ total_distance = self.distance_departure + self.distance_return
+
+ if self.vehicle_type and total_distance > 0:
+ biaya_per_km = 0
+ if self.vehicle_type == 'car':
+ biaya_per_km = 1000 # Rp 10.000 / 10 km
+ self.price_unit = biaya_per_km
+ elif self.vehicle_type == 'motor':
+ biaya_per_km = 500 # Rp 10.000 / 20 km
+ self.price_unit = biaya_per_km
+ self.total = total_distance * biaya_per_km
+ self.quantity = total_distance
+ else:
+ self.total = 0
+
+class AdvancePaymentSettlement(models.Model):
+ _name = 'advance.payment.settlement'
+ _description = 'Advance Payment Settlement'
+ _inherit = ['mail.thread']
+ _rec_name = 'name'
+ _order = 'create_date desc'
+
+ pum_id = fields.Many2one('advance.payment.request', string='No PUM', ondelete='cascade')
+ name = fields.Char(string='Nama', readonly=True, tracking=3)
+ title = fields.Char(string='Judul', tracking=3)
+ goals = fields.Text(string='Tujuan', tracking=3)
+ related = fields.Char(string='Dok. Terkait', tracking=3)
+
+ # pemberian_line_ids = fields.One2many(
+ # 'advance.payment.settlement.line', 'realization_id', string='Rincian Pemberian'
+ # )
+ penggunaan_line_ids = fields.One2many(
+ 'advance.payment.usage.line', 'realization_id', string='Rincian Penggunaan'
+ )
+
+ nominal_pum = fields.Float(
+ string='Nominal Pemberian PUM',
+ related='pum_id.nominal',
+ readonly=True )
+
+ # grand_total = fields.Float(string='Grand Total Pemberian', tracking=3, compute='_compute_grand_total')
+ grand_total_use = fields.Float(string='Grand Total Penggunaan', tracking=3, compute='_compute_grand_total_use')
+ # value_down_payment = fields.Float(string='PUM', tracking=3)
+ remaining_value = fields.Float(string='Sisa Uang PUM', tracking=3, compute='_compute_remaining_value')
+
+ def _get_default_note_approval(self):
+ template = (
+ "Demikian dokumen Realisasi Uang Muka ini saya buat, dengan ini saya meminta persetujuan dibawah atas hasil penggunaan uang muka yang saya gunakan untuk kebutuhan realisasi "
+ )
+ return template
+
+ note_approval = fields.Text(string='Note Persetujuan', tracking=3, default=_get_default_note_approval)
+
+ name_approval_departement = fields.Char(string='Approval Departement')
+ name_approval_ap = fields.Char(string='Approval AP')
+ name_approval_pimpinan = fields.Char(string='Approval Pimpinan')
+
+ date_approved_department = fields.Datetime(string="Date Approved Department")
+ date_approved_ap = fields.Datetime(string="Date Approved AP")
+ date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan")
+
+ position_department = fields.Char(string='Position Departement')
+ position_ap = fields.Char(string='Position AP')
+ position_pimpinan = fields.Char(string='Position Pimpinan')
+
+ approved_by = fields.Char(string='Approved By', track_visibility='always')
+
+ status = fields.Selection([
+ ('pengajuan1', 'Menunggu Approval Departement'),
+ ('pengajuan2', 'Menunggu Approval AP'),
+ ('pengajuan3', 'Menunggu Approval Pimpinan'),
+ ('approved', 'Approved'),
+ ], string='Status', default='pengajuan1', tracking=3, index=True, track_visibility='onchange')
+
+ # --- DIHAPUS ---
+ # done_status = fields.Selection([
+ # ('remaining', 'Remaining'),
+ # ('done_not_realized', 'Done Not Realized'),
+ # ('done_realized', 'Done Realized')
+ # ], string='Status Realisasi', tracking=3, default='remaining')
+ # date_done_not_realized = fields.Date(string='Tanggal Done Not Realized', tracking=3)
+ # --- BATAS DIHAPUS ---
+
+ currency_id = fields.Many2one(
+ 'res.currency', string='Currency',
+ default=lambda self: self.env.company.currency_id
+ )
+
+ attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image')
+ attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf')
+ attachment_filename_image = fields.Char(string='Filename Image')
+ attachment_filename_pdf = fields.Char(string='Filename PDF')
+
+ attachment_type = fields.Selection([
+ ('pdf', 'PDF'),
+ ('image', 'Image'),
+ ], string="Attachment Type", default='pdf')
+
+ lot_of_attachment = fields.Selection([
+ ('one_for_all_line', '1 Attachment Untuk Semua Line Penggunaan PUM'),
+ ('one_for_one_line', '1 Attachment per 1 Line Penggunaan PUM'),
+ ], string = "Banyaknya Attachment", default='one_for_one_line')
+
+ move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')])
+ is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible')
+
+ user_id = fields.Many2one(
+ 'res.users',
+ string='Diajukan Oleh',
+ related='pum_id.user_id',
+ readonly=True
+ )
+ applicant_name = fields.Many2one(
+ 'res.users',
+ string='Nama Pemohon',
+ related='pum_id.applicant_name',
+ readonly=True
+ )
+ is_current_user_ap = fields.Boolean(
+ string="Is Current User AP",
+ compute='_compute_is_current_user_ap'
+ )
+
+ def _compute_is_current_user_ap(self):
+ ap_user_ids = [23, 9468, 16729]
+ is_ap = self.env.user.id in ap_user_ids
+ for line in self:
+ line.is_current_user_ap = is_ap
+
+ def action_toggle_check_attachment(self):
+ ap_user_ids = [23, 9468, 16729]
+ if self.env.user.id not in ap_user_ids:
+ raise UserError('Hanya User AP yang dapat menggunakan tombol ini.')
+
+ for rec in self:
+ if not rec.penggunaan_line_ids:
+ continue
+
+ if all(line.done_attachment for line in rec.penggunaan_line_ids):
+ for line in rec.penggunaan_line_ids:
+ line.done_attachment = False
+ else:
+ for line in rec.penggunaan_line_ids:
+ line.done_attachment = True
+
+ @api.onchange('lot_of_attachment')
+ def _onchange_lot_of_attachment(self):
+ if self.lot_of_attachment == 'one_for_all_line':
+ for line in self.penggunaan_line_ids:
+ line.attachment_file_pdf = False
+ line.attachment_file_image = False
+ line.attachment_filename_pdf = False
+ line.attachment_filename_image = False
+
+
+ @api.depends('move_id.state')
+ def _compute_is_cab_visible(self):
+ for rec in self:
+ move = rec.move_id
+ rec.is_cab_visible = bool(move and move.state == 'posted')
+
+ def action_view_journal_uangmuka(self):
+ self.ensure_one()
+
+ ap_user_ids = [23, 9468, 16729]
+ is_it = self.env.user.has_group('indoteknik_custom.group_role_it')
+ if self.env.user.id not in ap_user_ids and not is_it:
+ raise UserError('Hanya User AP yang dapat menggunakan fitur ini.')
+
+ if not self.move_id:
+ raise UserError("Journal Uang Muka belum tersedia.")
+
+ return {
+ 'name': 'Journal Entry',
+ 'view_mode': 'form',
+ 'res_model': 'account.move',
+ 'type': 'ir.actions.act_window',
+ 'res_id': self.move_id.id,
+ 'target': 'current',
+ }
+
+
+ @api.onchange('attachment_type')
+ def _onchange_attachment_type(self):
+ self.attachment_file_image = False
+ self.attachment_filename_image = False
+ self.attachment_file_pdf = False
+ self.attachment_filename_pdf = False
+
+ # @api.depends('pemberian_line_ids.value')
+ # def _compute_grand_total(self):
+ # for rec in self:
+ # rec.grand_total = sum(line.value for line in rec.pemberian_line_ids)
+
+ @api.depends('penggunaan_line_ids.nominal')
+ def _compute_grand_total_use(self):
+ for rec in self:
+ rec.grand_total_use = sum(line.nominal for line in rec.penggunaan_line_ids)
+
+ @api.depends('nominal_pum', 'grand_total_use')
+ def _compute_remaining_value(self):
+ for rec in self:
+ rec.remaining_value = rec.nominal_pum - rec.grand_total_use
+ return
+
+ # --- DIHAPUS ---
+ # def action_validation(self):
+ # self.ensure_one()
+
+ # # Validasi hanya AP yang bisa validasi
+ # ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP
+ # if self.env.user.id not in ap_user_ids:
+ # raise UserError('Hanya AP yang dapat melakukan validasi realisasi.')
+
+ # if self.done_status == 'remaining':
+ # self.done_status = 'done_not_realized'
+ # self.date_done_not_realized = fields.Date.today()
+ # elif self.done_status == 'done_not_realized':
+ # self.done_status = 'done_realized'
+ # else:
+ # raise UserError('Realisasi sudah berstatus Done Realized.')
+
+ # # Opsional: Tambah log di chatter
+ # self.message_post(body=f"Status realisasi diperbarui menjadi <b>{dict(self._fields['done_status'].selection).get(self.done_status)}</b> oleh {self.env.user.name}.")
+ # --- BATAS DIHAPUS ---
+
+ def action_cab(self):
+ self.ensure_one()
+
+ ap_user_ids = [23, 9468, 16729] # List user ID yang boleh approve sebagai Finance AP
+ if self.env.user.id not in ap_user_ids:
+ raise UserError('Hanya User AP yang dapat menggunakan ini.')
+ if self.move_id:
+ raise UserError("CAB / Jurnal sudah pernah dibuat untuk Realisasi ini.")
+
+ if not self.pum_id or not self.pum_id.move_id:
+ raise UserError("PUM terkait atau CAB belum tersedia.")
+
+ # partner_id = self.pum_id.user_id.partner_id.id
+ partner_id = self.pum_id.applicant_name.partner_id.id
+ cab_move = self.pum_id.move_id
+
+ # Account Bank Intransit dari CAB:
+ bank_intransit_line = cab_move.line_ids.filtered(lambda l: l.account_id.id in [573, 389, 392])
+ if not bank_intransit_line:
+ raise UserError("Account Bank Intransit dengan tidak ditemukan di CAB terkait.")
+ account_sisa_pum = bank_intransit_line[0].account_id.id
+
+ # Account Uang Muka Operasional
+ account_uang_muka = 403
+
+ # --- PENYESUAIAN LOGIKA ---
+ # Tanggal pakai create_date atau hari ini
+ account_date = fields.Date.context_today(self)
+ # --- BATAS PENYESUAIAN ---
+
+ ref_label = f"Realisasi {self.pum_id.number} {self.pum_id.detail_note} ({cab_move.name})"
+
+ label_sisa_pum = f"Sisa PUM {self.pum_id.detail_note} {self.pum_id.number} ({cab_move.name})"
+
+ lines = []
+
+ # Sisa PUM (Debit)
+ if self.remaining_value > 0:
+ lines.append((0, 0, {
+ 'account_id': account_sisa_pum,
+ 'partner_id': partner_id,
+ 'name': label_sisa_pum,
+ 'debit': self.remaining_value,
+ 'credit': 0,
+ }))
+
+ # Biaya Penggunaan (Debit)
+ total_biaya = 0
+ for line in self.penggunaan_line_ids:
+ lines.append((0, 0, {
+ 'account_id': line.account_id.id,
+ 'partner_id': partner_id,
+ 'name': f"{line.description} ({line.date})",
+ 'debit': line.nominal,
+ 'credit': 0,
+ }))
+ total_biaya += line.nominal
+
+ # Uang Muka Operasional (Credit)
+ total_credit = self.remaining_value + total_biaya
+ if total_credit > 0:
+ lines.append((0, 0, {
+ 'account_id': account_uang_muka,
+ 'partner_id': partner_id,
+ 'name': ref_label,
+ 'debit': 0,
+ 'credit': total_credit,
+ }))
+
+ move = self.env['account.move'].create({
+ 'ref': ref_label,
+ 'date': account_date,
+ 'journal_id': 11, # MISC
+ 'line_ids': lines,
+ })
+
+ # self.message_post(body=f"Jurnal CAB telah dibuat dengan nomor: <b>{move.name}</b>.")
+
+ self.move_id = move.id
+
+ return {
+ 'name': _('Journal Entry'),
+ 'view_mode': 'form',
+ 'res_model': 'account.move',
+ 'type': 'ir.actions.act_window',
+ 'res_id': move.id,
+ 'target': 'current',
+ }
+
+ def action_approval_check(self):
+ jakarta_tz = pytz.timezone('Asia/Jakarta')
+ now = datetime.now(jakarta_tz).replace(tzinfo=None)
+ formatted_date = now.strftime('%d %B %Y %H:%M')
+
+ for rec in self:
+ if not rec.pum_id.departement_type:
+ raise UserError("Field 'departement_type' wajib diisi sebelum approval.")
+
+ approver_id = rec.pum_id._get_departement_approver()
+
+ if rec.status == 'pengajuan1':
+ if self.env.user.id != approver_id:
+ raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.")
+ rec.name_approval_departement = self.env.user.name
+ rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement
+ rec.date_approved_department = now
+
+ # Mapping posisi berdasarkan departement_type
+ department_titles = {
+ 'sales': 'Sales Manager',
+ 'merchandiser': 'Merchandiser Manager',
+ 'marketing': 'Marketing Manager',
+ 'logistic': 'Logistic Manager',
+ 'procurement': 'Procurement Manager',
+ 'fat': 'Finance & Accounting Manager',
+ 'it': 'IT Manager',
+ 'hr_ga': 'HR & GA Manager',
+ 'pimpinan': 'Pimpinan',
+ }
+ rec.position_department = department_titles.get(rec.pum_id.departement_type, 'Departement Manager')
+
+ rec.status = 'pengajuan2'
+
+ rec.message_post(
+ body=f"Approval <b>Departement</b> oleh <b>{self.env.user.name}</b> "
+ f"pada <i>{formatted_date}</i>."
+ )
+
+ elif rec.status == 'pengajuan2':
+ ap_user_ids = [23, 9468, 16729] # List user ID yang boleh approve sebagai Finance AP
+ if self.env.user.id not in ap_user_ids:
+ raise UserError("Hanya AP yang berhak menyetujui tahap ini.")
+ rec.name_approval_ap = self.env.user.name
+ rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap
+ rec.date_approved_ap = now
+ rec.position_ap = 'Finance AP'
+ rec.status = 'pengajuan3'
+
+ rec.message_post(
+ body=f"Approval <b>AP</b> oleh <b>{self.env.user.name}</b> "
+ f"pada <i>{formatted_date}</i>."
+ )
+
+ elif rec.status == 'pengajuan3':
+ if self.env.user.id != 7: # ID user Pimpinan
+ raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.")
+ rec.name_approval_pimpinan = self.env.user.name
+ rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan
+ rec.date_approved_pimpinan = now
+ rec.position_pimpinan = 'Pimpinan'
+ rec.status = 'approved'
+ # --- DIHAPUS ---
+ # rec.done_status = 'done_not_realized' # Set status done untuk realisasi
+ # --- BATAS DIHAPUS ---
+
+ rec.message_post(
+ body=f"Approval <b>Pimpinan</b> oleh <b>{self.env.user.name}</b> "
+ f"pada <i>{formatted_date}</i>."
+ )
+
+ else:
+ raise UserError("Status saat ini tidak bisa di-approve lagi.")
+
+ # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap <b>{rec.status}</b>.")
+
+ def _check_remaining_value(self):
+ for rec in self:
+ # Cek sisa PUM
+ if rec.remaining_value < 0:
+ raise ValidationError(
+ "Sisa uang PUM tidak boleh kurang dari Rp 0.\n"
+ "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse."
+ )
+
+ @api.model
+ def create(self, vals):
+ jakarta_tz = pytz.timezone('Asia/Jakarta')
+ # --- PENYESUAIAN LOGIKA WAKTU ---
+ now = datetime.now(jakarta_tz).replace(tzinfo=None)
+ # Gunakan fields.Datetime.now() agar konsisten
+ # now = fields.Datetime.now()
+ # --- BATAS PENYESUAIAN ---
+
+ pum_id = vals.get('pum_id')
+ initial_status = ''
+ if pum_id:
+ pum_request = self.env['advance.payment.request'].browse(pum_id)
+ if pum_request:
+ position_dari_pum = pum_request.position_type
+ department_dari_pum = pum_request.departement_type
+ if position_dari_pum == 'staff':
+ if department_dari_pum == 'hr_ga':
+ initial_status = 'pengajuan2'
+ else:
+ initial_status = 'pengajuan1'
+ elif position_dari_pum == 'manager':
+ initial_status = 'pengajuan2'
+ applicant_name_str = pum_request.applicant_name.name or ''
+ department_type = pum_request.departement_type
+ department_titles = pum_request._get_department_titles_mapping()
+ dept_position = department_titles.get(department_type, 'Departement Manager')
+ vals['date_approved_department'] = now
+ vals['name_approval_departement'] = applicant_name_str
+ vals['position_department'] = dept_position
+ elif position_dari_pum == 'pimpinan':
+ initial_status = 'pengajuan2'
+ applicant_name_str = pum_request.applicant_name.name or ''
+ vals['date_approved_pimpinan'] = now
+ vals['name_approval_pimpinan'] = applicant_name_str
+ vals['position_pimpinan'] = 'Pimpinan'
+
+ # --- PENYESUAIAN LOGIKA: SET DEFAULT JIKA KOSONG ---
+ vals['status'] = initial_status or 'pengajuan1'
+ # --- BATAS PENYESUAIAN ---
+
+ rec = super().create(vals)
+ rec._check_remaining_value()
+ return rec
+
+ def write(self, vals):
+ res = super().write(vals)
+ self._check_remaining_value()
+ return res
+
+
+class AdvancePaymentCreateBill(models.TransientModel):
+ _name = 'advance.payment.create.bill'
+ _description = 'Create Bill from Advance Payment'
+
+ apr_id = fields.Many2one('advance.payment.request', string='Advance Payment Request', required=True)
+ account_id = fields.Many2one(
+ 'account.account', string='Bank Intransit', required=True,
+ domain="[('id', 'in', [573, 389, 392, 683, 380])]" # ID Bank Intransit
+ )
+ nominal = fields.Float(string='Nominal', related='apr_id.nominal')
+
+ def action_create_cab(self):
+ self.ensure_one()
+
+ # if self.env.user.id != 23:
+ # raise UserError('Hanya AP yang dapat menggunakan ini.')
+
+ apr = self.apr_id
+ # partner_id = apr.user_id.partner_id.id
+ partner_id = apr.applicant_name.partner_id.id
+
+ ref_label = f'{apr.number} - {apr.detail_note or "-"}'
+
+ move = self.env['account.move'].create({
+ 'ref': ref_label,
+ 'date': fields.Date.context_today(self),
+ 'journal_id': 11, # Cash & Bank
+ 'line_ids': [
+ (0, 0, {
+ 'account_id': 403, # Uang Muka Operasional
+ 'partner_id': partner_id,
+ 'name': ref_label,
+ 'debit': apr.nominal,
+ 'credit': 0,
+ }),
+ (0, 0, {
+ 'account_id': self.account_id.id, # Bank Intransit yang dipilih
+ 'partner_id': partner_id,
+ 'name': ref_label,
+ 'debit': 0,
+ 'credit': apr.nominal,
+ })
+ ]
+ })
+
+ apr.move_id = move.id # jika ada field untuk menampung move_id
+
+ return {
+ 'name': _('Journal Entry'),
+ 'view_mode': 'form',
+ 'res_model': 'account.move',
+ 'type': 'ir.actions.act_window',
+ 'res_id': move.id,
+ 'target': 'current',
+ }
+
+class CreateReimburseCabWizard(models.TransientModel):
+ _name = 'create.reimburse.cab.wizard'
+ _description = 'Wizard untuk Membuat Jurnal Reimburse'
+
+ # Field untuk menampung ID request yang sedang diproses
+ request_id = fields.Many2one('advance.payment.request', string='Pengajuan', readonly=True)
+
+ # Field untuk memilih salah satu dari dua bank Anda
+ account_id = fields.Many2one(
+ 'account.account',
+ string='Bank Intransit (Credit)',
+ required=True,
+ # Domain untuk membatasi pilihan hanya pada ID 573 dan 389
+ domain="[('id', 'in', [573, 389])]"
+ )
+
+ # Field untuk menampilkan total agar pengguna bisa konfirmasi
+ total_reimburse = fields.Monetary(
+ string='Total Reimburse',
+ related='request_id.grand_total_reimburse',
+ )
+ currency_id = fields.Many2one(related='request_id.currency_id', readonly=True)
+
+ def action_create_reimburse_cab(self):
+ """Metode ini yang akan membuat Journal Entry (CAB)."""
+ self.ensure_one()
+ request = self.request_id
+
+ # --- Validasi ---
+ if request.move_id:
+ raise UserError("Jurnal sudah pernah dibuat untuk pengajuan ini.")
+ if not request.reimburse_line_ids:
+ raise UserError("Tidak ada rincian reimburse yang bisa dijurnalkan.")
+
+ lines = []
+ # partner_id = request.user_id.partner_id.id
+ partner_id = request.applicant_name.partner_id.id
+ ref_label = f'{request.number} - {request.detail_note or "-"}'
+ # 1. Buat Jurnal DEBIT dari setiap baris reimburse
+ for line in request.reimburse_line_ids:
+ if not line.account_id:
+ raise UserError(f"Jenis Biaya pada baris '{line.description}' belum diisi oleh AP.")
+
+ lines.append((0, 0, {
+ 'account_id': line.account_id.id,
+ 'partner_id': partner_id,
+ 'name': line.description,
+ 'debit': line.total,
+ 'credit': 0,
+ }))
+
+ # 2. Buat satu Jurnal CREDIT ke bank yang dipilih di wizard
+ lines.append((0, 0, {
+ 'account_id': self.account_id.id,
+ 'partner_id': partner_id,
+ 'name': ref_label,
+ 'debit': 0,
+ 'credit': request.grand_total_reimburse,
+ }))
+
+
+ # 3. Buat Journal Entry
+ move = self.env['account.move'].create({
+ 'ref': ref_label,
+ 'date': fields.Date.context_today(self),
+ 'journal_id': 11, # PENTING: Ganti 11 dengan ID Journal "Miscellaneous" Anda
+ 'line_ids': lines,
+ })
+
+ # 4. Tautkan journal yang baru dibuat ke request
+ request.move_id = move.id
+
+ # 5. Buka tampilan form journal yang baru dibuat
+ return {
+ 'name': _('Journal Entry'),
+ 'view_mode': 'form',
+ 'res_model': 'account.move',
+ 'type': 'ir.actions.act_window',
+ 'res_id': move.id,
+ 'target': 'current',
+ } \ No newline at end of file
diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py
index d9ec17f4..4b0ce325 100644
--- a/indoteknik_custom/models/automatic_purchase.py
+++ b/indoteknik_custom/models/automatic_purchase.py
@@ -239,7 +239,8 @@ class AutomaticPurchase(models.Model):
'picking_type_id': 28,
'date_order': current_time,
'from_apo': True,
- 'note_description': 'Automatic PO'
+ 'note_description': 'Automatic PO',
+ 'show_description': False if vendor_id == 5571 else True,
}
new_po = self.env['purchase.order'].create(param_header)
@@ -283,7 +284,8 @@ class AutomaticPurchase(models.Model):
'ending_price': line.last_price,
'taxes_id': [(6, 0, [line.taxes_id.id])] if line.taxes_id else False,
'so_line_id': sales_match.sale_line_id.id if sales_match else None,
- 'so_id': sales_match.sale_id.id if sales_match else None
+ 'so_id': sales_match.sale_id.id if sales_match else None,
+ 'show_description': False if vendor_id == 5571 else True,
}
new_po_line = self.env['purchase.order.line'].create(param_line)
line.current_po_id = new_po.id
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index a937e2d0..441dd54f 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -423,8 +423,8 @@ class CustomerCommision(models.Model):
def action_confirm_customer_commision(self):
jakarta_tz = pytz.timezone('Asia/Jakarta')
now = datetime.now(jakarta_tz)
-
- now_naive = now.replace(tzinfo=None)
+ now_utc = now.astimezone(pytz.utc)
+ now_naive = now_utc.replace(tzinfo=None)
if not self.status or self.status == 'draft':
self.status = 'pengajuan1'
diff --git a/indoteknik_custom/models/commission_internal.py b/indoteknik_custom/models/commission_internal.py
new file mode 100644
index 00000000..cd6da380
--- /dev/null
+++ b/indoteknik_custom/models/commission_internal.py
@@ -0,0 +1,392 @@
+from odoo import models, api, fields
+from odoo.exceptions import AccessError, UserError, ValidationError
+from datetime import timedelta, date
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class CommissionInternal(models.Model):
+ _name = 'commission.internal'
+ _description = 'Commission Internal'
+ _order = 'date_doc, id desc'
+ _inherit = ['mail.thread']
+ _rec_name = 'number'
+
+ number = fields.Char(string='Document No', index=True, copy=False, readonly=True)
+ date_doc = fields.Date(string='Document Date', required=True)
+ month = fields.Selection([
+ ('01', 'January'), ('02', 'February'), ('03', 'March'),
+ ('04', 'April'), ('05', 'May'), ('06', 'June'),
+ ('07', 'July'), ('08', 'August'), ('09', 'September'),
+ ('10', 'October'), ('11', 'November'), ('12', 'December')
+ ], string="Commission Month")
+ year = fields.Selection([(str(y), str(y)) for y in range(2025, 2036)], string="Commission Year")
+ description = fields.Char(string='Description')
+ comment = fields.Char(string='Comment')
+ commission_internal_line = fields.One2many('commission.internal.line', 'commission_internal_id', string='Lines',
+ auto_join=True, order='account_move_id asc')
+ commission_internal_result = fields.One2many('commission.internal.result', 'commission_internal_id', string='Result',
+ auto_join=True, order='account_move_id asc')
+
+ @api.model
+ def create(self, vals):
+ vals['number'] = self.env['ir.sequence'].next_by_code('commission.internal') or '0'
+ result = super(CommissionInternal, self).create(vals)
+ return result
+
+ def _get_commission_internal_bank_account_ids(self):
+ bank_ids = self.env['ir.config_parameter'].sudo().get_param('commission.internal.bank.account.id')
+ if not bank_ids:
+ return []
+ return [int(x.strip()) for x in bank_ids.split(',') if x.strip().isdigit()]
+
+ def _get_period_range(self, period_year, period_month):
+ """Return (date_start, date_end) using separate year and month fields."""
+ self.ensure_one() # make sure it's called on a single record
+
+ year_str = period_year or ''
+ month_str = period_month or ''
+
+ # Validate both fields exist
+ if not (year_str.isdigit() and month_str.isdigit()):
+ return None, None
+
+ year = int(year_str)
+ month = int(month_str)
+
+ # First day of this month
+ dt_start = date(year, month, 1)
+
+ # Compute first day of next month
+ if month == 12:
+ next_month = date(year + 1, 1, 1)
+ else:
+ next_month = date(year, month + 1, 1)
+
+ # Last day = one day before next month's first day
+ dt_end = next_month - timedelta(days=1)
+
+ return dt_start, dt_end
+
+ # CREDIT > 0
+ def _calculate_exclude_credit(self):
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('credit', '>', 0),
+ ('status', '=', False)
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ for line in lines:
+ line.helper1 = 'CREDIT'
+
+ # INV/20
+ def _calculate_keyword_invoice(self):
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper1', '=', False),
+ ('label', 'ilike', '%INV/20%'),
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ # parse label and set helper
+ for line in lines:
+ line.helper1 = 'INV/20'
+
+ # ONGKOS KIRIM SO/20
+ def _calculate_keyword_deliveryamt(self):
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper1', '=', False),
+ ('label', 'ilike', '%ONGKOS KIRIM SO/20%'),
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ for line in lines:
+ line.helper1 = 'ONGKOS KIRIM SO/20'
+
+ # Payment SO/20
+ def _calculate_keyword_payment(self):
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper1', '=', False),
+ ('label', 'ilike', '%Payment SO/20%'),
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ for line in lines:
+ line.helper1 = 'Payment SO/20'
+
+ # UANG MUKA PENJUALAN SO/20
+ def _calculate_keyword_dp(self):
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper1', '=', False),
+ ('label', 'ilike', '%UANG MUKA PENJUALAN SO/20%'),
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ for line in lines:
+ line.helper1 = 'UANG MUKA PENJUALAN SO/20'
+
+ def _calculate_keyword_undefined(self):
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper1', '=', False),
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ for line in lines:
+ line.helper1 = 'UNDEFINED'
+
+ def _parse_label_helper2(self):
+ exception = ['CREDIT', 'UNDEFINED']
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper1', 'not in', exception)
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ for line in lines:
+ clean_label = line.label.replace('-', ' ').replace(',', ' ')
+ list_label = clean_label.split()
+ list_helper2 = []
+ for key in list_label:
+ clean_key = key.replace(',', '') # delete commas for make sure
+ if clean_key[:6] == 'INV/20':
+ list_helper2.append(clean_key)
+ elif clean_key[:5] == 'SO/20':
+ list_invoice = self._switch_so_to_inv(clean_key)
+ str_invoice = ' '.join(list_invoice)
+ list_helper2.append(str_invoice)
+ result_helper2 = ' '.join(list_helper2)
+ line.helper2 = result_helper2
+
+ def _switch_so_to_inv(self, order):
+ list_state = ['sale', 'done']
+ result = []
+ query = [
+ ('state', 'in', list_state),
+ ('name', '=', order)
+ ]
+ sales = self.env['sale.order'].search(query)
+ if sales:
+ for sale in sales:
+ if sale.invoice_ids:
+ for invoice in sale.invoice_ids:
+ if invoice.state == 'posted':
+ result.append(invoice.name)
+ else:
+ result.append(order)
+ else:
+ result.append(order)
+ return result
+
+ # fill later TODO @stephan
+ def calculate_commission_internal_result(self):
+ exception = ['ONGKOS KIRIM SO/20']
+ query = [
+ ('commission_internal_id.id', '=', self.id),
+ ('helper2', '!=', False),
+ ('helper1', 'not in', exception)
+ ]
+ lines = self.env['commission.internal.line'].search(query)
+ all_invoices_and_sales = []
+ for line in lines:
+ list_so = list_invoice = []
+ list_key = line.helper2.split()
+ for key in list_key:
+ if key[:6] == 'INV/20':
+ list_invoice.append(key)
+ if key[:5] == 'SO/20':
+ list_so.append(key)
+ invoices = self.env['account.move'].search([('name', 'in', list_invoice)])
+ orders = self.env['sale.order'].search([('name', 'in', list_so)])
+ invoice_data = invoices.mapped(lambda r: {
+ 'res_name': 'account.move',
+ 'res_id': r.id,
+ 'name': r.name,
+ 'date': r.invoice_date,
+ 'customer': r.partner_id.name,
+ 'salesperson': r.user_id.name,
+ 'amount_untaxed': r.amount_untaxed,
+ 'amount_tax': r.amount_tax,
+ 'amount_total': r.amount_total,
+ 'uang_masuk_line_id': line.account_move_line_id.id,
+ 'uang_masuk_id': line.account_move_id.id,
+ 'date_uang_masuk': line.date,
+ 'label_uang_masuk': line.label,
+ 'nomor_uang_masuk': line.number,
+ 'uang_masuk': line.balance,
+ # 'linenetamt_prorate': net_amount_prorate,
+ 'helper1': line.helper1,
+ 'commission_internal_id': line.commission_internal_id.id,
+ 'commission_internal_line_id': line.id,
+ 'helper2': r.state,
+ })
+ sale_data = orders.mapped(lambda r: {
+ 'res_name': 'sale.order',
+ 'res_id': r.id,
+ 'name': r.name,
+ 'date': r.date_order,
+ 'customer': r.partner_id.name,
+ 'salesperson': r.user_id.name,
+ 'amount_untaxed': r.amount_untaxed,
+ 'amount_tax': r.amount_tax,
+ 'amount_total': r.grand_total,
+ 'uang_masuk_line_id': line.account_move_line_id.id,
+ 'uang_masuk_id': line.account_move_id.id,
+ 'date_uang_masuk': line.date,
+ 'label_uang_masuk': line.label,
+ 'nomor_uang_masuk': line.number,
+ 'uang_masuk': line.balance,
+ # 'linenetamt_prorate': net_amount_prorate,
+ 'helper1': line.helper1,
+ 'commission_internal_id': line.commission_internal_id.id,
+ 'commission_internal_line_id': line.id,
+ 'helper2': r.state,
+ })
+ invoices_and_sales = invoice_data + sale_data
+ sum_amount_total = sum(item['amount_total'] for item in invoices_and_sales)
+ for item in invoices_and_sales:
+ item['sum_amount_total'] = sum_amount_total
+ all_invoices_and_sales.extend(invoices_and_sales)
+
+ for data in all_invoices_and_sales:
+ # total_amount = sum(item.get('amount_total', 0.0) for item in invoices_and_sales)
+ # net_amount_prorate = data.get('amount_total', 0.0) * prorate
+ prorate = data.get('amount_total', 0.0) / data.get('sum_amount_total', 0.0)
+ net_amount_prorate = data.get('uang_masuk', 0.0) * prorate
+ self.env['commission.internal.result'].create([{
+ 'commission_internal_id': data['commission_internal_id'],
+ 'commission_internal_line_id': data['commission_internal_line_id'],
+ 'date_doc': data['date'],
+ 'number': data['name'],
+ 'res_name': data['res_name'],
+ 'res_id': data['res_id'],
+ 'name': data['name'],
+ 'salesperson': data['salesperson'],
+ 'totalamt': data['amount_total'],
+ 'uang_masuk_line_id': data['uang_masuk_line_id'],
+ 'uang_masuk_id': data['uang_masuk_id'],
+ 'date_uang_masuk': data['date_uang_masuk'],
+ 'label_uang_masuk': data['label_uang_masuk'],
+ 'nomor_uang_masuk': data['nomor_uang_masuk'],
+ 'uang_masuk': data['uang_masuk'],
+ 'linenetamt_prorate': net_amount_prorate,
+ 'helper1': data['helper1'],
+ 'helper2': data['helper2']
+ }])
+ print(1)
+
+ # this button / method works for train data in July 2025
+ def calculate_commission_internal_from_keyword(self):
+ if not self.commission_internal_line:
+ raise UserError('Commission Internal Line kosong, click Copy GL terlebih dahulu')
+ # execute helper1 for mark keyword
+ self._calculate_exclude_credit()
+ self._calculate_keyword_invoice()
+ self._calculate_keyword_deliveryamt()
+ self._calculate_keyword_payment()
+ self._calculate_keyword_dp()
+ self._calculate_keyword_undefined()
+ # execute helper2 for parse the label into INV/ or SO/ and switch SO to INV if available
+ self._parse_label_helper2()
+
+ def generate_commission_from_generate_ledger(self):
+ if self.commission_internal_line:
+ raise UserError('Harus hapus semua line jika ingin generate ulang')
+ if not self.month:
+ raise UserError('Commission Month harus diisi')
+ if not self.year:
+ raise UserError('Commission Year harus diisi')
+
+ dt_start, dt_end = self._get_period_range(self.year, self.month)
+
+ account_bank_ids = self._get_commission_internal_bank_account_ids()
+ query = [
+ ('move_id.state', '=', 'posted'),
+ ('account_id', 'in', account_bank_ids),
+ ('date', '>=', dt_start),
+ ('date', '<=', dt_end)
+ ]
+ ledgers = self.env['account.move.line'].search(query)
+ count = 0
+ for ledger in ledgers:
+ _logger.info("Read General Ledger Account Move Line %s" % ledger.id)
+ self.env['commission.internal.line'].create([{
+ 'commission_internal_id': self.id,
+ 'date': ledger.date,
+ 'number': ledger.move_id.name,
+ 'account_move_id': ledger.move_id.id,
+ 'account_move_line_id': ledger.id,
+ 'account_id': ledger.account_id.id,
+ 'label': ledger.name,
+ 'debit': ledger.debit,
+ 'credit': ledger.credit,
+ 'balance': ledger.balance
+ }])
+ count += 1
+ _logger.info("Commission Internal Line generated %s" % count)
+
+
+class CommissionInternalLine(models.Model):
+ _name = 'commission.internal.line'
+ _description = 'Line'
+ _order = 'number asc, id'
+
+ commission_internal_id = fields.Many2one('commission.internal', string='Internal Ref', required=True,
+ ondelete='cascade', index=True, copy=False)
+ date = fields.Date(string='Date')
+ number = fields.Char(string='Number')
+ account_move_id = fields.Many2one('account.move', string='Account Move')
+ account_move_line_id = fields.Many2one('account.move.line', string='Account Move Line')
+ account_id = fields.Many2one('account.account', string='Account')
+ label = fields.Char(string='Label')
+ debit = fields.Float(string='Debit')
+ credit = fields.Float(string='Credit')
+ balance = fields.Float(string='Balance')
+ ongkir = fields.Float(string='Ongkir')
+ refund = fields.Float(string='Refund')
+ pph = fields.Float(string='PPh23')
+ others = fields.Float(string='Others')
+ linenetamt = fields.Float(string='Net Amount')
+ dpp = fields.Float(string='DPP')
+ status = fields.Char(string='Status')
+ salespersons = fields.Char(string='Salespersons')
+ invoices = fields.Char(string='Invoices')
+ helper1 = fields.Char(string='Helper1')
+ helper2 = fields.Char(string='Helper2')
+ helper3 = fields.Char(string='Helper3')
+ helper4 = fields.Char(string='Helper4')
+ helper5 = fields.Char(string='Helper5')
+
+
+class CommissionInternalResult(models.Model):
+ _name = 'commission.internal.result'
+ _description = 'Result'
+ _order = 'number asc, id'
+
+ commission_internal_id = fields.Many2one('commission.internal', string='Internal Ref', required=True,
+ ondelete='cascade', index=True, copy=False)
+ commission_internal_line_id = fields.Many2one('commission.internal.line', string='Line Ref')
+ res_name = fields.Char(string='Res Name')
+ res_id = fields.Integer(string='Res ID')
+ date_doc = fields.Date(string='Date Doc')
+ number = fields.Char(string='Number')
+ name = fields.Char(string='Name')
+ salesperson = fields.Char(string='Salesperson')
+ totalamt = fields.Float(string='Total Amount')
+ uang_masuk_line_id = fields.Many2one('account.move.line', string='Uang Masuk Line ID')
+ uang_masuk_id = fields.Many2one('account.move', string='Uang Masuk ID')
+ date_uang_masuk = fields.Date(string='Date Uang Masuk')
+ label_uang_masuk = fields.Char(string='Label Uang Masuk')
+ nomor_uang_masuk = fields.Char(string='Nomor Uang Masuk')
+ uang_masuk = fields.Float(string='Uang Masuk')
+ ongkir = fields.Float(string='Ongkir')
+ refund = fields.Float(string='Refund')
+ pph = fields.Float(string='PPh23')
+ others = fields.Float(string='Others')
+ linenetamt = fields.Float(string='Net Amount')
+ linenetamt_prorate = fields.Float(string='Net Amount Prorate')
+ dpp = fields.Float(string='DPP')
+ status = fields.Char(string='Status')
+ helper1 = fields.Char(string='Helper1')
+ helper2 = fields.Char(string='Helper2')
+ helper3 = fields.Char(string='Helper3')
+ helper4 = fields.Char(string='Helper4')
+ helper5 = fields.Char(string='Helper5')
diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py
index ce94306f..cabcd5d6 100644
--- a/indoteknik_custom/models/coretax_fatur.py
+++ b/indoteknik_custom/models/coretax_fatur.py
@@ -147,7 +147,7 @@ class CoretaxFaktur(models.Model):
subtotal = line_price_subtotal
quantity = line_quantity
total_discount = round(line_discount, 2)
-
+ coretax_id = line.product_uom_id.coretax_id
# Calculate other tax values
otherTaxBase = round(subtotal * (11 / 12), 2) if subtotal else 0
vat_amount = round(otherTaxBase * 0.12, 2)
@@ -159,7 +159,7 @@ class CoretaxFaktur(models.Model):
ET.SubElement(good_service, 'Opt').text = 'A'
ET.SubElement(good_service, 'Code').text = '000000'
ET.SubElement(good_service, 'Name').text = line_name
- ET.SubElement(good_service, 'Unit').text = 'UM.0018'
+ ET.SubElement(good_service, 'Unit').text = coretax_id
# ET.SubElement(good_service, 'Price').text = str(round(line_price_unit, 2)) if line_price_unit else '0'
ET.SubElement(good_service, 'Price').text = str(price_per_unit)
ET.SubElement(good_service, 'Qty').text = str(quantity)
diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py
index 9feea1d1..2562c305 100644
--- a/indoteknik_custom/models/dunning_run.py
+++ b/indoteknik_custom/models/dunning_run.py
@@ -150,4 +150,5 @@ class DunningRunLine(models.Model):
open_amt = fields.Float(string='Open Amount')
due_date = fields.Date(string='Due Date')
payment_term = fields.Many2one('account.payment.term', related='invoice_id.invoice_payment_term_id', string='Payment Term')
+ information_line = fields.Text(string='Information')
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index 30956082..02679458 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -308,6 +308,9 @@ class CheckBomProduct(models.Model):
if not self.code_product:
return
+ if self.production_id.qty_producing == 0:
+ raise UserError("Isi dan Save dahulu Quantity To Produce yang diinginkan!")
+
# Cari product berdasarkan default_code, barcode, atau barcode_box
product = self.env['product.product'].search([
'|',
diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py
index 4df7da1e..519f505c 100644
--- a/indoteknik_custom/models/partial_delivery.py
+++ b/indoteknik_custom/models/partial_delivery.py
@@ -115,9 +115,13 @@ class PartialDeliveryWizard(models.TransientModel):
raise UserError(_("Picking harus dalam status Ready (assigned)."))
lines_by_qty = self.line_ids.filtered(lambda l: l.selected_qty > 0)
+ lines_validation = self.line_ids.filtered(lambda l: l.selected_qty > l.reserved_qty)
lines_by_selected = self.line_ids.filtered(lambda l: l.selected and not l.selected_qty)
selected_lines = lines_by_qty | lines_by_selected # gabung dua domain hasil filter
+ if lines_validation:
+ raise UserError(_("Jumlah yang dipilih melebihi jumlah yang terdapat di DO."))
+
if not selected_lines:
raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya."))
@@ -172,9 +176,11 @@ class PartialDeliveryWizard(models.TransientModel):
for line in selected_lines:
if line.selected_qty > line.reserved_qty:
raise UserError(_("Jumlah produk %s yang dipilih melebihi jumlah reserved.") % line.product_id.display_name)
+
move = line.move_id
move._do_unreserve()
+ # 🔹 Kalau cuma selected tanpa qty → anggap kirim semua reserved qty
if line.selected and not line.selected_qty:
line.selected_qty = line.reserved_qty
@@ -186,12 +192,20 @@ class PartialDeliveryWizard(models.TransientModel):
if line.selected_qty < move.product_uom_qty:
qty_to_keep = move.product_uom_qty - line.selected_qty
+
new_move = move.copy(default={
'product_uom_qty': line.selected_qty,
'picking_id': new_picking.id,
'partial': True,
})
+
+ if move.move_dest_ids:
+ for dest_move in move.move_dest_ids:
+ # dest_move.write({'move_orig_ids': [(4, new_move.id)]})
+ new_move.write({'move_dest_ids': [(4, dest_move.id)]})
+
move.write({'product_uom_qty': qty_to_keep})
+
else:
move.write({'picking_id': new_picking.id, 'partial': True})
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index e79417aa..3312e7fd 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -7,6 +7,8 @@ from pytz import timezone, utc
import io
import base64
from odoo.tools import lazy_property
+import socket
+
try:
from odoo.tools.misc import xlsxwriter
except ImportError:
@@ -120,7 +122,13 @@ class PurchaseOrder(models.Model):
string='Show Description',
default=True
)
-
+
+ @staticmethod
+ def is_local_env():
+ hostname = socket.gethostname().lower()
+ keywords = ['andri', 'miqdad', 'fin', 'stephan', 'hafid', 'nathan']
+ return any(keyword in hostname for keyword in keywords)
+
@api.onchange('show_description')
def onchange_show_description(self):
if self.show_description == True:
@@ -1043,7 +1051,7 @@ class PurchaseOrder(models.Model):
# test = line.product_uom_qty
# test2 = line.product_id.plafon_qty
# test3 = test2 + line.product_uom_qty
- if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and not self.env.user.id == 21:
+ if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and self.env.user.id not in [21, 7]:
raise UserError('Product '+line.product_id.name+' melebihi plafon, harus Approval Rafly')
def check_different_vendor_so_po(self):
@@ -1123,6 +1131,8 @@ class PurchaseOrder(models.Model):
if not self.not_update_purchasepricelist:
self.add_product_to_pricelist()
for line in self.order_line:
+ if line.product_id.type == 'product' and not line.product_id.categ_id:
+ raise UserError("Product %s kategorinya kosong" % line.product_id.name)
if not line.product_id.purchase_ok:
raise UserError("Terdapat barang yang tidak bisa diproses")
# Validasi pajak
@@ -1137,6 +1147,9 @@ class PurchaseOrder(models.Model):
break
if send_email:
+ if self.is_local_env():
+ _logger.warning("📪 Local environment detected — skip sending email reminders.")
+ return
self._send_mail()
if self.revisi_po:
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 47565dfc..cbc0b717 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -326,8 +326,24 @@ class RefundSaleOrder(models.Model):
domain.append(('ref', 'ilike', n))
moves2 = self.env['account.move'].search(domain)
+ moves3 = self.env['account.move']
+ if so_ids:
+ so_names = self.env['sale.order'].browse(so_ids).mapped('name')
+ domain = [
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ('ref', 'ilike', 'uang muka penjualan'),
+ ('ref', 'not ilike', 'reklas'),
+ ]
+ if so_names:
+ domain += ['|'] * (len(so_names) - 1)
+ for n in so_names:
+ domain.append(('ref', 'ilike', n))
+ moves3 = self.env['account.move'].search(domain)
+
has_moves = bool(moves)
has_moves2 = bool(moves2)
+ has_moves3 = bool(moves3)
has_piutangmdr = bool(piutangmdr)
has_piutangbca = bool(piutangbca)
has_misc = bool(misc)
@@ -349,6 +365,8 @@ class RefundSaleOrder(models.Model):
# sisanya bisa dijumlahkan tanpa konflik
if has_moves2:
amounts.append(sum(moves2.mapped('amount_total_signed')))
+ if has_moves3:
+ amounts.append(sum(moves3.mapped('amount_total_signed')))
if has_piutangbca:
amounts.append(sum(piutangbca.mapped('amount_total_signed')))
if has_piutangmdr:
@@ -573,9 +591,10 @@ class RefundSaleOrder(models.Model):
domain = [
('journal_id', '=', 11),
('state', '=', 'posted'),
- '|',
+ '|', '|',
('ref', 'ilike', 'dp'),
('ref', 'ilike', 'payment'),
+ ('ref', 'ilike', 'uang muka penjualan'),
]
domain += ['|'] * (len(so_names) - 1)
for n in so_names:
@@ -653,6 +672,7 @@ class RefundSaleOrder(models.Model):
('journal_id', '=', 13),
('state', '=', 'posted'),
])
+
moves2 = self.env['account.move']
if so_ids:
so_records = self.env['sale.order'].browse(so_ids)
@@ -668,9 +688,27 @@ class RefundSaleOrder(models.Model):
domain.append(('ref', 'ilike', n))
moves2 = self.env['account.move'].search(domain)
+
+ moves3 = self.env['account.move']
+ if so_ids:
+ so_records = self.env['sale.order'].browse(so_ids)
+ so_names = so_records.mapped('name')
+
+ domain = [
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ('ref', 'ilike', 'uang muka penjualan'),
+ ('ref', 'not ilike', 'reklas'),
+ ]
+ domain += ['|'] * (len(so_names) - 1)
+ for n in so_names:
+ domain.append(('ref', 'ilike', n))
+
+ moves3 = self.env['account.move'].search(domain)
has_moves = bool(moves)
has_moves2 = bool(moves2)
+ has_moves3 = bool(moves3)
has_piutangmdr = bool(piutangmdr)
has_piutangbca = bool(piutangbca)
has_misc = bool(misc)
@@ -685,6 +723,8 @@ class RefundSaleOrder(models.Model):
amounts.append(sum(moves.mapped('amount_total_signed')))
if has_moves2:
amounts.append(sum(moves2.mapped('amount_total_signed')))
+ if has_moves3:
+ amounts.append(sum(moves3.mapped('amount_total_signed')))
if has_piutangbca:
amounts.append(sum(piutangbca.mapped('amount_total_signed')))
if has_piutangmdr:
@@ -712,39 +752,39 @@ class RefundSaleOrder(models.Model):
if self.sale_order_ids:
self.partner_id = self.sale_order_ids[0].partner_id
- @api.constrains('sale_order_ids')
- def _check_sale_orders_payment(self):
- """ Validasi SO harus punya uang masuk (Journal Uang Muka / Midtrans) """
- for rec in self:
- invalid_orders = []
-
- for so in rec.sale_order_ids:
- # cari journal uang muka
- moves = self.env['account.move'].search([
- ('sale_id', '=', so.id),
- ('journal_id', '=', 11), # Journal Uang Muka
- ('state', '=', 'posted'),
- ])
- piutangbca = self.env['account.move'].search([
- ('ref', 'in', rec.invoice_ids.mapped('name')),
- ('journal_id', '=', 4),
- ('state', '=', 'posted'),
- ])
- piutangmdr = self.env['account.move'].search([
- ('ref', 'in', rec.invoice_ids.mapped('name')),
- ('journal_id', '=', 7),
- ('state', '=', 'posted'),
- ])
-
- if not moves and so.payment_status != 'settlement' and not piutangbca and not piutangmdr:
- invalid_orders.append(so.name)
-
- if invalid_orders:
- raise ValidationError(
- f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} "
- "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Payment Invoice/Midtrans).\n"
- "Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid."
- )
+ # @api.constrains('sale_order_ids')
+ # def _check_sale_orders_payment(self):
+ # """ Validasi SO harus punya uang masuk (Journal Uang Muka / Midtrans) """
+ # for rec in self:
+ # invalid_orders = []
+
+ # for so in rec.sale_order_ids:
+ # # cari journal uang muka
+ # moves = self.env['account.move'].search([
+ # ('sale_id', '=', so.id),
+ # ('journal_id', '=', 11), # Journal Uang Muka
+ # ('state', '=', 'posted'),
+ # ])
+ # piutangbca = self.env['account.move'].search([
+ # ('ref', 'in', rec.invoice_ids.mapped('name')),
+ # ('journal_id', '=', 4),
+ # ('state', '=', 'posted'),
+ # ])
+ # piutangmdr = self.env['account.move'].search([
+ # ('ref', 'in', rec.invoice_ids.mapped('name')),
+ # ('journal_id', '=', 7),
+ # ('state', '=', 'posted'),
+ # ])
+
+ # if not moves and so.payment_status != 'settlement' and not piutangbca and not piutangmdr:
+ # invalid_orders.append(so.name)
+
+ # if invalid_orders:
+ # raise ValidationError(
+ # f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} "
+ # "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Payment Invoice/Midtrans).\n"
+ # "Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid."
+ # )
@api.onchange('refund_type')
def _onchange_refund_type(self):
@@ -753,10 +793,11 @@ class RefundSaleOrder(models.Model):
line_vals = []
for so in self.sale_order_ids:
for line in so.order_line:
- if line.qty_delivered == 0:
+ barang_kurang = line.product_uom_qty - line.qty_delivered
+ if line.qty_delivered == 0 or barang_kurang > 0:
line_vals.append((0, 0, {
'product_id': line.product_id.id,
- 'quantity': line.product_uom_qty,
+ 'quantity': barang_kurang,
'from_name': so.name,
'prod_id': so.id,
'reason': '',
@@ -988,7 +1029,7 @@ class RefundSaleOrder(models.Model):
for rec in self:
if self.env.uid not in allowed_user_ids and not is_fat:
raise UserError("❌ Hanya user yang bersangkutan atau Finance (FAT) yang bisa melakukan penolakan.")
- if rec.status not in ['refund', 'reject']:
+ if rec.status != 'reject':
rec.status = 'reject'
rec.status_payment = 'reject'
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index ef1a5cf4..7f4feb75 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -182,6 +182,7 @@ class ResPartner(models.Model):
payment_history_url = fields.Text(string='Payment History URL')
is_cbd_locked = fields.Boolean("Locked to CBD?", default=False, tracking=True, help="Jika dicentang, maka partner ini terkunci pada payment term CBD karena memiliki invoice yang sudah jatuh tempo lebih dari 30 hari.")
+ cbd_lock_date = fields.Datetime("CBD Lock Date", tracking=True, help="Tanggal ketika partner ini dikunci pada payment term CBD.")
@api.model
@@ -199,6 +200,9 @@ class ResPartner(models.Model):
string='Previous Payment Term'
)
+ property_product_pricelist = fields.Many2one(
+ tracking=True
+ )
@api.depends("street", "street2", "city", "state_id", "country_id", "blok", "nomor", "rt", "rw", "kelurahan_id",
"kecamatan_id")
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index a5e2f7c4..2ed4046f 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -397,8 +397,52 @@ class SaleOrder(models.Model):
string="Partner Locked CBD",
compute="_compute_partner_is_cbd_locked"
)
+ internal_notes_contact = fields.Text(related='partner_id.comment', string="Internal Notes", readonly=True, help="Internal Notes dari contact utama customer.")
+ is_so_fiktif = fields.Boolean('SO Fiktif?', tracking=3)
+ team_id = fields.Many2one(tracking=True)
+
+
+
+ def action_set_shipping_id(self):
+ for rec in self:
+ bu_pick = self.env['stock.picking'].search([
+ ('state', 'not in', ['cancel']),
+ ('picking_type_id', '=', 30),
+ ('sale_id', '=', rec.id),
+ ('linked_manual_bu_out', '=', False)
+ ])
+ # bu_out = bu_pick_has_out.mapped('linked_manual_bu_out')
+ bu_out = self.env['stock.picking'].search([
+ ('sale_id', '=', rec.id),
+ ('picking_type_id', '=', 29),
+ ('state', 'not in', ['cancel', 'done'])
+ ])
+ bu_pick_has_out = self.env['stock.picking'].search([
+ ('state', 'not in', ['cancel']),
+ ('picking_type_id', '=', 30),
+ ('sale_id', '=', rec.id),
+ ('linked_manual_bu_out.id', '=', bu_out.id),
+ ('linked_manual_bu_out.state', 'not in', ['done', 'cancel'])
+ ])
+ for pick in bu_pick_has_out:
+ linked_out = pick.linked_manual_bu_out
+ if pick.real_shipping_id != rec.real_shipping_id or linked_out.partner_id != rec.partner_shipping_id:
+ pick.real_shipping_id = rec.real_shipping_id
+ pick.partner_id = rec.partner_shipping_id
+ linked_out.partner_id = rec.partner_shipping_id
+ linked_out.real_shipping_id = rec.real_shipping_id
+ _logger.info('Updated bu_pick [%s] and bu_out [%s]', pick.name, linked_out.name)
+
+ for pick in bu_pick:
+ if pick.real_shipping_id != rec.real_shipping_id:
+ pick.real_shipping_id = rec.real_shipping_id
+ pick.partner_id = rec.partner_shipping_id
+ bu_out.partner_id = rec.partner_shipping_id
+ bu_out.real_shipping_id = rec.real_shipping_id
+ _logger.info('Updated bu_pick [%s] without bu_out', pick.name)
def action_open_partial_delivery_wizard(self):
+ # raise UserError("Fitur ini sedang dalam pengembangan")
self.ensure_one()
pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name)
return {
@@ -421,7 +465,7 @@ class SaleOrder(models.Model):
order.partner_is_cbd_locked = order.partner_id.is_cbd_locked
- @api.constrains('payment_term_id', 'partner_id', 'state')
+ @api.constrains('payment_term_id', 'partner_id')
def _check_cbd_lock_sale_order(self):
cbd_term = self.env['account.payment.term'].browse(26)
for rec in self:
@@ -1984,10 +2028,10 @@ class SaleOrder(models.Model):
# raise UserError('Kelurahan Real Delivery Address harus diisi')
def generate_payment_link_midtrans_sales_order(self):
- midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox
- midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox
- # midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production
- # midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production
+ # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox
+ # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox
+ midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production
+ midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production
so_number = self.name
so_number = so_number.replace('/', '-')
@@ -2000,8 +2044,8 @@ class SaleOrder(models.Model):
}
# ==== ENV ====
- check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox
- # check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production
+ # check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox
+ check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production
# =============================================
check_response = requests.get(check_url, headers=headers)
@@ -2276,7 +2320,7 @@ class SaleOrder(models.Model):
raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name))
def sale_order_approve(self):
- # self.check_duplicate_product()
+ self.check_duplicate_product()
self.check_product_bom()
self.check_credit_limit()
self.check_limit_so_to_invoice()
@@ -2340,27 +2384,38 @@ class SaleOrder(models.Model):
# return self._create_notification_action('Notification',
# 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
- if not order.with_context(ask_approval=True)._is_request_to_own_team_leader():
- return self._create_notification_action(
- 'Peringatan',
- 'Hanya bisa konfirmasi SO tim Anda.'
- )
if order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
+ order.message_post(body="Mengajukan approval ke Pimpinan")
return self._create_approval_notification('Pimpinan')
elif order._requires_approval_margin_manager():
self.check_product_bom()
self.check_credit_limit()
self.check_limit_so_to_invoice()
order.approval_status = 'pengajuan1'
+ order.message_post(body="Mengajukan approval ke Sales Manager")
return self._create_approval_notification('Sales Manager')
elif order._requires_approval_team_sales():
self.check_product_bom()
self.check_credit_limit()
self.check_limit_so_to_invoice()
order.approval_status = 'pengajuan1'
+ order.message_post(body="Mengajukan approval ke Team Sales")
return self._create_approval_notification('Team Sales')
+
+ if not order.with_context(ask_approval=True)._is_request_to_own_team_leader():
+ return self._create_notification_action(
+ 'Peringatan',
+ 'Hanya bisa konfirmasi SO tim Anda.'
+ )
+ user = self.env.user
+ is_sales_admin = user.id in (3401, 20, 3988, 17340)
+ if is_sales_admin:
+ order.approval_status = 'pengajuan1'
+ order.message_post(body="Mengajukan approval ke Sales")
+ return self._create_approval_notification('Sales')
+
raise UserError("Bisa langsung Confirm")
def send_notif_to_salesperson(self, cancel=False):
@@ -2546,7 +2601,7 @@ class SaleOrder(models.Model):
for order in self:
order._validate_delivery_amt()
order._validate_uniform_taxes()
- # order.check_duplicate_product()
+ order.check_duplicate_product()
order.check_product_bom()
order.check_credit_limit()
order.check_limit_so_to_invoice()
@@ -2595,6 +2650,7 @@ class SaleOrder(models.Model):
return self._create_approval_notification('Sales Manager')
elif order._requires_approval_team_sales():
order.approval_status = 'pengajuan1'
+ order.message_post(body="Mengajukan approval ke Team Sales")
return self._create_approval_notification('Team Sales')
order.approval_status = 'approved'
@@ -2701,6 +2757,7 @@ class SaleOrder(models.Model):
def _requires_approval_team_sales(self):
return (
18 <= self.total_percent_margin <= 24
+ # self.total_percent_margin >= 18
and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra
and not self.env.user.is_sales_manager
and not self.env.user.is_leader
@@ -2714,12 +2771,12 @@ class SaleOrder(models.Model):
if user.is_leader or user.is_sales_manager:
return True
- if user.id in (3401, 20, 3988, 17340): # admin (fida, nabila, ninda)
+ if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988, 17340):
return True
-
- if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988):
- return True
-
+
+ if not self.env.context.get("ask_approval") and user.id in (3401, 20, 3988, 17340): # admin (fida, nabila, ninda, feby)
+ raise UserError("Sales Admin tidak bisa confirm SO, silahkan hubungi Salesperson yang bersangkutan.")
+
salesperson_id = self.user_id.id
approver_id = user.id
team_leader_id = self.team_id.user_id.id
@@ -3371,6 +3428,9 @@ class SaleOrder(models.Model):
# if updated_vals:
# line.write(updated_vals)
+
+ if 'real_shipping_id' in vals:
+ self.action_set_shipping_id()
return res
def button_refund(self):
diff --git a/indoteknik_custom/models/sj_tele.py b/indoteknik_custom/models/sj_tele.py
index 3ef4b877..ed363f59 100644
--- a/indoteknik_custom/models/sj_tele.py
+++ b/indoteknik_custom/models/sj_tele.py
@@ -5,6 +5,7 @@ import json
import logging, subprocess
import time
from collections import OrderedDict
+import socket
_logger = logging.getLogger(__name__)
@@ -18,8 +19,18 @@ class SjTele(models.Model):
sale_name = fields.Char(string='Sale Name')
create_date = fields.Datetime(string='Create Date')
date_doc_kirim = fields.Datetime(string='Tanggal Kirim SJ')
+ is_sent = fields.Boolean(default=False)
+
+ @staticmethod
+ def is_local_env():
+ hostname = socket.gethostname().lower()
+ keywords = ['andri', 'miqdad', 'fin', 'stephan', 'hafid', 'nathan']
+ return any(keyword in hostname for keyword in keywords)
def woi(self):
+ if self.is_local_env():
+ _logger.warning("📪 Local environment detected — skip sending email reminders.")
+ return
bot_mqdd = '8203414501:AAHy_XwiUAVrgRM2EJzW7sZx9npRLITZpb8'
chat_id_mqdd = '-1003087280519'
api_base = f'https://api.telegram.org/bot{bot_mqdd}'
@@ -27,7 +38,11 @@ class SjTele(models.Model):
# chat_id_testing = '-4920864331'
# api_testing = f'https://api.telegram.org/bot{bot_testing}'
- data = self.search([], order='create_date asc')
+ # Select Data
+ data = self.search([('is_sent', '=', False)], order='create_date asc')
+
+ # Old
+ # data = self.search([], order='create_date asc')
if not data:
text = "✅ tidak ada data (semua sudah tercatat)."
@@ -83,6 +98,9 @@ class SjTele(models.Model):
_logger.exception("Gagal kirim Telegram (batch %s-%s): %s", i + 1, min(i + BUB, len(lines)), e)
time.sleep(5) # jeda kecil biar rapi & aman rate limit
+ # Set sent = true ketika sudah terkirim
+ data.write({'is_sent': True})
+
return True
# header = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n"
diff --git a/indoteknik_custom/models/solr/apache_solr_queue.py b/indoteknik_custom/models/solr/apache_solr_queue.py
index 1b51538f..3d6bd733 100644
--- a/indoteknik_custom/models/solr/apache_solr_queue.py
+++ b/indoteknik_custom/models/solr/apache_solr_queue.py
@@ -1,10 +1,10 @@
from odoo import models, fields
from datetime import datetime, timedelta
-import logging, time
-
+import logging, time, traceback # <-- tambah traceback
_logger = logging.getLogger(__name__)
+
class ApacheSolrQueue(models.Model):
_name = 'apache.solr.queue'
@@ -19,6 +19,7 @@ class ApacheSolrQueue(models.Model):
], 'Execute Status')
execute_date = fields.Datetime('Execute Date')
description = fields.Text('Description')
+ log = fields.Text('Log')
def _compute_display_name(self):
for rec in self:
@@ -39,7 +40,7 @@ class ApacheSolrQueue(models.Model):
if elapsed_time > max_exec_time:
break
rec.execute_queue()
-
+
def open_target_record(self):
return {
'name': '',
@@ -67,17 +68,21 @@ class ApacheSolrQueue(models.Model):
if model_instance:
getattr(model_instance, function_name)()
rec.execute_status = 'success'
+ rec.log = traceback.format_exc()
else:
rec.execute_status = 'not_found'
except Exception as e:
- rec.description = e
+ # simpan error ringkas + traceback lengkap
+ rec.description = str(e)
+ rec.log = traceback.format_exc()
rec.execute_status = 'failed'
+
rec.execute_date = datetime.utcnow()
self.env.cr.commit()
def create_unique(self, payload={}):
count = self.search_count([
- ('res_model', '=', payload['res_model']),
+ ('res_model', '=', payload['res_model']),
('res_id', '=', payload['res_id']),
('function_name', '=', payload['function_name']),
('execute_status', '=', False)
@@ -90,8 +95,6 @@ class ApacheSolrQueue(models.Model):
('execute_status', '=', 'success'),
('execute_date', '>=', (datetime.utcnow() - timedelta(days=days_after))),
], limit=limit)
-
+
for rec in solr:
rec.unlink()
-
-
diff --git a/indoteknik_custom/models/solr/product_product.py b/indoteknik_custom/models/solr/product_product.py
index d8bc3973..7260c3ca 100644
--- a/indoteknik_custom/models/solr/product_product.py
+++ b/indoteknik_custom/models/solr/product_product.py
@@ -69,9 +69,9 @@ class ProductProduct(models.Model):
'product_id_i': variant.id,
'template_id_i': variant.product_tmpl_id.id,
'image_s': ir_attachment.api_image('product.template', 'image_512', variant.product_tmpl_id.id),
- 'image_carousel_s': [ir_attachment.api_image('image.carousel', 'image', carousel.id) for carousel in variant.product_tmpl_id.image_carousel_lines],
+ 'image_carousel_ss': [ir_attachment.api_image('image.carousel', 'image', carousel.id) for carousel in variant.product_tmpl_id.image_carousel_lines],
'image_mobile_s': ir_attachment.api_image('product.template', 'image_256', variant.product_tmpl_id.id),
- 'stock_total_f': variant.qty_stock_vendor,
+ 'stock_total_f': variant.qty_free_bandengan,
'weight_f': variant.weight,
'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0,
'manufacture_name_s': variant.product_tmpl_id.x_manufacture.x_name or '',
diff --git a/indoteknik_custom/models/solr/product_template.py b/indoteknik_custom/models/solr/product_template.py
index c4aefe19..a7beca12 100644
--- a/indoteknik_custom/models/solr/product_template.py
+++ b/indoteknik_custom/models/solr/product_template.py
@@ -59,9 +59,9 @@ class ProductTemplate(models.Model):
solr_model = self.env['apache.solr']
for template in self:
+ voucher = None
if template.x_manufacture:
voucher = self.get_voucher_pastihemat(template.x_manufacture.id)
- # Lakukan sesuatu dengan voucher
variant_names = ', '.join([x.display_name or '' for x in template.product_variant_ids])
variant_codes = ', '.join([x.default_code or '' for x in template.product_variant_ids])
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index 6622359d..320c175f 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -246,52 +246,100 @@ class StockMoveLine(models.Model):
line_no = fields.Integer('No', default=0)
note = fields.Char('Note')
manufacture = fields.Many2one('x_manufactures', string="Brands", related="product_id.x_manufacture", store=True)
- outstanding_qty = fields.Float(
- string='Outstanding Qty',
- compute='_compute_delivery_line_status',
- store=True
+ qty_yang_mau_dikirim = fields.Float(
+ string='Qty yang Mau Dikirim',
+ compute='_compute_delivery_status_detail',
+ store=False
+ )
+ qty_terkirim = fields.Float(
+ string='Qty Terkirim',
+ compute='_compute_delivery_status_detail',
+ store=False
+ )
+ qty_gantung = fields.Float(
+ string='Qty Gantung',
+ compute='_compute_delivery_status_detail',
+ store=False
)
delivery_status = fields.Selection([
('none', 'No Movement'),
('partial', 'Partial'),
('partial_final', 'Partial Final'),
('full', 'Full'),
- ], string='Delivery Status', compute='_compute_delivery_line_status', store=True)
+ ], string='Delivery Status', compute='_compute_delivery_status_detail', store=False)
@api.depends('qty_done', 'product_uom_qty', 'picking_id.state')
- def _compute_delivery_line_status(self):
- for line in self:
- line.outstanding_qty = 0.0
- line.delivery_status = 'none'
-
- if not line.picking_id or line.picking_id.picking_type_id.code != 'outgoing':
+ def _compute_delivery_status_detail(self):
+ for picking in self:
+ # Default values
+ picking.qty_yang_mau_dikirim = 0.0
+ picking.qty_terkirim = 0.0
+ picking.qty_gantung = 0.0
+ picking.delivery_status = 'none'
+
+ # Hanya berlaku untuk pengiriman (BU/OUT)
+ if picking.picking_id.picking_type_id.code != 'outgoing':
continue
- total_qty = line.move_id.product_uom_qty or 0
- done_qty = line.qty_done or 0
-
- line.outstanding_qty = max(total_qty - done_qty, 0)
+ if picking.picking_id.name not in ['BU/OUT']:
+ continue
- if total_qty == 0:
+ move_lines = picking
+ if not move_lines:
continue
- if done_qty >= total_qty:
- line.delivery_status = 'full'
- elif done_qty < total_qty:
- line.delivery_status = 'partial'
-
- elif 0 < done_qty < total_qty:
- backorder_exists = self.env['stock.picking'].search_count([
- ('group_id', '=', line.picking_id.group_id.id),
- ('name', 'ilike', 'BU/OUT'),
- ('id', '!=', line.picking_id.id),
- ('state', 'in', ['done', 'assigned']),
- ])
-
- if backorder_exists:
- line.delivery_status = 'partial'
- if done_qty >= total_qty:
- line.delivery_status = 'partial_final'
+ # ======================
+ # HITUNG QTY
+ # ======================
+ total_qty = move_lines.product_uom_qty
+
+ done_qty_total = move_lines.move_id.sale_line_id.qty_delivered
+ order_qty_total = move_lines.move_id.sale_line_id.product_uom_qty
+ gantung_qty_total = order_qty_total - done_qty_total - total_qty
+
+ picking.qty_yang_mau_dikirim = total_qty
+ picking.qty_terkirim = done_qty_total
+ picking.qty_gantung = gantung_qty_total
+
+ # if total_qty == 0:
+ # picking.delivery_status = 'none'
+ # continue
+
+ # if done_qty_total == 0:
+ # picking.delivery_status = 'none'
+ # continue
+
+ # ======================
+ # CEK BU/OUT LAIN (BACKORDER)
+ # ======================
+ # has_other_out = self.env['stock.picking'].search_count([
+ # ('group_id', '=', picking.group_id.id),
+ # ('name', 'ilike', 'BU/OUT'),
+ # ('id', '!=', picking.id),
+ # ('state', 'in', ['assigned', 'waiting', 'confirmed', 'done']),
+ # ])
+
+ # ======================
+ # LOGIKA STATUS
+ # ======================
+ if gantung_qty_total == 0 and done_qty_total == 0:
+ # Semua barang udah terkirim, ga ada picking lain
+ picking.delivery_status = 'full'
+
+ elif gantung_qty_total > 0 and total_qty > 0 and done_qty_total == 0:
+ # Masih ada picking lain dan sisa gantung → proses masih jalan
+ picking.delivery_status = 'partial'
+
+ # elif gantung_qty_total > 0:
+ # # Ini picking terakhir, tapi qty belum full
+ # picking.delivery_status = 'partial_final'
+
+ elif gantung_qty_total == 0 and done_qty_total > 0 and total_qty > 0:
+ # Udah kirim semua tapi masih ada picking lain (rare case)
+ picking.delivery_status = 'partial_final'
+
+ else:
+ picking.delivery_status = 'none'
# Ambil uom dari stock move
@api.model
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index c5b112a3..5a793382 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -5,16 +5,16 @@ from collections import defaultdict
from datetime import timedelta, datetime
from datetime import timedelta, datetime as waktu
from itertools import groupby
-import pytz, requests, json, requests
+import pytz, json
from dateutil import parser
import datetime
import hmac
import hashlib
-import base64
import requests
import time
import logging
import re
+import base64
_logger = logging.getLogger(__name__)
@@ -89,6 +89,7 @@ class StockPicking(models.Model):
readonly=True,
related="id",
)
+ sj_documentations = fields.One2many('stock.picking.sj.document','picking_id', string='Dokumentasi SJ (Multi)')
sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan")
paket_documentation = fields.Binary(string="Dokumentasi Paket")
dispatch_documentation = fields.Binary(string="Dokumentasi Dispatch")
@@ -177,6 +178,113 @@ class StockPicking(models.Model):
area_name = fields.Char(string="Area", compute="_compute_area_name")
is_bu_iu = fields.Boolean('Is BU/IU', compute='_compute_is_bu_iu', default=False, copy=False, readonl=True)
+ qty_yang_mau_dikirim = fields.Float(
+ string='Qty yang Mau Dikirim',
+ compute='_compute_delivery_status_detail',
+ store=False
+ )
+ qty_terkirim = fields.Float(
+ string='Qty Terkirim',
+ compute='_compute_delivery_status_detail',
+ store=False
+ )
+ qty_gantung = fields.Float(
+ string='Qty Gantung',
+ compute='_compute_delivery_status_detail',
+ store=False
+ )
+ delivery_status = fields.Selection([
+ ('none', 'No Movement'),
+ ('partial', 'Partial'),
+ ('partial_final', 'Partial Final'),
+ ('full', 'Full'),
+ ], string='Delivery Status', compute='_compute_delivery_status_detail', store=False)
+ so_num = fields.Char('SO Number', compute='_get_so_num')
+ is_so_fiktif = fields.Boolean('SO Fiktif?', compute='_compute_is_so_fiktif', tracking=3)
+
+ @api.depends('sale_id.is_so_fiktif')
+ def _compute_is_so_fiktif(self):
+ for picking in self:
+ picking.is_so_fiktif = picking.sale_id.is_so_fiktif if picking.sale_id else False
+
+
+ @api.depends('group_id')
+ def _get_so_num(self):
+ for record in self:
+ record.so_num = record.group_id.name
+
+ @api.depends('move_line_ids_without_package.qty_done', 'move_line_ids_without_package.product_uom_qty', 'state')
+ def _compute_delivery_status_detail(self):
+ for picking in self:
+ # Default values
+ picking.qty_yang_mau_dikirim = 0.0
+ picking.qty_terkirim = 0.0
+ picking.qty_gantung = 0.0
+ picking.delivery_status = 'none'
+
+ # Hanya berlaku untuk pengiriman (BU/OUT)
+ if picking.picking_type_id.code != 'outgoing':
+ continue
+
+ if picking.name not in ['BU/OUT']:
+ continue
+
+ move_lines = picking.move_line_ids_without_package
+ if not move_lines:
+ continue
+
+ # ======================
+ # HITUNG QTY
+ # ======================
+ total_qty = sum(line.product_uom_qty for line in move_lines)
+
+ done_qty_total = sum(line.sale_line_id.qty_delivered for line in picking.move_ids_without_package)
+ order_qty_total = sum(line.sale_line_id.product_uom_qty for line in picking.move_ids_without_package)
+ gantung_qty_total = order_qty_total - done_qty_total - total_qty
+
+ picking.qty_yang_mau_dikirim = total_qty
+ picking.qty_terkirim = done_qty_total
+ picking.qty_gantung = gantung_qty_total
+
+ # if total_qty == 0:
+ # picking.delivery_status = 'none'
+ # continue
+
+ # if done_qty_total == 0:
+ # picking.delivery_status = 'none'
+ # continue
+
+ # ======================
+ # CEK BU/OUT LAIN (BACKORDER)
+ # ======================
+ # has_other_out = self.env['stock.picking'].search_count([
+ # ('group_id', '=', picking.group_id.id),
+ # ('name', 'ilike', 'BU/OUT'),
+ # ('id', '!=', picking.id),
+ # ('state', 'in', ['assigned', 'waiting', 'confirmed', 'done']),
+ # ])
+
+ # ======================
+ # LOGIKA STATUS
+ # ======================
+ if gantung_qty_total == 0 and done_qty_total == 0:
+ # Semua barang udah terkirim, ga ada picking lain
+ picking.delivery_status = 'full'
+
+ elif gantung_qty_total > 0 and total_qty > 0 and done_qty_total == 0:
+ # Masih ada picking lain dan sisa gantung → proses masih jalan
+ picking.delivery_status = 'partial'
+
+ # elif gantung_qty_total > 0:
+ # # Ini picking terakhir, tapi qty belum full
+ # picking.delivery_status = 'partial_final'
+
+ elif gantung_qty_total == 0 and done_qty_total > 0 and total_qty > 0:
+ # Udah kirim semua tapi masih ada picking lain (rare case)
+ picking.delivery_status = 'partial_final'
+
+ else:
+ picking.delivery_status = 'none'
@api.depends('name')
def _compute_is_bu_iu(self):
@@ -366,7 +474,6 @@ class StockPicking(models.Model):
except ValueError:
return False
-
def action_get_kgx_pod(self, shipment=False):
self.ensure_one()
@@ -464,15 +571,15 @@ class StockPicking(models.Model):
rec.last_update_date_doc_kirim = datetime.datetime.utcnow()
- @api.constrains('scan_koli_lines')
- def _constrains_scan_koli_lines(self):
- now = datetime.datetime.utcnow()
- for picking in self:
- if len(picking.scan_koli_lines) > 0:
- if len(picking.scan_koli_lines) != picking.total_mapping_koli:
- raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli")
+ # @api.constrains('scan_koli_lines')
+ # def _constrains_scan_koli_lines(self):
+ # now = datetime.datetime.utcnow()
+ # for picking in self:
+ # if len(picking.scan_koli_lines) > 0:
+ # if len(picking.scan_koli_lines) != picking.total_mapping_koli:
+ # raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli")
- picking.driver_departure_date = now
+ # picking.driver_departure_date = now
@api.depends('total_so_koli')
def _compute_total_so_koli(self):
@@ -716,6 +823,37 @@ class StockPicking(models.Model):
picking.envio_cod_value = data.get("cod_value", 0.0)
picking.envio_cod_status = data.get("cod_status")
+ images_data = data.get('images', [])
+ for img in images_data:
+ image_url = img.get('image')
+ if image_url:
+ try:
+ # Download image from URL
+ img_response = requests.get(image_url)
+ img_response.raise_for_status()
+
+ # Encode image to base64
+ image_base64 = base64.b64encode(img_response.content)
+
+ # Create attachment in Odoo
+ attachment = self.env['ir.attachment'].create({
+ 'name': 'Envio Image',
+ 'type': 'binary',
+ 'datas': image_base64,
+ 'res_model': picking._name,
+ 'res_id': picking.id,
+ 'mimetype': 'image/png',
+ })
+
+ # Post log note with attachment
+ picking.message_post(
+ body="Image Envio",
+ attachment_ids=[attachment.id]
+ )
+
+ except Exception as e:
+ picking.message_post(body=f"Gagal ambil image Envio: {str(e)}")
+
# Menyimpan log terbaru
logs = data.get("logs", [])
if logs and isinstance(logs, list) and logs[0]:
@@ -1087,7 +1225,8 @@ class StockPicking(models.Model):
self.sale_id.date_doc_kirim = self.date_doc_kirim
def action_assign(self):
- if self.env.context.get('default_picking_type_id') and ('BU/INPUT' not in self.name or 'BU/PUT' not in self.name):
+ if self.env.context.get('default_picking_type_id') and (
+ 'BU/INPUT' not in self.name or 'BU/PUT' not in self.name):
pickings_to_assign = self.filtered(
lambda p: not (p.sale_id and p.sale_id.hold_outgoing)
)
@@ -1103,18 +1242,15 @@ class StockPicking(models.Model):
return res
-
def ask_approval(self):
# if self.env.user.is_accounting:
# if self.env.user.is_accounting and self.location_id.id == 57 or self.location_id == 57 and self.approval_status in ['pengajuan1', ''] and 'BU/IU' in self.name and self.approval_status == 'pengajuan1':
# raise UserError("Bisa langsung set ke approval logistik")
if self.env.user.is_accounting and self.approval_status == "pengajuan2" and 'BU/IU' in self.name:
raise UserError("Tidak perlu ask approval sudah approval logistik")
- if self.env.user.is_logistic_approver and self.location_id.id == 57 or self.location_id== 57 and self.approval_status == 'pengajuan2' and 'BU/IU' in self.name:
+ if self.env.user.is_logistic_approver and self.location_id.id == 57 or self.location_id == 57 and self.approval_status == 'pengajuan2' and 'BU/IU' in self.name:
raise UserError("Bisa langsung Validate")
-
-
# for calendar distribute only
# if self.is_internal_use:
# stock_move_lines = self.env['stock.move.line'].search([
@@ -1137,7 +1273,6 @@ class StockPicking(models.Model):
raise UserError("Qty tidak boleh 0")
pick.approval_status = 'pengajuan1'
-
def ask_receipt_approval(self):
if self.env.user.is_logistic_approver:
raise UserError('Bisa langsung validate tanpa Ask Receipt')
@@ -1281,6 +1416,8 @@ class StockPicking(models.Model):
group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
active_model = self.env.context.get('active_model')
+ if self.is_so_fiktif == True:
+ raise UserError("SO Fiktif tidak bisa di validate")
if self.location_id.id == 47 and self.env.user.id not in users_in_group.mapped(
'id') and self.state_approve_md != 'done':
self.state_approve_md = 'waiting' if self.state_approve_md != 'pending' else 'pending'
@@ -1304,6 +1441,9 @@ class StockPicking(models.Model):
and not self.so_lama):
raise UserError(_("Tidak ada scan koli! Harap periksa kembali."))
+ if 'BU/OUT/' in self.name:
+ self.driver_departure_date = datetime.datetime.utcnow()
+
# if self.driver_departure_date == False and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing':
# raise UserError(_("Isi Driver Departure Date dulu sebelum validate"))
@@ -1316,6 +1456,9 @@ class StockPicking(models.Model):
if len(self.check_product_lines) == 0 and 'BU/PICK/' in self.name:
raise UserError(_("Tidak ada Check Product! Harap periksa kembali."))
+ if len(self.check_product_lines) == 0 and 'BU/INPUT/' in self.name:
+ raise UserError(_("Tidak ada Check Product! Harap periksa kembali."))
+
if self.total_koli > self.total_so_koli:
raise UserError(_("Total Koli (%s) dan Total SO Koli (%s) tidak sama! Harap periksa kembali.")
% (self.total_koli, self.t1otal_so_koli))
@@ -1342,13 +1485,16 @@ class StockPicking(models.Model):
# if self.is_internal_use and not self.env.user.is_logistic_approver and self.location_id.id == 57 and self.approval_status == 'pengajuan2':
# raise UserError("Harus di Approve oleh Logistik")
- if self.is_internal_use and self.approval_status in ['pengajuan1', '', False] and 'BU/IU' in self.name and self.is_bu_iu == True:
+ if self.is_internal_use and self.approval_status in ['pengajuan1', '',
+ False] and 'BU/IU' in self.name and self.is_bu_iu == True:
raise UserError("Tidak Bisa Validate, set approval status ke approval logistik terlebih dahhulu")
- if self.is_internal_use and not self.env.user.is_logistic_approver and self.approval_status in ['pengajuan2'] and self.is_bu_iu == True and 'BU/IU' in self.name:
+ if self.is_internal_use and not self.env.user.is_logistic_approver and self.approval_status in [
+ 'pengajuan2'] and self.is_bu_iu == True and 'BU/IU' in self.name:
raise UserError("Harus di Approve oleh Logistik")
- if self.is_internal_use and not self.env.user.is_accounting and self.approval_status in ['pengajuan1', '', False] and self.is_bu_iu == False:
+ if self.is_internal_use and not self.env.user.is_accounting and self.approval_status in ['pengajuan1', '',
+ False] and self.is_bu_iu == False:
raise UserError("Harus di Approve oleh Accounting")
if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver:
@@ -1374,7 +1520,6 @@ class StockPicking(models.Model):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.date_reserved = current_time
-
# Validate Qty Demand Can't higher than Qty Product
if self.location_dest_id.id == 58 and 'BU/INPUT/' in self.name:
for move in self.move_ids_without_package:
@@ -1396,9 +1541,9 @@ class StockPicking(models.Model):
self.check_koli()
res = super(StockPicking, self).button_validate()
- # Penambahan link PO di Stock Journal untuk Picking BD
+ # Penambahan link PO di Stock Journal
for picking in self:
- if picking.name and 'BD/' in picking.name and picking.purchase_id:
+ if picking.name and picking.purchase_id:
stock_journal = self.env['account.move'].search([
('ref', 'ilike', picking.name + '%'),
('journal_id', '=', 3) # Stock Journal ID
@@ -1457,6 +1602,10 @@ class StockPicking(models.Model):
elif self.tukar_guling_po_id:
self.tukar_guling_po_id.update_doc_state()
+ user = self.env.user
+ if not user.has_group('indoteknik_custom.group_role_logistic') and 'BU/IU' in self.name:
+ raise UserWarning('Validate hnaya bisa di lakukan oleh logistik')
+
return res
def automatic_reserve_product(self):
@@ -1627,6 +1776,15 @@ class StockPicking(models.Model):
'indoteknik_custom.group_role_logistic') and self.picking_type_code == 'outgoing':
raise UserError("Button ini hanya untuk Logistik")
+ if len(self.check_product_lines) >= 1:
+ raise UserError("Tidak Bisa cancel karena sudah di check product")
+
+ if not self.env.user.is_logistic_approver and not self.env.user.has_group('indoteknik_custom.group_role_logistic'):
+ for picking in self:
+ if picking.name and ('BU/PICK' in picking.name or 'BU/OUT' in picking.name or 'BU/ORT' in picking.name or 'BU/SRT' in picking.name):
+ if picking.state not in ['cancel']:
+ raise UserError("Button ini hanya untuk Logistik")
+
res = super(StockPicking, self).action_cancel()
return res
@@ -1775,7 +1933,8 @@ class StockPicking(models.Model):
'name': move_line.product_id.name,
'code': move_line.product_id.default_code,
'qty': move_line.qty_done,
- 'image': self.env['ir.attachment'].api_image('product.template', 'image_128', move_line.product_id.product_tmpl_id.id),
+ 'image': self.env['ir.attachment'].api_image('product.template', 'image_128',
+ move_line.product_id.product_tmpl_id.id),
})
response = {
@@ -2141,7 +2300,6 @@ class CheckProduct(models.Model):
_order = 'picking_id, id'
_inherit = ['barcodes.barcode_events_mixin']
-
picking_id = fields.Many2one(
'stock.picking',
string='Picking Reference',
@@ -2627,8 +2785,6 @@ class ScanKoli(models.Model):
out_move.qty_done += qty_to_assign
qty_koli -= qty_to_assign
-
-
def _reset_qty_done_if_no_scan(self, picking_id):
product_bu_pick = self.env['stock.move.line'].search([('picking_id', '=', picking_id)])
@@ -2687,4 +2843,15 @@ class WarningModalWizard(models.TransientModel):
def action_continue(self):
if self.picking_id:
return self.picking_id.with_context(skip_koli_check=True).button_validate()
- return {'type': 'ir.actions.act_window_close'} \ No newline at end of file
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class StockPickingSjDocument(models.Model):
+ _name = 'stock.picking.sj.document'
+ _description = 'Dokumentasi Surat Jalan (Multi)'
+ _order = 'sequence, id'
+ _rec_name = 'id'
+
+ picking_id = fields.Many2one('stock.picking', required=True, ondelete='cascade')
+ image = fields.Binary('Gambar', required=True, attachment=True)
+ sequence = fields.Integer('Urutan', default=10) \ No newline at end of file
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index cfa0ac77..aa116ce3 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -165,7 +165,7 @@ class TukarGuling(models.Model):
@api.onchange('operations')
def _onchange_operations(self):
"""Auto-populate lines ketika operations dipilih"""
- if self.operations.picking_type_id.id not in [29, 30]:
+ if self.operations.picking_type_id.id not in [29, 30] and self.env.user.id != 1102:
raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
for rec in self:
if rec.operations and rec.operations.picking_type_id.id == 30:
@@ -412,7 +412,7 @@ class TukarGuling(models.Model):
def write(self, vals):
self.ensure_one()
- if self.operations.picking_type_id.id not in [29, 30]:
+ if self.operations.picking_type_id.id not in [29, 30] and self.env.user.id != 1102:
raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
# self._check_invoice_on_retur_so()
operasi = self.operations.picking_type_id.id
@@ -492,12 +492,12 @@ class TukarGuling(models.Model):
self.ensure_one()
self._check_not_allow_tukar_guling_on_bu_pick()
- # existing_tukar_guling = self.env['tukar.guling'].search([
- # ('operations', '=', self.operations.id),
- # ('id', '!=', self.id),
- # ('state', '!=', 'cancel'),
- # ], limit=1)
- #
+ existing_tukar_guling = self.env['tukar.guling'].search([
+ ('operations', '=', self.operations.id),
+ ('id', '!=', self.id),
+ ('state', '!=', 'cancel'),
+ ], limit=1)
+
# if existing_tukar_guling:
# raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name)
picking = self.operations
@@ -737,14 +737,18 @@ class TukarGuling(models.Model):
if mapping_koli and record.operations.picking_type_id.id == 29:
for prod in mapping_koli.mapped('product_id'):
qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
- move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
- if not move:
- raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}")
- srt_return_lines.append((0, 0, {
- 'product_id': prod.id,
- 'quantity': qty_total,
- 'move_id': move.id,
- }))
+
+ move_lines = bu_out.move_line_ids.filtered(
+ lambda ml: ml.product_id == prod and ml.qty_done > 0 and not ml.package_id
+ )
+
+ for ml in move_lines:
+ srt_return_lines.append((0, 0, {
+ 'product_id': ml.product_id.id,
+ 'quantity': ml.qty_done,
+ 'move_id': ml.move_id.id,
+ }))
+
_logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
elif not mapping_koli and record.operations.picking_type_id.id == 29:
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
index 2a5ca3dd..739898a1 100644
--- a/indoteknik_custom/models/tukar_guling_po.py
+++ b/indoteknik_custom/models/tukar_guling_po.py
@@ -366,8 +366,8 @@ class TukarGulingPO(models.Model):
# if bu_put:
# raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!")
- if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
- raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling")
+ # if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
+ # raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling")
# if self.operations.picking_type_id.id != 28:
# if self._is_already_returned(self.operations):
diff --git a/indoteknik_custom/models/uom_uom.py b/indoteknik_custom/models/uom_uom.py
new file mode 100644
index 00000000..32e53d73
--- /dev/null
+++ b/indoteknik_custom/models/uom_uom.py
@@ -0,0 +1,6 @@
+from odoo import fields, models, api, _
+
+class Uom(models.Model):
+ _inherit = 'uom.uom'
+
+ coretax_id = fields.Char(string='Coretax ID')
diff --git a/indoteknik_custom/report/purchase_report.xml b/indoteknik_custom/report/purchase_report.xml
index 23fa4d52..208e6472 100644
--- a/indoteknik_custom/report/purchase_report.xml
+++ b/indoteknik_custom/report/purchase_report.xml
@@ -97,7 +97,8 @@
<!-- TEKS -->
<div style="display:flex; flex-direction:column; flex:1;">
<span style="font-weight:bold; margin-bottom:2px;">
- <t t-esc="line_index + 1"/>. <t t-esc="line.product_id.display_name"/>
+ <t t-esc="line_index + 1"/>. <t t-esc="line.name"/>
+ <!-- <t t-esc="line_index + 1"/>. <t t-esc="line.product_id.display_name"/> -->
</span>
</div>
</td>
@@ -130,7 +131,7 @@
</tr>
<!-- WEBSITE DESCRIPTION -->
- <t t-if="line.show_description">
+ <t t-if="line.show_description == True">
<tr t-attf-style="background-color: #{ '#fef5f5' if line_index % 2 == 0 else '#fffafa' }; ">
<td colspan="6" style="padding: 10px 14px; font-size:10px; line-height:1.3; font-style:italic; color:#555; border-left:1px solid #ccc; border-right:1px solid #ccc; border-bottom:1px solid #ccc;">
<div t-raw="line.product_id.website_description"/>
diff --git a/indoteknik_custom/report/report_tutup_tempo.xml b/indoteknik_custom/report/report_tutup_tempo.xml
index 1aa1367d..5fa5552f 100644
--- a/indoteknik_custom/report/report_tutup_tempo.xml
+++ b/indoteknik_custom/report/report_tutup_tempo.xml
@@ -91,19 +91,72 @@
Berdasarkan catatan kami, pembayaran atas beberapa invoice yang telah melewati batas waktu 30 (tiga puluh) hari adalah sebagai berikut:
</p>
- <table class="table table-sm" style="font-size:13px; border:1px solid #000; margin-top:16px; margin-bottom:16px;">
+ <table class="table table-sm o_main_table"
+ style="font-size:13px; border:1px solid #000; border-collapse: collapse; width:100%; table-layout: fixed;">
+
<thead style="background:#f5f5f5;">
<tr>
- <th style="border:1px solid #000; padding:4px; font-weight: bold;">Invoice</th>
- <th style="border:1px solid #000; padding:4px; font-weight: bold;">Due Date</th>
- <th style="border:1px solid #000; padding:4px; font-weight: bold;" class="text-center">Day to Due</th>
+ <th style="border:1px solid #000; padding:4px; width:5%; font-weight: bold;" class="text-center">No.</th>
+ <th style="border:1px solid #000; padding:4px; width:16%; font-weight: bold;">Invoice Number</th>
+ <th style="border:1px solid #000; padding:4px; width:10%; font-weight: bold;">Invoice Date</th>
+ <th style="border:1px solid #000; padding:4px; width:10%; font-weight: bold;">Due Date</th>
+ <th style="border:1px solid #000; padding:4px; width:6%; font-weight: bold;" class="text-center">Day to Due</th>
+ <th style="border:1px solid #000; padding:4px; width:16%; font-weight: bold;">Reference</th>
+ <th style="border:1px solid #000; padding:4px; width:17%; font-weight: bold;" class="text-right">Amount Due</th>
+ <th style="border:1px solid #000; padding:4px; width:11%; font-weight: bold;">Payment Terms</th>
</tr>
</thead>
<tbody>
- <tr t-foreach="selected_lines" t-as="line">
- <td style="border:1px solid #000; padding:4px;"><t t-esc="line.invoice_number"/></td>
- <td style="border:1px solid #000; padding:4px;"><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y')"/></td>
- <td style="border:1px solid #000; padding:4px;" class="text-center"><t t-esc="line.new_invoice_day_to_due"/></td>
+ <tr t-foreach="doc.line_ids.filtered(lambda l: l.selected)" t-as="line">
+
+ <!-- Nomor Urut -->
+ <td style="border:1px solid #000; padding:4px; text-align:center;">
+ <t t-esc="line.sort or '-'"/>
+ </td>
+
+ <!-- Invoice Number -->
+ <td style="border:1px solid #000; padding:4px; word-wrap: break-word;">
+ <t t-esc="line.invoice_number or '-'"/>
+ </td>
+
+ <!-- Invoice Date -->
+ <td style="border:1px solid #000; padding:4px;">
+ <t t-esc="line.invoice_date and line.invoice_date.strftime('%d-%m-%Y') or '-'"/>
+ </td>
+
+ <!-- Due Date -->
+ <td style="border:1px solid #000; padding:4px;">
+ <t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y') or '-'"/>
+ </td>
+
+ <!-- Day to Due -->
+ <td style="border:1px solid #000; padding:4px; text-align:center;">
+ <t t-esc="line.new_invoice_day_to_due or '-'"/>
+ </td>
+
+ <!-- Reference -->
+ <td style="border:1px solid #000; padding:4px; word-wrap: break-word;">
+ <t t-esc="line.ref or '-'"/>
+ </td>
+
+ <!-- Amount Due -->
+ <td style="border:1px solid #000; padding:4px; text-align:right;">
+ Rp. <t t-esc="'{:,.0f}'.format(line.amount_residual).replace(',', '.')"/>
+ </td>
+
+ <!-- Payment Terms -->
+ <td style="border:1px solid #000; padding:4px; word-wrap: break-word;">
+ <t t-esc="line.payment_term_id.name or '-'"/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="5" class="text-left" style="border:1px solid #000; padding:4px; word-wrap: break-word; white-space: normal; font-weight: bold;">
+ GRAND TOTAL INVOICE YANG BELUM DIBAYAR DAN TELAH JATUH TEMPO
+ </td>
+ <td colspan="3" class="text-right" style="border:1px solid #000; padding:4px; word-wrap: break-word; white-space: normal; font-weight: bold;">
+ Rp. <t t-esc="'{:,.0f}'.format(doc.grand_total).replace(',', '.')"/>
+ (<t t-esc="doc.grand_total_text or '-'"/>)
+ </td>
</tr>
</tbody>
</table>
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 53560f44..f6dcd4fc 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -164,6 +164,9 @@ access_purchase_order_unlock_wizard,access.purchase.order.unlock.wizard,model_pu
access_sales_order_koli,access.sales.order.koli,model_sales_order_koli,,1,1,1,1
access_stock_backorder_confirmation,access.stock.backorder.confirmation,model_stock_backorder_confirmation,,1,1,1,1
access_warning_modal_wizard,access.warning.modal.wizard,model_warning_modal_wizard,,1,1,1,1
+access_commission_internal,access.commission.internal,model_commission_internal,,1,1,1,1
+access_commission_internal_line,access.commission.internal.line,model_commission_internal_line,,1,1,1,1
+access_commission_internal_result,access.commission.internal.result,model_commission_internal_result,,1,1,1,1
access_User_pengajuan_tempo_line,access.user.pengajuan.tempo.line,model_user_pengajuan_tempo_line,,1,1,1,1
access_user_pengajuan_tempo,access.user.pengajuan.tempo,model_user_pengajuan_tempo,,1,1,1,1
@@ -190,7 +193,12 @@ access_partial_delivery_wizard_line,access.partial.delivery.wizard.line,model_pa
access_apo_domain_config,access.apo.domain.config,model_apo_domain_config,base.group_user,1,1,1,1
access_refund_sale_order,access.refund.sale.order,model_refund_sale_order,base.group_user,1,1,1,1
access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1
-
+access_advance_payment_request,access.advance.payment.request,model_advance_payment_request,,1,1,1,1
+access_reimburse_line,access.reimburse.line,model_reimburse_line,,1,1,1,1
+access_advance_payment_settlement,access.advance.payment.settlement,model_advance_payment_settlement,,1,1,1,1
+access_advance_payment_usage_line,access.advance.payment.usage.line,model_advance_payment_usage_line,,1,1,1,1
+access_advance_payment_create_bill,access.advance.payment.create.bill,model_advance_payment_create_bill,,1,1,1,1
+access_create_reimburse_cab_wizard_user,create.reimburse.cab.wizard user,model_create_reimburse_cab_wizard,,1,1,1,1
access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1
access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1
@@ -207,4 +215,5 @@ access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,
access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
access_stock_location,access.stock.location,model_stock_location,,1,1,1,1
-access_stock_quant,access.stock.quant,model_stock_quant,,1,1,1,1 \ No newline at end of file
+access_stock_quant,access.stock.quant,model_stock_quant,,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
diff --git a/indoteknik_custom/views/account_asset_views.xml b/indoteknik_custom/views/account_asset_views.xml
index 90c53623..776ab51f 100644
--- a/indoteknik_custom/views/account_asset_views.xml
+++ b/indoteknik_custom/views/account_asset_views.xml
@@ -12,6 +12,9 @@
type="object"
/>
</button>
+ <field name="invoice_id" position="after">
+ <field name="asset_type"/>
+ </field>
</field>
</record>
</data>
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index ba86277a..c5f9580c 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -63,6 +63,7 @@
decoration-info="payment_difficulty == 'normal'"
decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')"
decoration-danger="payment_difficulty == 'bermasalah'"/>
+ <field name="internal_notes_contact" readonly="1"/>
<field name="invoice_origin"/>
<field name="date_kirim_tukar_faktur"/>
<field name="shipper_faktur_id"/>
@@ -80,6 +81,7 @@
type="object"
class="btn-primary"
help="Sync Janji Bayar Customer ke Invoices dengan jumlah Due Date yang sama"/>
+
</field>
<field name="to_check" position="after">
<field name="already_paid"/>
@@ -138,6 +140,8 @@
<field name="shipper_faktur_id" optional="hide"/>
<field name="resi_tukar_faktur" optional="hide"/>
<field name="date_terima_tukar_faktur" optional="hide"/>
+ <field name="payment_info" optional="hide" widget="html"/>
+ <field name="customer_promise_date" optional="hide"/>
</field>
</field>
</record>
diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml
new file mode 100644
index 00000000..4e73bb28
--- /dev/null
+++ b/indoteknik_custom/views/advance_payment_request.xml
@@ -0,0 +1,305 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="view_form_advance_payment_request" model="ir.ui.view">
+ <field name="name">advance.payment.request.form</field>
+ <field name="model">advance.payment.request</field>
+ <field name="arch" type="xml">
+ <form string="Advance Payment Request &amp; Reimburse" duplicate="0">
+ <header>
+ <button name="action_realisasi_pum"
+ type="object"
+ string="Realisasi"
+ class="btn-primary"
+ attrs="{'invisible': ['|',
+ ('status', '!=', 'approved'),
+ '|', ('type_request', '!=', 'pum'),
+ ('has_settlement', '=', True)]}"/>
+ <button name="action_approval_check"
+ type="object"
+ string="Checking/Approval"
+ class="btn-success"
+ attrs="{'invisible': [('status', 'in', ['approved','reject','draft'])]}"/>
+ <button name="action_confirm_payment"
+ type="object"
+ string="Konfirmasi Pembayaran"
+ class="btn-info"
+ attrs="{'invisible': ['|', ('status', 'not in', ['approved']), ('status_pay_down_payment', '=', 'payment')]}"/>
+ <button name="action_ap_only"
+ type="object"
+ string="Buat Jurnal PUM"
+ class="btn-info"
+ attrs="{'invisible': ['|',
+ ('status', 'not in', ['approved']),
+ '|',
+ ('is_cab_visible', '=', True),
+ ('type_request', '!=', 'pum')
+ ]}"/>
+ <button name="action_open_create_reimburse_cab"
+ type="object"
+ string="Buat Jurnal Reimburse"
+ class="btn-info"
+ attrs="{'invisible': ['|',
+ ('status', 'not in', ['approved']),
+ '|',
+ ('is_cab_visible', '=', True),
+ ('type_request', '!=', 'reimburse')
+ ]}"/>
+ <field name="status" widget="statusbar"
+ statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,approved"
+ statusbar_colors='{"reject":"red"}'
+ readonly="1"/>
+ </header>
+ <sheet>
+ <widget name="web_ribbon" title="Payment" attrs="{'invisible': ['|', ('status_pay_down_payment', '!=', 'payment'), ('status', '=', 'draft')]}"/>
+ <widget name="web_ribbon" title="Pending" bg_color="bg-danger" attrs="{'invisible': ['|', ('status_pay_down_payment', '!=', 'pending'), ('status', '=', 'draft')]}"/>
+ <div class="oe_button_box" name="button_box" style="right: 150px;">
+ <field name="has_settlement" invisible="1"/>
+ <button name="action_realisasi_pum"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-check-square-o"
+ style="width: 280px;"
+ attrs="{'invisible': ['|', ('status', '!=', 'approved'), ('has_settlement', '=', False)]}">
+ <!-- <field name="settlement_ids" widget="statinfo" string="Realisasi PUM"/> -->
+ <field name="settlement_name" widget="statinfo" string="Realisasi PUM"/>
+ </button>
+ <field name="is_cab_visible" invisible="1"/>
+ <field name="is_current_user_ap" invisible="1"/>
+ <button type="object"
+ name="action_view_journal_uangmuka"
+ class="oe_stat_button"
+ icon="fa-book"
+ attrs="{'invisible': [('is_cab_visible', '=', False)]}"
+ style="width: 200px;">
+ <field name="move_id" widget="statinfo" string="Journal Uang Muka"/>
+ <span class="o_stat_text">
+ <t t-esc="record.move_id.name"/>
+ </span>
+ </button>
+ </div>
+ <div class="oe_title">
+ <h1>
+ <field name="number" readonly="1"/>
+ </h1>
+ </div>
+ <group col="2">
+ <group string=" ">
+ <field name="type_request" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <field name="is_represented" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <field name="applicant_name" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <field name="position_type" force_save="1" readonly="1"/>
+ <field name="nominal" colspan="2" attrs="{'readonly': ['|', ('status', '=', 'approved'), ('type_request', '=', 'reimburse')]}" force_save="1"/>
+ <p style="font-size: 10px; color: grey; font-style: italic" attrs="{'invisible': [('type_request', '!=', 'reimburse')]}">*Nominal terisi otomatis sesuai grand total rincian reimburse</p>
+ <field name="bank_name" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <field name="account_name" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <field name="bank_account" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <field name="detail_note" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+ <br/>
+ <field name="user_id" readonly="1"/>
+ <!-- <field name="position_type" readonly="1"/> -->
+ <!-- <field name="partner_id" readonly="1"/> -->
+ <field name="departement_type"/>
+ <field name="apr_perjalanan" attrs="{'invisible': [('type_request', '=', 'reimburse')]}"/>
+ <field name="date_back_to_office" attrs="{'invisible': [('apr_perjalanan', '=', False)]}"/>
+ <p style="font-size: 10px; color: grey; font-style: italic" attrs="{'invisible': [('apr_perjalanan', '=', False)]}">*Setelah tanggal kembali, pemohon diharapkan untuk segera memproses realisasi PUM</p>
+ <field name="estimated_return_date" readonly="1" widget="badge" attrs="{'invisible': [('type_request', '=', 'reimburse')]}"/>
+ <field name="days_remaining" readonly="1" widget="badge" attrs="{'invisible': [('type_request', '=', 'reimburse')]}"/>
+ <field name="approved_by" readonly="1"/>
+ <field name="create_date" readonly="1"/>
+ <field name="status_pay_down_payment"
+ readonly="1"
+ decoration-success="status_pay_down_payment == 'payment'"
+ decoration-danger="status_pay_down_payment == 'pending'"
+ widget="badge" invisible = "1"/>
+ </group>
+ <group string="Bukti Transfer">
+ <field name="upload_attachment_date" readonly="1"/>
+ <field name="attachment_type" attrs="{'readonly': [('is_current_user_ap', '=', False)]}" />
+ <field name="attachment_file_pdf" filename="attachment_filename"
+ widget="pdf_viewer"
+ attrs="{'invisible': [('attachment_type', '!=', 'pdf')], 'readonly': [('is_current_user_ap', '=', False)]}"/>
+
+ <field name="attachment_file_image" filename="attachment_filename"
+ widget="image"
+ attrs="{'invisible': [('attachment_type', '!=', 'image')], 'readonly': [('is_current_user_ap', '=', False)]}"
+ style="max-width:250px; max-height:250px; object-fit:contain;"/>
+ <br/>
+ </group>
+ </group>
+ <notebook attrs="{'invisible': [('type_request', '!=', 'reimburse')]}">
+ <page string="Rincian Reimburse">
+ <field name="reimburse_line_ids">
+ <tree>
+ <field name="date"/>
+ <field name="description"/>
+ <field name="account_id"/>
+ <!-- <field name="distance"/> -->
+ <field name="quantity"/>
+ <field name="price_unit"/>
+ <field name="total" sum="Total"/>
+ <field name="is_checked"/>
+ <field name="currency_id" invisible="1"/>
+ </tree>
+ <form>
+ <group col="2">
+ <group string="Form">
+ <field name="request_id" invisible="1"/>
+ <field name="date"/>
+ <field name="is_vehicle"/>
+ <field name="vehicle_type" attrs="{'invisible': [('is_vehicle', '=', False)]}"/>
+ <field name="description"/>
+ <field name="distance_departure" attrs="{'invisible': [('is_vehicle', '=', False)]}"/>
+ <field name="distance_return" attrs="{'invisible': [('is_vehicle', '=', False)]}"/>
+ <field name="quantity"/>
+ <field name="price_unit" attrs="{'readonly': [('is_vehicle', '=', True)]}" force_save ="1"/>
+ <field name="total" readonly="1"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="attachment_type"/>
+ <field name="attachment_pdf" filename="attachment_filename"
+ widget="pdf_viewer"
+ attrs="{'invisible': [('attachment_type', '!=', 'pdf')]}"/>
+ <field name="attachment_image" filename="attachment_filename"
+ widget="image"
+ attrs="{'invisible': [('attachment_type', '!=', 'image')]}"
+ style="max-width:250px; max-height:250px; object-fit:contain;"/>
+ </group>
+ <group string="Finance">
+ <field name="is_current_user_ap" invisible="1"/>
+ <field name="is_checked" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/>
+ <field name="account_id" placeholder="Hanya Finance yang boleh isi" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/>
+ </group>
+ </group>
+ </form>
+ </field>
+ <group class="oe_subtotal_footer oe_right" name="reimburse_total">
+ <field name="currency_id" invisible="1"/>
+ <field name="grand_total_reimburse"
+ widget="monetary"
+ options="{'currency_field': 'currency_id'}"/>
+ </group>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_tree_advance_payment_request" model="ir.ui.view">
+ <field name="name">advance.payment.request.tree</field>
+ <field name="model">advance.payment.request</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="number"/>
+ <field name="user_id" optional='hide'/>
+ <field name="applicant_name"/>
+ <field name="nominal"/>
+ <field name="departement_type" optional='hide'/>
+ <field name="status"
+ readonly="1"
+ decoration-success="status == 'approved'"
+ decoration-danger="status == 'reject'"
+ widget="badge" optional="show"/>
+ <field name="status_pay_down_payment"
+ readonly="1"
+ decoration-success="status_pay_down_payment == 'payment'"
+ decoration-danger="status_pay_down_payment == 'pending'"
+ widget="badge"/>
+ <field name="days_remaining" readonly="1" widget="badge" optional="hide"/>
+ <field name="estimated_return_date" widget="badge" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_advance_payment_request_filter" model="ir.ui.view">
+ <field name="name">advance.payment.request.filter</field>
+ <field name="model">advance.payment.request</field>
+ <field name="arch" type="xml">
+ <search string="Search APR &amp; Reimburse">
+ <filter string="My Requests" name="my_requests" domain="[('applicant_name','=',uid)]"/>
+ <separator/>
+ <filter string="PUM" name="filter_pum" domain="[('type_request','=','pum')]"/>
+ <filter string="Reimburse" name="filter_reimburse" domain="[('type_request','=','reimburse')]"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="action_advance_payment_request" model="ir.actions.act_window">
+ <field name="name">Pengajuan Uang Muka &amp; Reimburse</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">advance.payment.request</field>
+ <field name="view_mode">tree,form</field>
+ <field name="context">{'search_default_my_requests': 1}</field>
+ <field name="search_view_id" ref="view_advance_payment_request_filter"/>
+ </record>
+
+ <menuitem id="menu_advance_payment_request_acct"
+ name="Pengajuan Uang Muka &amp; Reimburse"
+ parent="account.menu_finance_entries"
+ sequence="114"
+ action="action_advance_payment_request"
+ />
+
+ <menuitem id="menu_advance_payment_request_sales"
+ name="Pengajuan Uang Muka &amp; Reimburse"
+ parent="indoteknik_custom.menu_monitoring_in_sale"
+ sequence="101"
+ action="action_advance_payment_request"
+ />
+
+ <record id="view_advance_payment_create_bill_form" model="ir.ui.view">
+ <field name="name">advance.payment.create.bill.form</field>
+ <field name="model">advance.payment.create.bill</field>
+ <field name="arch" type="xml">
+ <form string="Create CAB AP Only">
+ <group>
+ <field name="nominal"/>
+ <field name="account_id"/>
+ </group>
+ <footer>
+ <button name="action_create_cab" type="object" string="Create CAB" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_advance_payment_create_bill" model="ir.actions.act_window">
+ <field name="name">Create CAB AP Only</field>
+ <field name="res_model">advance.payment.create.bill</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_advance_payment_create_bill_form"/>
+ <field name="target">new</field>
+ </record>
+
+
+ <record id="view_form_create_reimburse_cab_wizard" model="ir.ui.view">
+ <field name="name">create.reimburse.cab.wizard.form</field>
+ <field name="model">create.reimburse.cab.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Buat Jurnal Reimburse">
+ <p>Pilih akun bank yang akan digunakan untuk jurnal kredit.</p>
+ <group>
+ <field name="total_reimburse"/>
+ <field name="account_id" options="{'no_create': True, 'no_open': True}"/>
+ <field name="currency_id" invisible="1"/>
+ </group>
+ <footer>
+ <button name="action_create_reimburse_cab" type="object" string="Buat Jurnal" class="btn-primary"/>
+ <button string="Batal" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_create_reimburse_cab_wizard" model="ir.actions.act_window">
+ <field name="name">Buat Jurnal Reimburse</field>
+ <field name="res_model">create.reimburse.cab.wizard</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_form_create_reimburse_cab_wizard"/>
+ <field name="target">new</field>
+ </record>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/advance_payment_settlement.xml b/indoteknik_custom/views/advance_payment_settlement.xml
new file mode 100644
index 00000000..050e3933
--- /dev/null
+++ b/indoteknik_custom/views/advance_payment_settlement.xml
@@ -0,0 +1,186 @@
+<odoo>
+ <record id="view_form_advance_payment_settlement" model="ir.ui.view">
+ <field name="name">advance.payment.settlement.form</field>
+ <field name="model">advance.payment.settlement</field>
+ <field name="arch" type="xml">
+ <form string="Advance Payment Settlement" duplicate="0">
+ <header>
+ <button name="action_cab"
+ type="object"
+ class="btn-info"
+ attrs="{'invisible': [ '|', ('is_cab_visible', '=', True),('status', '!=', 'approved')]}"
+ string="Buat Jurnal Realisasi"/>
+ <button name="action_approval_check"
+ type="object"
+ string="Checking/Approval"
+ class="btn-success"
+ attrs="{'invisible': [('status', '=', 'approved')]}"/>
+ <field name="status" widget="statusbar"
+ statusbar_visible="pengajuan1,pengajuan2,pengajuan3,approved"
+ statusbar_colors='{"reject":"red"}'
+ readonly="1"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <field name="is_cab_visible" invisible="1"/>
+ <field name="is_current_user_ap" invisible="1"/>
+ <button type="object"
+ name="action_view_journal_uangmuka"
+ class="oe_stat_button"
+ icon="fa-book"
+ attrs="{'invisible': [('is_cab_visible', '=', False)], 'readonly': [('is_current_user_ap', '=', False)]}"
+ style="width: 200px;">
+ <field name="move_id" widget="statinfo" string="Journal Entries"/>
+ <span class="o_stat_text">
+ <t t-esc="record.move_misc_id.name"/>
+ </span>
+ </button>
+ </div>
+ <div class="oe_title">
+ <h1>
+ <field name="name" readonly="1"/>
+ </h1>
+ </div>
+ <group col="2">
+ <group>
+ <field name="pum_id" readonly="1"/>
+ <field name="title" required="1"/>
+ <field name="goals" required="1"/>
+ <field name="related" required="1"/>
+ <field name="note_approval"/>
+ <p style="font-size: 11px; color: grey; font-style: italic">*Lengkapi keterangan berikut selaras dengan realisasi yang dilakukan (sesuai dengan format surat yang ada)</p>
+ <br/><br/>
+ <field name="lot_of_attachment"/>
+ <field name="approved_by" readonly="1"/>
+ <field name="applicant_name" readonly="1"/>
+ <field name="user_id" readonly="1"/>
+ </group>
+ <group attrs="{'invisible': [('lot_of_attachment', '!=', 'one_for_all_line')]}">
+ <field name="attachment_type" attrs="{'readonly': [('status', '=', 'approved')]}"/>
+
+ <field name="attachment_file_pdf" filename="attachment_filename"
+ widget="pdf_viewer"
+ attrs="{'invisible': [('attachment_type', '!=', 'pdf')], 'readonly': [('status', '=', 'approved')]}"/>
+
+ <field name="attachment_file_image" filename="attachment_filename"
+ widget="image"
+ attrs="{'invisible': [('attachment_type', '!=', 'image')], 'readonly': [('status', '=', 'approved')]}"
+ style="max-width:250px; max-height:250px; object-fit:contain;"/>
+ <br/>
+ </group>
+ </group>
+
+ <notebook>
+ <page string="Rincian Penggunaan">
+ <field name="penggunaan_line_ids" nolabel="1">
+ <tree>
+ <field name="date"/>
+ <field name="description"/>
+ <field name="nominal" sum="Total Penggunaan"/>
+ <field name="done_attachment"/>
+ </tree>
+
+ <form>
+ <group col="2">
+ <group string = "Form">
+ <field name="lot_of_attachment" invisible="1"/>
+ <field name="date"/>
+ <field name="description"/>
+ <field name="nominal"/>
+ <field name="attachment_type"
+ attrs="{
+ 'invisible': [('lot_of_attachment', '=', 'one_for_all_line')]
+ }"/>
+ <field name="attachment_file_pdf"
+ filename="attachment_filename_pdf"
+ widget="pdf_viewer"
+ attrs="{
+ 'invisible': [
+ '|',
+ ('lot_of_attachment', '=', 'one_for_all_line'),
+ ('attachment_type', '!=', 'pdf')
+ ]
+ }"/>
+ <field name="attachment_file_image"
+ filename="attachment_filename_image"
+ widget="image"
+ attrs="{
+ 'invisible': [
+ '|',
+ ('lot_of_attachment', '=', 'one_for_all_line'),
+ ('attachment_type', '!=', 'image')
+ ]
+ }"
+ style="max-width:250px; max-height:250px; object-fit:contain;"/>
+
+ </group>
+ <group string="Finance">
+ <field name="is_current_user_ap" invisible="1"/>
+ <field name="account_id" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/>
+ <field name="done_attachment" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/>
+ </group>
+ </group>
+ </form>
+ </field>
+ </page>
+ </notebook>
+
+ <div style="text-align:right;">
+ <button name="action_toggle_check_attachment"
+ type="object"
+ string="Check/Uncheck All Line Use PUM"
+ class="btn-secondary"/>
+ </div>
+
+ <group col="2">
+ <group class="oe_subtotal_footer oe_right">
+ <field name="currency_id" invisible="1"/>
+ <field name="grand_total_use" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
+ <field name="nominal_pum" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}" style="font-weight: bold;"/>
+ <field name="remaining_value" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
+ </group>
+ </group>
+ </sheet>
+
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_advance_payment_settlement" model="ir.actions.act_window">
+ <field name="name">Realisasi Pengajuan Uang Muka</field>
+ <field name="res_model">advance.payment.settlement</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <record id="view_tree_advance_payment_settlement" model="ir.ui.view">
+ <field name="name">advance.payment.settlement.tree</field>
+ <field name="model">advance.payment.settlement</field>
+ <field name="arch" type="xml">
+ <tree create="false" delete="false">
+ <field name="name"/>
+ <field name="pum_id"/>
+ <field name="grand_total_use" string="Total Realisasi"/>
+ <field name="remaining_value" string="Sisa PUM"/>
+ <field name="status" widget="badge" decoration-success="status == 'approved'"/>
+ </tree>
+ </field>
+ </record>
+
+ <menuitem id="menu_advance_payment_settlement_acct"
+ name="Realisasi PUM"
+ parent="account.menu_finance_entries"
+ sequence="114"
+ action="action_advance_payment_settlement"
+ />
+
+ <menuitem id="menu_advance_payment_settlement_sales"
+ name="Realisasi PUM"
+ parent="indoteknik_custom.menu_monitoring_in_sale"
+ sequence="101"
+ action="action_advance_payment_settlement"
+ />
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/apache_solr_queue.xml b/indoteknik_custom/views/apache_solr_queue.xml
index 4c145b9f..08972b28 100644
--- a/indoteknik_custom/views/apache_solr_queue.xml
+++ b/indoteknik_custom/views/apache_solr_queue.xml
@@ -9,7 +9,7 @@
<field name="display_name" readonly="1" />
<field name="res_model" readonly="1" />
<field name="res_id" readonly="1" />
- <field name="function_name" readonly="1" />
+ <field name="function_name" readonly="1" optional="hide"/>
<field
name="execute_status"
widget="badge"
@@ -18,6 +18,7 @@
decoration-success="execute_status == 'success'"
decoration-primary="execute_status == 'not_found'"
/>
+ <field name = "log" readonly="1" optional="hide"/>
<field name="execute_date" readonly="1" />
<field name="create_date" readonly="1" />
</tree>
diff --git a/indoteknik_custom/views/commission_internal.xml b/indoteknik_custom/views/commission_internal.xml
new file mode 100644
index 00000000..2f3db5b0
--- /dev/null
+++ b/indoteknik_custom/views/commission_internal.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="commission_internal_tree" model="ir.ui.view">
+ <field name="name">commission.internal.tree</field>
+ <field name="model">commission.internal</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="number"/>
+ <field name="date_doc"/>
+ <field name="month"/>
+ <field name="year"/>
+ <field name="description"/>
+ <field name="comment"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="commission_internal_form" model="ir.ui.view">
+ <field name="name">commission.internal.form</field>
+ <field name="model">commission.internal</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet string="Header">
+ <div class="oe_button_box" name="button_box"/>
+ <group>
+ <group>
+ <field name="number"/>
+ <field name="date_doc"/>
+ <field name="description"/>
+ <field name="comment" readonly="1"/>
+ </group>
+ <group>
+ <field name="month"/>
+ <field name="year"/>
+ <div>
+ <button name="generate_commission_from_generate_ledger"
+ string="Copy GL"
+ type="object"
+ />
+ </div>
+ <div>
+ <button name="calculate_commission_internal_from_keyword"
+ string="Calculate"
+ type="object"
+ />
+ </div>
+ <div>
+ <button name="calculate_commission_internal_result"
+ string="Result"
+ type="object"
+ />
+ </div>
+ </group>
+ </group>
+ <notebook>
+ <page id="commission_internal_line_tab" string="Lines">
+ <field name="commission_internal_line"/>
+ </page>
+ <page id="commission_internal_result_tab" string="Result">
+ <field name="commission_internal_result"/>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="commission_internal_line_tree" model="ir.ui.view">
+ <field name="name">commission.internal.line.tree</field>
+ <field name="model">commission.internal.line</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="date"/>
+ <field name="number"/>
+ <field name="account_move_id" optional="hide"/>
+ <field name="account_move_line_id" optional="hide"/>
+ <field name="label"/>
+ <field name="debit"/>
+ <field name="credit"/>
+ <field name="balance"/>
+ <field name="ongkir"/>
+ <field name="refund"/>
+ <field name="pph"/>
+ <field name="others"/>
+ <field name="linenetamt"/>
+ <field name="dpp"/>
+ <field name="status"/>
+ <field name="helper1" optional="hide"/>
+ <field name="helper2" optional="hide"/>
+ <field name="helper3" optional="hide"/>
+ <field name="helper4" optional="hide"/>
+ <field name="helper5" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="commission_internal_result_tree" model="ir.ui.view">
+ <field name="name">commission.internal.result.tree</field>
+ <field name="model">commission.internal.result</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="date_doc"/>
+ <field name="number"/>
+ <field name="res_name" optional="hide"/>
+ <field name="res_id" optional="hide"/>
+ <field name="name"/>
+ <field name="salesperson"/>
+ <field name="totalamt"/>
+ <field name="uang_masuk_line_id" optional="hide"/>
+ <field name="uang_masuk_id" optional="hide"/>
+ <field name="date_uang_masuk"/>
+ <field name="label_uang_masuk"/>
+ <field name="nomor_uang_masuk"/>
+ <field name="uang_masuk"/>
+ <field name="ongkir"/>
+ <field name="refund"/>
+ <field name="pph"/>
+ <field name="others"/>
+ <field name="linenetamt"/>
+ <field name="linenetamt_prorate"/>
+ <field name="dpp"/>
+ <field name="status" optional="hide"/>
+ <field name="helper1" optional="hide"/>
+ <field name="helper2" optional="hide"/>
+ <field name="helper3" optional="hide"/>
+ <field name="helper4" optional="hide"/>
+ <field name="helper5" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_commission_internal_filter" model="ir.ui.view">
+ <field name="name">commission.internal.list.select</field>
+ <field name="model">commission.internal</field>
+ <field name="priority" eval="15"/>
+ <field name="arch" type="xml">
+ <search string="Search Commission Internal">
+ <field name="number"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="commission_internal_action" model="ir.actions.act_window">
+ <field name="name">Commission Internal</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">commission.internal</field>
+ <field name="search_view_id" ref="view_commission_internal_filter"/>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem id="menu_commission_internal"
+ name="Commission Internal"
+ action="commission_internal_action"
+ parent="account.menu_finance_reports"
+ sequence="251"/>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/dunning_run.xml b/indoteknik_custom/views/dunning_run.xml
index 51377f78..911a372d 100644
--- a/indoteknik_custom/views/dunning_run.xml
+++ b/indoteknik_custom/views/dunning_run.xml
@@ -25,6 +25,7 @@
<field name="arch" type="xml">
<tree>
<field name="partner_id"/>
+ <field name="information_line"/>
<field name="reference"/>
<field name="invoice_id"/>
<field name="date_invoice"/>
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index 4b8fec53..46148606 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -219,6 +219,28 @@
<field name="number_increment">1</field>
</record>
+ <record id="sequence_advance_payment_request" model="ir.sequence">
+ <field name="name">Advance Payment Request Sequence</field>
+ <field name="code">advance.payment.request</field>
+ <field name="prefix">PUM/%(year)s/%(month)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ <field name="use_date_range" eval="True"/>
+ <field name="active">True</field>
+ </record>
+
+ <record id="sequence_reimburse_request" model="ir.sequence">
+ <field name="name">Reimburse Request Sequence</field>
+ <field name="code">reimburse.request</field>
+ <field name="prefix">RMK/%(year)s/%(month)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ <field name="use_date_range" eval="True"/>
+ <field name="active">True</field>
+ </record>
+
<record id="seq_refund_sale_order" model="ir.sequence">
<field name="name">Refund Sales Order</field>
<field name="code">refund.sale.order</field>
@@ -226,6 +248,7 @@
<field name="padding">4</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
+ <field name="use_date_range" eval="True"/>
<field name="active">True</field>
</record>
diff --git a/indoteknik_custom/views/mail_template_pum.xml b/indoteknik_custom/views/mail_template_pum.xml
new file mode 100644
index 00000000..81f8ada8
--- /dev/null
+++ b/indoteknik_custom/views/mail_template_pum.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="0">
+
+ <record id="mail_template_pum_reminder_today" model="mail.template">
+ <field name="name">Reminder PUM: Hari Ini</field>
+ <field name="model_id" ref="indoteknik_custom.model_advance_payment_request"/>
+ <field name="subject">Reminder Realisasi PUM - ${object.number}</field>
+ <field name="email_from">${object.email_ap}</field>
+ <field name="email_to">andrifebriyadiputra@gmail.com</field>
+ <field name="body_html" type="html">
+ <div>
+ <p><b>Dengan Hormat Bpk/Ibu ${object.user_id.display_name},</b></p>
+
+ <p>
+ Berikut terlampir pengajuan PUM <b>${object.number}</b> sebesar
+ <b>${format_amount(object.nominal, object.currency_id)}</b> dari PT. INDOTEKNIK DOTCOM GEMILANG
+ pada tanggal ${format_date(object.create_date, 'd MMMM yyyy')}.<br/>
+ Tolong segera selesaikan realisasi PUM tersebut dengan menyertakan dokumen asli untuk mendukung realisasi tersebut
+ <b>maksimal 7 hari dari sekarang</b>.<br/>
+ Terima Kasih
+ </p>
+
+ <br/><br/>
+
+ <p><b>Best Regards,</b></p>
+
+ <br/>
+ <p><b>
+ Dept. Finance<br/>
+ PT. INDOTEKNIK DOTCOM GEMILANG<br/>
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" alt="Indoteknik" style="max-width: 18%; height: auto;"></img><br/>
+ </b></p>
+ <p><i>Email ini dikirim otomatis, abaikan bila sudah melakukan realisasi.</i></p>
+ </div>
+ </field>
+ <field name="auto_delete" eval="True"/>
+ </record>
+
+ <record id="mail_template_pum_reminder_h_2" model="mail.template">
+ <field name="name">Reminder Realisasi PUM: H-2</field>
+ <field name="model_id" ref="indoteknik_custom.model_advance_payment_request"/>
+ <field name="subject">Reminder Realisasi PUM (H-2) - ${object.number}</field>
+ <field name="email_from">${object.email_ap}</field>
+ <field name="email_to">andrifebriyadiputra@gmail.com</field>
+ <field name="body_html" type="html">
+ <div>
+ <p><b>Dengan Hormat Bpk/Ibu ${object.user_id.display_name},</b></p>
+
+ <p>
+ Berikut terlampir pengajuan PUM <b>${object.number}</b> sebesar
+ <b>${format_amount(object.nominal, object.currency_id)}</b> dari PT. INDOTEKNIK DOTCOM GEMILANG
+ pada tanggal ${format_date(object.create_date, 'd MMMM yyyy')}.<br/>
+ Tolong segera selesaikan realisasi PUM tersebut dengan menyertakan dokumen asli untuk mendukung PUM tersebut
+ <b>batas waktu tersisa 2 hari lagi</b>.<br/>
+ Terima Kasih
+ </p>
+
+ <br/><br/>
+
+ <p><b>Best Regards,</b></p>
+
+ <br/>
+ <p><b>
+ Dept. Finance<br/>
+ PT. INDOTEKNIK DOTCOM GEMILANG<br/>
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" alt="Indoteknik" style="max-width: 18%; height: auto;"></img><br/>
+ </b></p>
+ <p><i>Email ini dikirim otomatis, abaikan bila sudah melakukan realisasi.</i></p>
+ </div>
+ </field>
+ <field name="auto_delete" eval="True"/>
+ </record>
+
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml
index afa7c1cb..fbe17093 100644
--- a/indoteknik_custom/views/refund_sale_order.xml
+++ b/indoteknik_custom/views/refund_sale_order.xml
@@ -57,7 +57,7 @@
<button name="action_trigger_cancel"
type="object"
string="Cancel"
- attrs="{'invisible': ['|', ('status_payment', '!=', 'pending'), ('status', 'in', ['reject', 'refund'])]}" />
+ attrs="{'invisible': ['|', ('status_payment', '!=', 'pending'), ('status', '=', 'reject')]}" />
<button name="action_confirm_refund"
type="object"
string="Confirm Payment"
@@ -257,7 +257,7 @@
</group>
</page>
- <page string="Cancel Reason" attrs="{'invisible': [('status', '=', 'refund')]}">
+ <page string="Cancel Reason" attrs="{'invisible': [('status_payment', '=', 'done')]}">
<group>
<field name="reason_reject"/>
</group>
diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml
index 72751187..8378bf34 100644
--- a/indoteknik_custom/views/res_partner.xml
+++ b/indoteknik_custom/views/res_partner.xml
@@ -23,6 +23,7 @@
<field name="property_payment_term_id" position="after">
<field name="previous_payment_term_id" readonly="1"/>
<field name="is_cbd_locked" readonly="1"/>
+ <field name="cbd_lock_date" readonly="1"/>
<field name="user_payment_terms_sales" readonly="1"/>
<field name="date_payment_terms_sales" readonly="1"/>
</field>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 82daa36f..23fbe155 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -55,6 +55,10 @@
attrs="{'invisible':['|', ('partner_is_cbd_locked','=',False), ('state', 'not in', ['draft', 'cancel'])]}">
<strong>Warning!</strong> Payment Terms Customer terkunci menjadi <b>Cash Before Delivery (C.B.D.)</b> karena ada invoice telah jatuh tempo <b>30 hari</b>. Silakan ajukan <b>Approval Payment Term</b> untuk membuka kunci.
</div>
+ <widget name="web_ribbon"
+ title="FIKTIF"
+ bg_color="bg-danger"
+ attrs="{'invisible': [('is_so_fiktif', '=', False)]}"/>
</xpath>
<div class="oe_button_box" name="button_box">
<field name="advance_payment_move_ids" invisible="1"/>
@@ -130,17 +134,23 @@
</field>
<field name="approval_status" position="after">
<field name="notes"/>
+ <field name="is_so_fiktif"/>
</field>
<field name="source_id" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="user_id" position="after">
+ <field name="internal_notes_contact" readonly="1"/>
<field name="hold_outgoing" readonly="1" />
<field name="date_hold" readonly="1" widget="datetime" />
<field name="date_unhold" readonly="1" widget="datetime" />
<field name="helper_by_id" readonly="1" />
<field name="compute_fullfillment" invisible="1" />
</field>
+ <xpath expr="//field[@name='team_id']" position="attributes">
+ <attribute name="attrs">{'readonly': [('state', 'in', ['sale', 'done'])]}</attribute>
+ </xpath>
+
<field name="tag_ids" position="after">
<!-- <field name="eta_date_start"/> -->
<t t-esc="' to '"/>
@@ -481,6 +491,7 @@
<field name="pareto_status" optional="hide"/>
<field name="shipping_method_picking" optional="hide"/>
<field name="hold_outgoing" optional="hide"/>
+ <field name="is_so_fiktif" optional="hide" readonly="1"/>
</field>
</field>
</record>
@@ -497,8 +508,8 @@
<field name="date_kirim_ril"/>
<field name="date_driver_departure"/>
<field name="date_driver_arrival"/>
- <field name="payment_state_custom" widget="badge"
- decoration-danger="payment_state_custom == 'unpaid'"
+ <field name="payment_state_custom" widget="badge"
+ decoration-danger="payment_state_custom == 'unpaid'"
decoration-success="payment_state_custom == 'paid'"
decoration-warning="payment_state_custom == 'partial'" optional="hide"/>
<field name="unreserved_percent" widget="percentpie" string="Unreserved" optional="hide"/>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 7668946c..bad85963 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -23,6 +23,7 @@
<field name="state_packing" widget="badge" decoration-success="state_packing == 'packing_done'"
decoration-danger="state_packing == 'not_packing'" optional="hide"/>
<field name="final_seq"/>
+ <field name="is_so_fiktif" optional="hide" readonly="1"/>
<field name="state_approve_md" widget="badge" decoration-success="state_approve_md == 'done'"
decoration-warning="state_approve_md == 'pending'" optional="hide"/>
<!-- <field name="countdown_hours" optional="hide"/>
@@ -115,6 +116,7 @@
position="after">
<field name="product_image" widget="image"
style="height:128px;width:128px;" readonly="1"/>
+
</xpath> -->
<field name="backorder_id" position="after">
<field name="select_shipping_option_so"/>
@@ -147,7 +149,7 @@
<attribute name="attrs">{'readonly': [('parent.picking_type_code', '=', 'incoming')]}</attribute>
</field> -->
<field name="date_done" position="after">
- <field name="arrival_time"/>
+ <field name="arrival_time" attrs="{'invisible': [('picking_type_id', 'in', [29,30])]}"/>
</field>
<field name="scheduled_date" position="attributes">
<attribute name="readonly">1</attribute>
@@ -162,11 +164,13 @@
<field name="origin" position="after">
<!-- <field name="show_state_approve_md" invisible="1" optional="hide"/>-->
- <field name="state_approve_md" widget="badge"/>
+ <field name="state_approve_md" widget="badge"
+ attrs="{'invisible': [('picking_type_id', 'in', [29,30])]}"/>
<field name="purchase_id"/>
<field name="sale_order"/>
<field name="invoice_status"/>
- <field name="is_bu_iu" />
+ <field name="is_bu_iu"/>
+ <field name="is_so_fiktif" readonly="1"/>
<field name="approval_status" attrs="{'invisible': [('is_bu_iu', '=', False)]}"/>
<field name="date_doc_kirim" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/>
<field name="summary_qty_operation"/>
@@ -180,6 +184,7 @@
'required': [['is_internal_use', '=', True]]
}"
/>
+ <field name="so_num" attrs="{'invisible': [('picking_type_id', 'not in', [29,30,73,74])]}"/>
</field>
<field name="group_id" position="before">
<field name="date_reserved"/>
@@ -226,6 +231,10 @@
<group>
<group>
<field name="notee"/>
+ <field name="qty_yang_mau_dikirim"/>
+ <field name="qty_terkirim"/>
+ <field name="qty_gantung"/>
+ <field name="delivery_status"/>
<field name="note_logistic"/>
<field name="note_info"/>
<field name="responsible"/>
@@ -244,6 +253,16 @@
attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/>
<field name="driver_id"/>
<field name='sj_return_date'/>
+ <field name="sj_documentations" context="{'default_picking_id': active_id}">
+ <tree editable="bottom">
+ <field name="image" widget="image" options="{'size':[128,128]}"/>
+ </tree>
+ <form>
+ <group>
+ <field name="image" widget="image" options="{'size':[512,512]}"/>
+ </group>
+ </form>
+ </field>
<field name="sj_documentation" widget="image"/>
<field name="paket_documentation" widget="image"/>
<field name="dispatch_documentation" widget="image"/>
@@ -325,6 +344,22 @@
</field>
</record>
+ <record id="view_picking_form_inherit_ribbon" model="ir.ui.view">
+ <field name="name">stock.picking.form.ribbon</field>
+ <field name="model">stock.picking</field>
+ <field name="inherit_id" ref="stock.view_picking_form"/>
+ <field name="arch" type="xml">
+ <!-- Sisipkan sebelum elemen dengan class oe_title -->
+ <xpath expr="//sheet/div[@class='oe_title']" position="before">
+ <widget name="web_ribbon"
+ title="FIKTIF"
+ bg_color="bg-danger"
+ attrs="{'invisible': [('is_so_fiktif', '=', False)]}"/>
+ </xpath>
+ </field>
+ </record>
+
+
<record id="scan_koli_tree" model="ir.ui.view">
<field name="name">scan.koli.tree</field>
<field name="model">scan.koli</field>
@@ -394,8 +429,11 @@
decoration-danger="qty_done&gt;product_uom_qty and state!='done' and parent.picking_type_code != 'incoming'"
decoration-success="qty_done==product_uom_qty and state!='done' and not result_package_id">
<field name="note" placeholder="Add a note here"/>
- <field name="outstanding_qty"/>
- <field name="delivery_status" widget="badge" options="{'colors': {'full': 'success', 'partial': 'warning', 'partial_final': 'danger'}}"/>
+ <field name="qty_yang_mau_dikirim"/>
+ <field name="qty_terkirim"/>
+ <field name="qty_gantung"/>
+ <field name="delivery_status" widget="badge"
+ options="{'colors': {'full': 'success', 'partial': 'warning', 'partial_final': 'danger'}}"/>
</tree>
</field>
</record>
@@ -419,7 +457,9 @@
<form string="Peringatan Koli Belum Diperiksa">
<sheet>
<div class="oe_title">
- <h2><span>⚠️ Perhatian!</span></h2>
+ <h2>
+ <span>⚠️ Perhatian!</span>
+ </h2>
</div>
<group>
<field name="message" readonly="1" nolabel="1" widget="text"/>
diff --git a/indoteknik_custom/views/uom_uom.xml b/indoteknik_custom/views/uom_uom.xml
new file mode 100644
index 00000000..a7fb55e5
--- /dev/null
+++ b/indoteknik_custom/views/uom_uom.xml
@@ -0,0 +1,14 @@
+<odoo>
+ <data>
+ <record id="uom_form_view_inherit" model="ir.ui.view">
+ <field name="name">Uom</field>
+ <field name="model">uom.uom</field>
+ <field name="inherit_id" ref="uom.product_uom_form_view"/>
+ <field name="arch" type="xml">
+ <field name="rounding" position="after">
+ <field name="coretax_id"/>
+ </field>
+ </field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file