diff options
26 files changed, 1414 insertions, 337 deletions
diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py index dde30fec..3511bc52 100644 --- a/indoteknik_api/controllers/api_v1/user.py +++ b/indoteknik_api/controllers/api_v1/user.py @@ -89,7 +89,9 @@ class User(controller.Controller): 'name': name, 'login': email, 'oauth_provider_id': request.env.ref('auth_oauth.provider_google').id, - 'sel_groups_1_9_10': 9 + 'sel_groups_1_9_10': 9, + 'active': True, + } user = request.env['res.users'].create(user_data) diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 360303b8..09a3aa6f 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -166,16 +166,19 @@ 'report/report_invoice.xml', 'report/report_picking.xml', 'report/report_sale_order.xml', + 'report/purchase_report.xml', 'views/vendor_sla.xml', 'views/coretax_faktur.xml', 'views/public_holiday.xml', 'views/stock_inventory.xml', 'views/sale_order_delay.xml', + 'views/refund_sale_order.xml', 'views/tukar_guling.xml', # 'views/tukar_guling_return_views.xml' 'views/tukar_guling_po.xml', # 'views/refund_sale_order.xml', 'views/update_date_planned_po_wizard_view.xml', + # 'views/reimburse.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index c44cad78..70cd07e4 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -105,6 +105,34 @@ class AccountMove(models.Model): tracking=True ) + # def _check_and_lock_cbd(self): + # cbd_term = self.env['account.payment.term'].browse(26) + # today = date.today() + + # # Cari semua invoice overdue + # overdue_invoices = self.search([ + # ('move_type', '=', 'out_invoice'), + # ('state', '=', 'posted'), + # ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']), + # ('invoice_date_due', '!=', False), + # ('invoice_date_due', '<=', today - timedelta(days=30)), + # ], limit=3) + + # _logger.info(f"Found {len(overdue_invoices)} overdue invoices for CBD lock check.") + # _logger.info(f"Overdue Invoices: {overdue_invoices.mapped('name')}") + + # # Ambil partner unik dari invoice + # partners_to_lock = overdue_invoices.mapped('partner_id').filtered(lambda p: not p.is_cbd_locked) + # _logger.info(f"Partners to lock: {partners_to_lock.mapped('name')}") + + # # Lock hanya partner yang belum locked + # if partners_to_lock: + # partners_to_lock.write({ + # 'is_cbd_locked': True, + # 'property_payment_term_id': cbd_term.id, + # }) + + def compute_partial_payment(self): for move in self: if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial': @@ -179,9 +207,8 @@ class AccountMove(models.Model): ('state', '=', 'posted'), ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']), ('invoice_date_due', 'in', target_dates), - ('date_terima_tukar_faktur', '!=', False), - ('partner_id', 'in' , [94603]) - ], limit=5) + ('date_terima_tukar_faktur', '!=', False) + ]) _logger.info(f"Invoices: {invoices}") invoices = invoices.filtered( diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 8618856a..449bd90b 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -69,8 +69,8 @@ class ApprovalPaymentTerm(models.Model): return res def _track_changes_for_user_688(self, vals, old_values_dict): - if self.env.user.id != 688: - return + # if self.env.user.id != 688: + # return tracked_fields = {"blocking_stage", "warning_stage", "property_payment_term_id"} @@ -106,7 +106,8 @@ class ApprovalPaymentTerm(models.Model): if changes: timestamp = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S') - rec.change_log_688 = f"{timestamp} - Perubahan oleh Widya:\n" + "\n".join(changes) + user = self.env.user + rec.change_log_688 = f"{timestamp} - Perubahan oleh {user.name}:\n" + "\n".join(changes) @staticmethod @@ -171,7 +172,8 @@ class ApprovalPaymentTerm(models.Model): 'blocking_stage': self.blocking_stage, 'warning_stage': self.warning_stage, 'active_limit': self.active_limit, - 'property_payment_term_id': self.property_payment_term_id.id + 'property_payment_term_id': self.property_payment_term_id.id, + 'is_cbd_locked': False, }) self.approve_date = datetime.utcnow() self.state = 'approved' diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index 5a6aebac..9feea1d1 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -1,6 +1,6 @@ from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError -from datetime import timedelta +from datetime import timedelta, date import logging @@ -149,4 +149,5 @@ class DunningRunLine(models.Model): total_amt = fields.Float(string='Total Amount') open_amt = fields.Float(string='Open Amount') due_date = fields.Date(string='Due Date') + payment_term = fields.Many2one('account.payment.term', related='invoice_id.invoice_payment_term_id', string='Payment Term') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 50913a80..18811b85 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -66,7 +66,7 @@ class PurchaseOrder(models.Model): sale_order = fields.Char(string='Sale Order') matches_so = fields.Many2many('sale.order', string='Matches SO', compute='_compute_matches_so') is_create_uangmuka = fields.Boolean(string='Uang Muka?') - move_id = fields.Many2one('account.move', string='Journal Entries Uang Muka', domain=[('move_type', '=', 'entry')]) + move_id = fields.Many2one('account.move', string='Journal Entries Uang Muka', domain=[('move_type', '=', 'entry')], copy=False) logbook_bill_id = fields.Many2one('report.logbook.bill', string='Logbook Bill') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py index b18864f3..084b93f7 100644 --- a/indoteknik_custom/models/purchase_order_sales_match.py +++ b/indoteknik_custom/models/purchase_order_sales_match.py @@ -39,7 +39,7 @@ class PurchaseOrderSalesMatch(models.Model): ('sale_line_id', '=', rec.sale_line_id.id), ]) if stock_move: - rec.bu_pick = stock_move.picking_id.id + rec.bu_pick = stock_move[0].picking_id.id else: rec.bu_pick = None diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 11bfd07f..f511ed5d 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -10,20 +10,24 @@ from lxml import etree class RefundSaleOrder(models.Model): _name = 'refund.sale.order' _description = 'Refund Sales Order' - _inherit = ['mail.thread'] + _inherit = ['mail.thread', 'mail.activity.mixin'] _rec_name = 'name' name = fields.Char(string='Refund Number', default='New', copy=False, readonly=True) note_refund = fields.Text(string='Note Refund') sale_order_ids = fields.Many2many('sale.order', string='Sales Order Numbers') uang_masuk = fields.Float(string='Uang Masuk', required=True) - total_invoice = fields.Float(string='Total Invoice') + total_invoice = fields.Float(string='Total Order') ongkir = fields.Float(string='Ongkir', required=True, default=0.0) amount_refund = fields.Float(string='Total Refund', required=True) amount_refund_text = fields.Char(string='Total Refund Text', compute='_compute_refund_text') user_ids = fields.Many2many('res.users', string='Salespersons', compute='_compute_user_ids', domain=[('active', 'in', [True, False])]) create_uid = fields.Many2one('res.users', string='Created By', readonly=True) created_date = fields.Date(string='Tanggal Request Refund', readonly=True) + sale_order_count = fields.Integer( + string="Sale Order Count", + compute="_compute_sale_order_count", + ) status = fields.Selection([ ('draft', 'Draft'), ('pengajuan1', 'Approval Sales Manager'), @@ -45,6 +49,7 @@ class RefundSaleOrder(models.Model): bank = fields.Char(string='Bank', required=True) account_name = fields.Char(string='Account Name', required=True) account_no = fields.Char(string='Account No', required=True) + kcp = fields.Char(string='Alamat KCP') finance_note = fields.Text(string='Finance Note') invoice_names = fields.Html(string="Group Invoice Number", compute="_compute_invoice_names") so_names = fields.Html(string="Group SO Number", compute="_compute_so_names") @@ -55,9 +60,36 @@ class RefundSaleOrder(models.Model): ('uang', 'Refund Lebih Bayar'), ('retur_half', 'Refund Retur Sebagian'), ('retur', 'Refund Retur Full'), - ('lainnya', 'Lainnya') + ('salah_transfer', 'Salah Transfer') ], string='Refund Type', required=True) + tukar_guling_ids = fields.One2many( + 'tukar.guling', 'refund_id', string="Pengajuan Return SO", + ) + + picking_ids = fields.Many2many( + 'stock.picking', + string="Pickings", + compute="_compute_picking_ids", + ) + + transfer_move_id = fields.Many2one( + 'account.move', + string="Journal Payment", + copy=False, + help="Pilih transaksi salah transfer dari jurnal Uang Muka (journal_id=11) yang tidak terkait SO." + ) + + tukar_guling_count = fields.Integer( + string="Tukar Guling Count", + compute="_compute_tukar_guling_count" + ) + + has_picking = fields.Boolean( + string="Has Picking", + compute="_compute_has_picking", + ) + refund_type_display = fields.Char(string="Refund Type Label", compute="_compute_refund_type_display") line_ids = fields.One2many('refund.sale.order.line', 'refund_id', string='Refund Lines') @@ -89,7 +121,7 @@ class RefundSaleOrder(models.Model): bukti_refund_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), - ], string="Attachment Type", default='image') + ], string="Attachment Type") bukti_uang_masuk_image = fields.Binary(string="Upload Bukti Uang Masuk") bukti_transfer_refund_image = fields.Binary(string="Upload Bukti Transfer Refund") bukti_uang_masuk_pdf = fields.Binary(string="Upload Bukti Uang Masuk") @@ -107,28 +139,80 @@ class RefundSaleOrder(models.Model): is_locked = fields.Boolean(string="Locked", compute="_compute_is_locked") sale_order_names_jasper = fields.Char(string='Sales Order List', compute='_compute_order_invoice_names') invoice_names_jasper = fields.Char(string='Invoice List', compute='_compute_order_invoice_names') - - - - @api.depends('refund_type') - def _compute_refund_type_display(self): - for rec in self: - rec.refund_type_display = dict(self.fields_get(allfields=['refund_type'])['refund_type']['selection']).get(rec.refund_type, '') + so_order_line_ids = fields.Many2many( + "sale.order.line", string="SO Order Lines", compute="_compute_so_order_lines", store=False + ) + currency_id = fields.Many2one( + "res.currency", string="Currency", + default=lambda self: self.env.company.currency_id, required=True + ) + + amount_untaxed = fields.Monetary( + string="Untaxed Amount", compute="_compute_amount_from_so", + ) + amount_tax = fields.Monetary( + string="Taxes", compute="_compute_amount_from_so", + ) + amount_total = fields.Monetary( + string="Total", compute="_compute_amount_from_so", + ) + total_margin = fields.Monetary( + string="Total Margin", compute="_compute_amount_from_so", + ) + grand_total = fields.Monetary( + string="Grand Total", compute="_compute_amount_from_so", + ) + delivery_amt = fields.Monetary( + string="Delivery Amount", help="Ongkos kirim yang Dibayarkan Customer", default=0.0, compute="_compute_amount_from_so", + ) + remaining_refundable = fields.Float( + string="Sisa Uang Masuk", + help="Sisa uang masuk yang masih bisa direfund (hanya berlaku untuk 1 SO)", + ) + show_return_alert = fields.Boolean(compute="_compute_show_return_alert") + show_approval_alert = fields.Boolean(compute="_compute_show_approval_alert") + + + @api.onchange('refund_type', 'partner_id') + def _onchange_refund_type_partner(self): + if self.refund_type == 'salah_transfer' and self.partner_id: + return { + 'domain': { + 'transfer_move_id': [ + ('journal_id', '=', 11), + ('line_ids.partner_id', '=', self.partner_id.id), + ('state', '=', 'posted'), + ('sale_id', '=', False), + ] + } + } + else: + return { + 'domain': {'transfer_move_id': [('id', '=', 0)]} + } + + @api.onchange('transfer_move_id') + def _onchange_transfer_move_id(self): + """Set nilai uang_masuk dari move yang dipilih""" + if self.transfer_move_id and self.refund_type == 'salah_transfer': + self.uang_masuk = self.transfer_move_id.amount_total_signed + elif self.refund_type != 'salah_transfer' and not self.sale_order_ids: + self.uang_masuk = 0.0 - @api.model def create(self, vals): allowed_user_ids = [23, 19, 688, 7] if not ( self.env.user.has_group('indoteknik_custom.group_role_sales') or self.env.user.has_group('indoteknik_custom.group_role_fat') or - self.env.user.id not in allowed_user_ids + self.env.user.id in allowed_user_ids ): - raise UserError("❌ Hanya user Sales dan Finance yang boleh membuat refund.") + raise UserError("❌ Hanya Sales dan Finance yang boleh membuat refund.") if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('refund.sale.order') or 'New' + vals['created_date'] = fields.Date.context_today(self) vals['create_uid'] = self.env.user.id @@ -138,6 +222,9 @@ class RefundSaleOrder(models.Model): so_ids = so_cmd[0][2] if so_cmd and so_cmd[0][0] == 6 else [] if so_ids: sale_orders = self.env['sale.order'].browse(so_ids) + partner = sale_orders.mapped('partner_id.id') + if len(partner) > 1: + raise UserError("❌ Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.") vals['partner_id'] = sale_orders[0].partner_id.id invoices = sale_orders.mapped('invoice_ids').filtered( @@ -150,40 +237,79 @@ class RefundSaleOrder(models.Model): refund_type = vals.get('refund_type') invoice_ids_data = vals.get('invoice_ids', []) invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else [] - if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: - raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice") + raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice") - if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: - raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") + if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']: + raise UserError("Refund type Lebih Bayar dan Barang Kosong Sebagian Hanya Bisa dipilih Jika Ada Invoice") + if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and so_ids: + sale_orders = self.env['sale.order'].browse(so_ids) + + if refund_type == 'barang_kosong': + zero_delivery_lines = sale_orders.mapped('order_line').filtered( + lambda l: l.qty_delivered == 0 and l.product_uom_qty > 0 + ) + if not zero_delivery_lines: + raise UserError("❌ Tidak ada barang kosong di SO yang terpilih.") + + elif refund_type == 'barang_kosong_sebagian': + partial_delivery_lines = sale_orders.mapped('order_line').filtered( + lambda l: l.qty_delivered >= 0 and l.product_uom_qty > l.qty_delivered + ) + if not partial_delivery_lines: + raise UserError("❌ Tidak ada barang yang tidak Terkirim/Kosong di SO yang dipilih.") - if not so_ids and refund_type != 'lainnya': - raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") - - refund = refund_type in ['retur', 'retur_half'] - if refund and so_ids: - so = self.env['sale.order'].browse(so_ids) - pickings = self.env['stock.picking'].search([ - ('state', '=', 'done'), - ('picking_type_id', '=', 73), - ('sale_id', 'in', so_ids) + + if not so_ids and refund_type != 'salah_transfer': + raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Salah Transfer'.") + + if refund_type == 'salah_transfer' and vals.get('transfer_move_id'): + move = self.env['account.move'].browse(vals['transfer_move_id']) + if move: + vals['uang_masuk'] = move.amount_total_signed + vals['remaining_refundable'] = 0 + else: + # ==== perhitungan normal ==== + moves = self.env['account.move'].search([ + ('sale_id', 'in', so_ids), + ('journal_id', '=', 11), + ('state', '=', 'posted'), ]) - if not pickings: - raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.") + total_uang_muka = sum(moves.mapped('amount_total_signed')) if moves else 0.0 + total_midtrans = sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) if so_ids else 0.0 + total_pembayaran = total_uang_muka + total_midtrans + + existing_refunds = self.env['refund.sale.order'].search([ + ('sale_order_ids', 'in', so_ids) + ], order='id desc', limit=1) - if refund_type == 'retur_half' and not invoice_ids: - raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.") + if existing_refunds: + sisa_uang_masuk = existing_refunds.remaining_refundable + else: + sisa_uang_masuk = total_pembayaran - total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total')) if invoice_ids else 0.0 - uang_masuk = vals.get('uang_masuk', 0.0) - ongkir = vals.get('ongkir', 0.0) - pengurangan = total_invoice + ongkir + if sisa_uang_masuk < 0: + raise UserError("❌ Tidak ada sisa transaksi untuk di-refund.") - if uang_masuk > pengurangan: - vals['amount_refund'] = uang_masuk - pengurangan - else: - raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir untuk melakukan refund") + vals['uang_masuk'] = sisa_uang_masuk + + total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 + vals['total_invoice'] = total_invoice + amount_refund = vals.get('amount_refund', 0.0) + if amount_refund <= 0.00: + raise ValidationError('Total Refund harus lebih dari 0 jika ingin mengajukan refund') + + if so_ids and len(so_ids) > 1: + existing_refund = self.search([('sale_order_ids', 'in', so_ids)], limit=1) + if existing_refund: + raise UserError("❌ Refund multi SO hanya bisa 1 kali.") + vals['remaining_refundable'] = 0.0 + elif so_ids and len(so_ids) == 1 and refund_type != 'salah_transfer': + remaining = vals['uang_masuk'] - amount_refund + if remaining < 0: + raise ValidationError("❌ Tidak ada sisa transaksi untuk di-refund di SO ini. Semua dana sudah dikembalikan.") + vals['remaining_refundable'] = remaining return super().create(vals) @@ -212,6 +338,9 @@ class RefundSaleOrder(models.Model): if so_ids: sale_orders = self.env['sale.order'].browse(so_ids) + partner = sale_orders.mapped('partner_id.id') + if len(partner) > 1: + raise UserError("❌ Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.") vals['partner_id'] = sale_orders[0].partner_id.id sale_orders = self.env['sale.order'].browse(so_ids) @@ -229,8 +358,13 @@ class RefundSaleOrder(models.Model): refund_type = vals.get('refund_type', rec.refund_type) - if not so_ids and refund_type != 'lainnya': - raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") + if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and sale_orders: + zero_delivery_lines = sale_orders.mapped('order_line').filtered(lambda l: l.qty_delivered >= 0 or l.product_uom_qty > l.qty_delivered) + if not zero_delivery_lines: + raise UserError("❌ Tidak ada barang yang Tidak Terikirim di Sales Order yang dipilih.") + + if not so_ids and refund_type != 'salah_transfer': + raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Salah Transfer'.") invoice_ids = vals.get('invoice_ids', False) @@ -245,44 +379,53 @@ class RefundSaleOrder(models.Model): else: invoice_ids = rec.invoice_ids.ids - if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: - raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice") + if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur']: + raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice") - if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: + if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']: raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") + if refund_type == 'salah_transfer' and vals.get('transfer_move_id'): + move = self.env['account.move'].browse(vals['transfer_move_id']) + if move: + vals['uang_masuk'] = move.amount_total_signed - if refund_type in ['retur', 'retur_half'] and so_ids: - so = self.env['sale.order'].browse(so_ids) - pickings = self.env['stock.picking'].search([ - ('state', '=', 'done'), - ('picking_type_id', '=', 73), - ('sale_id', 'in', so_ids) - ]) + if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids', 'amount_refund']): + total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) + vals['total_invoice'] = total_invoice + uang_masuk = rec.uang_masuk - if not pickings: - raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.") + amount_refund = vals.get('amount_refund', rec.amount_refund) - if refund_type == 'retur_half' and not invoice_ids: - raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk retur sebagian.") + if amount_refund <= 0: + raise ValidationError("Total Refund harus lebih dari 0.") - if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids']): - total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total')) - uang_masuk = vals.get('uang_masuk', rec.uang_masuk) - ongkir = vals.get('ongkir', rec.ongkir) + existing_refunds = self.search([ + ('sale_order_ids', 'in', so_ids), + ('id', '!=', rec.id) + ]) + total_refunded = sum(existing_refunds.mapped('amount_refund')) + if existing_refunds: + remaining = uang_masuk - total_refunded + else: + remaining = uang_masuk - amount_refund - if uang_masuk <= (total_invoice + ongkir): - raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir") - vals['amount_refund'] = uang_masuk - (total_invoice + ongkir) + if remaining < 0: + raise ValidationError("Semua dana sudah dikembalikan, tidak bisa mengajukan refund") - if vals.get('status') == 'refund' and not vals.get('refund_date'): - vals['refund_date'] = fields.Date.context_today(self) + vals['remaining_refundable'] = remaining return super().write(vals) + + @api.onchange('amount_refund') + def _onchange_refund_fields(self): + for rec in self: + refund_input = rec.amount_refund or 0.0 + rec.remaining_refundable = (rec.uang_masuk or 0.0) - refund_input - @api.depends('status_payment') + @api.depends('status_payment', 'status') def _compute_is_locked(self): for rec in self: - rec.is_locked = rec.status_payment in ['done', 'reject'] + rec.is_locked = rec.status_payment in ['done', 'reject'] or rec.status in ['pengajuan3', 'refund', 'reject'] @api.depends('sale_order_ids.name', 'invoice_ids.name') def _compute_order_invoice_names(self): @@ -319,13 +462,28 @@ class RefundSaleOrder(models.Model): all_invoices = self.env['account.move'] total_invoice = 0.0 + so_ids = self.sale_order_ids.ids + amount_refund_before = 0.0 for so in self.sale_order_ids: self.ongkir += so.delivery_amt or 0.0 valid_invoices = so.invoice_ids.filtered( lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel' ) all_invoices |= valid_invoices - total_invoice += sum(valid_invoices.mapped('amount_total')) + total_invoice += sum(valid_invoices.mapped('amount_total_signed')) + refunds = self.env['refund.sale.order'].search([ + ('sale_order_ids', 'in', so_ids) + ]) + amount_refund_before += sum(refunds.mapped('amount_refund')) if refunds else 0.0 + + moves = self.env['account.move'].search([ + ('sale_id', 'in', so_ids), + ('journal_id', '=', 11), + ('state', '=', 'posted'), + ]) + total_uang_muka = sum(moves.mapped('amount_total_signed')) if moves else 0.0 + total_midtrans = sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) if so_ids else 0.0 + self.uang_masuk = (total_uang_muka + total_midtrans) - amount_refund_before self.invoice_ids = all_invoices @@ -341,6 +499,37 @@ class RefundSaleOrder(models.Model): if self.sale_order_ids: self.partner_id = self.sale_order_ids[0].partner_id + @api.constrains('sale_order_ids') + def _check_sale_orders_payment(self): + """ Validasi SO harus punya uang masuk (Journal Uang Muka / Midtrans) """ + for rec in self: + invalid_orders = [] + total_uang_masuk = 0.0 + + for so in rec.sale_order_ids: + # cari journal uang muka + moves = self.env['account.move'].search([ + ('sale_id', '=', so.id), + ('journal_id', '=', 11), # Journal Uang Muka + ('state', '=', 'posted'), + ]) + + if not moves and so.payment_status != 'settlement': + invalid_orders.append(so.name) + + if moves: + total_uang_muka = sum(moves.mapped('amount_total_signed')) or 0.0 + total_uang_masuk += total_uang_muka + else: + # fallback Midtrans gross_amount + total_uang_masuk += so.gross_amount or 0.0 + + if invalid_orders: + raise ValidationError( + f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} " + "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Midtrans).\n" + "Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid." + ) @api.onchange('refund_type') def _onchange_refund_type(self): @@ -353,8 +542,15 @@ class RefundSaleOrder(models.Model): line_vals.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, + 'from_name': so.name, + 'prod_id': so.id, 'reason': '', + 'price_unit': line.price_unit, + 'discount': line.discount, + 'tax_amt': line.price_tax, + 'tax': [(6, 0, line.tax_id.ids)], })) + self.line_ids = line_vals @@ -362,20 +558,78 @@ class RefundSaleOrder(models.Model): line_vals = [] StockPicking = self.env['stock.picking'] for so in self.sale_order_ids: - pickings = StockPicking.search([ + # BU/SRT + pickings_srt = StockPicking.search([ ('state', '=', 'done'), ('picking_type_id', '=', 73), ('sale_id', 'in', so.ids) ]) - - for picking in pickings: - for move in picking.move_lines: - line_vals.append((0, 0, { - 'product_id': move.product_id.id, - 'quantity': move.product_uom_qty, - 'reason': '', - })) - self.line_ids = line_vals + # BU/ORT + pickings_ort = StockPicking.search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 74), + ('sale_id', 'in', so.ids) + ]) + if not pickings_ort and not pickings_srt: + # BU/OUT + product_out = StockPicking.search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 29), + ('sale_id', 'in', so.ids) + ]) + for picking in product_out: + for move in picking.move_lines: + so_lines = so.order_line.filtered( + lambda l: l.product_id == move.product_id + ) + for so_line in so_lines: + line_vals.append((0, 0, { + 'product_id': move.product_id.id, + 'ref_id': picking.id, + 'from_name': picking.name, + 'quantity': move.product_uom_qty, + 'reason': '', + 'price_unit': so_line.price_unit, + 'discount': so_line.discount, + 'tax': [(6, 0, so_line.tax_id.ids)], + })) + + has_bu_pick = any(p.picking_type_id.id == 30 for p in so.picking_ids) + if not has_bu_pick: + for picking in pickings_srt: + for move in picking.move_lines: + so_lines = so.order_line.filtered( + lambda l: l.product_id == move.product_id + ) + for so_line in so_lines: + line_vals.append((0, 0, { + 'product_id': move.product_id.id, + 'ref_id': picking.id, + 'from_name': picking.name, + 'quantity': move.product_uom_qty, + 'reason': '', + 'price_unit': so_line.price_unit, + 'discount': so_line.discount, + 'tax': [(6, 0, so_line.tax_id.ids)], + })) + else: + for picking in pickings_ort: + for move in picking.move_lines: + so_lines = so.order_line.filtered( + lambda l: l.product_id == move.product_id + ) + for so_line in so_lines: + line_vals.append((0, 0, { + 'product_id': move.product_id.id, + 'ref_id': picking.id, + 'from_name': picking.name, + 'quantity': move.product_uom_qty, + 'reason': '', + 'price_unit': so_line.price_unit, + 'discount': so_line.discount, + 'tax': [(6, 0, so_line.tax_id.ids)], + })) + self.line_ids = line_vals @api.depends('invoice_ids') @@ -400,10 +654,10 @@ class RefundSaleOrder(models.Model): record.amount_refund_text = '' def unlink(self): - not_draft = self.filtered(lambda r: r.status != 'draft') - if not_draft: - names = ', '.join(not_draft.mapped('name')) - raise UserError(f"Refund hanya bisa dihapus jika statusnya masih draft.\nTidak bisa hapus: {names}") + incantdelete = self.filtered(lambda r: r.status in ['refund', 'reject']) + if incantdelete: + names = ', '.join(incantdelete.mapped('name')) + raise UserError(f"Refund tidak dapat di hapus jika sudah Confirm/Cancel.\nTidak bisa hapus: {names}") return super().unlink() @api.depends('invoice_ids') @@ -433,30 +687,6 @@ class RefundSaleOrder(models.Model): pengurangan = rec.total_invoice + rec.ongkir refund = rec.uang_masuk - pengurangan rec.amount_refund = refund if refund > 0 else 0.0 - - @api.model - def default_get(self, fields_list): - res = super().default_get(fields_list) - sale_order_id = self.env.context.get('default_sale_order_id') - if sale_order_id: - so = self.env['sale.order'].browse(sale_order_id) - res['sale_order_ids'] = [(6, 0, [so.id])] - invoice_ids = so.invoice_ids.filtered( - lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel' - ).ids - res['invoice_ids'] = [(6, 0, invoice_ids)] - res['uang_masuk'] = 0.0 - res['ongkir'] = so.delivery_amt or 0.0 - line_vals = [] - for line in so.order_line: - line_vals.append((0, 0, { - 'product_id': line.product_id.id, - 'quantity': line.product_uom_qty, - 'reason': '', - })) - res['line_ids'] = line_vals - res['refund_type'] = 'uang' if invoice_ids else False - return res @api.onchange('invoice_ids') def _onchange_invoice_ids(self): @@ -464,10 +694,26 @@ class RefundSaleOrder(models.Model): if self.refund_type not in ['uang', 'barang_kosong']: self.refund_type = False - self.total_invoice = sum(self.invoice_ids.mapped('amount_total')) + self.total_invoice = sum(self.invoice_ids.mapped('amount_total_signed')) def action_ask_approval(self): for rec in self: + if rec.refund_type in ['retur', 'retur_half']: + so = rec.sale_order_ids + if so: + retur_done = self.env['stock.picking'].search_count([ + ('sale_id', '=', so.id), + ('picking_type_id', 'in', [73, 74]), + ('state', '=', 'done') + ]) + if retur_done == 0: + raise ValidationError( + f"⚠️ SO {so.name} memiliki refund tipe Retur. Selesaikan pengajuan retur untuk melanjutkan refund" + ) + allowed_sales_ids = rec.sale_order_ids.mapped("user_id.id") + if self.env.user.id not in allowed_sales_ids and rec.refund_type != 'salah_transfer': + raise ValidationError("❌ Hanya Sales pemilik Sales Order terkait yang boleh meminta approval refund ini.") + if rec.status == 'draft': rec.status = 'pengajuan1' @@ -481,6 +727,19 @@ class RefundSaleOrder(models.Model): now = datetime.now(jakarta_tz).replace(tzinfo=None) for rec in self: + if rec.refund_type in ['retur', 'retur_half']: + so = rec.sale_order_ids + if so: + retur_done = self.env['stock.picking'].search_count([ + ('sale_id', '=', so.id), + ('picking_type_id', 'in', [73, 74]), + ('state', '=', 'done') + ]) + if retur_done == 0: + raise ValidationError( + f"⚠️ SO {so.name} memiliki refund tipe Retur. Selesaikan retur untuk melanjutkan refund" + ) + user_name = self.env.user.name if not rec.status or rec.status == 'draft': @@ -512,7 +771,7 @@ class RefundSaleOrder(models.Model): is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') allowed_user_ids = [19, 688, 7] for rec in self: - if self.user.id not in allowed_user_ids and not is_fat: + if self.env.uid not in allowed_user_ids and not is_fat: raise UserError("❌ Hanya user yang bersangkutan atau Finance (FAT) yang bisa melakukan penolakan.") if rec.status not in ['refund', 'reject']: rec.status = 'reject' @@ -528,7 +787,7 @@ class RefundSaleOrder(models.Model): is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') for rec in self: if not is_fat: - raise UserError("Hanya Finance yang dapat mengkonfirmasi refund.") + raise UserError("Hanya Finance yang dapat mengkonfirmasi pembayaran refund.") if rec.status_payment == 'pending': rec.status_payment = 'done' rec.refund_date = fields.Date.context_today(self) @@ -565,15 +824,26 @@ class RefundSaleOrder(models.Model): # Ambil label refund type refund_type_label = dict( self.fields_get(allfields=['refund_type'])['refund_type']['selection'] - ).get(refund.refund_type, '').replace("Refund ", "").upper() - + ).get(refund.refund_type, '') + + # Normalisasi + refund_type_label = refund_type_label.upper() + + if refund.refund_type in ['barang_kosong', 'barang_kosong_sebagian']: + refund_type_label = "REFUND BARANG KOSONG" + elif refund.refund_type in ['retur_half', 'retur']: + refund_type_label = "REFUND RETUR BARANG" + elif refund.refund_type == 'uang': + refund_type_label = "REFUND LEBIH BAYAR" + elif refund.refund_type == 'salah_transfer': + refund_type_label = "REFUND SALAH TRANSFER" if not partner: raise UserError("❌ Partner tidak ditemukan.") # Ref format - ref_text = f"REFUND {refund_type_label} {refund.name or ''} {partner.display_name}".upper() + ref_text = f"{refund_type_label} {refund.name or ''} {partner.display_name}".upper() # Buat Account Move (Journal Entry) account_move = self.env['account.move'].create({ @@ -586,8 +856,8 @@ class RefundSaleOrder(models.Model): }) amount = refund.amount_refund - - second_account_id = 450 if has_invoice else 668 + # 450 Penerimaan Belum Teridentifikasi, 668 Penerimaan Belum Alokasi + second_account_id = 450 if refund.refund_type not in ['barang_kosong', 'barang_kosong_sebagian'] else 668 debit_line = { 'move_id': account_move.id, @@ -623,12 +893,20 @@ class RefundSaleOrder(models.Model): def _compute_journal_refund_move_id(self): for rec in self: move = self.env['account.move'].search([ - ('refund_id', '=', rec.id) + ('refund_id', '=', rec.id), + ('state', '!=', 'cancel') ], limit=1) rec.journal_refund_move_id = move def action_open_journal_refund(self): self.ensure_one() + + is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') + allowed_user_ids = [19, 688, 7] + + if not is_fat and self.env.user.id not in allowed_user_ids: + raise UserError(_('Anda tidak memiliki akses untuk membuka Journal Refund.')) + if self.journal_refund_move_id: return { 'name': _('Journal Refund'), @@ -640,14 +918,211 @@ class RefundSaleOrder(models.Model): } + @api.depends( + "sale_order_ids", + "sale_order_ids.order_line.price_subtotal", + "sale_order_ids.order_line.price_tax", + "sale_order_ids.order_line.price_total", + "sale_order_ids.order_line.purchase_price", + "sale_order_ids.order_line.product_uom_qty", + "sale_order_ids.delivery_amt", + "sale_order_ids.shipping_cost_covered", + ) + def _compute_amount_from_so(self): + for rec in self: + untaxed = tax = total_margin = delivery = 0.0 + for so in rec.sale_order_ids: + if so.shipping_cost_covered == 'customer': + delivery += so.delivery_amt or 0.0 + for line in so.order_line: + untaxed += line.price_subtotal + tax += line.price_tax + cost = line.purchase_price * line.product_uom_qty + margin = line.price_subtotal - cost + total_margin += margin + rec.amount_untaxed = untaxed + rec.amount_tax = tax + rec.amount_total = untaxed + tax + rec.total_margin = total_margin + rec.delivery_amt = delivery + rec.grand_total = rec.amount_total + rec.delivery_amt + + + @api.depends("sale_order_ids", "sale_order_ids.order_line") + def _compute_so_order_lines(self): + for rec in self: + rec.so_order_line_ids = rec.sale_order_ids.mapped("order_line") + + + + @api.depends('refund_type') + def _compute_refund_type_display(self): + for rec in self: + rec.refund_type_display = dict(self.fields_get(allfields=['refund_type'])['refund_type']['selection']).get(rec.refund_type, '') + + + def _compute_sale_order_count(self): + for rec in self: + rec.sale_order_count = len(rec.sale_order_ids) + + def _compute_show_return_alert(self): + for rec in self: + retur_ort = self.env['stock.picking'].search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 74), + ('sale_id', 'in', rec.sale_order_ids.ids) + ]) + + retur_srt = self.env['stock.picking'].search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 73), + ('sale_id', 'in', rec.sale_order_ids.ids) + ]) + rec.show_return_alert = not retur_ort and not retur_srt and rec.refund_type in ['retur', 'retur_half'] + + def _compute_show_approval_alert(self): + for rec in self: + retur_ort = self.env['stock.picking'].search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 74), + ('sale_id', 'in', rec.sale_order_ids.ids) + ]) + + retur_srt = self.env['stock.picking'].search([ + ('state', '=', 'done'), + ('picking_type_id', '=', 73), + ('sale_id', 'in', rec.sale_order_ids.ids) + ]) + rec.show_approval_alert = retur_ort or retur_srt and rec.refund_type in ['retur', 'retur_half'] + + @api.depends('tukar_guling_ids', 'tukar_guling_ids.picking_ids') + def _compute_picking_ids(self): + for rec in self: + rec.picking_ids = rec.tukar_guling_ids.mapped('picking_ids') + + def action_view_picking(self): + self.ensure_one() + action = self.env.ref('stock.action_picking_tree_all').read()[0] + if len(self.picking_ids) == 1: + action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = self.picking_ids.id + else: + action['domain'] = [('id', 'in', self.picking_ids.ids)] + return action + + @api.depends('picking_ids') + def _compute_has_picking(self): + for rec in self: + rec.has_picking = bool(rec.picking_ids) + + def action_create_tukar_guling(self): + for refund in self: + if refund.refund_type not in ['retur', 'retur_half']: + raise UserError("Refund Type harus Retur Full atau Retur Sebagian untuk membuat Tukar Guling.") + + tg_records = [] + for picking in refund.line_ids.mapped('ref_id'): + if not picking: + continue + + lines = refund.line_ids.filtered(lambda l: l.ref_id.id == picking.id) + line_vals = [] + koli_lines = [] + for r_line in lines: + qty_done = 0.0 + move_line = r_line.ref_id.move_line_ids_without_package.filtered( + lambda ml: ml.product_id.id == r_line.product_id.id + ) + if move_line: + qty_done = sum(move_line.mapped('qty_done')) + line_vals.append((0, 0, { + 'product_id': r_line.product_id.id, + 'product_uom_qty': r_line.quantity, + 'name':r_line.product_id.name, + 'product_uom':r_line.product_id.uom_id.id + })) + + if r_line.ref_id.konfirm_koli_lines.pick_id: + koli_lines.append((0, 0,{ + 'pick_id': r_line.ref_id.konfirm_koli_lines.pick_id.id, + 'product_id': r_line.product_id.id, + 'qty_done': qty_done, + 'qty_return': r_line.quantity, + })) + + tg = self.env['tukar.guling'].create({ + 'partner_id': refund.partner_id.id, + 'origin': ','.join(refund.sale_order_ids.mapped('name')), + 'origin_so': refund.sale_order_ids.id, + 'operations': picking.id, + 'return_type': 'revisi_so', + 'invoice_id': [(6, 0, refund.invoice_ids.ids)], + 'refund_id': refund.id, + 'line_ids': line_vals, + 'mapping_koli_ids': koli_lines + }) + tg_records.append(tg.id) + + return { + 'type': 'ir.actions.act_window', + 'name': 'Pengajuan Retur SO', + 'res_model': 'tukar.guling', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', tg_records)], + } + + def _compute_tukar_guling_count(self): + for rec in self: + rec.tukar_guling_count = len(rec.tukar_guling_ids) + + + def action_open_tukar_guling(self): + self.ensure_one() + return { + 'name': 'Pengajuan Return SO', + 'type': 'ir.actions.act_window', + 'view_mode': 'tree,form', + 'res_model': 'tukar.guling', + 'domain': [('id', 'in', self.tukar_guling_ids.ids)], + 'context': dict(self.env.context, default_refund_id=self.id), + } class RefundSaleOrderLine(models.Model): _name = 'refund.sale.order.line' _description = 'Refund Sales Order Line' - _inherit = ['mail.thread'] refund_id = fields.Many2one('refund.sale.order', string='Refund Ref') product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Qty') reason = fields.Char(string='Reason') + ref_id = fields.Many2one('stock.picking', string='Picking Reference') + prod_id = fields.Many2one('sale.order', string='Sales Order Reference') + from_name = fields.Char(string="Product Reference") + price_unit = fields.Float(string="Unit Price") + tax_amt = fields.Float(string="Amount Tax", compute='_compute_amounts') + discount = fields.Float(string="Discount %") + tax = fields.Many2many('account.tax',string="Taxes") + subtotal = fields.Float(string="Subtotal", compute='_compute_amounts') + total = fields.Float(string="Grand Total", compute='_compute_amounts') + + @api.depends('quantity', 'price_unit', 'discount', 'tax') + def _compute_amounts(self): + for line in self: + price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + + subtotal = price_unit * line.quantity + tax_amount = 0.0 + if line.tax: + taxes = line.tax.compute_all( + price_unit=price_unit, # Gunakan harga setelah diskon + quantity=line.quantity, + product=line.product_id, + partner=line.refund_id.partner_id + ) + tax_amount = taxes['total_included'] - taxes['total_excluded'] + subtotal = taxes['total_excluded'] + + line.subtotal = subtotal + line.tax_amt = tax_amount + line.total = subtotal + tax_amount
\ No newline at end of file diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 148a3fd0..36570e8f 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -1,6 +1,6 @@ from odoo import models, fields, api from odoo.exceptions import UserError, ValidationError -from datetime import datetime +from datetime import datetime, timedelta from odoo.http import request import re import requests @@ -181,10 +181,8 @@ class ResPartner(models.Model): payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) payment_history_url = fields.Text(string='Payment History URL') - # no compute - # payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) - - # tidak terpakai + is_cbd_locked = fields.Boolean("Locked to CBD?", default=False, tracking=True, help="Jika dicentang, maka partner ini terkunci pada payment term CBD karena memiliki invoice yang sudah jatuh tempo lebih dari 30 hari.") + @api.model def _default_payment_term(self): @@ -193,9 +191,10 @@ class ResPartner(models.Model): property_payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', - default=_default_payment_term + default=_default_payment_term, tracking=3 ) + @api.depends("street", "street2", "city", "state_id", "country_id", "blok", "nomor", "rt", "rw", "kelurahan_id", "kecamatan_id") def _alamat_lengkap_text(self): diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 77fee068..e7dd582d 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -234,9 +234,9 @@ class SaleOrder(models.Model): customer_type = fields.Selection([ ('pkp', 'PKP'), ('nonpkp', 'Non PKP') - ], required=True, compute='_compute_partner_field') - sppkp = fields.Char(string="SPPKP", required=True, tracking=True, compute='_compute_partner_field') - npwp = fields.Char(string="NPWP", required=True, tracking=True, compute='_compute_partner_field') + ], related="partner_id.customer_type", string="Customer Type", readonly=True) + sppkp = fields.Char(string="SPPKP", related="partner_id.sppkp") + npwp = fields.Char(string="NPWP", related="partner_id.npwp") purchase_total = fields.Monetary(string='Purchase Total', compute='_compute_purchase_total') voucher_id = fields.Many2one(comodel_name='voucher', string='Voucher', copy=False) applied_voucher_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False) @@ -358,7 +358,6 @@ class SaleOrder(models.Model): help="Tanggal pertama kali barang berhasil di-reservasi pada DO (BU/PICK/) yang berstatus Siap Dikirim." ) refund_ids = fields.Many2many('refund.sale.order', compute='_compute_refund_ids', string='Refunds') - has_refund = fields.Boolean(string='Has Refund', compute='_compute_has_refund') refund_count = fields.Integer(string='Refund Count', compute='_compute_refund_count') advance_payment_move_id = fields.Many2one( 'account.move', @@ -394,6 +393,26 @@ class SaleOrder(models.Model): ('paid', 'Full Paid'), ('no_invoice', 'No Invoice'), ], string="Payment Status Invoice", compute="_compute_payment_state_custom", store=False) + partner_is_cbd_locked = fields.Boolean( + string="Partner Locked CBD", + compute="_compute_partner_is_cbd_locked" + ) + + @api.depends('partner_id.is_cbd_locked') + def _compute_partner_is_cbd_locked(self): + for order in self: + order.partner_is_cbd_locked = order.partner_id.is_cbd_locked + + + @api.constrains('payment_term_id', 'partner_id', 'state') + def _check_cbd_lock_sale_order(self): + # cbd_term = self.env['account.payment.term'].browse(26) + for rec in self: + if rec.state == 'draft' and rec.partner_id.is_cbd_locked: + # if rec.payment_term_id and rec.payment_term_id != cbd_term: + raise ValidationError( + "Customer ini terkunci ke CBD, hanya boleh pakai Payment Term CBD." + ) @api.depends('invoice_ids.payment_state', 'invoice_ids.amount_total', 'invoice_ids.amount_residual') def _compute_payment_state_custom(self): @@ -2030,22 +2049,22 @@ class SaleOrder(models.Model): # return [('id', 'not in', order_ids)] # return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)] - @api.depends('partner_id') - def _compute_partner_field(self): - for order in self: - partner = order.partner_id.parent_id or order.partner_id - order.npwp = partner.npwp - order.sppkp = partner.sppkp - order.customer_type = partner.customer_type + # @api.depends('partner_id') + # def _compute_partner_field(self): + # for order in self: + # partner = order.partner_id.parent_id or order.partner_id + # order.npwp = partner.npwp + # order.sppkp = partner.sppkp + # order.customer_type = partner.customer_type @api.onchange('partner_id') def onchange_partner_contact(self): parent_id = self.partner_id.parent_id parent_id = parent_id if parent_id else self.partner_id - self.npwp = parent_id.npwp - self.sppkp = parent_id.sppkp - self.customer_type = parent_id.customer_type + # self.npwp = parent_id.npwp + # self.sppkp = parent_id.sppkp + # self.customer_type = parent_id.customer_type self.email = parent_id.email self.pareto_status = parent_id.pareto_status self.user_id = parent_id.user_id @@ -2116,15 +2135,21 @@ class SaleOrder(models.Model): if self.payment_term_id.id == 31 and self.total_percent_margin < 25: raise UserError("Jika ingin menggunakan Tempo 90 Hari maka margin harus di atas 25%") - if self.warehouse_id.id != 8 and self.warehouse_id.id != 10: # GD Bandengan - raise UserError('Gudang harus Bandengan') + if self.warehouse_id.id != 8 and self.warehouse_id.id != 10 and self.warehouse_id.id != 12: # GD Bandengan / Pameran + raise UserError('Gudang harus Bandengan atau Pameran') if self.state not in ['draft', 'sent']: raise UserError("Status harus draft atau sent") - self._validate_npwp() - def _validate_npwp(self): + if not self.npwp: + raise UserError("NPWP partner kosong, silahkan isi terlebih dahulu npwp nya di contact partner") + + if not self.customer_type: + raise UserError("Customer Type partner kosong, silahkan isi terlebih dahulu Customer Type nya di contact partner") + + if not self.sppkp: + raise UserError("SPPKP partner kosong, silahkan isi terlebih dahulu SPPKP nya di contact partner") num_digits = sum(c.isdigit() for c in self.npwp) if num_digits < 10: @@ -2138,6 +2163,7 @@ class SaleOrder(models.Model): self._validate_order() for order in self: + order._validate_npwp() order._validate_uniform_taxes() order.order_line.validate_line() @@ -2192,9 +2218,8 @@ class SaleOrder(models.Model): if self.validate_different_vendor() and not self.vendor_approval: return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor') self.check_due() - - self._validate_order() for order in self: + order._validate_npwp() order._validate_delivery_amt() order._validate_uniform_taxes() order.order_line.validate_line() @@ -2467,6 +2492,7 @@ class SaleOrder(models.Model): order.check_data_real_delivery_address() order.sale_order_check_approve() order._validate_order() + order._validate_npwp() order.order_line.validate_line() main_parent = order.partner_id.get_main_parent() @@ -2652,23 +2678,17 @@ class SaleOrder(models.Model): def _set_sppkp_npwp_contact(self): partner = self.partner_id.parent_id or self.partner_id - if not partner.sppkp: - partner.sppkp = self.sppkp - if not partner.npwp: - partner.npwp = self.npwp + # if not partner.sppkp: + # partner.sppkp = self.sppkp + # if not partner.npwp: + # partner.npwp = self.npwp if not partner.email: partner.email = self.email - if not partner.customer_type: - partner.customer_type = self.customer_type + # if not partner.customer_type: + # partner.customer_type = self.customer_type if not partner.user_id: partner.user_id = self.user_id.id - # if not partner.sppkp or not partner.npwp or not partner.email or partner.customer_type: - # partner.customer_type = self.customer_type - # partner.npwp = self.npwp - # partner.sppkp = self.sppkp - # partner.email = self.email - def _compute_total_margin(self): for order in self: total_margin = sum(line.item_margin for line in order.order_line if line.product_id) @@ -3110,52 +3130,6 @@ class SaleOrder(models.Model): # order._update_partner_details() return order - # def write(self, vals): - # Call the super method to handle the write operation - # res = super(SaleOrder, self).write(vals) - # self._compute_etrts_date() - # Check if the update is coming from a save operation - # if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']): - # self._update_partner_details() - - # return res - - def _update_partner_details(self): - for order in self: - partner = order.partner_id.parent_id or order.partner_id - if partner: - # Update partner details - partner.sppkp = order.sppkp - partner.npwp = order.npwp - partner.email = order.email - partner.customer_type = order.customer_type - - # Save changes to the partner record - partner.write({ - 'sppkp': partner.sppkp, - 'npwp': partner.npwp, - 'email': partner.email, - 'customer_type': partner.customer_type, - }) - - # def write(self, vals): - # for order in self: - # if order.state in ['sale', 'cancel']: - # if 'order_line' in vals: - # new_lines = vals.get('order_line', []) - # for command in new_lines: - # if command[0] == 0: # A new line is being added - # raise UserError( - # "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - # - # res = super(SaleOrder, self).write(vals) - # # self._check_total_margin_excl_third_party() - # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - # self._validate_delivery_amt() - # if any(field in vals for field in ["order_line", "client_order_ref"]): - # self._calculate_etrts_date() - # return res - # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): def is_empty(val): @@ -3284,20 +3258,58 @@ class SaleOrder(models.Model): def button_refund(self): self.ensure_one() + if self.state not in ['cancel', 'sale']: + raise UserError(f"❌ SO {self.name} tidak bisa direfund. Status harus Cancel atau Sale.") + if self.state == 'sale': + not_done_pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel']) + if not_done_pickings: + raise UserError( + f"❌ SO {self.name} Belum melakukan kirim barang " + f"({', '.join(not_done_pickings.mapped('name'))}). Selesaikan Pengiriman untuk melakukan refund." + ) + moves = self.env['account.move'].search([ + ('sale_id', '=', self.id), + ('journal_id', '=', 11), + ('state', '=', 'posted'), + ]) + + # Default 0 + total_uang_muka = 0.0 + + has_moves = bool(moves) + has_settlement = self.payment_status == 'settlement' + + if has_moves and has_settlement: + total_uang_muka = sum(moves.mapped('amount_total_signed')) + self.gross_amount + elif has_moves: + total_uang_muka = sum(moves.mapped('amount_total_signed')) + elif has_settlement: + total_uang_muka = self.gross_amount + else: + raise UserError( + "Tidak bisa melakukan refund karena SO tidak memiliki Record Uang Masuk " + "(Journal Uang Muka/Midtrans Payment)." + ) invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel') + total_refunded = sum(self.refund_ids.mapped('amount_refund')) + sisa_uang_muka = total_uang_muka - total_refunded + + if sisa_uang_muka <= 0: + raise UserError("❌ Tidak ada sisa transaksi untuk di-refund. Semua dana sudah dikembalikan.") return { 'name': 'Refund Sale Order', 'type': 'ir.actions.act_window', 'res_model': 'refund.sale.order', 'view_mode': 'form', + 'target':'new', 'target': 'current', 'context': { 'default_sale_order_ids': [(6, 0, [self.id])], 'default_invoice_ids': [(6, 0, invoice_ids.ids)], - 'default_uang_masuk': sum(invoice_ids.mapped('amount_total')) + (self.delivery_amt or 0.0) + 1000, + 'default_uang_masuk': sisa_uang_muka, 'default_ongkir': self.delivery_amt or 0.0, - 'default_bank': '', # bisa isi default bank kalau mau + 'default_bank': '', 'default_account_name': '', 'default_account_no': '', 'default_refund_type': '', @@ -3308,6 +3320,30 @@ class SaleOrder(models.Model): if not self: raise UserError("Tidak ada Sale Order yang dipilih.") + if len(self) > 1: + not_cancel_orders = self.filtered(lambda so: so.state != 'cancel') + if not_cancel_orders: + raise ValidationError( + f"❌ Refund Multi SO hanya bisa dibuat untuk SO dengan status Cancel. " + f"SO berikut tidak Cancel: {', '.join(not_cancel_orders.mapped('name'))}" + ) + + + invalid_status_orders = [] + for order in self: + if order.state not in ['cancel', 'sale']: + invalid_status_orders.append(order.name) + elif order.state == 'sale': + not_done_pickings = order.picking_ids.filtered(lambda p: p.state != 'done') + if not_done_pickings: + invalid_status_orders.append(order.name) + + if invalid_status_orders: + raise ValidationError( + f"❌ Refund tidak bisa dibuat untuk SO {', '.join(invalid_status_orders)}. " + f"SO harus Cancel atau Sale dengan semua Pengiriman sudah selesai." + ) + partner_set = set(self.mapped('partner_id.id')) if len(partner_set) > 1: raise UserError("Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.") @@ -3316,14 +3352,43 @@ class SaleOrder(models.Model): if len(invoice_status_set) > 1: raise UserError("Tidak dapat membuat refund untuk SO dengan status invoice berbeda. Harus memiliki status invoice yang sama.") - already_refunded = self.filtered(lambda so: so.has_refund) - if already_refunded: - so_names = ', '.join(already_refunded.mapped('name')) - raise UserError(f"❌ Tidak bisa refund ulang. {so_names} sudah melakukan refund.") + refunded_orders = self.filtered(lambda so: self.env['refund.sale.order'].search([('sale_order_ids', 'in', so.id)], limit=1)) + if refunded_orders: + raise ValidationError( + f"SO {', '.join(refunded_orders.mapped('name'))} sudah pernah di-refund dan tidak bisa ikut dalam refund Multi SO." + ) + + total_uang_masuk = 0.0 + invalid_orders=[] + for order in self: + moves = self.env['account.move'].search([ + ('sale_id', '=', order.id), + ('journal_id', '=', 11), + ('state', '=', 'posted'), + ]) + + total_uang_muka = 0.0 + + if moves and order.payment_status == 'settlement': + total_uang_muka = order.gross_amount + sum(moves.mapped('amount_total_signed')) or 0.0 + elif moves: + total_uang_muka = sum(moves.mapped('amount_total_signed')) or 0.0 + elif order.payment_status == 'settlement': + total_uang_muka = order.gross_amount + else: + invalid_orders.append(order.name) + + total_uang_masuk += total_uang_muka + + if invalid_orders: + raise ValidationError( + f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Midtrans).\n" + "Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid." + ) + invoice_ids = self.mapped('invoice_ids').filtered(lambda inv: inv.state != 'cancel') delivery_total = sum(self.mapped('delivery_amt')) - total_invoice = sum(invoice_ids.mapped('amount_total')) return { 'type': 'ir.actions.act_window', @@ -3334,7 +3399,7 @@ class SaleOrder(models.Model): 'context': { 'default_sale_order_ids': [(6, 0, self.ids)], 'default_invoice_ids': [(6, 0, invoice_ids.ids)], - 'default_uang_masuk': total_invoice + delivery_total + 1000, + 'default_uang_masuk': total_uang_masuk, 'default_ongkir': delivery_total, 'default_bank': '', 'default_account_name': '', @@ -3343,11 +3408,6 @@ class SaleOrder(models.Model): } } - @api.depends('refund_ids') - def _compute_has_refund(self): - for so in self: - so.has_refund = bool(so.refund_ids) - def action_view_related_refunds(self): self.ensure_one() return { diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 47a24264..1f2ea1fb 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -71,23 +71,17 @@ class SaleOrderLine(models.Model): if order_qty > 0: for move in line.move_ids: - # --- CASE 1: Move belum selesai --- if move.state not in ('done', 'cancel'): reserved_qty += move.reserved_availability or 0.0 continue - # --- CASE 2: Move sudah done --- if move.location_dest_id.usage == 'customer': - # Barang dikirim ke customer delivered_qty += move.quantity_done or 0.0 elif move.location_id.usage == 'customer': - # Barang balik dari customer (retur) delivered_qty -= move.quantity_done or 0.0 - # Clamp supaya delivered gak minus delivered_qty = max(delivered_qty, 0) - # Hitung persen line.reserved_percent = min((reserved_qty / order_qty) * 100, 100) if order_qty else 0 line.delivered_percent = min((delivered_qty / order_qty) * 100, 100) if order_qty else 0 line.unreserved_percent = max(100 - line.reserved_percent - line.delivered_percent, 0) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index 90ab30a4..d6505a86 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -1,6 +1,9 @@ from odoo import fields, models, api from odoo.tools.misc import format_date, OrderedSet from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) class StockMove(models.Model): _inherit = 'stock.move' @@ -15,6 +18,7 @@ class StockMove(models.Model): barcode = fields.Char(string='Barcode', related='product_id.barcode') vendor_id = fields.Many2one('res.partner' ,string='Vendor') hold_outgoingg = fields.Boolean('Hold Outgoing', default=False) + product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly=True) # @api.model_create_multi # def create(self, vals_list): diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index 1fc8d088..88acf83c 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -110,7 +110,7 @@ class ReturnPicking(models.TransientModel): if mapping_koli_vals: context['default_mapping_koli_ids'] = mapping_koli_vals - if picking.purchase_id or 'PO' in picking.origin: + if picking.purchase_id or 'PO' in (picking.origin or ''): _logger.info("Redirect ke Tukar Guling PO via purchase_id / origin") return { 'name': _('Tukar Guling PO'), @@ -120,7 +120,7 @@ class ReturnPicking(models.TransientModel): 'target': 'current', 'context': context, } - else: + if picking.sale_id or 'SO' in (picking.origin or ''): _logger.info("This picking is NOT from a PO, fallback to SO.") return { 'name': _('Tukar Guling SO'), @@ -130,6 +130,9 @@ class ReturnPicking(models.TransientModel): 'target': 'current', 'context': context, } + else: + _logger.info("Bukan SO/PO → retur standar (create_returns)") + return super(ReturnPicking, self).create_returns() class ReturnPickingLine(models.TransientModel): diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 699ee670..d718ba0f 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -27,6 +27,10 @@ class TukarGuling(models.Model): if_so = fields.Boolean('Is SO', default=True) if_po = fields.Boolean('Is PO', default=False) real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') + refund_id = fields.Many2one( + 'refund.sale.order', + string="Refund Ref" + ) picking_ids = fields.One2many( 'stock.picking', 'tukar_guling_id', @@ -92,41 +96,45 @@ class TukarGuling(models.Model): so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1) rec.origin_so = so.id if so else False - @api.depends('origin', 'origin_so', 'partner_id', 'line_ids.product_id') + @api.depends('origin', 'origin_so', 'partner_id', 'line_ids.product_id', 'invoice_id', 'operations') def _compute_is_has_invoice(self): Move = self.env['account.move'] for rec in self: - rec.is_has_invoice = False - rec.invoice_id = [(5, 0, 0)] - - product_ids = rec.line_ids.mapped('product_id').ids - if not product_ids: - continue - - domain = [ - ('move_type', 'in', ['out_invoice', 'in_invoice']), - ('state', 'not in', ['draft', 'cancel']), - ('invoice_line_ids.product_id', 'in', product_ids), - ] - - extra = [] - if rec.origin: - extra.append(('invoice_origin', 'ilike', rec.origin)) - if rec.origin_so: - extra.append(('invoice_line_ids.sale_line_ids.order_id', '=', rec.origin_so.id)) - if extra: - domain = domain + ['|'] * (len(extra) - 1) + extra - - invoices = Move.search(domain).with_context(active_test=False) - if invoices: - rec.invoice_id = [(6, 0, invoices.ids)] - rec.is_has_invoice = True + invoices = rec.invoice_id + + if not invoices: + product_ids = rec.line_ids.mapped('product_id').ids + if product_ids: + domain = [ + ('move_type', 'in', ['out_invoice', 'out_refund', 'in_invoice']), + ('state', 'not in', ['draft', 'cancel']), + ('invoice_line_ids.product_id', 'in', product_ids), + ] + + # if rec.partner_id: + # domain.append( + # ('partner_id.commercial_partner_id', '=', rec.partner_id.commercial_partner_id.id) + # ) + + extra = [] + if rec.origin: + extra.append(('invoice_origin', 'ilike', rec.origin)) + if rec.origin_so: + extra.append(('invoice_line_ids.sale_line_ids.order_id', '=', rec.origin_so.id)) + if extra: + domain += ['|'] * (len(extra) - 1) + extra + + invoices = Move.search(domain).with_context(active_test=False) + if invoices: + rec.invoice_id = [(6, 0, invoices.ids)] + + rec.is_has_invoice = bool(invoices) def set_opt(self): if not self.val_inv_opt and self.is_has_invoice == True: raise UserError("Kalau sudah ada invoice Return Invoice Option harus diisi!") for rec in self: - if rec.val_inv_opt == 'cancel_invoice' and self.is_has_invoice == True: + if rec.val_inv_opt == 'cancel_invoice' and self.is_has_invoice == True and rec.invoice_id.state != 'cancel': raise UserError("Tidak bisa mengubah Return karena sudah ada invoice dan belum di cancel.") elif rec.val_inv_opt == 'tanpa_cancel' and self.is_has_invoice == True: continue @@ -435,9 +443,9 @@ class TukarGuling(models.Model): # if self.state == 'done': # raise UserError ("Tidak Boleh delete ketika sudahh done") for record in self: - if record.state == 'approved' or record.state == 'done': + if record.state in [ 'approved', 'done', 'approval_logistic', 'approval_finance', 'approval_sales']: raise UserError( - "Tidak bisa hapus pengajuan jika sudah Approved, set ke draft terlebih dahulu jika ingin menghapus") + "Tidak bisa hapus pengajuan jika sudah Proses Approval, set ke draft terlebih dahulu atau cancel jika ingin menghapus") ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'approved') for picking in ongoing_bu: picking.action_cancel() @@ -702,7 +710,7 @@ class TukarGuling(models.Model): ### ======== SRT dari BU/OUT ========= srt_return_lines = [] - if mapping_koli: + if mapping_koli and record.operations.picking_type_id.id == 29: for prod in mapping_koli.mapped('product_id'): qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) @@ -715,7 +723,7 @@ class TukarGuling(models.Model): })) _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") - elif not mapping_koli: + elif not mapping_koli and record.operations.picking_type_id.id == 29: for line in record.line_ids: move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id) if not move: @@ -1002,4 +1010,4 @@ class TukarGulingMappingKoli(models.Model): for rec in self: if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']: raise UserError("Tidak bisa menghapus Mapping Koli karena status Tukar Guling bukan Draft atau Cancel.") - return super(TukarGulingMappingKoli, self).unlink() + return super(TukarGulingMappingKoli, self).unlink()
\ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 94771f37..f2f37606 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -381,8 +381,8 @@ class TukarGulingPO(models.Model): def unlink(self): for record in self: - if record.state == 'done' or record.state == 'approved': - raise UserError("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu") + if record.state in [ 'approved', 'done', 'approval_logistic', 'approval_finance', 'approval_purchase']: + raise UserError("Tidak bisa hapus pengajuan jika sudah proses approval atau done, set ke draft atau cancel terlebih dahulu") ongoing_bu = self.po_picking_ids.filtered(lambda p: p.state != 'done') for picking in ongoing_bu: picking.action_cancel() diff --git a/indoteknik_custom/report/purchase_report.xml b/indoteknik_custom/report/purchase_report.xml new file mode 100644 index 00000000..168428a6 --- /dev/null +++ b/indoteknik_custom/report/purchase_report.xml @@ -0,0 +1,149 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <!-- Report Action --> + <record id="action_report_purchaseorder_website" model="ir.actions.report"> + <field name="name">Purchase Order (Website)</field> + <field name="model">purchase.order</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">indoteknik_custom.report_purchaseorder_website</field> + <field name="report_file">indoteknik_custom.report_purchaseorder_website</field> + <field name="print_report_name"> + ('PO - %s - %s' % (object.partner_id.name, object.name)) + </field> + <field name="binding_model_id" ref="purchase.model_purchase_order"/> + <field name="binding_type">report</field> + </record> + </data> + + <!-- Wrapper Template --> + <template id="report_purchaseorder_website"> + <t t-call="web.html_container"> + <t t-foreach="docs" t-as="doc"> + <t t-call="indoteknik_custom.report_purchaseorder_website_document" t-lang="doc.partner_id.lang"/> + </t> + </t> + </template> + + <template id="report_purchaseorder_website_document"> + <t t-call="web.html_container"> + <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" /> + + <!-- Header --> + <div class="header"> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521" + style="width:100%; display: block;"/> + </div> + + + <!-- PAGE CONTENT --> + <div class="article" style="margin: 0 1.5cm 0 1.5cm; "> + <!-- TITLE --> + <h2 style="text-align:center; margin:0; color:#d32f2f; font-weight:bold;"> + PURCHASE ORDER + </h2> + <h4 style="text-align:center; margin:0 0 20px 0;"> + No. <span t-field="doc.name"/> + </h4> + + <!-- TOP INFO --> + <table style="width:100%; margin-bottom:16px; font-size:14px;"> + <tr> + <td><strong>Term Of Payment:</strong> <span t-field="doc.payment_term_id.name"/></td> + <td><strong>Order Date:</strong> <span t-field="doc.date_order" t-options='{"widget": "date"}'/></td> + <td><strong>Responsible:</strong> <span t-field="doc.user_id"/></td> + </tr> + </table> + + <!-- VENDOR & DELIVERY --> + <table style="width:100%; margin-bottom:24px; border-collapse:separate; border-spacing:16px 0;"> + <tr> + <td style="width:50%; border:1px solid #ccc; padding:8px; vertical-align:top;"> + <strong>Alamat Pengiriman:</strong><br/> + PT Indoteknik (Bandengan 1 Depan)<br/> + Jl. Bandengan Utara Komp A 8 B<br/> + RT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET)<br/> + JK 14440 - Indonesia + </td> + <td style="width:50%; border:1px solid #ccc; padding:8px; vertical-align:top;"> + <strong>Nama Vendor:</strong><br/> + <span t-field="doc.partner_id.name"/><br/> + <span t-field="doc.partner_id.street"/><br/> + <span t-field="doc.partner_id.city"/> - <span t-field="doc.partner_id.zip"/> + </td> + </tr> + </table> + + <!-- ORDER LINES --> + <table style="border-collapse:collapse; width:100%; margin-top:16px;"> + <tbody> + <tr style="background:#f2f2f2;"> + <td style="border:1px solid #ccc;">Description</td> + <td style="border:1px solid #ccc; text-align:right;">Quantity</td> + <td style="border:1px solid #ccc; text-align:right;">Unit Price</td> + <td style="border:1px solid #ccc; text-align:right;">Taxes</td> + <td style="border:1px solid #ccc; text-align:right;">Subtotal</td> + </tr> + </tbody> + <tbody> + <t t-foreach="doc.order_line" t-as="line"> + <tr> + <td style="border:1px solid #ccc;"> + <span t-field="line.name"/> + </td> + <td style="border:1px solid #ccc; text-align:right;"> + <span t-field="line.product_qty"/> <span t-field="line.product_uom"/> + </td> + <td style="border:1px solid #ccc; text-align:right;"> + <span t-field="line.price_unit"/> + </td> + <td style="border:1px solid #ccc; text-align:right;"> + <span t-esc="', '.join(map(lambda x: (x.description or x.name), line.taxes_id))"/> + </td> + <td style="border:1px solid #ccc; text-align:right;"> + <span t-field="line.price_subtotal"/> + </td> + </tr> + <t t-if="line.product_id.website_description"> + <tr> + <td colspan="5" style="margin-top: 1rem;padding: 1rem; background:#fafafa; border-left:1px solid #ccc; border-right:1px solid #ccc;"> + <div t-raw="line.product_id.website_description"/> + </td> + </tr> + </t> + </t> + </tbody> + </table> + + <!-- TOTALS --> + <table style="margin-top:20px; margin-left:auto; width:40%; font-size:14px;"> + <tr> + <td><strong>Subtotal</strong></td> + <td style="text-align:right;"><span t-field="doc.amount_untaxed"/></td> + </tr> + <tr> + <td>Taxes</td> + <td style="text-align:right;"><span t-field="doc.amount_tax"/></td> + </tr> + <tr> + <td><strong>Total</strong></td> + <td style="text-align:right;"><span t-field="doc.amount_total"/></td> + </tr> + </table> + + <!-- NOTES --> + <div style="margin-top:24px;"> + <p t-field="doc.notes"/> + </div> + </div> + <!-- STATIC FOOTER --> + <div class="footer"> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2859765" + style="width:100%; display: block;"/> + </div> + + </t> + </template> + + +</odoo> diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 78b3dc0f..3a320510 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -161,7 +161,6 @@ access_konfirm_koli,access.konfirm.koli,model_konfirm_koli,,1,1,1,1 access_stock_immediate_transfer,access.stock.immediate.transfer,model_stock_immediate_transfer,,1,1,1,1 access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1 access_purchase_order_unlock_wizard,access.purchase.order.unlock.wizard,model_purchase_order_unlock_wizard,,1,1,1,1 -access_change_date_planned_wizard,access.change.date.planned.wizard,model_change_date_planned_wizard,,1,1,1,1 access_sales_order_koli,access.sales.order.koli,model_sales_order_koli,,1,1,1,1 access_stock_backorder_confirmation,access.stock.backorder.confirmation,model_stock_backorder_confirmation,,1,1,1,1 access_warning_modal_wizard,access.warning.modal.wizard,model_warning_modal_wizard,,1,1,1,1 @@ -184,7 +183,8 @@ access_production_purchase_match,access.production.purchase.match,model_producti access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1 access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1 access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,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_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 @@ -194,4 +194,5 @@ access_tukar_guling_line_po_all_users,tukar.guling.line.po.all.users,model_tukar access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1 access_purchase_order_update_date_wizard,access.purchase.order.update.date.wizard,model_purchase_order_update_date_wizard,base.group_user,1,1,1,1 access_sync_promise_date_wizard,access.sync.promise.date.wizard,model_sync_promise_date_wizard,base.group_user,1,1,1,1 -access_sync_promise_date_wizard_line,access.sync.promise.date.wizard.line,model_sync_promise_date_wizard_line,base.group_user,1,1,1,1
\ No newline at end of file +access_sync_promise_date_wizard_line,access.sync.promise.date.wizard.line,model_sync_promise_date_wizard_line,base.group_user,1,1,1,1 +access_change_date_planned_wizard,access.change.date.planned.wizard,model_change_date_planned_wizard,,1,1,1,1
\ No newline at end of file diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index b399d4c9..c88effd5 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -38,9 +38,10 @@ <!-- <field name="purchase_order_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/> --> </field> <field name="ref" position="after"> - <field name="sale_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'entry')]}"/> - <!-- <field name="refund_so_links" readonly="1" widget="html" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/> - <field name="has_refund_so" invisible="1"/> --> + <field name="sale_id" readonly="1" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', True)]}"/> + <field name="refund_id" readonly="1" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/> + <field name="refund_so_links" readonly="1" widget="html" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/> + <field name="has_refund_so" invisible="1"/> </field> <field name="reklas_misc_id" position="after"> <field name="purchase_order_id" context="{'form_view_ref': 'purchase.purchase_order_form'}" options="{'no_create': True}"/> diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml index 5c130f3f..b0b99689 100644 --- a/indoteknik_custom/views/approval_payment_term.xml +++ b/indoteknik_custom/views/approval_payment_term.xml @@ -7,7 +7,7 @@ <tree default_order="create_date desc"> <field name="number"/> <field name="partner_id"/> - <field name="parent_id"/> + <field name="parent_id" optional="hide"/> <field name="property_payment_term_id"/> <field name="create_date" optional="hide"/> <field name="approve_date" optional="hide"/> diff --git a/indoteknik_custom/views/dunning_run.xml b/indoteknik_custom/views/dunning_run.xml index 210f7917..51377f78 100644 --- a/indoteknik_custom/views/dunning_run.xml +++ b/indoteknik_custom/views/dunning_run.xml @@ -13,7 +13,7 @@ <field name="resi_tukar_faktur"/> <field name="date_terima_tukar_faktur"/> <field name="shipper_faktur_id"/> - <field name="grand_total"/> + <field name="grand_total" sum="Grand Total"/> <field name="create_uid" optional="hide"/> </tree> </field> @@ -25,13 +25,14 @@ <field name="arch" type="xml"> <tree> <field name="partner_id"/> + <field name="reference"/> <field name="invoice_id"/> <field name="date_invoice"/> - <field name="efaktur_id"/> - <field name="reference"/> + <field name="efaktur_id" optional="hide"/> <field name="total_amt" sum="Grand Total Amount"/> <field name="open_amt"/> <field name="due_date"/> + <field name="payment_term"/> </tree> </field> </record> diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index 4915e4c5..94c2cd07 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -220,7 +220,7 @@ </record> <record id="seq_refund_sale_order" model="ir.sequence"> - <field name="name">Refund Sale Order</field> + <field name="name">Refund Sales Order</field> <field name="code">refund.sale.order</field> <field name="prefix">RC/%(year)s/%(month)s/</field> <field name="padding">4</field> diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml new file mode 100644 index 00000000..0c6cd371 --- /dev/null +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -0,0 +1,309 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <!-- Tree View --> + <record id="view_refund_sale_order_tree" model="ir.ui.view"> + <field name="name">refund.sale.order.tree</field> + <field name="model">refund.sale.order</field> + <field name="arch" type="xml"> + <tree string="Refund Sales Orders"> + <field name="name" readonly="1"/> + <field name="created_date" readonly="1"/> + <field name="partner_id" readonly="1"/> + <field name="sale_order_ids" widget="many2many_tags" readonly="1"/> + <field name="uang_masuk" readonly="1"/> + <field name="ongkir" readonly="1"/> + <field name="total_invoice" readonly="1"/> + <field name="amount_refund" readonly="1"/> + <field name="remaining_refundable" readonly="1" optional="hide"/> + <field name="status" + decoration-info="status == 'draft'" + decoration-danger="status == 'reject'" + decoration-success="status == 'refund'" + decoration-warning="status == 'pengajuan1' or status == 'pengajuan2' or status == 'pengajuan3'" + widget="badge" + readonly="1"/> + <field name="status_payment" + decoration-info="status_payment == 'pending'" + decoration-danger="status_payment == 'reject'" + decoration-success="status_payment == 'done'" + widget="badge" + readonly="1"/> + <field name="refund_date" readonly="1"/> + <field name="amount_refund_text" readonly="1" optional="hide"/> + <field name="invoice_ids" readonly="1" optional="hide"/> + <field name="refund_type" readonly="1" optional="hide"/> + <field name="user_ids" readonly="1" optional="hide"/> + </tree> + </field> + </record> + + <!-- Form View --> + <record id="view_refund_sale_order_form" model="ir.ui.view"> + <field name="name">refund.sale.order.form</field> + <field name="model">refund.sale.order</field> + <field name="arch" type="xml"> + <form string="Refund Sales Order"> + <header> + <button name="action_ask_approval" + type="object" + string="Ask Approval" + attrs="{'invisible': [('status', '!=', 'draft')]}"/> + + <button name="action_approve_flow" + type="object" + string="Approve" + class="oe_highlight" + attrs="{'invisible': [('status', 'in', ['refund', 'reject', 'draft'])]}"/> + <button name="action_trigger_cancel" + type="object" + string="Cancel" + attrs="{'invisible': ['|', ('status_payment', '!=', 'pending'), ('status', 'in', ['reject', 'refund'])]}" /> + <button name="action_confirm_refund" + type="object" + string="Confirm Payment" + class="btn-primary" + attrs="{'invisible': ['|', ('status', 'not in', ['pengajuan3','refund']), ('status_payment', '!=', 'pending')]}"/> + <button name="action_create_journal_refund" + string="AP Only" + type="object" + class="oe_highlight" + attrs="{'invisible': ['|', ('journal_refund_state', 'in', ['posted', 'draft']), ('status', 'not in', ['pengajuan3','refund'])]}"/> + <button name="action_create_tukar_guling" + string="Create Return" + type="object" + class="oe_highlight" + attrs="{'invisible': [('refund_type', 'not in', ['retur_half', 'retur'])]}"/> + + <field name="status" + widget="statusbar" + statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,reject" + attrs="{'invisible': [('status', '!=', 'reject')]}" /> + + <field name="status" + widget="statusbar" + statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,refund" + attrs="{'invisible': [('status', '=', 'reject')]}" /> + </header> + <xpath expr="//sheet" position="inside"> + <field name="show_return_alert" invisible="1"/> + <div class="alert alert-danger" role="alert" + attrs="{'invisible': [('show_return_alert', '=', False)]}"> + ⚠️ SO belum melakukan retur barang. Silakan buat pengajuan retur. + </div> + <field name="show_approval_alert" invisible="1"/> + <div class="alert alert-info" role="alert" + attrs="{'invisible': ['|', ('show_approval_alert', '=', False), ('status', 'in', ['reject', 'refund'])]}"> + ⚠️ SO sudah melakukan retur barang. Silakan lanjutkan refund. + </div> + </xpath> + <sheet> + <div class="oe_button_box" name="button_box"> + <button name="action_open_journal_refund" + type="object" + class="oe_stat_button" + icon="fa-book" + width="250px" + attrs="{'invisible': [('journal_refund_move_id', '=', False)]}"> + <field name="journal_refund_move_id" string="Journal Refund" widget="statinfo"/> + </button> + + <button name="action_open_tukar_guling" + type="object" + class="oe_stat_button" + icon="fa-refresh" + attrs="{'invisible': ['|', ('tukar_guling_count','=', 0), ('has_picking','=',True)]}"> + <div class="o_stat_info"> + <field name="tukar_guling_count" widget="statinfo"/> + <span class="o_stat_text">Pengajuan Return SO</span> + </div> + </button> + + <button name="action_view_picking" + type="object" + class="oe_stat_button" + icon="fa-truck" + attrs="{'invisible': [('has_picking','=',False)]}"> + <field name="picking_ids" widget="statinfo" string="Delivery"/> + </button> + </div> + <widget name="web_ribbon" + title="PAID" + bg_color="bg-success" + attrs="{'invisible': [('status_payment', '!=', 'done')]}"/> + + <widget name="web_ribbon" + title="CANCEL" + bg_color="bg-danger" + attrs="{'invisible': [('status_payment', '!=', 'reject')]}"/> + <h1> + <field name="name" readonly="1"/> + </h1> + <group col="2"> + <group> + <field name="is_locked" invisible="1"/> + <field name="status_payment" invisible="1"/> + <field name="journal_refund_state" invisible="1"/> + + <field name="partner_id" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="sale_order_ids" widget="many2many_tags" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="invoice_ids" widget="many2many_tags" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="tukar_guling_count" invisible="1"/> + <field name="invoice_names" widget="html" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="so_names" widget="html" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="advance_move_names" widget="html" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="transfer_move_id" + attrs="{'invisible': [('refund_type', '!=', 'salah_transfer')], + 'required': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="refund_type" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="note_refund" attrs="{'readonly': [('is_locked', '=', True)]}"/> + </group> + <group> + <field name="uang_masuk" attrs="{'readonly': [('refund_type', '!=', 'salah_transfer')]}"/> + <field name="total_invoice" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="ongkir" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('refund_type', '=', 'salah_transfer')]}"/> + <field name="amount_refund" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="amount_refund_text" readonly="1"/> + <field name="sale_order_count" invisible="1"/> + <field name="has_picking" invisible="1"/> + <field name="tukar_guling_ids" invisible="1"/> + <field name="remaining_refundable" readonly="1" attrs="{'invisible': [('sale_order_count', '>', 1)]}"/> + <field name="uang_masuk_type" required="1" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="bukti_uang_masuk_image" widget="image" + attrs="{'invisible': [('uang_masuk_type', '=', 'pdf')], 'readonly': [('is_locked', '=', True)]}"/> + <field name="bukti_uang_masuk_pdf" widget="pdf_viewer" + attrs="{'invisible': [('uang_masuk_type', '=', 'image')], 'readonly': [('is_locked', '=', True)]}"/> + </group> + </group> + + <notebook> + <page string="Produk Line"> + <field name="line_ids" attrs="{'readonly': [('is_locked', '=', True)]}"> + <tree editable="bottom" create="0" delete="1"> + <field name="from_name"/> + <field name="prod_id" invisible="1"/> + <field name="ref_id" invisible="1"/> + <field name="product_id"/> + <field name="quantity"/> + <field name="price_unit"/> + <field name="discount"/> + <field name="subtotal"/> + <field name="tax" widget="many2many_tags"/> + <field name="tax_amt" widget="monetary" options="{'currency_field': 'currency_id'}"/> + <field name="total" widget="monetary" options="{'currency_field': 'currency_id'}" sum="Grand Total"/> + <field name="reason"/> + </tree> + </field> + </page> + + <page string="Other Info"> + <group col="2"> + <group> + <field name="user_ids" widget="many2many_tags" readonly="1"/> + <field name="created_date" readonly="1"/> + <field name="refund_date" attrs="{'readonly': [('status', 'not in', ['pengajuan3','refund'])]}"/> + </group> + <group> + <field name="bank" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="account_name" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="account_no" attrs="{'readonly': [('is_locked', '=', True)]}"/> + <field name="kcp" attrs="{'readonly': [('is_locked', '=', True)]}"/> + </group> + </group> + </page> + + <page string="Finance Note"> + <group col="2"> + <group> + <field name="finance_note"/> + </group> + <group> + <field name="bukti_refund_type" reqiured="1"/> + <field name="bukti_transfer_refund_pdf" widget="pdf_viewer" attrs="{'invisible': [('bukti_refund_type', '=', 'image')]}"/> + <field name="bukti_transfer_refund_image" widget="image" attrs="{'invisible': [('bukti_refund_type', '=', 'pdf')]}"/> + </group> + </group> + </page> + <page string="Sales Order Lines"> + <field name="so_order_line_ids" nolabel="1" readonly="1"> + <tree> + <field name="order_id"/> + <field name="product_id"/> + <field name="purchase_price"/> + <field name="product_uom_qty"/> + <field name="price_unit"/> + <field name="tax_id" widget="many2many_tags"/> + <field name="discount"/> + <field name="price_subtotal"/> + <field name="item_percent_margin"/> + <field name="item_percent_margin_before"/> + </tree> + </field> + <group class="oe_subtotal_footer oe_right" colspan="2" name="refund_total"> + <field name="amount_untaxed" widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/> + <field name="amount_tax" widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/> + <div class="oe_subtotal_footer_separator oe_inline o_td_label"> + <label for="amount_total"/> + </div> + <field name="amount_total" nolabel="1" class="oe_subtotal_footer_separator" + widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/> + <field name="delivery_amt" widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/> + <div class="oe_subtotal_footer_separator oe_inline o_td_label"> + <label for="grand_total"/> + </div> + <field name="grand_total" nolabel="1" class="oe_subtotal_footer_separator" + widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/> + <field name="total_margin" widget="monetary" options="{'currency_field': 'currency_id'}" readonly="1"/> + </group> + </page> + + <page string="Cancel Reason" attrs="{'invisible': [('status', '=', 'refund')]}"> + <group> + <field name="reason_reject"/> + </group> + </page> + + <page string="Return Line" attrs="{'invisible': ['|', ('tukar_guling_count','=', 0), ('has_picking', '=', False)]}"> + <group> + <field name="tukar_guling_ids" readonly="1" nolabel="1"> + <tree> + <field name="name"/> + <field name="partner_id" string="Customer"/> + <field name="origin" string="SO Number"/> + <field name="operations" string="Operations"/> + <field name="return_type" string="Return Type"/> + <field name="state" widget="badge" + decoration-info="state in ('draft', 'approval_sales', 'approval_finance','approval_logistic')" + decoration-warning="state == 'approved'" + decoration-success="state == 'done'" + decoration-muted="state == 'cancel'" + /> + <field name="ba_num" string="Nomor BA"/> + <field name="date"/> + </tree> + </field> + </group> + </page> + </notebook> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + <field name="activity_ids" widget="mail_activity"/> + </div> + </form> + </field> + </record> + <!-- Action --> + <record id="action_refund_sale_order" model="ir.actions.act_window"> + <field name="name">Refund Sales Order</field> + <field name="res_model">refund.sale.order</field> + <field name="view_mode">tree,form</field> + </record> + + <!-- Menu --> + <menuitem id="menu_refund_sale_order" + name="Refund" + parent="sale.sale_order_menu" + sequence="10" + action="action_refund_sale_order"/> +</odoo> diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml index ca1a36de..c32151d8 100644 --- a/indoteknik_custom/views/res_partner.xml +++ b/indoteknik_custom/views/res_partner.xml @@ -21,6 +21,7 @@ <field name="reference_number"/> </field> <field name="property_payment_term_id" position="after"> + <field name="is_cbd_locked" readonly="1"/> <field name="user_payment_terms_sales" readonly="1"/> <field name="date_payment_terms_sales" readonly="1"/> </field> @@ -35,9 +36,9 @@ <field name="pareto_status"/> <field name="digital_invoice_tax"/> </field> - <field name="nama_wajib_pajak" position="attributes"> + <!-- <field name="nama_wajib_pajak" position="attributes"> <attribute name="required">1</attribute> - </field> + </field> --> <field name="kota_id" position="attributes"> <attribute name="required">0</attribute> </field> @@ -47,14 +48,14 @@ <field name="kelurahan_id" position="attributes"> <attribute name="required">0</attribute> </field> - <field name="npwp" position="attributes"> + <!-- <field name="npwp" position="attributes"> <attribute name="required">1</attribute> </field> <field name="alamat_lengkap_text" position="attributes"> <attribute name="required">1</attribute> - </field> + </field> --> <field name="npwp" position="before"> - <field name="customer_type" required="1"/> + <field name="customer_type"/> </field> <field name="alamat_lengkap_text" position="after"> <field name="nitku" /> @@ -107,7 +108,7 @@ <field name="reminder_invoices"/> </xpath> <xpath expr="//field[@name='property_payment_term_id']" position="attributes"> - <attribute name="readonly">0</attribute> + <attribute name="readonly">1</attribute> </xpath> <xpath expr="//field[@name='property_supplier_payment_term_id']" position="attributes"> <attribute name="readonly">1</attribute> diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index a1a5e0cd..44da3e13 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -35,13 +35,21 @@ string="UangMuka" type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/> </button> - <!-- <xpath expr="//header" position="inside"> + <xpath expr="//header" position="inside"> <button name="button_refund" type="object" string="Refund" - class="btn-primary" - attrs="{'invisible': ['|', ('state', 'not in', ['sale', 'done']), ('has_refund', '=', True)]}" /> - </xpath> --> + class="btn-primary" /> + </xpath> + <xpath expr="//sheet" position="before"> + <field name="partner_is_cbd_locked" invisible="1"/> + <div class="alert alert-danger" + role="alert" + style="height: 40px; margin-bottom:0px;" + attrs="{'invisible':['|', ('partner_is_cbd_locked','=',False), ('state', 'not in', ['draft', 'cancel'])]}"> + <strong>Warning!</strong> Payment Terms Customer terkunci menjadi <b>Cash Before Delivery (C.B.D.)</b> karena ada invoice telah jatuh tempo <b>30 hari</b>. Silakan ajukan <b>Approval Payment Term</b> untuk membuka kunci. + </div> + </xpath> <div class="oe_button_box" name="button_box"> <field name="advance_payment_move_ids" invisible="1"/> <button name="action_open_advance_payment_moves" @@ -52,13 +60,13 @@ <field name="advance_payment_move_count" widget="statinfo" string="Journals"/> </button> - <!-- <button type="object" + <button type="object" name="action_view_related_refunds" class="oe_stat_button" icon="fa-refresh" attrs="{'invisible': [('refund_count', '=', 0)]}"> <field name="refund_count" widget="statinfo" string="Refund"/> - </button> --> + </button> </div> <field name="payment_term_id" position="after"> <field name="create_uid" invisible="1"/> @@ -140,9 +148,9 @@ <field name="pareto_status"/> </field> <field name="analytic_account_id" position="after"> - <field name="customer_type" readonly="1"/> - <field name="npwp" placeholder='99.999.999.9-999.999' readonly="1"/> - <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}" readonly="1"/> + <field name="customer_type"/> + <field name="npwp" placeholder='99.999.999.9-999.999'/> + <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}"/> <field name="email" required="1"/> <field name="unreserve_id"/> <field name="due_id" readonly="1"/> @@ -177,7 +185,6 @@ <field name="expected_ready_to_ship"/> <field name="eta_date_start"/> <field name="eta_date" readonly="1"/> - <!-- <field name="has_refund" readonly="1"/> --> </group> <group string="Return Doc"> <field name="ccm_id" readonly="1"/> @@ -674,7 +681,7 @@ </record> </data> - <!-- <data> + <data> <record id="sale_order_multi_create_refund_ir_actions_server" model="ir.actions.server"> <field name="name">Refund</field> <field name="model_id" ref="sale.model_sale_order"/> @@ -682,7 +689,7 @@ <field name="state">code</field> <field name="code">action = records.open_form_multi_create_refund()</field> </record> - </data> --> + </data> <data> <record id="mail_template_sale_order_notification_to_salesperson" model="mail.template"> diff --git a/indoteknik_custom/views/stock_move_line.xml b/indoteknik_custom/views/stock_move_line.xml index 757d2522..94c0bf53 100644 --- a/indoteknik_custom/views/stock_move_line.xml +++ b/indoteknik_custom/views/stock_move_line.xml @@ -3,18 +3,19 @@ <record id="stock_move_line_form_view_inherited" model="ir.ui.view"> <field name="name">Stock Move Line</field> <field name="model">stock.move.line</field> - <field name="inherit_id" ref="stock.view_move_line_form" /> + <field name="inherit_id" ref="stock.view_move_line_form"/> + <field name="priority" eval="100"/> <field name="arch" type="xml"> - <field name="qty_done" position="after"> + <xpath expr="(//form//group[.//field[@name='qty_done']])[last()]" position="inside"> <field name="manufacture"/> - </field> + </xpath> </field> </record> <record id="stock_move_line_tree_view_inherited" model="ir.ui.view"> <field name="name">Stock Move Line</field> <field name="model">stock.move.line</field> - <field name="inherit_id" ref="stock.view_move_line_tree" /> + <field name="inherit_id" ref="stock.view_move_line_tree"/> <field name="arch" type="xml"> <field name="product_id" position="after"> <field name="manufacture"/> diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index b3f0ce9f..fc8be790 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -8,7 +8,7 @@ <field name="arch" type="xml"> <tree position="attributes"> <attribute name="default_order">final_seq asc</attribute> - <!-- <attribute name="default_order">create_date desc</attribute> --> + <!-- <attribute name="default_order">create_date desc</attribute> --> </tree> <field name="json_popover" position="after"> <field name="date_done" optional="hide"/> @@ -20,9 +20,11 @@ <field name="sj_return_date" optional="hide"/> <field name="date_reserved" optional="hide"/> <field name="state_reserve" optional="hide"/> - <field name="state_packing" widget="badge" decoration-success="state_packing == 'packing_done'" decoration-danger="state_packing == 'not_packing'" optional="hide"/> + <field name="state_packing" widget="badge" decoration-success="state_packing == 'packing_done'" + decoration-danger="state_packing == 'not_packing'" optional="hide"/> <field name="final_seq"/> - <field name="state_approve_md" widget="badge" decoration-success="state_approve_md == 'done'" decoration-warning="state_approve_md == 'pending'" optional="hide"/> + <field name="state_approve_md" widget="badge" decoration-success="state_approve_md == 'done'" + decoration-warning="state_approve_md == 'pending'" optional="hide"/> <!-- <field name="countdown_hours" optional="hide"/> <field name="countdown_ready_to_ship" /> --> </field> @@ -50,11 +52,11 @@ type="object" attrs="{'invisible': ['|', ('state', 'in', ['done']), ('approval_receipt_status', '=', 'pengajuan1')]}" /> -<!-- <button name="ask_return_approval"--> -<!-- string="Ask Return/Acc"--> -<!-- type="object"--> -<!-- attrs="{'invisible': [('state', 'in', ['draft', 'cancel', 'assigned'])]}"--> -<!-- />--> + <!-- <button name="ask_return_approval"--> + <!-- string="Ask Return/Acc"--> + <!-- type="object"--> + <!-- attrs="{'invisible': [('state', 'in', ['draft', 'cancel', 'assigned'])]}"--> + <!-- />--> <button name="action_create_invoice_from_mr" string="Create Bill" type="object" @@ -64,12 +66,12 @@ string="Biteship" type="object" /> - <!-- <button name="action_sync_biteship_tracking" - type="object" - string="Lacak dari Biteship" - class="btn-primary" - attrs="{'invisible': [('biteship_id', '=', False)]}" - /> --> + <!-- <button name="action_sync_biteship_tracking" + type="object" + string="Lacak dari Biteship" + class="btn-primary" + attrs="{'invisible': [('biteship_id', '=', False)]}" + /> --> <button name="track_envio_shipment" string="Tracking Envio" type="object" @@ -97,6 +99,11 @@ attrs="{'invisible': [('state_approve_md', 'not in', ['waiting'])]}" /> </button> + <!-- <xpath expr="//field[@name='move_ids_without_package']//tree//field[@name='product_uom']" + position="after"> + <field name="product_image" widget="image" + style="height:128px;width:128px;" readonly="1"/> + </xpath> --> <field name="backorder_id" position="after"> <field name="select_shipping_option_so"/> <field name="shipping_method_so_id"/> @@ -105,7 +112,8 @@ <field name="count_line_detail"/> <field name="dokumen_tanda_terima"/> <field name="dokumen_pengiriman"/> - <field name="quantity_koli" attrs="{'invisible': [('location_dest_id', '!=', 60)], 'required': [('location_dest_id', '=', 60)]}"/> + <field name="quantity_koli" + attrs="{'invisible': [('location_dest_id', '!=', 60)], 'required': [('location_dest_id', '=', 60)]}"/> <field name="total_mapping_koli" attrs="{'invisible': [('location_id', '!=', 60)]}"/> <field name="total_koli_display" readonly="1" attrs="{'invisible': [('location_id', '!=', 60)]}"/> <field name="linked_out_picking_id" readonly="1" attrs="{'invisible': [('location_id', '=', 60)]}"/> @@ -132,8 +140,13 @@ <field name="scheduled_date" position="attributes"> <attribute name="readonly">1</attribute> </field> + <xpath expr="//field[@name='move_ids_without_package']/form/group/field[@name='description_picking']" + position="after"> + <field name="product_image" widget="image" string="Product Image"/> + </xpath> + <field name="origin" position="after"> -<!-- <field name="show_state_approve_md" invisible="1" optional="hide"/>--> + <!-- <field name="show_state_approve_md" invisible="1" optional="hide"/>--> <field name="state_approve_md" widget="badge"/> <field name="purchase_id"/> <field name="sale_order"/> @@ -141,7 +154,8 @@ <field name="date_doc_kirim" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> <field name="summary_qty_operation"/> <field name="count_line_operation"/> - <field name="linked_manual_bu_out" attrs="{'invisible': [('location_id', '=', 60)]}" domain="[('picking_type_code', '=', 'outgoing'),('state', 'not in', ['done','cancel']), ('group_id', '=', group_id)]"/> + <field name="linked_manual_bu_out" attrs="{'invisible': [('location_id', '=', 60)]}" + domain="[('picking_type_code', '=', 'outgoing'),('state', 'not in', ['done','cancel']), ('group_id', '=', group_id)]"/> <field name="account_id" attrs="{ 'readonly': [['state', 'in', ['done', 'cancel']]], @@ -189,29 +203,35 @@ </group> </group> </page> - <page string="Delivery" name="delivery_order" attrs="{'invisible': [('location_dest_id', '=', 60)]}"> + <page string="Delivery" name="delivery_order" + attrs="{'invisible': [('location_dest_id', '=', 60)]}"> <group> <group> <field name="notee"/> <field name="note_logistic"/> <field name="note_info"/> - <field name="responsible" /> - <field name="carrier_id" attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}" /> + <field name="responsible"/> + <field name="carrier_id" + attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/> <field name="biteship_id" invisible="1"/> <field name="out_code" attrs="{'invisible': [['out_code', '=', False]]}"/> <field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/> - <field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/> - <field name="driver_departure_date" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> + <field name="picking_code" + string="Picking code (akan digenerate ketika sudah di-validate)" + attrs="{'invisible': [['picking_code', '!=', False]]}"/> + <field name="driver_departure_date" + attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> <field name="driver_arrival_date"/> - <field name="delivery_tracking_no" attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/> + <field name="delivery_tracking_no" + attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/> <field name="driver_id"/> <field name='sj_return_date'/> - <field name="sj_documentation" widget="image" /> - <field name="paket_documentation" widget="image" /> + <field name="sj_documentation" widget="image"/> + <field name="paket_documentation" widget="image"/> </group> <!-- Biteship Group --> <group attrs="{'invisible': [('select_shipping_option_so', '!=', 'biteship')]}"> - <field name="delivery_tracking_no" /> + <field name="delivery_tracking_no"/> <field name="shipping_method_so_id"/> <field name="shipping_option_so_id"/> <field name="biteship_shipping_price" readonly="1"/> @@ -220,7 +240,8 @@ <field name="biteship_driver_name" readonly="1"/> <field name="biteship_driver_phone" readonly="1"/> <field name="biteship_driver_plate_number" readonly="1"/> - <button name="action_open_biteship_tracking" string="Visit Biteship Tracking" type="object"/> + <button name="action_open_biteship_tracking" string="Visit Biteship Tracking" + type="object"/> </group> <group attrs="{'invisible': [('carrier_id', '!=', 151)]}"> @@ -261,23 +282,27 @@ </group> </group> </page> - <page string="Check Product" name="check_product" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"> + <page string="Check Product" name="check_product" + attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"> <field name="check_product_lines"/> </page> - <page string="Barcode Product" name="barcode_product" attrs="{'invisible': [('picking_type_code', '!=', 'incoming')]}"> + <page string="Barcode Product" name="barcode_product" + attrs="{'invisible': [('picking_type_code', '!=', 'incoming')]}"> <field name="barcode_product_lines"/> </page> <page string="Check Koli" name="check_koli" attrs="{'invisible': [('location_dest_id', '!=', 60)]}"> <field name="check_koli_lines"/> </page> - <page string="Mapping Koli" name="konfirm_koli" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"> + <page string="Mapping Koli" name="konfirm_koli" + attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"> <field name="konfirm_koli_lines"/> </page> - <page string="Konfirm Koli" name="scan_koli" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"> + <page string="Konfirm Koli" name="scan_koli" + attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"> <field name="scan_koli_lines"/> </page> </page> - + </field> </record> @@ -287,18 +312,20 @@ <field name="arch" type="xml"> <tree editable="bottom"> <field name="code_koli"/> - <field name="koli_id" options="{'no_create': True}" domain="[('state', '=', 'not_delivered')]"/> + <field name="koli_id" options="{'no_create': True}" domain="[('state', '=', 'not_delivered')]"/> <field name="scan_koli_progress"/> </tree> </field> </record> + <record id="konfirm_koli_tree" model="ir.ui.view"> <field name="name">konfirm.koli.tree</field> <field name="model">konfirm.koli</field> <field name="arch" type="xml"> <tree editable="bottom"> - <field name="pick_id" options="{'no_create': True}" required="1" domain="[('picking_type_code', '=', 'internal'), ('group_id', '=', parent.group_id), ('linked_manual_bu_out', '=', parent.id)]"/> + <field name="pick_id" options="{'no_create': True}" required="1" + domain="[('picking_type_code', '=', 'internal'), ('group_id', '=', parent.group_id), ('linked_manual_bu_out', '=', parent.id)]"/> </tree> </field> </record> @@ -307,7 +334,7 @@ <field name="name">check.koli.tree</field> <field name="model">check.koli</field> <field name="arch" type="xml"> - <tree editable="bottom"> + <tree editable="bottom"> <field name="koli"/> <field name="reserved_id"/> <field name="check_koli_progress"/> @@ -344,12 +371,14 @@ <field name="model">stock.move.line</field> <field name="inherit_id" ref="stock.view_stock_move_line_detailed_operation_tree"/> <field name="arch" type="xml"> - <tree editable="bottom" decoration-muted="(state == 'done' and is_locked == True)" 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"> + <tree editable="bottom" decoration-muted="(state == 'done' and is_locked == True)" + 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"/> </tree> </field> </record> - + <record id="view_picking_internal_search_inherit" model="ir.ui.view"> <field name="name">stock.picking.internal.search.inherit</field> @@ -382,7 +411,7 @@ </form> </field> </record> - + <record id="action_warning_modal_wizard" model="ir.actions.act_window"> <field name="name">Peringatan Koli</field> <field name="res_model">warning.modal.wizard</field> |
