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