diff options
25 files changed, 2770 insertions, 137 deletions
diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py index e97a7ff8..2f546078 100644 --- a/indoteknik_api/controllers/api_v1/product.py +++ b/indoteknik_api/controllers/api_v1/product.py @@ -103,17 +103,21 @@ class Product(controller.Controller): @controller.Controller.must_authorized() def get_product_template_stock_by_id(self, **kw): id = int(kw.get('id')) - date_7_days_ago = datetime.now() - timedelta(days=7) + # date_7_days_ago = datetime.now() - timedelta(days=7) + product = request.env['product.product'].search([('id', '=', id)], limit=1) + if not product: + return self.response({'qty': 0, 'sla_date': 'N/A'}, headers=[('Cache-Control', 'max-age=600, private')]) + product_pruchase = request.env['purchase.pricelist'].search([ ('product_id', '=', id), ('is_winner', '=', True) - ]) - stock_vendor = request.env['stock.vendor'].search([ - ('product_variant_id', '=', id), - ('write_date', '>=', date_7_days_ago.strftime("%Y-%m-%d %H:%M:%S")) ], limit=1) + # stock_vendor = request.env['stock.vendor'].search([ + # ('product_variant_id', '=', id), + # ('write_date', '>=', date_7_days_ago.strftime("%Y-%m-%d %H:%M:%S")) + # ], limit=1) - product = product_pruchase.product_id + # product = product_pruchase.product_id vendor_sla = request.env['vendor.sla'].search([('id_vendor', '=', product_pruchase.vendor_id.id)], limit=1) slatime = 15 @@ -132,42 +136,45 @@ class Product(controller.Controller): if qty_available < 1 : qty_available = 0 - qty = 0 + # qty = 0 + qty = qty_available sla_date = f'{slatime} Hari' + if qty_available > 0: + sla_date = '1 Hari' # Qty Stock Vendor - qty_vendor = stock_vendor.quantity - qty_vendor -= int(qty_vendor * 0.1) - qty_vendor = math.ceil(float(qty_vendor)) - total_excell = qty_vendor - - is_altama_product = product.x_manufacture.id in [10, 122, 89] - if is_altama_product: - try: - # Qty Altama - qty_altama = request.env['product.template'].get_stock_altama( - product.default_code) - qty_altama -= int(qty_altama * 0.1) - qty_altama = math.ceil(float(qty_altama)) - total_adem = qty_altama - - if qty_available > 0: - qty = qty_available + total_adem + total_excell - sla_date = '1 Hari' - elif qty_altama > 0 or qty_vendor > 0: - qty = total_adem if qty_altama > 0 else total_excell - sla_date = f'{slatime} Hari' - else: - sla_date = f'{slatime} Hari' - except: - print('error') - else: - if qty_available > 0: - qty = qty_available - sla_date = f'1 Hari' - elif qty_vendor > 0: - qty = total_excell - sla_date = f'{slatime} Hari' + # qty_vendor = stock_vendor.quantity + # qty_vendor -= int(qty_vendor * 0.1) + # qty_vendor = math.ceil(float(qty_vendor)) + # total_excell = qty_vendor + + # is_altama_product = product.x_manufacture.id in [10, 122, 89] + # if is_altama_product: + # try: + # # Qty Altama + # qty_altama = request.env['product.template'].get_stock_altama( + # product.default_code) + # qty_altama -= int(qty_altama * 0.1) + # qty_altama = math.ceil(float(qty_altama)) + # total_adem = qty_altama + + # if qty_available > 0: + # qty = qty_available + total_adem + total_excell + # sla_date = '1 Hari' + # elif qty_altama > 0 or qty_vendor > 0: + # qty = total_adem if qty_altama > 0 else total_excell + # sla_date = f'{slatime} Hari' + # else: + # sla_date = f'{slatime} Hari' + # except: + # print('error') + # else: + # if qty_available > 0: + # qty = qty_available + # sla_date = f'1 Hari' + # elif qty_vendor > 0: + # qty = total_excell + # sla_date = f'{slatime} Hari' data = { 'qty': qty, @@ -245,6 +252,8 @@ class Product(controller.Controller): [('id', '=', id)], limit=1) qty_available = product.qty_free_bandengan + if qty_available < 1: + qty_available = 0 data = { 'qty': qty_available, diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 392e848d..961e6a94 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -175,6 +175,9 @@ 'views/stock_inventory.xml', 'views/sale_order_delay.xml', 'views/refund_sale_order.xml', + 'views/advance_payment_request.xml', + 'views/advance_payment_settlement.xml', + # 'views/refund_sale_order.xml', 'views/tukar_guling.xml', # 'views/tukar_guling_return_views.xml' 'views/tukar_guling_po.xml', @@ -183,6 +186,7 @@ 'views/unpaid_invoice_view.xml', 'views/letter_receivable.xml', 'views/letter_receivable_mail_template.xml', + 'views/mail_template_pum.xml', # 'views/reimburse.xml', 'views/sj_tele.xml', 'views/close_tempo_mail_template.xml', diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 36a992d2..7f946b57 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -154,6 +154,7 @@ from . import approval_invoice_date from . import approval_payment_term from . import refund_sale_order # from . import patch +from . import advance_payment_request from . import tukar_guling from . import tukar_guling_po from . import update_date_planned_po_wizard diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 44b3cb76..684ef335 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -106,6 +106,7 @@ class AccountMove(models.Model): help="Tanggal janji bayar dari customer setelah reminder dikirim.", tracking=True ) + internal_notes_contact = fields.Text(related='partner_id.comment', string="Internal Notes", readonly=True, help="Internal Notes dari contact utama customer.") # def _check_and_lock_cbd(self): # cbd_term = self.env['account.payment.term'].browse(26) diff --git a/indoteknik_custom/models/advance_payment_request.py b/indoteknik_custom/models/advance_payment_request.py new file mode 100644 index 00000000..5a465ca4 --- /dev/null +++ b/indoteknik_custom/models/advance_payment_request.py @@ -0,0 +1,1544 @@ +from odoo import models, api, fields, _ +from odoo.exceptions import UserError, ValidationError +from datetime import date, datetime, timedelta +# import datetime +import logging +_logger = logging.getLogger(__name__) +from terbilang import Terbilang +import pytz +from pytz import timezone +import base64 + + +class AdvancePaymentRequest(models.Model): + _name = 'advance.payment.request' + _description = 'Advance Payment Request or Reimburse' + _rec_name = 'number' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'create_date desc' + + user_id = fields.Many2one('res.users', string='Diajukan Oleh', default=lambda self: self.env.user, tracking=3) + partner_id = fields.Many2one('res.partner', string='Partner', related='user_id.partner_id', readonly=True) + + number = fields.Char(string='No. Dokumen', default='New Draft', tracking=3) + + applicant_name = fields.Many2one('res.users', string='Nama Pemohon', default=lambda self: self.env.user, required=True, tracking=3, domain="[('groups_id', 'in', [1])]") + # applicant_name = fields.One2many(string='Nama Pemohon', related='res.users') + nominal = fields.Float(string='Nominal', tracking=3, required=True) + + bank_name = fields.Char(string='Bank', tracking=3, required=True) + account_name = fields.Many2one('res.users', string='Nama Account', default=lambda self: self.env.user, required=True, tracking=3, domain="[('groups_id', 'in', [1])]") + # account_name = fields.Char(string='Nama Account', tracking=3, required=True) + bank_account = fields.Char(string='No. Rekening', tracking=3, required=True) + detail_note = fields.Text(string='Keterangan Penggunaan Rinci', tracking=3) + + date_back_to_office = fields.Date( + string='Tanggal Kembali ke Kantor', + tracking=3 + ) + + estimated_return_date = fields.Date( + string='Batas Pengajuan', + help='Tanggal batas maksimal durasi pengajuan realisasi' + ) + + days_remaining = fields.Integer( + string='Sisa Hari Realisasi', + compute='_compute_days_remaining', + help='Sisa hari batas maksimal pengajuan realisasi setelah kembali ke kantor. ' + '7 hari setelah tanggal kembali.' + ) + + status = fields.Selection([ + ('draft', 'Draft'), + ('pengajuan1', 'Menunggu Approval Departement'), + ('pengajuan2', 'Menunggu Approval AP'), + ('pengajuan3', 'Menunggu Approval Pimpinan'), + ('approved', 'Approved'), + ], string='Status', default='draft', tracking=3, index=True, track_visibility='onchange') + + + status_pay_down_payment = fields.Selection([ + ('pending', 'Pending'), + ('payment', 'Payment'), + ], string='Status Pembayaran', default='pending', tracking=3) + + name_approval_departement = fields.Char(string='Approval Departement') + name_approval_ap = fields.Char(string='Approval AP') + email_ap = fields.Char(string = 'Email AP') + name_approval_pimpinan = fields.Char(string='Approval Pimpinan') + + date_approved_department = fields.Datetime(string="Date Approved Department") + date_approved_ap = fields.Datetime(string="Date Approved AP") + date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan") + + position_department = fields.Char(string='Position Departement') + position_ap = fields.Char(string='Position AP') + position_pimpinan = fields.Char(string='Position Pimpinan') + + approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') + + departement_type = fields.Selection([ + ('sales', 'Sales'), + ('merchandiser', 'Merchandiser'), + ('marketing', 'Marketing'), + ('logistic', 'Logistic'), + ('procurement', 'Procurement'), + ('fat', 'FAT'), + ('it', 'IT'), + ('hr_ga', 'HR & GA'), + ('pimpinan', 'Pimpinan') + ], string='Departement Type', tracking=3, required=True) + + attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') + attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') + attachment_filename_image = fields.Char(string='Filename Image') + attachment_filename_pdf = fields.Char(string='Filename PDF') + + attachment_type = fields.Selection([ + ('pdf', 'PDF'), + ('image', 'Image'), + ], string="Attachment Type") + + move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')]) + is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible') + + + currency_id = fields.Many2one( + 'res.currency', string='Currency', + default=lambda self: self.env.company.currency_id + ) + + type_request = fields.Selection([ + ('pum', 'PUM'), + ('reimburse', 'Reimburse')], string='Tipe Pengajuan', tracking=3) + + position_type = fields.Selection([ + ('staff', 'Staff'), + ('manager', 'Manager'), + ('pimpinan', 'Pimpinan')], string='Jabatan') + + settlement_type = fields.Selection([ + ('no_settlement', 'Belum Realisasi'), + ('settlement', 'Realisasi') + ]) + + is_represented = fields.Boolean(string='Nama Pemohon Berbeda?', default=False) + + apr_perjalanan = fields.Boolean(string = "PUM Perjalanan?", default = False) + reimburse_line_ids = fields.One2many('reimburse.line', 'request_id', string='Rincian Reimburse') + upload_attachment_date = fields.Datetime(string='Upload Attachment Date', tracking=3) + settlement_ids = fields.One2many( + 'advance.payment.settlement', + 'pum_id', + string='Realisasi' + ) + has_settlement = fields.Boolean( + string='Has Settlement', + compute='_compute_has_settlement' + ) + settlement_name = fields.Char( + string="Nama Realisasi", + compute='_compute_settlement_name' + ) + + grand_total_reimburse = fields.Monetary( + string='Total Reimburse', + compute='_compute_grand_total_reimburse', + currency_field='currency_id' + ) + + is_current_user_ap = fields.Boolean( + string="Is Current User AP", + compute='_compute_is_current_user_ap' + ) + + @api.onchange('grand_total_reimburse', 'type_request') + def _onchange_reimburse_line_update_nominal(self): + if self.type_request == 'reimburse': + self.nominal = self.grand_total_reimburse + + def _compute_is_current_user_ap(self): + ap_user_ids = [23, 9468] + is_ap = self.env.user.id in ap_user_ids + for line in self: + line.is_current_user_ap = is_ap + + @api.depends('reimburse_line_ids.total') + def _compute_grand_total_reimburse(self): + for request in self: + request.grand_total_reimburse = sum(request.reimburse_line_ids.mapped('total')) + + def action_open_create_reimburse_cab(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Buat Jurnal Reimburse', + 'res_model': 'create.reimburse.cab.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_request_id': self.id, + 'default_total_reimburse': self.grand_total_reimburse, + } + } + + @api.depends('settlement_ids') + def _compute_has_settlement(self): + for rec in self: + rec.has_settlement = bool(rec.settlement_ids) + + @api.depends('settlement_ids', 'settlement_ids.name') + def _compute_settlement_name(self): + for request in self: + if request.settlement_ids: + request.settlement_name = request.settlement_ids[0].name + else: + request.settlement_name = False + + @api.onchange('is_represented') + def _onchange_is_represented(self): + if self.is_represented: + self.account_name = False + self.applicant_name = False + else: + self.account_name = self.env.user.id + self.applicant_name = self.env.user.id + + @api.onchange('nominal') + def _onchange_nominal_no_minus(self): + if self.nominal and self.nominal < 0: + self.nominal = 0 + return { + 'warning': { + 'title': _('Nominal Tidak Valid'), + 'message': _( + "Nominal tidak boleh diisi minus.\n" + "Nilai di set menjadi nol." + ) + } + } + + def _get_jasper_attachment(self): + self.ensure_one() + report = self.env['ir.actions.report'].browse(1134) # ID Downpayment Report + if not report: + raise UserError("Report Jasper tidak ditemukan.") + + data = report.render_jasper(self.ids, data={})[0] + filename = f"{self.number}.pdf" + return { + 'name': filename, + 'datas': base64.b64encode(data), + 'type': 'binary', + 'mimetype': 'application/pdf', + 'filename': filename, + } + + # def action_send_pum_reminder(self): + # """ + # Kirim email reminder PUM otomatis. + # - PUM Perjalanan: + # - Hari H kembali ke kantor = template 'mail_template_pum_reminder_today' + # - H-2 dari due date = template 'mail_template_pum_reminder_h_2' + # - PUM Non-Perjalanan: + # - H-2 dari due date = template 'mail_template_pum_reminder_h_2' + # """ + # today = date.today() + + # # Penyesuaian 1: Cari semua PUM yang sudah disetujui (bukan draft/reject) + # # Kita tidak filter 'date_back_to_office' di sini lagi. + # pum_ids = self.search([ + # ('status', 'not in', ['draft', 'reject']), + # ('type_request', '=', 'pum') + # ]) + + # template_today = self.env.ref('indoteknik_custom.mail_template_pum_reminder_today', raise_if_not_found=False) + # template_h2 = self.env.ref('indoteknik_custom.mail_template_pum_reminder_h_2', raise_if_not_found=False) + + # if not template_today or not template_h2: + # _logger.warning("Salah satu template email PUM (today/h2) tidak ditemukan.") + # return + + # for pum in pum_ids: + # _logger.info(f"[REMINDER] Memproses PUM {pum.number}") + + # # Penyesuaian 2: Logika ini sudah benar (sesuai update kita sebelumnya) + # # Jika realisasi sudah dibuat, PUM tidak aktif lagi, lewati. + # realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) + # if realization: + # _logger.info(f"[REMINDER] Lewati PUM {pum.number}, realisasi sudah dibuat.") + # continue + + # if not pum.email_ap or not pum.user_id.partner_id.email: + # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} karena email_ap atau email user kosong.") + # continue + + # # Penyesuaian 3: Logika penentuan Due Date (Wajib) + # due_date = False + # base_date_for_today_check = False # Khusus PUM Perjalanan + + # if pum.apr_perjalanan: + # if pum.date_back_to_office: + # due_date = pum.date_back_to_office + timedelta(days=7) + # base_date_for_today_check = pum.date_back_to_office + # else: + # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} (perjalanan) karena tgl kembali kosong.") + # continue + # else: + # # Ini adalah PUM Non-Perjalanan + # if not pum.create_date: + # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} (non-perjalanan) karena create_date kosong.") + # continue + # base_date = pum.create_date.date() + # due_date = base_date + timedelta(days=7) + + # # Hitung sisa hari + # days_remaining = (due_date - today).days + + # # Penyesuaian 4: Tentukan template berdasarkan sisa hari + # template = False + # if pum.apr_perjalanan and base_date_for_today_check == today: + # # Hari H kembali ke kantor (HANYA PUM Perjalanan) + # template = template_today + # elif days_remaining == 2: + # # H-2 due date (Untuk SEMUA jenis PUM) + # template = template_h2 + # else: + # _logger.info(f"[REMINDER] Lewati PUM {pum.number}, hari ini bukan tgl pengingat (Sisa hari: {days_remaining}).") + # continue + + # # --- Sisanya (Generate attachment & kirim email) sudah aman --- + + # # Generate attachment + # try: + # attachment_vals = pum._get_jasper_attachment() + # attachment = self.env['ir.attachment'].create({ + # 'name': attachment_vals['name'], + # 'type': 'binary', + # 'datas': attachment_vals['datas'], + # 'res_model': 'advance.payment.request', + # 'res_id': pum.id, + # 'mimetype': 'application/pdf', + # }) + # except Exception as e: + # _logger.error(f"[REMINDER] Gagal membuat attachment untuk PUM {pum.number}: {str(e)}") + # continue + + # email_values = { + # # 'email_to': pum.user_id.partner_id.email, + # 'email_to': 'andrifebriyadiputra@gmail.com', # Masih hardcode + # 'email_from': pum.email_ap, + # 'email_from': 'finance@indoteknik.co.id', + # 'attachment_ids': [(6, 0, [attachment.id])] + # } + + # _logger.info(f"[REMINDER] Mengirim email PUM {pum.number} ke {email_values['email_to']} dari {email_values['email_from']}") + + # try: + # body_html = template._render_field('body_html', [pum.id])[pum.id] + # template.send_mail(pum.id, force_send=True, email_values=email_values) + # _logger.info(f"[REMINDER] Email berhasil dikirim untuk PUM {pum.number}") + + # # Post info sederhana + # pum.message_post( + # body="Email Reminder Berhasil dikirimkan", + # message_type="comment", + # subtype_xmlid="mail.mt_note", + # ) + + # user_system = self.env['res.users'].browse(25) + # system_id = user_system.partner_id.id if user_system else False + + # # Post isi email ke chatter + # pum.message_post( + # body=body_html, + # message_type="comment", + # subtype_xmlid="mail.mt_note", + # author_id=system_id, + # ) + # except Exception as e: + # _logger.error(f"[REMINDER] Gagal mengirim email untuk PUM {pum.number}: {str(e)}") + + # return True + + + @api.depends('move_id.state') + def _compute_is_cab_visible(self): + for rec in self: + move = rec.move_id + rec.is_cab_visible = bool(move and move.state == 'posted') + + def action_view_journal_uangmuka(self): + self.ensure_one() + + ap_user_ids = [23, 9468] + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') + + if not self.move_id: + raise UserError("Journal Uang Muka belum tersedia.") + + return { + 'name': 'Journal Entry', + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': self.move_id.id, + 'target': 'current', + } + + @api.onchange('attachment_type') + def _onchange_attachment_type(self): + self.attachment_file_image = False + self.attachment_filename_image = False + self.attachment_file_pdf = False + self.attachment_filename_pdf = False + + # Sales & MD : Darren ID 19 + # Marketing : Iwan ID 216 + # Logistic & Procurement : Rafly H ID 21 + # FAT & IT : Stephan ID 28 + # HR & GA : Akbar ID 7 / Pimpinan + # --------------------------------------- + # AP : Manzila (Finance) ID 23 + + def _get_approver_mapping(self): + return { + 'sales': 19, + 'merchandiser': 19, + 'marketing': 216, + 'logistic': 21, + 'procurement': 21, + 'fat': 28, + 'it': 28, + 'hr_ga': 7, + } + + def _get_departement_approver(self): + mapping = self._get_approver_mapping() + return mapping.get(self.departement_type) + + @api.constrains('apr_perjalanan', 'date_back_to_office') + def _check_date_back_to_office(self): + if self.apr_perjalanan and not self.date_back_to_office: + raise ValidationError("Tanggal Kembali ke Kantor wajib diisi jika PUM Perjalanan dicentang.") + + @api.onchange('applicant_name') + def _onchange_applicant_name_set_position(self): + if self.applicant_name: + user_id = self.applicant_name.id + mapping = self._get_approver_mapping() + manager_ids = set(mapping.values()) + pimpinan_id = 7 + if user_id == pimpinan_id: + self.position_type = 'pimpinan' + elif user_id in manager_ids: + self.position_type = 'manager' + else: + self.position_type = 'staff' + else: + self.position_type = False + + # @api.model + # def default_get(self, fields_list): + # defaults = super(AdvancePaymentRequest, self).default_get(fields_list) + # user_id = defaults.get('user_id', self.env.uid) + # mapping = self._get_approver_mapping() + # manager_ids = set(mapping.values()) + # pimpinan_id = 7 + + # position = 'staff' + # if user_id == pimpinan_id: + # position = 'pimpinan' + # elif user_id in manager_ids: + # position = 'manager' + + # defaults['position_type'] = position + # return defaults + + def action_realisasi_pum(self): + self.ensure_one() + + realization = self.env['advance.payment.settlement'].search([('pum_id', '=', self.id)], limit=1) + + if realization: + return { + 'type': 'ir.actions.act_window', + 'name': 'Realisasi PUM', + 'res_model': 'advance.payment.settlement', + 'view_mode': 'form', + 'target': 'current', + 'res_id': realization.id, + } + else: + return { + 'type': 'ir.actions.act_window', + 'name': 'Realisasi PUM', + 'res_model': 'advance.payment.settlement', + 'view_mode': 'form', + 'target': 'current', + 'context': { + 'default_pum_id': self.id, + # 'default_value_down_payment': self.nominal, + 'default_name': f'Realisasi - {self.number or ""}', + # 'default_pemberian_line_ids': [ + # (0, 0, { + # 'date': self.create_date.date() if self.create_date else fields.Date.today(), + # 'description': 'Uang Muka', + # 'value': self.nominal + # }) + # ] + } + } + + + def action_confirm_payment(self): + # jakarta_tz = pytz.timezone('Asia/Jakarta') + # now = datetime.now(jakarta_tz).replace(tzinfo=None) + + ap_user_ids = [23, 9468] + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') + + for rec in self: + if not rec.attachment_file_image and not rec.attachment_file_pdf: + raise UserError( + f'Tidak bisa konfirmasi pembayaran PUM {rec.name or ""} ' + f'karena belum ada bukti attachment (PDF/Image).' + ) + + rec.status_pay_down_payment = 'payment' + rec.upload_attachment_date = datetime.utcnow() + + rec.message_post( + body="Bukti transfer telah di upload oleh <b>Finance AP</b>.", + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + + + # def action_approval_check(self): + # for record in self: + # # user = record.user_id + # user = self.env['res.users'].browse(3401) + # roles = sorted(set( + # f"{group + # .name} (Category: {group.category_id.name})" + # for group in user.groups_id + # if group.category_id.name == 'Roles' + # )) + # _logger.info(f"[ROLE CHECK] User: {user.name} (Login: {user.login}) Roles: {roles}") + # return + + def action_approval_check(self): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz).replace(tzinfo=None) + formatted_date = now.strftime('%d %B %Y %H:%M') + + for rec in self: + if not rec.departement_type: + raise UserError("Field 'departement_type' wajib diisi sebelum approval.") + + approver_id = rec._get_departement_approver() + + if rec.status == 'pengajuan1': + if self.env.user.id != approver_id: + raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") + rec.name_approval_departement = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement + rec.date_approved_department = now + + # Mapping posisi berdasarkan departement_type + department_titles = { + 'sales': 'Sales Manager', + 'merchandiser': 'Merchandiser Manager', + 'marketing': 'Marketing Manager', + 'logistic': 'Logistic Manager', + 'procurement': 'Procurement Manager', + 'fat': 'Finance & Accounting Manager', + 'it': 'IT Manager', + 'hr_ga': 'HR & GA Manager', + 'pimpinan': 'Pimpinan', + } + rec.position_department = department_titles.get(rec.departement_type, 'Departement Manager') + + rec.status = 'pengajuan2' + + rec.message_post( + body=f"Approval <b>Departement</b> oleh <b>{self.env.user.name}</b> " + f"pada <i>{formatted_date}</i>." + ) + + elif rec.status == 'pengajuan2': + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + if self.env.user.id not in ap_user_ids: + raise UserError("Hanya AP yang berhak menyetujui tahap ini.") + rec.name_approval_ap = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap + rec.email_ap = self.env.user.email + rec.date_approved_ap = now + rec.position_ap = 'Finance AP' + if rec.position_type == 'pimpinan': + rec.status = 'approved' + else: + rec.status = 'pengajuan3' + + rec.message_post( + body=f"Approval <b>AP</b> oleh <b>{self.env.user.name}</b> " + f"pada <i>{formatted_date}</i>." + ) + + elif rec.status == 'pengajuan3': + if self.env.user.id != 7: # ID user Pimpinan + raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + rec.name_approval_pimpinan = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan + rec.date_approved_pimpinan = now + rec.position_pimpinan = 'Pimpinan' + rec.status = 'approved' + + rec.message_post( + body=f"Approval <b>Pimpinan</b> oleh <b>{self.env.user.name}</b> " + f"pada <i>{formatted_date}</i>." + ) + + else: + raise UserError("Status saat ini tidak bisa di-approve lagi.") + + # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap <b>{rec.status}</b>.") + + + + def action_ap_only(self): + self.ensure_one() + + ap_user_ids = [23, 9468] # Ganti sesuai kebutuhan + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') + + if self.move_id: + raise UserError('CAB / Jurnal sudah pernah dibuat untuk PUM ini.') + + return { + 'name': 'Create CAB AP Only', + 'type': 'ir.actions.act_window', + 'res_model': 'advance.payment.create.bill', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_nominal': self.nominal, + 'default_apr_id': self.id, + } + } + + + @api.depends('date_back_to_office', 'status', 'apr_perjalanan', 'create_date', 'settlement_ids.status', 'type_request') + def _compute_days_remaining(self): + today = date.today() + for rec in self: + + current_days = rec.days_remaining or 0 + current_due_date = rec.estimated_return_date or False + if rec.type_request == 'pum': + is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids) + if not is_settlement_approved: + due_date = False + + if rec.apr_perjalanan: + # Alur PUM Perjalanan + if rec.date_back_to_office: + due_date = rec.date_back_to_office + timedelta(days=7) + effective_today = max(today, rec.date_back_to_office) + + current_due_date = due_date + current_days = (due_date - effective_today).days + else: + + current_due_date = False + current_days = 0 + else: + # Alur PUM Non-Perjalanan + if rec.create_date: + base_date = rec.create_date.date() + due_date = base_date + timedelta(days=7) + + current_due_date = due_date + current_days = (due_date - today).days + else: + current_due_date = False + current_days = 0 + else: + current_due_date = False + current_days = 0 + + rec.days_remaining = current_days + rec.estimated_return_date = current_due_date + + @api.onchange('date_back_to_office') + def _onchange_date_back_to_office(self): + if self.date_back_to_office and self.date_back_to_office < date.today(): + return { + 'warning': { + 'title': _('Tanggal Tidak Valid'), + 'message': _('Tanggal kembali ke kantor tidak boleh lebih awal dari hari ini.') + } + } + + @api.onchange('applicant_name') + def _onchange_applicant_name(self): + if self.applicant_name: + self.account_name = self.applicant_name.id + + @api.onchange('account_name') + def _onchange_account_name(self): + if self.account_name: + self.applicant_name = self.account_name.id + + @api.onchange('user_id') + def _onchange_user_id_limit_check(self): + if self.type_request != 'pum': + return + if not self.user_id: + return + + pum_ids = self.search([ + ('user_id', '=', self.user_id.id), + ('status', '!=', 'reject'), + ('type_request', '=', 'pum') + ]) + + active_pum_count = 0 + for pum in pum_ids: + realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) + if not realization: + active_pum_count += 1 + + if active_pum_count >= 2: + return { + 'warning': { + 'title': 'Batas Pengajuan Tercapai', + 'message': 'User ini sudah memiliki 2 PUM aktif. Tidak dapat mengajukan lagi sampai salah satu direalisasi.', + } + } + + def _get_department_titles_mapping(self): + return { + 'sales': 'Sales Manager', + 'merchandiser': 'Merchandiser Manager', + 'marketing': 'Marketing Manager', + 'logistic': 'Logistic Manager', + 'procurement': 'Procurement Manager', + 'fat': 'Finance & Accounting Manager', + 'it': 'IT Manager', + 'hr_ga': 'HR & GA Manager', + } + + @api.model + def create(self, vals): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz).replace(tzinfo=None) + user = self.env.user + + pum_ids = self.search([ + ('user_id', '=', user.id), + ('status', '!=', 'reject'), + ('type_request', '=', 'pum') + ]) + + active_pum_count = 0 + for pum in pum_ids: + realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) + if not realization: + active_pum_count += 1 + + if active_pum_count >= 2 and vals.get('type_request') == 'pum': + raise UserError("Anda hanya dapat mengajukan maksimal 2 PUM aktif. Silakan realisasikan salah satunya terlebih dahulu.") + + if not vals.get('apr_perjalanan'): + if 'estimated_return_date' not in vals: + today = date.today() + due_date = today + timedelta(days=7) + vals['estimated_return_date'] = due_date + + initial_status = '' + position = vals.get('position_type') + department = vals.get('departement_type') + if department == 'hr_ga' or position in ('manager', 'pimpinan'): + initial_status = 'pengajuan2' + else: + initial_status = 'pengajuan1' + + vals['status'] = initial_status + + if initial_status == 'pengajuan2' and department != 'hr_ga': + applicant_name = vals.get('applicant_name') + vals['name_approval_departement'] = self.env['res.users'].browse(applicant_name).name or '' + vals['date_approved_department'] = now + department_type = vals.get('departement_type') + department_titles = self._get_department_titles_mapping() + vals['position_department'] = department_titles.get(department_type, 'Departement Manager') + + if position == 'pimpinan' and department != 'hr_ga': + vals['name_approval_pimpinan'] = self.env['res.users'].browse(vals.get('applicant_name')).name or '' + vals['position_pimpinan'] = 'Pimpinan' + vals['date_approved_pimpinan'] = now + # if position == 'staff': + # initial_status = 'pengajuan1' + # elif position == 'manager': + # initial_status = 'pengajuan2' + # applicant_name = vals.get('applicant_name') + # vals['name_approval_departement'] = self.env['res.users'].browse(applicant_name).name or '' + # vals['date_approved_department'] = now + # department_type = vals.get('departement_type') + # department_titles = self._get_department_titles_mapping() + # vals['position_department'] = department_titles.get(department_type, 'Departement Manager') + # elif position == 'pimpinan': + # initial_status = 'pengajuan2' + # applicant_name = vals.get('applicant_name') + # vals['name_approval_pimpinan'] = self.env['res.users'].browse(applicant_name).name or '' + # vals['position_pimpinan'] = 'Pimpinan' + # vals['date_approved_pimpinan'] = now + + # vals['status'] = initial_status + + if not vals.get('number') or vals['number'] == 'New Draft': + if vals.get('type_request') == 'reimburse': + vals['number'] = self.env['ir.sequence'].next_by_code('reimburse.request') or 'New Draft' + else: + vals['number'] = self.env['ir.sequence'].next_by_code('advance.payment.request') or 'New Draft' + + # vals['status'] = 'pengajuan1' + # return super(AdvancePaymentRequest, self).create(vals) + rec = super(AdvancePaymentRequest, self).create(vals) + if rec.type_request == 'reimburse': + rec._compute_grand_total_reimburse() + rec.nominal = rec.grand_total_reimburse + return rec + + +class AdvancePaymentUsageLine(models.Model): + _name = 'advance.payment.usage.line' + _description = 'Advance Payment Usage Line' + + realization_id = fields.Many2one('advance.payment.settlement', string='Realization') + date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) + description = fields.Text(string='Description', required=True) + nominal = fields.Float(string='Nominal', required=True) + done_attachment = fields.Boolean(string='Checked', default=False) + + lot_of_attachment = fields.Selection( + related='realization_id.lot_of_attachment', + string='Lot of Attachment (Related)', + store=False + ) + + attachment_type = fields.Selection([ + ('pdf', 'PDF'), + ('image', 'Image'), + ], string="Attachment Type", default='pdf') + + attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') + attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') + attachment_filename_image = fields.Char(string='Filename Image') + attachment_filename_pdf = fields.Char(string='Filename PDF') + + account_id = fields.Many2one( + 'account.account', string='Jenis Biaya', tracking=3, + domain="[('id', 'in', [484, 486, 488, 506, 507, 625, 471, 519, 527, 528, 529, 530, 565])]" # ID Jenis Biaya yang dibutuhkan + ) + + is_current_user_ap = fields.Boolean( + string="Is Current User AP", + compute='_compute_is_current_user_ap' + ) + + def _compute_is_current_user_ap(self): + ap_user_ids = [23, 9468] + is_ap = self.env.user.id in ap_user_ids + for line in self: + line.is_current_user_ap = is_ap + + # @api.onchange('account_id') + # def _onchange_account_id(self): + # for rec in self: + # if rec.account_id: + # rec.description = rec.account_id.name + " - " + + @api.onchange('attachment_type') + def _onchange_attachment_type(self): + self.attachment_file_image = False + self.attachment_filename_image = False + self.attachment_file_pdf = False + self.attachment_filename_pdf = False + + @api.onchange('done_attachment') + def _onchange_done_attachment(self): + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + + if self.done_attachment and self.env.user.id not in ap_user_ids: + self.done_attachment = False + return { + 'warning': { + 'title': _('Tidak Diizinkan'), + 'message': _('Hanya user AP yang bisa mencentang Done Attachment.') + } + } + + @api.onchange('nominal') + def _onchange_nominal_no_minus(self): + if self.nominal and self.nominal < 0: + self.nominal = 0 + return { + 'warning': { + 'title': _('Nominal Tidak Valid'), + 'message': _( + "Nominal penggunaan PUM tidak boleh diisi minus.\n" + "Nilai di Set menjadi nol." + ) + } + } + +class ReimburseLine(models.Model): + _name = 'reimburse.line' + _description = 'Reimburse Line' + + request_id = fields.Many2one('advance.payment.request', string='Request') + date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) + account_id = fields.Many2one( + 'account.account', + string='Jenis Biaya', tracking=3, + domain="[('id', 'in', [484, 486, 527, 529, 530, 471, 473, 492, 493, 488, 625, 528, 533, 534])]" + ) + description = fields.Text(string='Description', required=True, tracking=3) + distance_departure = fields.Float(string='Pergi (Km)', tracking=3) + distance_return = fields.Float(string='Pulang (Km)', tracking=3) + quantity = fields.Float(string='Quantity', tracking=3, default=1) + price_unit = fields.Float(string='Price', tracking=3) + total = fields.Float(string='Total', tracking=3, compute='_compute_total') + # total = fields.Float(string='Total', tracking=3) + currency_id = fields.Many2one(related='request_id.currency_id') + + is_vehicle = fields.Boolean(string='Berkendara?') + vehicle_type = fields.Selection([ + ('motor', 'Motor'), + ('car', 'Mobil'), + ], string='Tipe Kendaraan', tracking=3) + + attachment_image = fields.Binary(string='Image', attachment_filename='attachment_name_image') + attachment_pdf = fields.Binary(string='PDF', attachment_filename='attachment_name_pdf') + attachment_name_image = fields.Char(string='Filename Image') + attachment_name_pdf = fields.Char(string='Filename PDF') + + attachment_type = fields.Selection([ + ('pdf', 'PDF'), + ('image', 'Image'), + ], string="Attachment Type") + + is_checked = fields.Boolean(string='Checked', default=False) + + is_current_user_ap = fields.Boolean( + string="Is Current User AP", + compute='_compute_is_current_user_ap' + ) + + def _compute_is_current_user_ap(self): + ap_user_ids = [23, 9468] + is_ap = self.env.user.id in ap_user_ids + for line in self: + line.is_current_user_ap = is_ap + + @api.depends('quantity', 'price_unit', 'is_vehicle') + def _compute_total(self): + for line in self: + line.total = line.quantity * line.price_unit + + @api.onchange('is_vehicle', 'vehicle_type', 'distance_departure', 'distance_return') + def _onchange_vehicle_data(self): + if not self.is_vehicle: + self.vehicle_type = False + self.distance_departure = 0 + self.distance_return = 0 + self.price_unit = 0 + return + + total_distance = self.distance_departure + self.distance_return + + if self.vehicle_type and total_distance > 0: + biaya_per_km = 0 + if self.vehicle_type == 'car': + biaya_per_km = 1000 # Rp 10.000 / 10 km + self.price_unit = biaya_per_km + elif self.vehicle_type == 'motor': + biaya_per_km = 500 # Rp 10.000 / 20 km + self.price_unit = biaya_per_km + self.total = total_distance * biaya_per_km + self.quantity = total_distance + else: + self.total = 0 + +class AdvancePaymentSettlement(models.Model): + _name = 'advance.payment.settlement' + _description = 'Advance Payment Settlement' + _inherit = ['mail.thread'] + _rec_name = 'name' + _order = 'create_date desc' + + pum_id = fields.Many2one('advance.payment.request', string='No PUM', ondelete='cascade') + name = fields.Char(string='Nama', readonly=True, tracking=3) + title = fields.Char(string='Judul', tracking=3) + goals = fields.Text(string='Tujuan', tracking=3) + related = fields.Char(string='Terkait', tracking=3) + + # pemberian_line_ids = fields.One2many( + # 'advance.payment.settlement.line', 'realization_id', string='Rincian Pemberian' + # ) + penggunaan_line_ids = fields.One2many( + 'advance.payment.usage.line', 'realization_id', string='Rincian Penggunaan' + ) + + nominal_pum = fields.Float( + string='Nominal Pemberian PUM', + related='pum_id.nominal', + readonly=True ) + + # grand_total = fields.Float(string='Grand Total Pemberian', tracking=3, compute='_compute_grand_total') + grand_total_use = fields.Float(string='Grand Total Penggunaan', tracking=3, compute='_compute_grand_total_use') + # value_down_payment = fields.Float(string='PUM', tracking=3) + remaining_value = fields.Float(string='Sisa Uang PUM', tracking=3, compute='_compute_remaining_value') + + note_approval = fields.Text(string='Note Persetujuan', tracking=3) + + name_approval_departement = fields.Char(string='Approval Departement') + name_approval_ap = fields.Char(string='Approval AP') + name_approval_pimpinan = fields.Char(string='Approval Pimpinan') + + date_approved_department = fields.Datetime(string="Date Approved Department") + date_approved_ap = fields.Datetime(string="Date Approved AP") + date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan") + + position_department = fields.Char(string='Position Departement') + position_ap = fields.Char(string='Position AP') + position_pimpinan = fields.Char(string='Position Pimpinan') + + approved_by = fields.Char(string='Approved By', track_visibility='always') + + status = fields.Selection([ + ('pengajuan1', 'Menunggu Approval Departement'), + ('pengajuan2', 'Menunggu Approval AP'), + ('pengajuan3', 'Menunggu Approval Pimpinan'), + ('approved', 'Approved'), + ], string='Status', default='pengajuan1', tracking=3, index=True, track_visibility='onchange') + + # --- DIHAPUS --- + # done_status = fields.Selection([ + # ('remaining', 'Remaining'), + # ('done_not_realized', 'Done Not Realized'), + # ('done_realized', 'Done Realized') + # ], string='Status Realisasi', tracking=3, default='remaining') + # date_done_not_realized = fields.Date(string='Tanggal Done Not Realized', tracking=3) + # --- BATAS DIHAPUS --- + + currency_id = fields.Many2one( + 'res.currency', string='Currency', + default=lambda self: self.env.company.currency_id + ) + + attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') + attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') + attachment_filename_image = fields.Char(string='Filename Image') + attachment_filename_pdf = fields.Char(string='Filename PDF') + + attachment_type = fields.Selection([ + ('pdf', 'PDF'), + ('image', 'Image'), + ], string="Attachment Type", default='pdf') + + lot_of_attachment = fields.Selection([ + ('one_for_all_line', '1 Attachment Untuk Semua Line Penggunaan PUM'), + ('one_for_one_line', '1 Attachment per 1 Line Penggunaan PUM'), + ], string = "Banyaknya Attachment", default='one_for_one_line') + + move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')]) + is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible') + + user_id = fields.Many2one( + 'res.users', + string='Diajukan Oleh', + related='pum_id.user_id', + readonly=True + ) + applicant_name = fields.Many2one( + 'res.users', + string='Nama Pemohon', + related='pum_id.applicant_name', + readonly=True + ) + is_current_user_ap = fields.Boolean( + string="Is Current User AP", + compute='_compute_is_current_user_ap' + ) + + def _compute_is_current_user_ap(self): + ap_user_ids = [23, 9468] + is_ap = self.env.user.id in ap_user_ids + for line in self: + line.is_current_user_ap = is_ap + + def action_toggle_check_attachment(self): + ap_user_ids = [23, 9468] + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan tombol ini.') + + for rec in self: + if not rec.penggunaan_line_ids: + continue + + if all(line.done_attachment for line in rec.penggunaan_line_ids): + for line in rec.penggunaan_line_ids: + line.done_attachment = False + else: + for line in rec.penggunaan_line_ids: + line.done_attachment = True + + @api.onchange('lot_of_attachment') + def _onchange_lot_of_attachment(self): + if self.lot_of_attachment == 'one_for_all_line': + for line in self.penggunaan_line_ids: + line.attachment_file_pdf = False + line.attachment_file_image = False + line.attachment_filename_pdf = False + line.attachment_filename_image = False + + + @api.depends('move_id.state') + def _compute_is_cab_visible(self): + for rec in self: + move = rec.move_id + rec.is_cab_visible = bool(move and move.state == 'posted') + + def action_view_journal_uangmuka(self): + self.ensure_one() + + ap_user_ids = [23, 9468] + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') + + if not self.move_id: + raise UserError("Journal Uang Muka belum tersedia.") + + return { + 'name': 'Journal Entry', + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': self.move_id.id, + 'target': 'current', + } + + + @api.onchange('attachment_type') + def _onchange_attachment_type(self): + self.attachment_file_image = False + self.attachment_filename_image = False + self.attachment_file_pdf = False + self.attachment_filename_pdf = False + + # @api.depends('pemberian_line_ids.value') + # def _compute_grand_total(self): + # for rec in self: + # rec.grand_total = sum(line.value for line in rec.pemberian_line_ids) + + @api.depends('penggunaan_line_ids.nominal') + def _compute_grand_total_use(self): + for rec in self: + rec.grand_total_use = sum(line.nominal for line in rec.penggunaan_line_ids) + + @api.depends('nominal_pum', 'grand_total_use') + def _compute_remaining_value(self): + for rec in self: + rec.remaining_value = rec.nominal_pum - rec.grand_total_use + return + + # --- DIHAPUS --- + # def action_validation(self): + # self.ensure_one() + + # # Validasi hanya AP yang bisa validasi + # ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + # if self.env.user.id not in ap_user_ids: + # raise UserError('Hanya AP yang dapat melakukan validasi realisasi.') + + # if self.done_status == 'remaining': + # self.done_status = 'done_not_realized' + # self.date_done_not_realized = fields.Date.today() + # elif self.done_status == 'done_not_realized': + # self.done_status = 'done_realized' + # else: + # raise UserError('Realisasi sudah berstatus Done Realized.') + + # # Opsional: Tambah log di chatter + # self.message_post(body=f"Status realisasi diperbarui menjadi <b>{dict(self._fields['done_status'].selection).get(self.done_status)}</b> oleh {self.env.user.name}.") + # --- BATAS DIHAPUS --- + + def action_cab(self): + self.ensure_one() + + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan ini.') + if self.move_id: + raise UserError("CAB / Jurnal sudah pernah dibuat untuk Realisasi ini.") + + if not self.pum_id or not self.pum_id.move_id: + raise UserError("PUM terkait atau CAB belum tersedia.") + + partner_id = self.pum_id.user_id.partner_id.id + cab_move = self.pum_id.move_id + + # Account Bank Intransit dari CAB: + bank_intransit_line = cab_move.line_ids.filtered(lambda l: l.account_id.id in [573, 389, 392]) + if not bank_intransit_line: + raise UserError("Account Bank Intransit dengan tidak ditemukan di CAB terkait.") + account_sisa_pum = bank_intransit_line[0].account_id.id + + # Account Uang Muka Operasional + account_uang_muka = 403 + + # --- PENYESUAIAN LOGIKA --- + # Tanggal pakai create_date atau hari ini + account_date = fields.Date.context_today(self) + # --- BATAS PENYESUAIAN --- + + ref_label = f"Realisasi {self.pum_id.number} {self.pum_id.detail_note} ({cab_move.name})" + + label_sisa_pum = f"Sisa PUM {self.pum_id.detail_note} {self.pum_id.number} ({cab_move.name})" + + lines = [] + + # Sisa PUM (Debit) + if self.remaining_value > 0: + lines.append((0, 0, { + 'account_id': account_sisa_pum, + 'partner_id': partner_id, + 'name': label_sisa_pum, + 'debit': self.remaining_value, + 'credit': 0, + })) + + # Biaya Penggunaan (Debit) + total_biaya = 0 + for line in self.penggunaan_line_ids: + lines.append((0, 0, { + 'account_id': line.account_id.id, + 'partner_id': partner_id, + 'name': f"{line.description} ({line.date})", + 'debit': line.nominal, + 'credit': 0, + })) + total_biaya += line.nominal + + # Uang Muka Operasional (Credit) + total_credit = self.remaining_value + total_biaya + if total_credit > 0: + lines.append((0, 0, { + 'account_id': account_uang_muka, + 'partner_id': partner_id, + 'name': ref_label, + 'debit': 0, + 'credit': total_credit, + })) + + move = self.env['account.move'].create({ + 'ref': ref_label, + 'date': account_date, + 'journal_id': 11, # MISC + 'line_ids': lines, + }) + + # self.message_post(body=f"Jurnal CAB telah dibuat dengan nomor: <b>{move.name}</b>.") + + self.move_id = move.id + + return { + 'name': _('Journal Entry'), + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': move.id, + 'target': 'current', + } + + def action_approval_check(self): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz).replace(tzinfo=None) + formatted_date = now.strftime('%d %B %Y %H:%M') + + for rec in self: + if not rec.pum_id.departement_type: + raise UserError("Field 'departement_type' wajib diisi sebelum approval.") + + approver_id = rec.pum_id._get_departement_approver() + + if rec.status == 'pengajuan1': + if self.env.user.id != approver_id: + raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") + rec.name_approval_departement = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement + rec.date_approved_department = now + + # Mapping posisi berdasarkan departement_type + department_titles = { + 'sales': 'Sales Manager', + 'merchandiser': 'Merchandiser Manager', + 'marketing': 'Marketing Manager', + 'logistic': 'Logistic Manager', + 'procurement': 'Procurement Manager', + 'fat': 'Finance & Accounting Manager', + 'it': 'IT Manager', + 'hr_ga': 'HR & GA Manager', + 'pimpinan': 'Pimpinan', + } + rec.position_department = department_titles.get(rec.pum_id.departement_type, 'Departement Manager') + + rec.status = 'pengajuan2' + + rec.message_post( + body=f"Approval <b>Departement</b> oleh <b>{self.env.user.name}</b> " + f"pada <i>{formatted_date}</i>." + ) + + elif rec.status == 'pengajuan2': + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + if self.env.user.id not in ap_user_ids: + raise UserError("Hanya AP yang berhak menyetujui tahap ini.") + rec.name_approval_ap = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap + rec.date_approved_ap = now + rec.position_ap = 'Finance AP' + rec.status = 'pengajuan3' + + rec.message_post( + body=f"Approval <b>AP</b> oleh <b>{self.env.user.name}</b> " + f"pada <i>{formatted_date}</i>." + ) + + elif rec.status == 'pengajuan3': + if self.env.user.id != 7: # ID user Pimpinan + raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + rec.name_approval_pimpinan = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan + rec.date_approved_pimpinan = now + rec.position_pimpinan = 'Pimpinan' + rec.status = 'approved' + # --- DIHAPUS --- + # rec.done_status = 'done_not_realized' # Set status done untuk realisasi + # --- BATAS DIHAPUS --- + + rec.message_post( + body=f"Approval <b>Pimpinan</b> oleh <b>{self.env.user.name}</b> " + f"pada <i>{formatted_date}</i>." + ) + + else: + raise UserError("Status saat ini tidak bisa di-approve lagi.") + + # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap <b>{rec.status}</b>.") + + def _check_remaining_value(self): + for rec in self: + # Cek sisa PUM + if rec.remaining_value < 0: + raise ValidationError( + "Sisa uang PUM tidak boleh kurang dari Rp 0.\n" + "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse." + ) + + @api.model + def create(self, vals): + jakarta_tz = pytz.timezone('Asia/Jakarta') + # --- PENYESUAIAN LOGIKA WAKTU --- + now = datetime.now(jakarta_tz).replace(tzinfo=None) + # Gunakan fields.Datetime.now() agar konsisten + # now = fields.Datetime.now() + # --- BATAS PENYESUAIAN --- + + pum_id = vals.get('pum_id') + initial_status = '' + if pum_id: + pum_request = self.env['advance.payment.request'].browse(pum_id) + if pum_request: + position_dari_pum = pum_request.position_type + department_dari_pum = pum_request.departement_type + if position_dari_pum == 'staff': + if department_dari_pum == 'hr_ga': + initial_status = 'pengajuan2' + else: + initial_status = 'pengajuan1' + elif position_dari_pum == 'manager': + initial_status = 'pengajuan2' + applicant_name_str = pum_request.applicant_name.name or '' + department_type = pum_request.departement_type + department_titles = pum_request._get_department_titles_mapping() + dept_position = department_titles.get(department_type, 'Departement Manager') + vals['date_approved_department'] = now + vals['name_approval_departement'] = applicant_name_str + vals['position_department'] = dept_position + elif position_dari_pum == 'pimpinan': + initial_status = 'pengajuan2' + applicant_name_str = pum_request.applicant_name.name or '' + vals['date_approved_pimpinan'] = now + vals['name_approval_pimpinan'] = applicant_name_str + vals['position_pimpinan'] = 'Pimpinan' + + # --- PENYESUAIAN LOGIKA: SET DEFAULT JIKA KOSONG --- + vals['status'] = initial_status or 'pengajuan1' + # --- BATAS PENYESUAIAN --- + + rec = super().create(vals) + rec._check_remaining_value() + return rec + + def write(self, vals): + res = super().write(vals) + self._check_remaining_value() + return res + + +class AdvancePaymentCreateBill(models.TransientModel): + _name = 'advance.payment.create.bill' + _description = 'Create Bill from Advance Payment' + + apr_id = fields.Many2one('advance.payment.request', string='Advance Payment Request', required=True) + account_id = fields.Many2one( + 'account.account', string='Bank Intransit', required=True, + domain="[('id', 'in', [573, 389, 392])]" # ID Bank Intransit + ) + nominal = fields.Float(string='Nominal', related='apr_id.nominal') + + def action_create_cab(self): + self.ensure_one() + + # if self.env.user.id != 23: + # raise UserError('Hanya AP yang dapat menggunakan ini.') + + apr = self.apr_id + partner_id = apr.user_id.partner_id.id + + ref_label = f'{apr.number} - {apr.detail_note or "-"}' + + move = self.env['account.move'].create({ + 'ref': ref_label, + 'date': fields.Date.context_today(self), + 'journal_id': 11, # Cash & Bank + 'line_ids': [ + (0, 0, { + 'account_id': 403, # Uang Muka Operasional + 'partner_id': partner_id, + 'name': ref_label, + 'debit': apr.nominal, + 'credit': 0, + }), + (0, 0, { + 'account_id': self.account_id.id, # Bank Intransit yang dipilih + 'partner_id': partner_id, + 'name': ref_label, + 'debit': 0, + 'credit': apr.nominal, + }) + ] + }) + + apr.move_id = move.id # jika ada field untuk menampung move_id + + return { + 'name': _('Journal Entry'), + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': move.id, + 'target': 'current', + } + +class CreateReimburseCabWizard(models.TransientModel): + _name = 'create.reimburse.cab.wizard' + _description = 'Wizard untuk Membuat Jurnal Reimburse' + + # Field untuk menampung ID request yang sedang diproses + request_id = fields.Many2one('advance.payment.request', string='Pengajuan', readonly=True) + + # Field untuk memilih salah satu dari dua bank Anda + account_id = fields.Many2one( + 'account.account', + string='Bank Intransit (Credit)', + required=True, + # Domain untuk membatasi pilihan hanya pada ID 573 dan 389 + domain="[('id', 'in', [573, 389])]" + ) + + # Field untuk menampilkan total agar pengguna bisa konfirmasi + total_reimburse = fields.Monetary( + string='Total Reimburse', + related='request_id.grand_total_reimburse', + ) + currency_id = fields.Many2one(related='request_id.currency_id', readonly=True) + + def action_create_reimburse_cab(self): + """Metode ini yang akan membuat Journal Entry (CAB).""" + self.ensure_one() + request = self.request_id + + # --- Validasi --- + if request.move_id: + raise UserError("Jurnal sudah pernah dibuat untuk pengajuan ini.") + if not request.reimburse_line_ids: + raise UserError("Tidak ada rincian reimburse yang bisa dijurnalkan.") + + lines = [] + partner_id = request.user_id.partner_id.id + + # 1. Buat Jurnal DEBIT dari setiap baris reimburse + for line in request.reimburse_line_ids: + if not line.account_id: + raise UserError(f"Jenis Biaya pada baris '{line.description}' belum diisi oleh AP.") + + lines.append((0, 0, { + 'account_id': line.account_id.id, + 'partner_id': partner_id, + 'name': line.description, + 'debit': line.total, + 'credit': 0, + })) + + # 2. Buat satu Jurnal CREDIT ke bank yang dipilih di wizard + lines.append((0, 0, { + 'account_id': self.account_id.id, + 'partner_id': partner_id, + 'name': f'Reimburse {request.number}', + 'debit': 0, + 'credit': request.grand_total_reimburse, + })) + + ref_label = f'{request.number} - {request.detail_note or "-"}' + + # 3. Buat Journal Entry + move = self.env['account.move'].create({ + 'ref': ref_label, + 'date': fields.Date.context_today(self), + 'journal_id': 11, # PENTING: Ganti 11 dengan ID Journal "Miscellaneous" Anda + 'line_ids': lines, + }) + + # 4. Tautkan journal yang baru dibuat ke request + request.move_id = move.id + + # 5. Buka tampilan form journal yang baru dibuat + return { + 'name': _('Journal Entry'), + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': move.id, + 'target': 'current', + }
\ No newline at end of file diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py index 4df7da1e..519f505c 100644 --- a/indoteknik_custom/models/partial_delivery.py +++ b/indoteknik_custom/models/partial_delivery.py @@ -115,9 +115,13 @@ class PartialDeliveryWizard(models.TransientModel): raise UserError(_("Picking harus dalam status Ready (assigned).")) lines_by_qty = self.line_ids.filtered(lambda l: l.selected_qty > 0) + lines_validation = self.line_ids.filtered(lambda l: l.selected_qty > l.reserved_qty) lines_by_selected = self.line_ids.filtered(lambda l: l.selected and not l.selected_qty) selected_lines = lines_by_qty | lines_by_selected # gabung dua domain hasil filter + if lines_validation: + raise UserError(_("Jumlah yang dipilih melebihi jumlah yang terdapat di DO.")) + if not selected_lines: raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya.")) @@ -172,9 +176,11 @@ class PartialDeliveryWizard(models.TransientModel): for line in selected_lines: if line.selected_qty > line.reserved_qty: raise UserError(_("Jumlah produk %s yang dipilih melebihi jumlah reserved.") % line.product_id.display_name) + move = line.move_id move._do_unreserve() + # 🔹 Kalau cuma selected tanpa qty → anggap kirim semua reserved qty if line.selected and not line.selected_qty: line.selected_qty = line.reserved_qty @@ -186,12 +192,20 @@ class PartialDeliveryWizard(models.TransientModel): if line.selected_qty < move.product_uom_qty: qty_to_keep = move.product_uom_qty - line.selected_qty + new_move = move.copy(default={ 'product_uom_qty': line.selected_qty, 'picking_id': new_picking.id, 'partial': True, }) + + if move.move_dest_ids: + for dest_move in move.move_dest_ids: + # dest_move.write({'move_orig_ids': [(4, new_move.id)]}) + new_move.write({'move_dest_ids': [(4, dest_move.id)]}) + move.write({'product_uom_qty': qty_to_keep}) + else: move.write({'picking_id': new_picking.id, 'partial': True}) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index e79417aa..534d8122 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1043,7 +1043,7 @@ class PurchaseOrder(models.Model): # test = line.product_uom_qty # test2 = line.product_id.plafon_qty # test3 = test2 + line.product_uom_qty - if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and not self.env.user.id == 21: + if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and self.env.user.id not in [21, 7]: raise UserError('Product '+line.product_id.name+' melebihi plafon, harus Approval Rafly') def check_different_vendor_so_po(self): @@ -1123,6 +1123,8 @@ class PurchaseOrder(models.Model): if not self.not_update_purchasepricelist: self.add_product_to_pricelist() for line in self.order_line: + if not line.product_id.public_categ_ids: + raise UserError("Product %s kategorinya kosong" % line.product_id.name) if not line.product_id.purchase_ok: raise UserError("Terdapat barang yang tidak bisa diproses") # Validasi pajak diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 5c8f34c6..494aeaa2 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -397,23 +397,24 @@ class SaleOrder(models.Model): string="Partner Locked CBD", compute="_compute_partner_is_cbd_locked" ) + internal_notes_contact = fields.Text(related='partner_id.comment', string="Internal Notes", readonly=True, help="Internal Notes dari contact utama customer.") def action_open_partial_delivery_wizard(self): - raise UserError("Fitur ini sedang dalam pengembangan") - # self.ensure_one() - # pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name) - # return { - # 'type': 'ir.actions.act_window', - # 'name': 'Partial Delivery', - # 'res_model': 'partial.delivery.wizard', - # 'view_mode': 'form', - # 'target': 'new', - # 'context': { - # 'default_sale_id': self.id, - # # kasih langsung list of int biar ga ribet di wizard - # 'default_picking_ids': pickings.ids, - # } - # } + # raise UserError("Fitur ini sedang dalam pengembangan") + self.ensure_one() + pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name) + return { + 'type': 'ir.actions.act_window', + 'name': 'Partial Delivery', + 'res_model': 'partial.delivery.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_sale_id': self.id, + # kasih langsung list of int biar ga ribet di wizard + 'default_picking_ids': pickings.ids, + } + } @api.depends('partner_id.is_cbd_locked') @@ -422,7 +423,7 @@ class SaleOrder(models.Model): order.partner_is_cbd_locked = order.partner_id.is_cbd_locked - @api.constrains('payment_term_id', 'partner_id', 'state') + @api.constrains('payment_term_id', 'partner_id') def _check_cbd_lock_sale_order(self): cbd_term = self.env['account.payment.term'].browse(26) for rec in self: @@ -1985,10 +1986,10 @@ class SaleOrder(models.Model): # raise UserError('Kelurahan Real Delivery Address harus diisi') def generate_payment_link_midtrans_sales_order(self): - midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox - midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox - # midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production - # midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production + # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox + # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox + midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production + midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production so_number = self.name so_number = so_number.replace('/', '-') @@ -2001,8 +2002,8 @@ class SaleOrder(models.Model): } # ==== ENV ==== - check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox - # check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production + # check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox + check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production # ============================================= check_response = requests.get(check_url, headers=headers) @@ -2716,7 +2717,7 @@ class SaleOrder(models.Model): return True if user.id in (3401, 20, 3988, 17340): # admin (fida, nabila, ninda) - return True + raise UserError("Yahaha gabisa confirm so, minta ke sales nya ajah") if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988): return True diff --git a/indoteknik_custom/models/solr/apache_solr_queue.py b/indoteknik_custom/models/solr/apache_solr_queue.py index 1b51538f..3d6bd733 100644 --- a/indoteknik_custom/models/solr/apache_solr_queue.py +++ b/indoteknik_custom/models/solr/apache_solr_queue.py @@ -1,10 +1,10 @@ from odoo import models, fields from datetime import datetime, timedelta -import logging, time - +import logging, time, traceback # <-- tambah traceback _logger = logging.getLogger(__name__) + class ApacheSolrQueue(models.Model): _name = 'apache.solr.queue' @@ -19,6 +19,7 @@ class ApacheSolrQueue(models.Model): ], 'Execute Status') execute_date = fields.Datetime('Execute Date') description = fields.Text('Description') + log = fields.Text('Log') def _compute_display_name(self): for rec in self: @@ -39,7 +40,7 @@ class ApacheSolrQueue(models.Model): if elapsed_time > max_exec_time: break rec.execute_queue() - + def open_target_record(self): return { 'name': '', @@ -67,17 +68,21 @@ class ApacheSolrQueue(models.Model): if model_instance: getattr(model_instance, function_name)() rec.execute_status = 'success' + rec.log = traceback.format_exc() else: rec.execute_status = 'not_found' except Exception as e: - rec.description = e + # simpan error ringkas + traceback lengkap + rec.description = str(e) + rec.log = traceback.format_exc() rec.execute_status = 'failed' + rec.execute_date = datetime.utcnow() self.env.cr.commit() def create_unique(self, payload={}): count = self.search_count([ - ('res_model', '=', payload['res_model']), + ('res_model', '=', payload['res_model']), ('res_id', '=', payload['res_id']), ('function_name', '=', payload['function_name']), ('execute_status', '=', False) @@ -90,8 +95,6 @@ class ApacheSolrQueue(models.Model): ('execute_status', '=', 'success'), ('execute_date', '>=', (datetime.utcnow() - timedelta(days=days_after))), ], limit=limit) - + for rec in solr: rec.unlink() - - diff --git a/indoteknik_custom/models/solr/product_product.py b/indoteknik_custom/models/solr/product_product.py index d8bc3973..7260c3ca 100644 --- a/indoteknik_custom/models/solr/product_product.py +++ b/indoteknik_custom/models/solr/product_product.py @@ -69,9 +69,9 @@ class ProductProduct(models.Model): 'product_id_i': variant.id, 'template_id_i': variant.product_tmpl_id.id, 'image_s': ir_attachment.api_image('product.template', 'image_512', variant.product_tmpl_id.id), - 'image_carousel_s': [ir_attachment.api_image('image.carousel', 'image', carousel.id) for carousel in variant.product_tmpl_id.image_carousel_lines], + 'image_carousel_ss': [ir_attachment.api_image('image.carousel', 'image', carousel.id) for carousel in variant.product_tmpl_id.image_carousel_lines], 'image_mobile_s': ir_attachment.api_image('product.template', 'image_256', variant.product_tmpl_id.id), - 'stock_total_f': variant.qty_stock_vendor, + 'stock_total_f': variant.qty_free_bandengan, 'weight_f': variant.weight, 'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0, 'manufacture_name_s': variant.product_tmpl_id.x_manufacture.x_name or '', diff --git a/indoteknik_custom/models/solr/product_template.py b/indoteknik_custom/models/solr/product_template.py index c4aefe19..a7beca12 100644 --- a/indoteknik_custom/models/solr/product_template.py +++ b/indoteknik_custom/models/solr/product_template.py @@ -59,9 +59,9 @@ class ProductTemplate(models.Model): solr_model = self.env['apache.solr'] for template in self: + voucher = None if template.x_manufacture: voucher = self.get_voucher_pastihemat(template.x_manufacture.id) - # Lakukan sesuatu dengan voucher variant_names = ', '.join([x.display_name or '' for x in template.product_variant_ids]) variant_codes = ', '.join([x.default_code or '' for x in template.product_variant_ids]) diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py index 3d4a404e..98f356de 100644 --- a/indoteknik_custom/models/sourcing_job_order.py +++ b/indoteknik_custom/models/sourcing_job_order.py @@ -59,13 +59,20 @@ class SourcingJobOrder(models.Model): line_sales_view_ids = fields.One2many( 'sourcing.job.order.line', 'order_id', string='Sales View Lines', - domain=[('price', '>', 0)] + domain=[('state', 'in', ['sourcing', 'done', 'cancel'])] + ) + line_sales_view_cancel_ids = fields.One2many( + 'sourcing.job.order.line', 'order_id', + string='Sales View Lines', + domain=[('state', '=', 'cancel')] ) has_price_in_lines = fields.Boolean( string='Has Line with Price', compute='_compute_has_price_in_lines', ) + is_creator_same_user = fields.Boolean(compute='_compute_is_creator_same_user') + can_convert_to_product = fields.Boolean(string="Can Convert", compute="_compute_can_convert_to_product") @api.depends('line_ids.subtotal') @@ -99,6 +106,21 @@ class SourcingJobOrder(models.Model): rec.user_id == current_user and bool(rec.takeover_request) ) + @api.depends('create_uid', 'user_id') + def _compute_is_creator_same_user(self): + for rec in self: + current_user = self.env.user + rec.is_creator_same_user = ( + rec.create_uid == current_user and + rec.user_id == current_user + ) + + @api.depends('line_md_edit_ids.state') + def _compute_can_convert_to_product(self): + """Cek apakah ada line dengan state 'done'.""" + for rec in self: + rec.can_convert_to_product = any(line.state == 'done' for line in rec.line_md_edit_ids) + @api.model def create(self, vals): """Hanya Sales & Merchandiser yang boleh membuat job.""" @@ -117,22 +139,112 @@ class SourcingJobOrder(models.Model): return rec def write(self, vals): - context_action = self.env.context.get('from_action_take', False) - - if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or - self.env.user.has_group('indoteknik_custom.group_role_merchandiser')): + bypass_actions = ( + self.env.context.get('from_action_take', False) + or self.env.context.get('from_multi_action_take', False) + or self.env.context.get('from_action_takeover', False) + ) + + if not ( + self.env.user.has_group('indoteknik_custom.group_role_sales') + or self.env.user.has_group('indoteknik_custom.group_role_merchandiser') + ): raise UserError("❌ Hanya Sales dan Merchandiser yang boleh mengedit Sourcing Job.") for rec in self: - if not rec.user_id and rec.create_uid != self.env.user and not vals.get('user_id') and not context_action: + if ( + not rec.user_id + and rec.create_uid != self.env.user + and not vals.get('user_id') + and not bypass_actions + ): raise UserError("❌ SJO ini belum memiliki MD Person. Tidak dapat melakukan edit.") - if rec.user_id != self.env.user and rec.create_uid != self.env.user and not context_action: + if ( + rec.user_id != self.env.user + and rec.create_uid != self.env.user + and not bypass_actions + ): raise UserError("❌ Hanya MD Person dan Creator SJO ini yang bisa melakukan Edit.") - - return super().write(vals) + + # --- Simpan data lama sebelum write (buat pembanding) + old_data = {} + for rec in self: + old_data[rec.id] = { + 'state': rec.state, + 'user_id': rec.user_id.id if rec.user_id else False, + 'approval_sales': rec.approval_sales, + 'line_data': { + line.id: { + 'state': line.state, + 'vendor_id': line.vendor_id.id if line.vendor_id else False, + 'price': line.price, + } + for line in rec.line_md_edit_ids + }, + } + + res = super().write(vals) + + # --- Bandingkan setelah write dan buat log + for rec in self: + changes = [] + old = old_data.get(rec.id, {}) + + # === Perubahan di field parent === + if old.get('state') != rec.state: + changes.append(f"State: <b>{old.get('state')}</b> → <b>{rec.state}</b>") + if old.get('user_id') != (rec.user_id.id if rec.user_id else False): + changes.append(f"MD Person: <b>{old.get('user_id')}</b> → <b>{rec.user_id.name if rec.user_id else '-'}</b>") + if old.get('approval_sales') != rec.approval_sales: + changes.append(f"Approval Status: <b>{old.get('approval_sales')}</b> → <b>{rec.approval_sales}</b>") + + # === Perubahan di line === + old_lines = old.get('line_data', {}) + for line in rec.line_md_edit_ids: + old_line = old_lines.get(line.id) + if not old_line: + continue # line baru, skip validasi perubahan + + # 💥 Validasi vendor berubah tapi price tidak berubah + if ( + old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False) + and old_line['price'] == line.price + ): + raise UserError( + f"⚠️ Harga untuk produk {line.product_name} belum diperbarui setelah mengganti Vendor." + ) + + # Catat perubahan state, vendor, atau harga untuk log note + sub_changes = [] + + if old_line['state'] != line.state: + sub_changes.append(f"- state: <b>{old_line['state']}</b> → <b>{line.state}</b>") + + if old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False): + old_vendor = self.env['res.partner'].browse(old_line['vendor_id']).name if old_line['vendor_id'] else '-' + sub_changes.append(f"- vendor: <b>{old_vendor}</b> → <b>{line.vendor_id.name if line.vendor_id else '-'}</b>") + + if old_line['price'] != line.price: + sub_changes.append(f"- price: <b>{old_line['price']}</b> → <b>{line.price}</b>") + + if sub_changes: + joined = "<br/>".join(sub_changes) + changes.append(f"<b>{line.product_name}</b>:<br/>{joined}") + + # Post ke chatter + if changes: + message = "<br/><br/>".join(changes) + rec.message_post( + body=f"Perubahan pada Sourcing Job:<br/>{message}", + subtype_xmlid="mail.mt_comment", + ) + + return res + def action_take(self): + context_action = self.env.context.get('from_action_take', False) for rec in self: if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): raise UserError("❌ Hanya Merchandiser yang dapat mengambil Sourcing Job.") @@ -145,17 +257,52 @@ class SourcingJobOrder(models.Model): rec.message_post(body=("Job <b>%s</b> diambil oleh %s") % (rec.name, self.env.user.name)) def action_multi_take(self): + context_action = self.env.context.get('from_multi_action_take', True) untaken = self.filtered(lambda r: r.state == 'draft') if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): raise UserError("❌ Hanya Merchandiser yang bisa mengambil Sourcing Job.") if not untaken: - raise UserError("Tidak ada record draft untuk diambil.") + raise UserError("Tidak ada record Untaken untuk diambil.") untaken.write({ 'state': 'taken', 'user_id': self.env.user.id, }) + def action_confirm_by_md(self): + for rec in self: + if rec.user_id and rec.user_id != self.env.user: + raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Confirm.") + + invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel')) + if invalid_lines: + line_names = ', '.join(invalid_lines.mapped('product_name')) + raise UserError( + f"⚠️ Tidak dapat melakukan Confirm SJO.\n" + f"Masih ada line yang belum selesai disourcing: {line_names}" + ) + if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids): + raise UserError("⚠️ Tidak dapat melakukan Confirm SJO. Semua line pada SJO ini Unavailable.") + + rec.approval_sales = 'approve' + rec.state = 'done' + + rec.message_post( + body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'titl': 'Auto Approved', + 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.", + 'type': 'success', + 'sticky': False, + } + } + def action_cancel(self): for rec in self: if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): @@ -172,6 +319,7 @@ class SourcingJobOrder(models.Model): (rec.name, self.env.user.name, rec.cancel_reason)) def action_request_takeover(self): + context_action = self.env.context.get('from_action_takeover', True) for rec in self: if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'): raise UserError("❌ Hanya Merchandiser yang dapat Request Takeover Sourcing Job.") @@ -182,47 +330,14 @@ class SourcingJobOrder(models.Model): rec.takeover_request = self.env.user activity_type = self.env.ref('mail.mail_activity_data_todo') - self.env['mail.activity'].create({ - 'activity_type_id': activity_type.id, - 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.", - 'res_id': rec.id, - 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'), - 'user_id': rec.user_id.id, - }) - - rec.message_post( - body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.", - subtype_xmlid="mail.mt_comment" + rec.activity_schedule( + activity_type_id=activity_type.id, + user_id=rec.user_id.id, + note=f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.", ) - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': 'Request Sent', - 'message': f"Request takeover telah dikirim ke {rec.user_id.name}.", - 'type': 'success', - 'sticky': False, - } - } - def action_ask_approval(self): - for rec in self: - if rec.user_id != self.env.user: - raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.") - - rec.approval_sales = 'requested' - - activity_type = self.env.ref('mail.mail_activity_data_todo') - self.env['mail.activity'].create({ - 'activity_type_id': activity_type.id, - 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.", - 'res_id': rec.id, - 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'), - 'user_id': rec.user_id.id, - }) - rec.message_post( - body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.user_id.name}</b>.", + body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.", subtype_xmlid="mail.mt_comment" ) @@ -236,9 +351,7 @@ class SourcingJobOrder(models.Model): 'sticky': False, } } - - - + def action_approve_takeover(self): for rec in self: if self.env.user != rec.user_id: @@ -262,6 +375,7 @@ class SourcingJobOrder(models.Model): body=f"Takeover disetujui oleh <b>{self.env.user.name}</b>. Sourcing Job berpindah ke <b>{new_user.name}</b>.", subtype_xmlid="mail.mt_comment" ) + def action_reject_takeover(self): for rec in self: if self.env.user != rec.user_id: @@ -284,7 +398,98 @@ class SourcingJobOrder(models.Model): subtype_xmlid="mail.mt_comment" ) + def action_ask_approval(self): + for rec in self: + if rec.user_id != self.env.user: + raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.") + invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel')) + if invalid_lines: + line_names = ', '.join(invalid_lines.mapped('product_name')) + raise UserError( + f"⚠️ Tidak dapat melakukan Request Approval.\n" + f"Masih ada line yang belum selesai disourcing: {line_names}" + ) + if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids): + raise UserError("⚠️ Tidak dapat melakukan Request Approval. Semua line pada SJO ini Unavailable.") + + if rec.create_uid == rec.user_id: + rec.approval_sales = 'approve' + rec.state = 'done' + + rec.message_post( + body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Auto Approved', + 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.", + 'type': 'success', + 'sticky': False, + } + } + + rec.approval_sales = 'draft' + + activity_type = self.env.ref('mail.mail_activity_data_todo') + rec.activity_schedule( + activity_type_id=activity_type.id, + user_id=rec.create_uid.id, + note=f"{self.env.user.name} meminta approval untuk SJO '{rec.name}'.", + ) + + rec.message_post( + body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.create_uid.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Request Sent', + 'message': f"Request Approval telah dikirim ke {rec.create_uid.name}.", + 'type': 'success', + 'sticky': False, + } + } + + def action_confirm_approval(self): + for rec in self: + if rec.create_uid != self.env.user: + raise UserError("❌ Hanya Pembuat Sourcing Job ini yang dapat Confirm Approval.") + rec.approval_sales = 'approve' + rec.state = 'done' + + rec.activity_feedback(['mail.mail_activity_data_todo']) + + rec.message_post( + body=f"Sourcing Job disetujui oleh <b>{self.env.user.name}</b>.", + subtype_xmlid="mail.mt_comment" + ) + + rec.activity_schedule( + 'mail.mail_activity_data_todo', + user_id=rec.user_id.id, + note=f"✅ Sourcing Job <b>{rec.name}</b> telah disetujui oleh {self.env.user.name}.", + ) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Approval Confirmed', + 'message': f"Sourcing Job '{rec.name}' telah disetujui dan dikirim ke {rec.user_id.name}.", + 'type': 'success', + 'sticky': False, + } + } + + class SourcingJobOrderLine(models.Model): _name = 'sourcing.job.order.line' _description = 'Sourcing Job Order Line' @@ -293,15 +498,19 @@ class SourcingJobOrderLine(models.Model): product_name = fields.Char(string='Nama Barang', required=True) code = fields.Char(string='SKU') descriptions = fields.Text(string='Deskripsi / Spesifikasi') + reason = fields.Text(string='Reason Unavailable') + sla = fields.Char(string='SLA Product') quantity = fields.Float(string='Quantity Product', required=True) price = fields.Float(string='Purchase Price') tax_id = fields.Many2one('account.tax', string='Tax', domain=[('active', '=', True)]) vendor_id = fields.Many2one('res.partner', string="Vendor") product_category = fields.Many2one('product.category', string="Product Category") + product_class = fields.Many2one('product.public.category', string="Categories") state = fields.Selection([ ('draft', 'Unsource'), ('sourcing', 'On Sourcing'), ('done', 'Done Sourcing'), + ('convert', 'Converted'), ('cancel', 'Unavailable') ], default='draft', tracking=True) product_type = fields.Selection([ @@ -324,12 +533,18 @@ class SourcingJobOrderLine(models.Model): subtotal += subtotal * (line.tax_id.amount / 100) line.subtotal = subtotal - @api.constrains('product_type', 'product_category') + @api.constrains('product_type', 'product_category', 'product_class', 'code') def _check_required_fields_for_md(self): for rec in self: is_md = self.env.user.has_group('indoteknik_custom.group_role_merchandiser') - if is_md and (not rec.product_type or not rec.product_category): - raise ValidationError("MD wajib mengisi Product Type dan Product Category!") + if is_md and (not rec.product_type or not rec.product_category or not rec.product_class or not rec.code): + raise UserError("MD wajib mengisi SKU, Product Type, Product Category, dan Categories!") + + @api.depends('state') + def _check_unavailable_line(self): + for rec in self: + if rec.state == 'cancel' and not rec.reason: + raise UserError("Isi Reason Unavailable") @api.depends('price', 'vendor_id', 'order_id') def _compute_show_for_sales(self): @@ -337,3 +552,47 @@ class SourcingJobOrderLine(models.Model): rec.show_for_sales = bool( rec.order_id and rec.price not in (None, 0) and rec.vendor_id ) + + def action_convert_to_product(self): + type_map = { + 'servis': 'service', + 'product': 'product', + 'consu': 'consu', + } + + for rec in self: + if rec.order_id.user_id != self.env.user: + raise UserError("❌ Hanya MD Person SJO ini yang dapat convert Line ini ke product.") + exsisting = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1) + + if exsisting: + raise UserError(f"⚠️ Produk dengan Internal Reference '{rec.code}' sudah ada di sistem.") + + product = self.env['product.product'].create({ + 'name': rec.product_name, + 'default_code': rec.code or False, + 'description': rec.descriptions or '', + 'categ_id': rec.product_category.id, + 'type': type_map.get(rec.product_type, 'product'), + }) + + rec.state = 'convert' + return True + + @api.onchange('code') + def _oncange_code(self): + for rec in self: + if not rec.code: + continue + + product = self.env['product.product'].search([('default_code', '=', rec.code)], limit=1) + if not product: + return + + rec.product_name = product.name or rec.product_name + + pricelist = self.env['purchase.pricelist'].search([('product_id', '=', product.id), ('is_winner', '=', True)], limit=1) + if pricelist or tax or vendor: + rec.vendor_id = pricelist.vendor_id.id or False + rec.price = pricelist.include_price or 0.0 + rec.tax_id = pricelist.taxes_product_id.id or pricelist.taxes_system_id.id or False
\ No newline at end of file diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index 1da2befe..8f8ba66f 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -186,6 +186,56 @@ class StockMoveLine(models.Model): line_no = fields.Integer('No', default=0) note = fields.Char('Note') manufacture = fields.Many2one('x_manufactures', string="Brands", related="product_id.x_manufacture", store=True) + outstanding_qty = fields.Float( + string='Outstanding Qty', + compute='_compute_delivery_line_status', + store=False + ) + delivery_status = fields.Selection([ + ('none', 'No Movement'), + ('partial', 'Partial'), + ('partial_final', 'Partial Final'), + ('full', 'Full'), + ], string='Delivery Status', compute='_compute_delivery_line_status', store=False) + + @api.depends('qty_done', 'product_uom_qty', 'picking_id.state') + def _compute_delivery_line_status(self): + for line in self: + line.outstanding_qty = 0.0 + line.delivery_status = 'none' + + picking = line.picking_id + if not picking or picking.picking_type_id.code != 'outgoing': + continue + + total_qty = line.move_id.product_uom_qty or 0 + done_qty = line.qty_done or 0 + + line.outstanding_qty = max(total_qty - done_qty, 0) + + if total_qty == 0: + continue + + if done_qty == 0: + line.delivery_status = 'none' + elif done_qty > 0: + has_other_out = self.env['stock.picking'].search_count([ + ('group_id', '=', picking.group_id.id), + ('name', 'ilike', 'BU/OUT'), + ('id', '!=', picking.id), + ('state', '=', 'done'), + ]) + if has_other_out and done_qty == total_qty: + line.delivery_status = 'partial_final' + elif not has_other_out and done_qty >= total_qty: + line.delivery_status = 'full' + elif has_other_out and done_qty < total_qty: + line.delivery_status = 'partial' + elif done_qty < total_qty: + line.delivery_status = 'partial' + else: + line.delivery_status = 'none' + # Ambil uom dari stock move @api.model diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index d6096cc0..7f8523a3 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -177,6 +177,97 @@ class StockPicking(models.Model): area_name = fields.Char(string="Area", compute="_compute_area_name") is_bu_iu = fields.Boolean('Is BU/IU', compute='_compute_is_bu_iu', default=False, copy=False, readonl=True) + qty_yang_mau_dikirim = fields.Float( + string='Qty yang Mau Dikirim', + compute='_compute_delivery_status_detail', + store=False + ) + qty_terkirim = fields.Float( + string='Qty Terkirim', + compute='_compute_delivery_status_detail', + store=False + ) + qty_gantung = fields.Float( + string='Qty Gantung', + compute='_compute_delivery_status_detail', + store=False + ) + delivery_status = fields.Selection([ + ('none', 'No Movement'), + ('partial', 'Partial'), + ('partial_final', 'Partial Final'), + ('full', 'Full'), + ], string='Delivery Status', compute='_compute_delivery_status_detail', store=False) + + @api.depends('move_line_ids_without_package.qty_done', 'move_line_ids_without_package.product_uom_qty', 'state') + def _compute_delivery_status_detail(self): + for picking in self: + # Default values + picking.qty_yang_mau_dikirim = 0.0 + picking.qty_terkirim = 0.0 + picking.qty_gantung = 0.0 + picking.delivery_status = 'none' + + # Hanya berlaku untuk pengiriman (BU/OUT) + if picking.picking_type_id.code != 'outgoing': + continue + + move_lines = picking.move_line_ids_without_package + if not move_lines: + continue + + # ====================== + # HITUNG QTY + # ====================== + total_qty = sum(line.product_uom_qty for line in move_lines) + + done_qty_total = sum(line.sale_line_id.qty_delivered for line in picking.move_ids_without_package) + order_qty_total = sum(line.sale_line_id.product_uom_qty for line in picking.move_ids_without_package) + gantung_qty_total = order_qty_total - done_qty_total - total_qty + + picking.qty_yang_mau_dikirim = total_qty + picking.qty_terkirim = done_qty_total + picking.qty_gantung = gantung_qty_total + + # if total_qty == 0: + # picking.delivery_status = 'none' + # continue + + # if done_qty_total == 0: + # picking.delivery_status = 'none' + # continue + + # ====================== + # CEK BU/OUT LAIN (BACKORDER) + # ====================== + has_other_out = self.env['stock.picking'].search_count([ + ('group_id', '=', picking.group_id.id), + ('name', 'ilike', 'BU/OUT'), + ('id', '!=', picking.id), + ('state', 'in', ['assigned', 'waiting', 'confirmed', 'done']), + ]) + + # ====================== + # LOGIKA STATUS + # ====================== + if gantung_qty_total == 0 and done_qty_total == 0: + # Semua barang udah terkirim, ga ada picking lain + picking.delivery_status = 'full' + + elif gantung_qty_total > 0 and total_qty > 0 and done_qty_total == 0: + # Masih ada picking lain dan sisa gantung → proses masih jalan + picking.delivery_status = 'partial' + + # elif gantung_qty_total > 0: + # # Ini picking terakhir, tapi qty belum full + # picking.delivery_status = 'partial_final' + + elif gantung_qty_total == 0 and done_qty_total > 0 and total_qty > 0: + # Udah kirim semua tapi masih ada picking lain (rare case) + picking.delivery_status = 'partial_final' + + else: + picking.delivery_status = 'none' @api.depends('name') def _compute_is_bu_iu(self): diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index cb630a04..99a74505 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -165,7 +165,7 @@ class TukarGuling(models.Model): @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" - if self.operations.picking_type_id.id not in [29, 30]: + if self.operations.picking_type_id.id not in [29, 30] and self.env.user.id != 1102: raise UserError("❌ Picking type harus BU/OUT atau BU/PICK") for rec in self: if rec.operations and rec.operations.picking_type_id.id == 30: @@ -412,7 +412,7 @@ class TukarGuling(models.Model): def write(self, vals): self.ensure_one() - if self.operations.picking_type_id.id not in [29, 30]: + if self.operations.picking_type_id.id not in [29, 30] and self.env.user.id != 1102: raise UserError("❌ Picking type harus BU/OUT atau BU/PICK") # self._check_invoice_on_retur_so() operasi = self.operations.picking_type_id.id @@ -498,8 +498,8 @@ class TukarGuling(models.Model): ('state', '!=', 'cancel'), ], limit=1) - if existing_tukar_guling: - raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) + # if existing_tukar_guling: + # raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) picking = self.operations if picking.picking_type_id.id == 30 and self.return_type == 'tukar_guling': raise UserError("❌ BU/PICK tidak boleh di retur tukar guling") diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index b9934d7a..bbcbac84 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -190,7 +190,12 @@ access_partial_delivery_wizard_line,access.partial.delivery.wizard.line,model_pa access_apo_domain_config,access.apo.domain.config,model_apo_domain_config,base.group_user,1,1,1,1 access_refund_sale_order,access.refund.sale.order,model_refund_sale_order,base.group_user,1,1,1,1 access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1 - +access_advance_payment_request,access.advance.payment.request,model_advance_payment_request,,1,1,1,1 +access_reimburse_line,access.reimburse.line,model_reimburse_line,,1,1,1,1 +access_advance_payment_settlement,access.advance.payment.settlement,model_advance_payment_settlement,,1,1,1,1 +access_advance_payment_usage_line,access.advance.payment.usage.line,model_advance_payment_usage_line,,1,1,1,1 +access_advance_payment_create_bill,access.advance.payment.create.bill,model_advance_payment_create_bill,,1,1,1,1 +access_create_reimburse_cab_wizard_user,create.reimburse.cab.wizard user,model_create_reimburse_cab_wizard,,1,1,1,1 access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1 access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1 diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index ba86277a..9df03674 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -63,6 +63,7 @@ decoration-info="payment_difficulty == 'normal'" decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')" decoration-danger="payment_difficulty == 'bermasalah'"/> + <field name="internal_notes_contact" readonly="1"/> <field name="invoice_origin"/> <field name="date_kirim_tukar_faktur"/> <field name="shipper_faktur_id"/> @@ -80,6 +81,7 @@ type="object" class="btn-primary" help="Sync Janji Bayar Customer ke Invoices dengan jumlah Due Date yang sama"/> + </field> <field name="to_check" position="after"> <field name="already_paid"/> diff --git a/indoteknik_custom/views/advance_payment_request.xml b/indoteknik_custom/views/advance_payment_request.xml new file mode 100644 index 00000000..2a8e1318 --- /dev/null +++ b/indoteknik_custom/views/advance_payment_request.xml @@ -0,0 +1,290 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <record id="view_form_advance_payment_request" model="ir.ui.view"> + <field name="name">advance.payment.request.form</field> + <field name="model">advance.payment.request</field> + <field name="arch" type="xml"> + <form string="Advance Payment Request & Reimburse"> + <header> + <button name="action_realisasi_pum" + type="object" + string="Realisasi" + class="btn-primary" + attrs="{'invisible': ['|', + ('status', '!=', 'approved'), + '|', ('type_request', '!=', 'pum'), + ('has_settlement', '=', True)]}"/> + <button name="action_approval_check" + type="object" + string="Checking/Approval" + class="btn-success" + attrs="{'invisible': [('status', 'in', ['approved','reject','draft'])]}"/> + <button name="action_confirm_payment" + type="object" + string="Konfirmasi Pembayaran" + class="btn-info" + attrs="{'invisible': ['|', ('status', 'not in', ['approved']), ('status_pay_down_payment', '=', 'payment')]}"/> + <button name="action_ap_only" + type="object" + string="Buat Jurnal PUM" + class="btn-info" + attrs="{'invisible': ['|', + ('status', 'not in', ['approved']), + '|', + ('is_cab_visible', '=', True), + ('type_request', '!=', 'pum') + ]}"/> + <button name="action_open_create_reimburse_cab" + type="object" + string="Buat Jurnal Reimburse" + class="btn-info" + attrs="{'invisible': ['|', + ('status', 'not in', ['approved']), + '|', + ('is_cab_visible', '=', True), + ('type_request', '!=', 'reimburse') + ]}"/> + <field name="status" widget="statusbar" + statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,approved" + statusbar_colors='{"reject":"red"}' + readonly="1"/> + </header> + <sheet> + <widget name="web_ribbon" title="Payment" attrs="{'invisible': ['|', ('status_pay_down_payment', '!=', 'payment'), ('status', '=', 'draft')]}"/> + <widget name="web_ribbon" title="Pending" bg_color="bg-danger" attrs="{'invisible': ['|', ('status_pay_down_payment', '!=', 'pending'), ('status', '=', 'draft')]}"/> + <div class="oe_button_box" name="button_box" style="right: 150px;"> + <field name="has_settlement" invisible="1"/> + <button name="action_realisasi_pum" + type="object" + class="oe_stat_button" + icon="fa-check-square-o" + style="width: 280px;" + attrs="{'invisible': ['|', ('status', '!=', 'approved'), ('has_settlement', '=', False)]}"> + <!-- <field name="settlement_ids" widget="statinfo" string="Realisasi PUM"/> --> + <field name="settlement_name" widget="statinfo" string="Realisasi PUM"/> + </button> + <field name="is_cab_visible" invisible="1"/> + <field name="is_current_user_ap" invisible="1"/> + <button type="object" + name="action_view_journal_uangmuka" + class="oe_stat_button" + icon="fa-book" + attrs="{'invisible': [('is_cab_visible', '=', False)]}" + style="width: 200px;"> + <field name="move_id" widget="statinfo" string="Journal Uang Muka"/> + <span class="o_stat_text"> + <t t-esc="record.move_id.name"/> + </span> + </button> + </div> + <div class="oe_title"> + <h1> + <field name="number" readonly="1"/> + </h1> + </div> + <group col="2"> + <group string=" "> + <field name="type_request" attrs="{'readonly': [('status', '=', 'approved')]}"/> + <field name="is_represented" attrs="{'readonly': [('status', '=', 'approved')], 'invisible': [('type_request', '=', 'reimburse')]}"/> + <field name="applicant_name" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/> + <field name="position_type" force_save="1" readonly="1"/> + <field name="nominal" colspan="2" attrs="{'readonly': ['|', ('status', '=', 'approved'), ('type_request', '=', 'reimburse')]}" force_save="1"/> + <p style="font-size: 10px; color: grey; font-style: italic" attrs="{'invisible': [('type_request', '!=', 'reimburse')]}">*Nominal terisi otomatis sesuai grand total rincian reimburse</p> + <field name="bank_name" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/> + <field name="account_name" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/> + <field name="bank_account" colspan="2" attrs="{'readonly': [('status', '=', 'approved')]}"/> + <field name="detail_note" attrs="{'readonly': [('status', '=', 'approved')]}"/> + <br/> + <field name="user_id" readonly="1"/> + <!-- <field name="position_type" readonly="1"/> --> + <!-- <field name="partner_id" readonly="1"/> --> + <field name="departement_type"/> + <field name="apr_perjalanan" attrs="{'invisible': [('type_request', '=', 'reimburse')]}"/> + <field name="date_back_to_office" attrs="{'invisible': [('apr_perjalanan', '=', False)]}"/> + <p style="font-size: 10px; color: grey; font-style: italic" attrs="{'invisible': [('apr_perjalanan', '=', False)]}">*Setelah tanggal kembali, pemohon diharapkan untuk segera memproses realisasi PUM</p> + <field name="estimated_return_date" readonly="1" widget="badge" attrs="{'invisible': [('type_request', '=', 'reimburse')]}"/> + <field name="days_remaining" readonly="1" widget="badge" attrs="{'invisible': [('type_request', '=', 'reimburse')]}"/> + <field name="approved_by" readonly="1"/> + <field name="create_date" readonly="1"/> + <field name="status_pay_down_payment" + readonly="1" + decoration-success="status_pay_down_payment == 'payment'" + decoration-danger="status_pay_down_payment == 'pending'" + widget="badge" invisible = "1"/> + </group> + <group string="Bukti Transfer"> + <field name="upload_attachment_date" readonly="1"/> + <field name="attachment_type" attrs="{'readonly': [('is_current_user_ap', '=', False)]}" /> + <field name="attachment_file_pdf" filename="attachment_filename" + widget="pdf_viewer" + attrs="{'invisible': [('attachment_type', '!=', 'pdf')], 'readonly': [('is_current_user_ap', '=', False)]}"/> + + <field name="attachment_file_image" filename="attachment_filename" + widget="image" + attrs="{'invisible': [('attachment_type', '!=', 'image')], 'readonly': [('is_current_user_ap', '=', False)]}" + style="max-width:250px; max-height:250px; object-fit:contain;"/> + <br/> + </group> + </group> + <notebook attrs="{'invisible': [('type_request', '!=', 'reimburse')]}"> + <page string="Rincian Reimburse"> + <field name="reimburse_line_ids"> + <tree> + <field name="date"/> + <field name="description"/> + <field name="account_id"/> + <!-- <field name="distance"/> --> + <field name="quantity"/> + <field name="price_unit"/> + <field name="total" sum="Total"/> + <field name="is_checked"/> + <field name="currency_id" invisible="1"/> + </tree> + <form> + <group col="2"> + <group string="Form"> + <field name="request_id" invisible="1"/> + <field name="date"/> + <field name="is_vehicle"/> + <field name="vehicle_type" attrs="{'invisible': [('is_vehicle', '=', False)]}"/> + <field name="description"/> + <field name="distance_departure" attrs="{'invisible': [('is_vehicle', '=', False)]}"/> + <field name="distance_return" attrs="{'invisible': [('is_vehicle', '=', False)]}"/> + <field name="quantity"/> + <field name="price_unit" attrs="{'readonly': [('is_vehicle', '=', True)]}" force_save ="1"/> + <field name="total" readonly="1"/> + <field name="currency_id" invisible="1"/> + <field name="attachment_type"/> + <field name="attachment_pdf" filename="attachment_filename" + widget="pdf_viewer" + attrs="{'invisible': [('attachment_type', '!=', 'pdf')]}"/> + <field name="attachment_image" filename="attachment_filename" + widget="image" + attrs="{'invisible': [('attachment_type', '!=', 'image')]}" + style="max-width:250px; max-height:250px; object-fit:contain;"/> + </group> + <group string="Finance"> + <field name="is_current_user_ap" invisible="1"/> + <field name="is_checked" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/> + <field name="account_id" placeholder="Hanya Finance yang boleh isi" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/> + </group> + </group> + </form> + </field> + <group class="oe_subtotal_footer oe_right" name="reimburse_total"> + <field name="currency_id" invisible="1"/> + <field name="grand_total_reimburse" + widget="monetary" + options="{'currency_field': 'currency_id'}"/> + </group> + </page> + </notebook> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> + </form> + </field> + </record> + + <record id="view_tree_advance_payment_request" model="ir.ui.view"> + <field name="name">advance.payment.request.tree</field> + <field name="model">advance.payment.request</field> + <field name="arch" type="xml"> + <tree> + <field name="number"/> + <field name="user_id" optional='hide'/> + <field name="applicant_name"/> + <field name="nominal"/> + <field name="departement_type" optional='hide'/> + <field name="status" + readonly="1" + decoration-success="status == 'approved'" + decoration-danger="status == 'reject'" + widget="badge" optional="show"/> + <field name="status_pay_down_payment" + readonly="1" + decoration-success="status_pay_down_payment == 'payment'" + decoration-danger="status_pay_down_payment == 'pending'" + widget="badge"/> + <field name="days_remaining" readonly="1" widget="badge" optional="hide"/> + <field name="estimated_return_date" widget="badge" optional="hide"/> + </tree> + </field> + </record> + + <record id="action_advance_payment_request" model="ir.actions.act_window"> + <field name="name">Pengajuan Uang Muka & Reimburse</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">advance.payment.request</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="menu_advance_payment_request_acct" + name="Pengajuan Uang Muka & 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..1a9d7908 --- /dev/null +++ b/indoteknik_custom/views/advance_payment_settlement.xml @@ -0,0 +1,184 @@ +<odoo> + <record id="view_form_advance_payment_settlement" model="ir.ui.view"> + <field name="name">advance.payment.settlement.form</field> + <field name="model">advance.payment.settlement</field> + <field name="arch" type="xml"> + <form string="Advance Payment Settlement"> + <header> + <button name="action_cab" + type="object" + class="btn-info" + attrs="{'invisible': [ '|', ('is_cab_visible', '=', True),('status', '!=', 'approved')]}" + string="Buat Jurnal Realisasi"/> + <button name="action_approval_check" + type="object" + string="Checking/Approval" + class="btn-success" + attrs="{'invisible': [('status', '=', 'approved')]}"/> + <field name="status" widget="statusbar" + statusbar_visible="pengajuan1,pengajuan2,pengajuan3,approved" + statusbar_colors='{"reject":"red"}' + readonly="1"/> + </header> + <sheet> + <div class="oe_button_box" name="button_box"> + <field name="is_cab_visible" invisible="1"/> + <field name="is_current_user_ap" invisible="1"/> + <button type="object" + name="action_view_journal_uangmuka" + class="oe_stat_button" + icon="fa-book" + attrs="{'invisible': [('is_cab_visible', '=', False)], 'readonly': [('is_current_user_ap', '=', False)]}" + style="width: 200px;"> + <field name="move_id" widget="statinfo" string="Journal Entries"/> + <span class="o_stat_text"> + <t t-esc="record.move_misc_id.name"/> + </span> + </button> + </div> + <div class="oe_title"> + <h1> + <field name="name" readonly="1"/> + </h1> + </div> + <group col="2"> + <group> + <field name="pum_id" readonly="1"/> + <field name="title" required="1"/> + <field name="goals" required="1"/> + <field name="related" required="1"/> + <field name="note_approval" required="1"/> + <field name="lot_of_attachment"/> + <field name="approved_by" readonly="1"/> + <field name="applicant_name" readonly="1"/> + <field name="user_id" readonly="1"/> + </group> + <group attrs="{'invisible': [('lot_of_attachment', '!=', 'one_for_all_line')]}"> + <field name="attachment_type" attrs="{'readonly': [('status', '=', 'approved')]}"/> + + <field name="attachment_file_pdf" filename="attachment_filename" + widget="pdf_viewer" + attrs="{'invisible': [('attachment_type', '!=', 'pdf')], 'readonly': [('status', '=', 'approved')]}"/> + + <field name="attachment_file_image" filename="attachment_filename" + widget="image" + attrs="{'invisible': [('attachment_type', '!=', 'image')], 'readonly': [('status', '=', 'approved')]}" + style="max-width:250px; max-height:250px; object-fit:contain;"/> + <br/> + </group> + </group> + + <notebook> + <page string="Rincian Penggunaan"> + <field name="penggunaan_line_ids" nolabel="1"> + <tree> + <field name="date"/> + <field name="description"/> + <field name="nominal" sum="Total Penggunaan"/> + <field name="done_attachment"/> + </tree> + + <form> + <group col="2"> + <group string = "Form"> + <field name="lot_of_attachment" invisible="1"/> + <field name="date"/> + <field name="description"/> + <field name="nominal"/> + <field name="attachment_type" + attrs="{ + 'invisible': [('lot_of_attachment', '=', 'one_for_all_line')] + }"/> + <field name="attachment_file_pdf" + filename="attachment_filename_pdf" + widget="pdf_viewer" + attrs="{ + 'invisible': [ + '|', + ('lot_of_attachment', '=', 'one_for_all_line'), + ('attachment_type', '!=', 'pdf') + ] + }"/> + <field name="attachment_file_image" + filename="attachment_filename_image" + widget="image" + attrs="{ + 'invisible': [ + '|', + ('lot_of_attachment', '=', 'one_for_all_line'), + ('attachment_type', '!=', 'image') + ] + }" + style="max-width:250px; max-height:250px; object-fit:contain;"/> + + </group> + <group string="Finance"> + <field name="is_current_user_ap" invisible="1"/> + <field name="account_id" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/> + <field name="done_attachment" attrs="{'readonly': [('is_current_user_ap', '=', False)]}"/> + </group> + </group> + </form> + </field> + </page> + </notebook> + + <div style="text-align:right;"> + <button name="action_toggle_check_attachment" + type="object" + string="Check/Uncheck All Line Use PUM" + class="btn-secondary"/> + </div> + + <group col="2"> + <group class="oe_subtotal_footer oe_right"> + <field name="currency_id" invisible="1"/> + <field name="grand_total_use" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/> + <field name="nominal_pum" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}" style="font-weight: bold;"/> + <field name="remaining_value" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/> + </group> + </group> + </sheet> + + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> + </form> + </field> + </record> + + <record id="action_advance_payment_settlement" model="ir.actions.act_window"> + <field name="name">Realisasi Pengajuan Uang Muka</field> + <field name="res_model">advance.payment.settlement</field> + <field name="view_mode">tree,form</field> + </record> + + <record id="view_tree_advance_payment_settlement" model="ir.ui.view"> + <field name="name">advance.payment.settlement.tree</field> + <field name="model">advance.payment.settlement</field> + <field name="arch" type="xml"> + <tree create="false" delete="false"> + <field name="name"/> + <field name="pum_id"/> + <field name="grand_total_use" string="Total Realisasi"/> + <field name="remaining_value" string="Sisa PUM"/> + <field name="status" widget="badge" decoration-success="status == 'approved'"/> + </tree> + </field> + </record> + + <menuitem id="menu_advance_payment_settlement_acct" + name="Realisasi PUM" + parent="account.menu_finance_entries" + sequence="114" + action="action_advance_payment_settlement" + /> + + <menuitem id="menu_advance_payment_settlement_sales" + name="Realisasi PUM" + parent="indoteknik_custom.menu_monitoring_in_sale" + sequence="101" + action="action_advance_payment_settlement" + /> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/apache_solr_queue.xml b/indoteknik_custom/views/apache_solr_queue.xml index 4c145b9f..08972b28 100644 --- a/indoteknik_custom/views/apache_solr_queue.xml +++ b/indoteknik_custom/views/apache_solr_queue.xml @@ -9,7 +9,7 @@ <field name="display_name" readonly="1" /> <field name="res_model" readonly="1" /> <field name="res_id" readonly="1" /> - <field name="function_name" readonly="1" /> + <field name="function_name" readonly="1" optional="hide"/> <field name="execute_status" widget="badge" @@ -18,6 +18,7 @@ decoration-success="execute_status == 'success'" decoration-primary="execute_status == 'not_found'" /> + <field name = "log" readonly="1" optional="hide"/> <field name="execute_date" readonly="1" /> <field name="create_date" readonly="1" /> </tree> diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index 5ea7324b..543f1fd1 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -226,6 +226,27 @@ <field name="number_next_actual">1</field> <field name="number_increment">1</field> </record> + + <record id="sequence_advance_payment_request" model="ir.sequence"> + <field name="name">Advance Payment Request Sequence</field> + <field name="code">advance.payment.request</field> + <field name="prefix">PUM/%(year)s/%(month)s/</field> + <field name="padding">4</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + <field name="active">True</field> + </record> + + <record id="sequence_reimburse_request" model="ir.sequence"> + <field name="name">Reimburse Request Sequence</field> + <field name="code">reimburse.request</field> + <field name="prefix">RMK/%(year)s/%(month)s/</field> + <field name="padding">4</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + <field name="active">True</field> + </record> + <record id="seq_refund_sale_order" model="ir.sequence"> <field name="name">Refund Sales Order</field> <field name="code">refund.sale.order</field> diff --git a/indoteknik_custom/views/mail_template_pum.xml b/indoteknik_custom/views/mail_template_pum.xml new file mode 100644 index 00000000..81f8ada8 --- /dev/null +++ b/indoteknik_custom/views/mail_template_pum.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="0"> + + <record id="mail_template_pum_reminder_today" model="mail.template"> + <field name="name">Reminder PUM: Hari Ini</field> + <field name="model_id" ref="indoteknik_custom.model_advance_payment_request"/> + <field name="subject">Reminder Realisasi PUM - ${object.number}</field> + <field name="email_from">${object.email_ap}</field> + <field name="email_to">andrifebriyadiputra@gmail.com</field> + <field name="body_html" type="html"> + <div> + <p><b>Dengan Hormat Bpk/Ibu ${object.user_id.display_name},</b></p> + + <p> + Berikut terlampir pengajuan PUM <b>${object.number}</b> sebesar + <b>${format_amount(object.nominal, object.currency_id)}</b> dari PT. INDOTEKNIK DOTCOM GEMILANG + pada tanggal ${format_date(object.create_date, 'd MMMM yyyy')}.<br/> + Tolong segera selesaikan realisasi PUM tersebut dengan menyertakan dokumen asli untuk mendukung realisasi tersebut + <b>maksimal 7 hari dari sekarang</b>.<br/> + Terima Kasih + </p> + + <br/><br/> + + <p><b>Best Regards,</b></p> + + <br/> + <p><b> + Dept. Finance<br/> + PT. INDOTEKNIK DOTCOM GEMILANG<br/> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" alt="Indoteknik" style="max-width: 18%; height: auto;"></img><br/> + </b></p> + <p><i>Email ini dikirim otomatis, abaikan bila sudah melakukan realisasi.</i></p> + </div> + </field> + <field name="auto_delete" eval="True"/> + </record> + + <record id="mail_template_pum_reminder_h_2" model="mail.template"> + <field name="name">Reminder Realisasi PUM: H-2</field> + <field name="model_id" ref="indoteknik_custom.model_advance_payment_request"/> + <field name="subject">Reminder Realisasi PUM (H-2) - ${object.number}</field> + <field name="email_from">${object.email_ap}</field> + <field name="email_to">andrifebriyadiputra@gmail.com</field> + <field name="body_html" type="html"> + <div> + <p><b>Dengan Hormat Bpk/Ibu ${object.user_id.display_name},</b></p> + + <p> + Berikut terlampir pengajuan PUM <b>${object.number}</b> sebesar + <b>${format_amount(object.nominal, object.currency_id)}</b> dari PT. INDOTEKNIK DOTCOM GEMILANG + pada tanggal ${format_date(object.create_date, 'd MMMM yyyy')}.<br/> + Tolong segera selesaikan realisasi PUM tersebut dengan menyertakan dokumen asli untuk mendukung PUM tersebut + <b>batas waktu tersisa 2 hari lagi</b>.<br/> + Terima Kasih + </p> + + <br/><br/> + + <p><b>Best Regards,</b></p> + + <br/> + <p><b> + Dept. Finance<br/> + PT. INDOTEKNIK DOTCOM GEMILANG<br/> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" alt="Indoteknik" style="max-width: 18%; height: auto;"></img><br/> + </b></p> + <p><i>Email ini dikirim otomatis, abaikan bila sudah melakukan realisasi.</i></p> + </div> + </field> + <field name="auto_delete" eval="True"/> + </record> + + </data> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 82daa36f..a540caa7 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -135,6 +135,7 @@ <attribute name="invisible">1</attribute> </field> <field name="user_id" position="after"> + <field name="internal_notes_contact" readonly="1"/> <field name="hold_outgoing" readonly="1" /> <field name="date_hold" readonly="1" widget="datetime" /> <field name="date_unhold" readonly="1" widget="datetime" /> diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml index f9f8f386..3965c62f 100644 --- a/indoteknik_custom/views/sourcing.xml +++ b/indoteknik_custom/views/sourcing.xml @@ -1,10 +1,29 @@ <?xml version="1.0" encoding="UTF-8"?> <odoo> + <record id="view_sourcing_job_order_search" model="ir.ui.view"> + <field name="name">sourcing.job.order.search</field> + <field name="model">sourcing.job.order</field> + <field name="arch" type="xml"> + <search string="Search Sourcing Job Order"> + <field name="state" string="Name"/> + <field name="user_id" string="MD Person"/> + <filter name="my_job" + string="My Sourcing Job" + domain="[('user_id', '=', uid), ('state', '=', 'taken')]"/> + <filter name="untaken" + string="Untaken" + domain="[('state', '=', 'draft')]" /> + <filter name="done" + string="Complete" + domain="[('state', '=', 'done')]" /> + </search> + </field> + </record> <record id="view_sourcing_job_order_tree" model="ir.ui.view"> <field name="name">sourcing.job.order.tree</field> <field name="model">sourcing.job.order</field> <field name="arch" type="xml"> - <tree string="Sourcing Job Orders" + <tree string="Sourcing Job Orders" default_order="state asc, create_date desc" decoration-success="state=='done'" decoration-danger="state=='cancel'" decoration-warning="state=='taken'"> @@ -38,6 +57,28 @@ icon="fa-times" groups="indoteknik_custom.group_role_merchandiser" attrs="{'invisible': [('state', 'in', ['cancel', 'done'])]}"/> + + <button name="action_ask_approval" + string="Ask Approval" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': ['|','|', ('has_price_in_lines', '=', False), ('approval_sales', '=', 'approve'), ('is_creator_same_user', '=', True)]}"/> + + <button name="action_confirm_by_md" + string="Confirm" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_merchandiser" + attrs="{'invisible': [('is_creator_same_user', '=', False)]}"/> + + <button name="action_confirm_approval" + string="Approve" + type="object" + class="btn-primary" + groups="indoteknik_custom.group_role_sales" + attrs="{'invisible': [('approval_sales', 'in', [False, 'approve'])]}"/> + <button name="action_request_takeover" string="Request Takeover" type="object" @@ -61,10 +102,19 @@ <field name="state" widget="statusbar" statusbar_visible="draft,taken,done,cancel" - statusbar_colors='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/> + statusbar_color='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/> </header> <sheet> + <widget name="web_ribbon" + title="COMPLETE" + bg_color="bg-success" + attrs="{'invisible': [('state', '!=', 'done')]}"/> + + <widget name="web_ribbon" + title="CANCEL" + bg_color="bg-danger" + attrs="{'invisible': [('state', '!=', 'cancel')]}"/> <h1> <field name="name" readonly="1"/> </h1> @@ -73,6 +123,7 @@ <group> <!-- <field name="leads_id"/> --> <field name="eta_sales"/> + <field name="is_creator_same_user" invisible="1"/> <field name="takeover_request" invisible="1"/> <field name="can_request_takeover" invisible="1"/> <field name="can_approve_takeover" invisible="1"/> @@ -82,6 +133,7 @@ <group> <field name="create_uid" readonly="1" widget="many2one_avatar_user"/> <field name="user_id" readonly="1" widget="many2one_avatar_user"/> + <field name="approval_sales" readonly="1"/> </group> </group> @@ -99,7 +151,10 @@ <!-- MD EDIT --> <page string="MD Lines" groups="indoteknik_custom.group_role_merchandiser"> <field name="line_md_edit_ids"> - <tree editable="bottom"> + <tree editable="bottom" + decoration-success="state in ('done', 'convert')" + decoration-danger="state=='cancel'" + decoration-warning="state=='sourcing'"> <field name="code"/> <field name="product_name"/> <field name="descriptions"/> @@ -108,6 +163,16 @@ <field name="vendor_id"/> <field name="tax_id"/> <field name="subtotal"/> + <field name="product_category"/> + <field name="product_type"/> + <field name="product_class"/> + <field name="state"/> + <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/> + <button name="action_convert_to_product" + string="Convert" + type="object" + icon="fa-exchange" + attrs="{'invisible': [('state', '!=', 'done')]}"/> </tree> </field> </page> @@ -124,6 +189,7 @@ <field name="vendor_id" readonly="1"/> <field name="tax_id" readonly="1"/> <field name="subtotal" readonly="1"/> + <field name="state" readonly="1"/> </tree> </field> </page> @@ -162,6 +228,8 @@ <field name="name">Sourcing Job Orders</field> <field name="res_model">sourcing.job.order</field> <field name="view_mode">tree,form</field> + <field name="search_view_id" ref="view_sourcing_job_order_search"/> + <field name="context">{'search_default_untaken': 1, 'search_default_my_job': 1}</field> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face"> Buat Sourcing Job Order baru di sini ✨ diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 44ab6355..050fc819 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -226,6 +226,10 @@ <group> <group> <field name="notee"/> + <field name="qty_yang_mau_dikirim"/> + <field name="qty_terkirim"/> + <field name="qty_gantung"/> + <field name="delivery_status"/> <field name="note_logistic"/> <field name="note_info"/> <field name="responsible"/> @@ -394,6 +398,8 @@ decoration-danger="qty_done>product_uom_qty and state!='done' and parent.picking_type_code != 'incoming'" decoration-success="qty_done==product_uom_qty and state!='done' and not result_package_id"> <field name="note" placeholder="Add a note here"/> + <field name="outstanding_qty"/> + <field name="delivery_status" widget="badge" options="{'colors': {'full': 'success', 'partial': 'warning', 'partial_final': 'danger'}}"/> </tree> </field> </record> |
