summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-30 10:12:10 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-30 10:12:10 +0700
commita41ed8ed1db45b019d3c3f7d31a01d9658c55f78 (patch)
tree4d74ea77069a0b56bcde0df2c50007d9acdfdb77
parent7160ae85c1655c99b2c8f1be77cb3cdb6616043e (diff)
parent099bec753a310ec83ea3562a78c304dffb6d50d8 (diff)
merge apr
-rw-r--r--indoteknik_api/controllers/api_v1/product.py87
-rwxr-xr-xindoteknik_custom/__manifest__.py4
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/account_move.py1
-rw-r--r--indoteknik_custom/models/advance_payment_request.py1544
-rw-r--r--indoteknik_custom/models/partial_delivery.py14
-rwxr-xr-xindoteknik_custom/models/purchase_order.py4
-rwxr-xr-xindoteknik_custom/models/sale_order.py47
-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/sourcing_job_order.py367
-rw-r--r--indoteknik_custom/models/stock_move.py50
-rw-r--r--indoteknik_custom/models/stock_picking.py91
-rw-r--r--indoteknik_custom/models/tukar_guling.py8
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv7
-rw-r--r--indoteknik_custom/views/account_move.xml2
-rw-r--r--indoteknik_custom/views/advance_payment_request.xml290
-rw-r--r--indoteknik_custom/views/advance_payment_settlement.xml184
-rw-r--r--indoteknik_custom/views/apache_solr_queue.xml3
-rw-r--r--indoteknik_custom/views/ir_sequence.xml21
-rw-r--r--indoteknik_custom/views/mail_template_pum.xml76
-rwxr-xr-xindoteknik_custom/views/sale_order.xml1
-rw-r--r--indoteknik_custom/views/sourcing.xml74
-rw-r--r--indoteknik_custom/views/stock_picking.xml6
25 files changed, 2770 insertions, 137 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_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 392e848d..961e6a94 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,6 +186,7 @@
'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',
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 36a992d2..7f946b57 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
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 44b3cb76..684ef335 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -106,6 +106,7 @@ 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.")
# def _check_and_lock_cbd(self):
# cbd_term = self.env['account.payment.term'].browse(26)
diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py
new file mode 100644
index 00000000..5a465ca4
--- /dev/null
+++ b/indoteknik_custom/models/advance_payment_request.py
@@ -0,0 +1,1544 @@
+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]
+ 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]
+ if self.env.user.id not in ap_user_ids:
+ 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]
+ 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 PUM {rec.name 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] # 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] # 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,
+ 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]
+ 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] # 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,
+ domain="[('id', 'in', [484, 486, 527, 529, 530, 471, 473, 492, 493, 488, 625, 528, 533, 534])]"
+ )
+ 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]
+ 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='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')
+
+ note_approval = fields.Text(string='Note Persetujuan', tracking=3)
+
+ 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]
+ 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]
+ 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]
+ if self.env.user.id not in ap_user_ids:
+ 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] # 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
+ 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] # 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])]" # 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
+
+ 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
+
+ # 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': f'Reimburse {request.number}',
+ 'debit': 0,
+ 'credit': request.grand_total_reimburse,
+ }))
+
+ ref_label = f'{request.number} - {request.detail_note or "-"}'
+
+ # 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/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..534d8122 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -1043,7 +1043,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 +1123,8 @@ class PurchaseOrder(models.Model):
if not self.not_update_purchasepricelist:
self.add_product_to_pricelist()
for line in self.order_line:
+ if not line.product_id.public_categ_ids:
+ 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
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 5c8f34c6..494aeaa2 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -397,23 +397,24 @@ 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.")
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 {
- # 'type': 'ir.actions.act_window',
- # 'name': 'Partial Delivery',
- # 'res_model': 'partial.delivery.wizard',
- # 'view_mode': 'form',
- # 'target': 'new',
- # 'context': {
- # 'default_sale_id': self.id,
- # # kasih langsung list of int biar ga ribet di wizard
- # 'default_picking_ids': pickings.ids,
- # }
- # }
+ # 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 {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Partial Delivery',
+ 'res_model': 'partial.delivery.wizard',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'default_sale_id': self.id,
+ # kasih langsung list of int biar ga ribet di wizard
+ 'default_picking_ids': pickings.ids,
+ }
+ }
@api.depends('partner_id.is_cbd_locked')
@@ -422,7 +423,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:
@@ -1985,10 +1986,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('/', '-')
@@ -2001,8 +2002,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)
@@ -2716,7 +2717,7 @@ class SaleOrder(models.Model):
return True
if user.id in (3401, 20, 3988, 17340): # admin (fida, nabila, ninda)
- return True
+ raise UserError("Yahaha gabisa confirm so, minta ke sales nya ajah")
if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988):
return True
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/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py
index 3d4a404e..98f356de 100644
--- a/indoteknik_custom/models/sourcing_job_order.py
+++ b/indoteknik_custom/models/sourcing_job_order.py
@@ -59,13 +59,20 @@ class SourcingJobOrder(models.Model):
line_sales_view_ids = fields.One2many(
'sourcing.job.order.line', 'order_id',
string='Sales View Lines',
- domain=[('price', '>', 0)]
+ domain=[('state', 'in', ['sourcing', 'done', 'cancel'])]
+ )
+ line_sales_view_cancel_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='Sales View Lines',
+ domain=[('state', '=', 'cancel')]
)
has_price_in_lines = fields.Boolean(
string='Has Line with Price',
compute='_compute_has_price_in_lines',
)
+ is_creator_same_user = fields.Boolean(compute='_compute_is_creator_same_user')
+ can_convert_to_product = fields.Boolean(string="Can Convert", compute="_compute_can_convert_to_product")
@api.depends('line_ids.subtotal')
@@ -99,6 +106,21 @@ class SourcingJobOrder(models.Model):
rec.user_id == current_user and bool(rec.takeover_request)
)
+ @api.depends('create_uid', 'user_id')
+ def _compute_is_creator_same_user(self):
+ for rec in self:
+ current_user = self.env.user
+ rec.is_creator_same_user = (
+ rec.create_uid == current_user and
+ rec.user_id == current_user
+ )
+
+ @api.depends('line_md_edit_ids.state')
+ def _compute_can_convert_to_product(self):
+ """Cek apakah ada line dengan state 'done'."""
+ for rec in self:
+ rec.can_convert_to_product = any(line.state == 'done' for line in rec.line_md_edit_ids)
+
@api.model
def create(self, vals):
"""Hanya Sales & Merchandiser yang boleh membuat job."""
@@ -117,22 +139,112 @@ class SourcingJobOrder(models.Model):
return rec
def write(self, vals):
- context_action = self.env.context.get('from_action_take', False)
-
- if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or
- self.env.user.has_group('indoteknik_custom.group_role_merchandiser')):
+ bypass_actions = (
+ self.env.context.get('from_action_take', False)
+ or self.env.context.get('from_multi_action_take', False)
+ or self.env.context.get('from_action_takeover', False)
+ )
+
+ if not (
+ self.env.user.has_group('indoteknik_custom.group_role_sales')
+ or self.env.user.has_group('indoteknik_custom.group_role_merchandiser')
+ ):
raise UserError("❌ Hanya Sales dan Merchandiser yang boleh mengedit Sourcing Job.")
for rec in self:
- if not rec.user_id and rec.create_uid != self.env.user and not vals.get('user_id') and not context_action:
+ if (
+ not rec.user_id
+ and rec.create_uid != self.env.user
+ and not vals.get('user_id')
+ and not bypass_actions
+ ):
raise UserError("❌ SJO ini belum memiliki MD Person. Tidak dapat melakukan edit.")
- if rec.user_id != self.env.user and rec.create_uid != self.env.user and not context_action:
+ if (
+ rec.user_id != self.env.user
+ and rec.create_uid != self.env.user
+ and not bypass_actions
+ ):
raise UserError("❌ Hanya MD Person dan Creator SJO ini yang bisa melakukan Edit.")
-
- return super().write(vals)
+
+ # --- Simpan data lama sebelum write (buat pembanding)
+ old_data = {}
+ for rec in self:
+ old_data[rec.id] = {
+ 'state': rec.state,
+ 'user_id': rec.user_id.id if rec.user_id else False,
+ 'approval_sales': rec.approval_sales,
+ 'line_data': {
+ line.id: {
+ 'state': line.state,
+ 'vendor_id': line.vendor_id.id if line.vendor_id else False,
+ 'price': line.price,
+ }
+ for line in rec.line_md_edit_ids
+ },
+ }
+
+ res = super().write(vals)
+
+ # --- Bandingkan setelah write dan buat log
+ for rec in self:
+ changes = []
+ old = old_data.get(rec.id, {})
+
+ # === Perubahan di field parent ===
+ if old.get('state') != rec.state:
+ changes.append(f"State: <b>{old.get('state')}</b> → <b>{rec.state}</b>")
+ if old.get('user_id') != (rec.user_id.id if rec.user_id else False):
+ changes.append(f"MD Person: <b>{old.get('user_id')}</b> → <b>{rec.user_id.name if rec.user_id else '-'}</b>")
+ if old.get('approval_sales') != rec.approval_sales:
+ changes.append(f"Approval Status: <b>{old.get('approval_sales')}</b> → <b>{rec.approval_sales}</b>")
+
+ # === Perubahan di line ===
+ old_lines = old.get('line_data', {})
+ for line in rec.line_md_edit_ids:
+ old_line = old_lines.get(line.id)
+ if not old_line:
+ continue # line baru, skip validasi perubahan
+
+ # 💥 Validasi vendor berubah tapi price tidak berubah
+ if (
+ old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False)
+ and old_line['price'] == line.price
+ ):
+ raise UserError(
+ f"⚠️ Harga untuk produk {line.product_name} belum diperbarui setelah mengganti Vendor."
+ )
+
+ # Catat perubahan state, vendor, atau harga untuk log note
+ sub_changes = []
+
+ if old_line['state'] != line.state:
+ sub_changes.append(f"- state: <b>{old_line['state']}</b> → <b>{line.state}</b>")
+
+ if old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False):
+ old_vendor = self.env['res.partner'].browse(old_line['vendor_id']).name if old_line['vendor_id'] else '-'
+ sub_changes.append(f"- vendor: <b>{old_vendor}</b> → <b>{line.vendor_id.name if line.vendor_id else '-'}</b>")
+
+ if old_line['price'] != line.price:
+ sub_changes.append(f"- price: <b>{old_line['price']}</b> → <b>{line.price}</b>")
+
+ if sub_changes:
+ joined = "<br/>".join(sub_changes)
+ changes.append(f"<b>{line.product_name}</b>:<br/>{joined}")
+
+ # Post ke chatter
+ if changes:
+ message = "<br/><br/>".join(changes)
+ rec.message_post(
+ body=f"Perubahan pada Sourcing Job:<br/>{message}",
+ subtype_xmlid="mail.mt_comment",
+ )
+
+ return res
+
def action_take(self):
+ context_action = self.env.context.get('from_action_take', False)
for rec in self:
if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
raise UserError("❌ Hanya Merchandiser yang dapat mengambil Sourcing Job.")
@@ -145,17 +257,52 @@ class SourcingJobOrder(models.Model):
rec.message_post(body=("Job <b>%s</b> diambil oleh %s") % (rec.name, self.env.user.name))
def action_multi_take(self):
+ context_action = self.env.context.get('from_multi_action_take', True)
untaken = self.filtered(lambda r: r.state == 'draft')
if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
raise UserError("❌ Hanya Merchandiser yang bisa mengambil Sourcing Job.")
if not untaken:
- raise UserError("Tidak ada record draft untuk diambil.")
+ raise UserError("Tidak ada record Untaken untuk diambil.")
untaken.write({
'state': 'taken',
'user_id': self.env.user.id,
})
+ def action_confirm_by_md(self):
+ for rec in self:
+ if rec.user_id and rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Confirm.")
+
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
+ if invalid_lines:
+ line_names = ', '.join(invalid_lines.mapped('product_name'))
+ raise UserError(
+ f"⚠️ Tidak dapat melakukan Confirm SJO.\n"
+ f"Masih ada line yang belum selesai disourcing: {line_names}"
+ )
+ if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids):
+ raise UserError("⚠️ Tidak dapat melakukan Confirm SJO. Semua line pada SJO ini Unavailable.")
+
+ rec.approval_sales = 'approve'
+ rec.state = 'done'
+
+ rec.message_post(
+ body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'titl': 'Auto Approved',
+ 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
def action_cancel(self):
for rec in self:
if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
@@ -172,6 +319,7 @@ class SourcingJobOrder(models.Model):
(rec.name, self.env.user.name, rec.cancel_reason))
def action_request_takeover(self):
+ context_action = self.env.context.get('from_action_takeover', True)
for rec in self:
if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
raise UserError("❌ Hanya Merchandiser yang dapat Request Takeover Sourcing Job.")
@@ -182,47 +330,14 @@ class SourcingJobOrder(models.Model):
rec.takeover_request = self.env.user
activity_type = self.env.ref('mail.mail_activity_data_todo')
- self.env['mail.activity'].create({
- 'activity_type_id': activity_type.id,
- 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.",
- 'res_id': rec.id,
- 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'),
- 'user_id': rec.user_id.id,
- })
-
- rec.message_post(
- body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.",
- subtype_xmlid="mail.mt_comment"
+ rec.activity_schedule(
+ activity_type_id=activity_type.id,
+ user_id=rec.user_id.id,
+ note=f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.",
)
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Request Sent',
- 'message': f"Request takeover telah dikirim ke {rec.user_id.name}.",
- 'type': 'success',
- 'sticky': False,
- }
- }
- def action_ask_approval(self):
- for rec in self:
- if rec.user_id != self.env.user:
- raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.")
-
- rec.approval_sales = 'requested'
-
- activity_type = self.env.ref('mail.mail_activity_data_todo')
- self.env['mail.activity'].create({
- 'activity_type_id': activity_type.id,
- 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.",
- 'res_id': rec.id,
- 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'),
- 'user_id': rec.user_id.id,
- })
-
rec.message_post(
- body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.user_id.name}</b>.",
+ body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.",
subtype_xmlid="mail.mt_comment"
)
@@ -236,9 +351,7 @@ class SourcingJobOrder(models.Model):
'sticky': False,
}
}
-
-
-
+
def action_approve_takeover(self):
for rec in self:
if self.env.user != rec.user_id:
@@ -262,6 +375,7 @@ class SourcingJobOrder(models.Model):
body=f"Takeover disetujui oleh <b>{self.env.user.name}</b>. Sourcing Job berpindah ke <b>{new_user.name}</b>.",
subtype_xmlid="mail.mt_comment"
)
+
def action_reject_takeover(self):
for rec in self:
if self.env.user != rec.user_id:
@@ -284,7 +398,98 @@ class SourcingJobOrder(models.Model):
subtype_xmlid="mail.mt_comment"
)
+ def action_ask_approval(self):
+ for rec in self:
+ if rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.")
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
+ if invalid_lines:
+ line_names = ', '.join(invalid_lines.mapped('product_name'))
+ raise UserError(
+ f"⚠️ Tidak dapat melakukan Request Approval.\n"
+ f"Masih ada line yang belum selesai disourcing: {line_names}"
+ )
+ if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids):
+ raise UserError("⚠️ Tidak dapat melakukan Request Approval. Semua line pada SJO ini Unavailable.")
+
+ if rec.create_uid == rec.user_id:
+ rec.approval_sales = 'approve'
+ rec.state = 'done'
+
+ rec.message_post(
+ body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Auto Approved',
+ 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+ rec.approval_sales = 'draft'
+
+ activity_type = self.env.ref('mail.mail_activity_data_todo')
+ rec.activity_schedule(
+ activity_type_id=activity_type.id,
+ user_id=rec.create_uid.id,
+ note=f"{self.env.user.name} meminta approval untuk SJO '{rec.name}'.",
+ )
+
+ rec.message_post(
+ body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.create_uid.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Request Sent',
+ 'message': f"Request Approval telah dikirim ke {rec.create_uid.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+ def action_confirm_approval(self):
+ for rec in self:
+ if rec.create_uid != self.env.user:
+ raise UserError("❌ Hanya Pembuat Sourcing Job ini yang dapat Confirm Approval.")
+ rec.approval_sales = 'approve'
+ rec.state = 'done'
+
+ rec.activity_feedback(['mail.mail_activity_data_todo'])
+
+ rec.message_post(
+ body=f"Sourcing Job disetujui oleh <b>{self.env.user.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ rec.activity_schedule(
+ 'mail.mail_activity_data_todo',
+ user_id=rec.user_id.id,
+ note=f"✅ Sourcing Job <b>{rec.name}</b> telah disetujui oleh {self.env.user.name}.",
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Approval Confirmed',
+ 'message': f"Sourcing Job '{rec.name}' telah disetujui dan dikirim ke {rec.user_id.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+
class SourcingJobOrderLine(models.Model):
_name = 'sourcing.job.order.line'
_description = 'Sourcing Job Order Line'
@@ -293,15 +498,19 @@ class SourcingJobOrderLine(models.Model):
product_name = fields.Char(string='Nama Barang', required=True)
code = fields.Char(string='SKU')
descriptions = fields.Text(string='Deskripsi / Spesifikasi')
+ reason = fields.Text(string='Reason Unavailable')
+ sla = fields.Char(string='SLA Product')
quantity = fields.Float(string='Quantity Product', required=True)
price = fields.Float(string='Purchase Price')
tax_id = fields.Many2one('account.tax', string='Tax', domain=[('active', '=', True)])
vendor_id = fields.Many2one('res.partner', string="Vendor")
product_category = fields.Many2one('product.category', string="Product Category")
+ product_class = fields.Many2one('product.public.category', string="Categories")
state = fields.Selection([
('draft', 'Unsource'),
('sourcing', 'On Sourcing'),
('done', 'Done Sourcing'),
+ ('convert', 'Converted'),
('cancel', 'Unavailable')
], default='draft', tracking=True)
product_type = fields.Selection([
@@ -324,12 +533,18 @@ class SourcingJobOrderLine(models.Model):
subtotal += subtotal * (line.tax_id.amount / 100)
line.subtotal = subtotal
- @api.constrains('product_type', 'product_category')
+ @api.constrains('product_type', 'product_category', 'product_class', 'code')
def _check_required_fields_for_md(self):
for rec in self:
is_md = self.env.user.has_group('indoteknik_custom.group_role_merchandiser')
- if is_md and (not rec.product_type or not rec.product_category):
- raise ValidationError("MD wajib mengisi Product Type dan Product Category!")
+ if is_md and (not rec.product_type or not rec.product_category or not rec.product_class or not rec.code):
+ raise UserError("MD wajib mengisi SKU, Product Type, Product Category, dan Categories!")
+
+ @api.depends('state')
+ def _check_unavailable_line(self):
+ for rec in self:
+ if rec.state == 'cancel' and not rec.reason:
+ raise UserError("Isi Reason Unavailable")
@api.depends('price', 'vendor_id', 'order_id')
def _compute_show_for_sales(self):
@@ -337,3 +552,47 @@ class SourcingJobOrderLine(models.Model):
rec.show_for_sales = bool(
rec.order_id and rec.price not in (None, 0) and rec.vendor_id
)
+
+ def action_convert_to_product(self):
+ type_map = {
+ 'servis': 'service',
+ 'product': 'product',
+ 'consu': 'consu',
+ }
+
+ for rec in self:
+ if rec.order_id.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person SJO ini yang dapat convert Line ini ke product.")
+ exsisting = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1)
+
+ if exsisting:
+ raise UserError(f"⚠️ Produk dengan Internal Reference '{rec.code}' sudah ada di sistem.")
+
+ product = self.env['product.product'].create({
+ 'name': rec.product_name,
+ 'default_code': rec.code or False,
+ 'description': rec.descriptions or '',
+ 'categ_id': rec.product_category.id,
+ 'type': type_map.get(rec.product_type, 'product'),
+ })
+
+ rec.state = 'convert'
+ return True
+
+ @api.onchange('code')
+ def _oncange_code(self):
+ for rec in self:
+ if not rec.code:
+ continue
+
+ product = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1)
+ if not product:
+ return
+
+ rec.product_name = product.name or rec.product_name
+
+ pricelist = self.env['purchase.pricelist'].search([('product_id', '=', product.id), ('is_winner', '=', True)], limit=1)
+ if pricelist or tax or vendor:
+ rec.vendor_id = pricelist.vendor_id.id or False
+ rec.price = pricelist.include_price or 0.0
+ rec.tax_id = pricelist.taxes_product_id.id or pricelist.taxes_system_id.id or False \ No newline at end of file
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index 1da2befe..8f8ba66f 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -186,6 +186,56 @@ 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=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=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'
+
+ picking = line.picking_id
+ if not picking or picking.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 total_qty == 0:
+ continue
+
+ if done_qty == 0:
+ line.delivery_status = 'none'
+ elif done_qty > 0:
+ has_other_out = self.env['stock.picking'].search_count([
+ ('group_id', '=', picking.group_id.id),
+ ('name', 'ilike', 'BU/OUT'),
+ ('id', '!=', picking.id),
+ ('state', '=', 'done'),
+ ])
+ if has_other_out and done_qty == total_qty:
+ line.delivery_status = 'partial_final'
+ elif not has_other_out and done_qty >= total_qty:
+ line.delivery_status = 'full'
+ elif has_other_out and done_qty < total_qty:
+ line.delivery_status = 'partial'
+ elif done_qty < total_qty:
+ line.delivery_status = 'partial'
+ else:
+ line.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 d6096cc0..7f8523a3 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -177,6 +177,97 @@ 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)
+
+ @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
+
+ 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):
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index cb630a04..99a74505 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
@@ -498,8 +498,8 @@ class TukarGuling(models.Model):
('state', '!=', 'cancel'),
], limit=1)
- if existing_tukar_guling:
- raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name)
+ # if existing_tukar_guling:
+ # raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name)
picking = self.operations
if picking.picking_type_id.id == 30 and self.return_type == 'tukar_guling':
raise UserError("❌ BU/PICK tidak boleh di retur tukar guling")
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index b9934d7a..bbcbac84 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -190,7 +190,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
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index ba86277a..9df03674 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"/>
diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml
new file mode 100644
index 00000000..2a8e1318
--- /dev/null
+++ b/indoteknik_custom/views/advance_payment_request.xml
@@ -0,0 +1,290 @@
+<?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">
+ <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')], 'invisible': [('type_request', '=', 'reimburse')]}"/>
+ <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="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>
+ </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..1a9d7908
--- /dev/null
+++ b/indoteknik_custom/views/advance_payment_settlement.xml
@@ -0,0 +1,184 @@
+<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">
+ <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" required="1"/>
+ <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/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index 5ea7324b..543f1fd1 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -226,6 +226,27 @@
<field name="number_next_actual">1</field>
<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="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="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>
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/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 82daa36f..a540caa7 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -135,6 +135,7 @@
<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" />
diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml
index f9f8f386..3965c62f 100644
--- a/indoteknik_custom/views/sourcing.xml
+++ b/indoteknik_custom/views/sourcing.xml
@@ -1,10 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
+ <record id="view_sourcing_job_order_search" model="ir.ui.view">
+ <field name="name">sourcing.job.order.search</field>
+ <field name="model">sourcing.job.order</field>
+ <field name="arch" type="xml">
+ <search string="Search Sourcing Job Order">
+ <field name="state" string="Name"/>
+ <field name="user_id" string="MD Person"/>
+ <filter name="my_job"
+ string="My Sourcing Job"
+ domain="[('user_id', '=', uid), ('state', '=', 'taken')]"/>
+ <filter name="untaken"
+ string="Untaken"
+ domain="[('state', '=', 'draft')]" />
+ <filter name="done"
+ string="Complete"
+ domain="[('state', '=', 'done')]" />
+ </search>
+ </field>
+ </record>
<record id="view_sourcing_job_order_tree" model="ir.ui.view">
<field name="name">sourcing.job.order.tree</field>
<field name="model">sourcing.job.order</field>
<field name="arch" type="xml">
- <tree string="Sourcing Job Orders"
+ <tree string="Sourcing Job Orders" default_order="state asc, create_date desc"
decoration-success="state=='done'"
decoration-danger="state=='cancel'"
decoration-warning="state=='taken'">
@@ -38,6 +57,28 @@
icon="fa-times"
groups="indoteknik_custom.group_role_merchandiser"
attrs="{'invisible': [('state', 'in', ['cancel', 'done'])]}"/>
+
+ <button name="action_ask_approval"
+ string="Ask Approval"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': ['|','|', ('has_price_in_lines', '=', False), ('approval_sales', '=', 'approve'), ('is_creator_same_user', '=', True)]}"/>
+
+ <button name="action_confirm_by_md"
+ string="Confirm"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('is_creator_same_user', '=', False)]}"/>
+
+ <button name="action_confirm_approval"
+ string="Approve"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_sales"
+ attrs="{'invisible': [('approval_sales', 'in', [False, 'approve'])]}"/>
+
<button name="action_request_takeover"
string="Request Takeover"
type="object"
@@ -61,10 +102,19 @@
<field name="state" widget="statusbar"
statusbar_visible="draft,taken,done,cancel"
- statusbar_colors='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/>
+ statusbar_color='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/>
</header>
<sheet>
+ <widget name="web_ribbon"
+ title="COMPLETE"
+ bg_color="bg-success"
+ attrs="{'invisible': [('state', '!=', 'done')]}"/>
+
+ <widget name="web_ribbon"
+ title="CANCEL"
+ bg_color="bg-danger"
+ attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
<h1>
<field name="name" readonly="1"/>
</h1>
@@ -73,6 +123,7 @@
<group>
<!-- <field name="leads_id"/> -->
<field name="eta_sales"/>
+ <field name="is_creator_same_user" invisible="1"/>
<field name="takeover_request" invisible="1"/>
<field name="can_request_takeover" invisible="1"/>
<field name="can_approve_takeover" invisible="1"/>
@@ -82,6 +133,7 @@
<group>
<field name="create_uid" readonly="1" widget="many2one_avatar_user"/>
<field name="user_id" readonly="1" widget="many2one_avatar_user"/>
+ <field name="approval_sales" readonly="1"/>
</group>
</group>
@@ -99,7 +151,10 @@
<!-- MD EDIT -->
<page string="MD Lines" groups="indoteknik_custom.group_role_merchandiser">
<field name="line_md_edit_ids">
- <tree editable="bottom">
+ <tree editable="bottom"
+ decoration-success="state in ('done', 'convert')"
+ decoration-danger="state=='cancel'"
+ decoration-warning="state=='sourcing'">
<field name="code"/>
<field name="product_name"/>
<field name="descriptions"/>
@@ -108,6 +163,16 @@
<field name="vendor_id"/>
<field name="tax_id"/>
<field name="subtotal"/>
+ <field name="product_category"/>
+ <field name="product_type"/>
+ <field name="product_class"/>
+ <field name="state"/>
+ <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ <button name="action_convert_to_product"
+ string="Convert"
+ type="object"
+ icon="fa-exchange"
+ attrs="{'invisible': [('state', '!=', 'done')]}"/>
</tree>
</field>
</page>
@@ -124,6 +189,7 @@
<field name="vendor_id" readonly="1"/>
<field name="tax_id" readonly="1"/>
<field name="subtotal" readonly="1"/>
+ <field name="state" readonly="1"/>
</tree>
</field>
</page>
@@ -162,6 +228,8 @@
<field name="name">Sourcing Job Orders</field>
<field name="res_model">sourcing.job.order</field>
<field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="view_sourcing_job_order_search"/>
+ <field name="context">{'search_default_untaken': 1, 'search_default_my_job': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Buat Sourcing Job Order baru di sini ✨
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 44ab6355..050fc819 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -226,6 +226,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"/>
@@ -394,6 +398,8 @@
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'}}"/>
</tree>
</field>
</record>