From 414b741f6fdd3ba37754db516c695231e3fbca6e Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Fri, 25 Jul 2025 09:22:20 +0700 Subject: hold refund --- indoteknik_custom/__manifest__.py | 1 + indoteknik_custom/models/refund_sale_order.py | 8 ++++---- indoteknik_custom/models/sale_order.py | 2 +- indoteknik_custom/views/account_move.xml | 6 +++--- indoteknik_custom/views/sale_order.xml | 14 +++++++------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 21afc26f..93c437af 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -170,6 +170,7 @@ 'views/public_holiday.xml', 'views/stock_inventory.xml', 'views/sale_order_delay.xml', + 'views/refund_sale_order.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 11bfd07f..2a55f16d 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -175,7 +175,7 @@ class RefundSaleOrder(models.Model): if refund_type == 'retur_half' and not invoice_ids: raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.") - total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total')) if invoice_ids else 0.0 + total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 uang_masuk = vals.get('uang_masuk', 0.0) ongkir = vals.get('ongkir', 0.0) pengurangan = total_invoice + ongkir @@ -266,7 +266,7 @@ class RefundSaleOrder(models.Model): raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk retur sebagian.") 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')) + total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) uang_masuk = vals.get('uang_masuk', rec.uang_masuk) ongkir = vals.get('ongkir', rec.ongkir) @@ -325,7 +325,7 @@ class RefundSaleOrder(models.Model): 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')) self.invoice_ids = all_invoices @@ -464,7 +464,7 @@ 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: diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index febdaabd..c4de0d3a 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -3144,7 +3144,7 @@ class SaleOrder(models.Model): 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')) + total_invoice = sum(invoice_ids.mapped('amount_total_signed')) return { 'type': 'ir.actions.act_window', diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index 9b1c791b..ae944a4a 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -36,9 +36,9 @@ - - + + + diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 5bcd7641..1a4726d6 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -35,13 +35,13 @@ string="UangMuka" type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/> - +
- +
@@ -176,7 +176,7 @@ - + - + -- cgit v1.2.3 From b4fa8f4df995f946b642eda78ca5fc2b8badada2 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Mon, 28 Jul 2025 15:06:26 +0700 Subject: fixing journal --- indoteknik_custom/models/refund_sale_order.py | 32 ++++++++++++++++++++++----- indoteknik_custom/models/sale_order.py | 6 +++-- indoteknik_custom/views/refund_sale_order.xml | 6 ++--- indoteknik_custom/views/sale_order.xml | 2 +- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 2a55f16d..80d66d8d 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -124,7 +124,7 @@ class RefundSaleOrder(models.Model): self.env.user.has_group('indoteknik_custom.group_role_fat') or self.env.user.id not 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': @@ -150,13 +150,18 @@ 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") - if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']: + if not invoice_ids and refund_type and 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 in ['barang_kosong', 'barang_kosong_sebagian'] and so_ids: + sale_orders = self.env['sale.order'].browse(so_ids) + zero_delivery_lines = sale_orders.mapped('order_line').filtered(lambda l: l.qty_delivered == 0) + 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 != 'lainnya': raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") @@ -180,6 +185,10 @@ class RefundSaleOrder(models.Model): ongkir = vals.get('ongkir', 0.0) pengurangan = total_invoice + ongkir + if refund_type == 'barang_kosong_sebagian' and so_ids: + sale_orders = self.env['sale.order'].browse(so_ids) + vals['uang_masuk'] = sum(sale_orders.mapped('amount_total')) + if uang_masuk > pengurangan: vals['amount_refund'] = uang_masuk - pengurangan else: @@ -229,8 +238,16 @@ class RefundSaleOrder(models.Model): refund_type = vals.get('refund_type', rec.refund_type) + 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) + 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 != 'lainnya': raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") + + if refund_type == 'barang_kosong_sebagian' and sale_orders: + vals['uang_masuk'] = sum(sale_orders.mapped('amount_total')) invoice_ids = vals.get('invoice_ids', False) @@ -248,7 +265,7 @@ class RefundSaleOrder(models.Model): 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 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 in ['retur', 'retur_half'] and so_ids: @@ -358,6 +375,9 @@ class RefundSaleOrder(models.Model): self.line_ids = line_vals + if self.refund_type == 'barang_kosong_sebagian' and self.sale_order_ids: + self.uang_masuk = sum(self.sale_order_ids.mapped('amount_total')) + sum(self.sale_order_ids.mapped('delivery_amt')) + elif self.refund_type in ['retur', 'retur_half'] and self.sale_order_ids: line_vals = [] StockPicking = self.env['stock.picking'] @@ -586,8 +606,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, diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 4d0b1d7b..46dad6ff 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -3111,6 +3111,7 @@ class SaleOrder(models.Model): self.ensure_one() invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel') + total_so = sum(self.mapped('amount_total')) return { 'name': 'Refund Sale Order', @@ -3121,7 +3122,7 @@ class SaleOrder(models.Model): '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': total_so, 'default_ongkir': self.delivery_amt or 0.0, 'default_bank': '', # bisa isi default bank kalau mau 'default_account_name': '', @@ -3150,6 +3151,7 @@ class SaleOrder(models.Model): 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_signed')) + total_so = sum(self.mapped('amount_total')) return { 'type': 'ir.actions.act_window', @@ -3160,7 +3162,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_so, 'default_ongkir': delivery_total, 'default_bank': '', 'default_account_name': '', diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml index 4f791722..0c4eaa56 100644 --- a/indoteknik_custom/views/refund_sale_order.xml +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -66,7 +66,7 @@ string="Journal Refund" type="object" class="oe_highlight" - attrs="{'invisible': ['|', ('status', 'not in', ['pengajuan3','refund']), ('journal_refund_state', '=', 'posted')]}"/> + attrs="{'invisible': [('journal_refund_state', '=', 'posted')]}"/> - - + + diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 987303ba..08dcfbb7 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -40,7 +40,7 @@ type="object" string="Refund" class="btn-primary" - attrs="{'invisible': ['|', ('state', 'not in', ['sale', 'done']), ('has_refund', '=', True)]}" /> + attrs="{'invisible': [('has_refund', '=', 'True')]}" />
-- cgit v1.2.3 From 747075fb85372666b89d5ffd07a43664ecf169e1 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Mon, 4 Aug 2025 14:41:27 +0700 Subject: Fix problem --- indoteknik_custom/models/refund_sale_order.py | 20 ++++++++++++-------- indoteknik_custom/views/ir_sequence.xml | 2 +- indoteknik_custom/views/refund_sale_order.xml | 10 +++++----- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 65e93ba4..077809e9 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -181,14 +181,17 @@ class RefundSaleOrder(models.Model): raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.") total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 - uang_masuk = vals.get('uang_masuk', 0.0) ongkir = vals.get('ongkir', 0.0) + vals['total_invoice'] = total_invoice pengurangan = total_invoice + ongkir if refund_type == 'barang_kosong_sebagian' and so_ids: sale_orders = self.env['sale.order'].browse(so_ids) vals['uang_masuk'] = sum(sale_orders.mapped('amount_total')) + + uang_masuk = vals.get('uang_masuk', 0.0) + if uang_masuk > pengurangan: vals['amount_refund'] = uang_masuk - pengurangan else: @@ -284,6 +287,7 @@ class RefundSaleOrder(models.Model): 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_signed')) + vals['total_invoice'] = total_invoice uang_masuk = vals.get('uang_masuk', rec.uang_masuk) ongkir = vals.get('ongkir', rec.ongkir) @@ -296,10 +300,10 @@ class RefundSaleOrder(models.Model): return super().write(vals) - @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): @@ -506,19 +510,19 @@ class RefundSaleOrder(models.Model): if not rec.status or rec.status == 'draft': rec.status = 'pengajuan1' - elif rec.status == 'pengajuan1' and self.env.user.id == 19: + elif rec.status == 'pengajuan1': rec.status = 'pengajuan2' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_sales = now rec.position_sales = 'Sales Manager' - elif rec.status == 'pengajuan2' and self.env.user.id == 688: + elif rec.status == 'pengajuan2': rec.status = 'pengajuan3' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_ar = now rec.position_ar = 'AR' - elif rec.status == 'pengajuan3' and self.env.user.id == 7: + elif rec.status == 'pengajuan3': rec.status = 'refund' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_pimpinan = now @@ -532,7 +536,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' @@ -548,7 +552,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) 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 @@ - Refund Sale Order + Refund Sales Order refund.sale.order RC/%(year)s/%(month)s/ 4 diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml index e35e76b3..27c5feec 100644 --- a/indoteknik_custom/views/refund_sale_order.xml +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -56,7 +56,7 @@
@@ -163,8 +163,8 @@ - - + + -- cgit v1.2.3 From bbc454fd6e13d12e9674769a555264f2c2343b5b Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Wed, 20 Aug 2025 09:06:33 +0700 Subject: refund system sisa uang muka --- indoteknik_custom/models/refund_sale_order.py | 226 +++++++++++++++++++++----- indoteknik_custom/models/sale_order.py | 80 +++++++-- indoteknik_custom/views/refund_sale_order.xml | 40 ++++- indoteknik_custom/views/sale_order.xml | 4 +- 4 files changed, 292 insertions(+), 58 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 077809e9..2dc72f0f 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -17,13 +17,17 @@ class RefundSaleOrder(models.Model): 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'), @@ -55,7 +59,7 @@ 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) refund_type_display = fields.Char(string="Refund Type Label", compute="_compute_refund_type_display") @@ -89,7 +93,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,22 +111,44 @@ 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)", + ) - @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 Sales dan Finance yang boleh membuat refund.") @@ -158,9 +184,20 @@ class RefundSaleOrder(models.Model): if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and so_ids: sale_orders = self.env['sale.order'].browse(so_ids) - zero_delivery_lines = sale_orders.mapped('order_line').filtered(lambda l: l.qty_delivered == 0) - if not zero_delivery_lines: - raise UserError("❌ Tidak ada barang yang Tidak Terikirim di Sales Order yang dipilih.") + + 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': @@ -180,22 +217,40 @@ class RefundSaleOrder(models.Model): if refund_type == 'retur_half' and not invoice_ids: raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.") - total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 - ongkir = vals.get('ongkir', 0.0) - vals['total_invoice'] = total_invoice - pengurangan = total_invoice + ongkir - if refund_type == 'barang_kosong_sebagian' and so_ids: sale_orders = self.env['sale.order'].browse(so_ids) vals['uang_masuk'] = sum(sale_orders.mapped('amount_total')) - - uang_masuk = vals.get('uang_masuk', 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 uang_masuk > pengurangan: - vals['amount_refund'] = uang_masuk - pengurangan - else: - raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir untuk melakukan refund") + uang_masuk = total_uang_muka if moves else sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) + vals['uang_masuk'] = uang_masuk + ongkir = vals.get('ongkir', 0.0) + 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: + so = self.env['sale.order'].browse(so_ids[0]) + existing_refunds = self.search([('sale_order_ids', 'in', so_ids)]) + total_refunded = sum(existing_refunds.mapped('amount_refund')) + amount_refund + remaining = uang_masuk - total_refunded + 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) @@ -285,20 +340,41 @@ class RefundSaleOrder(models.Model): if refund_type == 'retur_half' and not invoice_ids: raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk retur sebagian.") - if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_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 = vals.get('uang_masuk', rec.uang_masuk) - ongkir = vals.get('ongkir', rec.ongkir) + uang_masuk = rec.uang_masuk + amount_refund = vals.get('amount_refund', rec.amount_refund) + + if amount_refund <= 0: + raise ValidationError("Total Refund harus lebih dari 0.") + + existing_refunds = self.search([ + ('sale_order_ids', 'in', so_ids), + ('id', '!=', rec.id) + ]) + total_refunded = sum(existing_refunds.mapped('amount_refund')) + amount_refund + remaining = uang_masuk - total_refunded - 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("❌ Dana uang masuk telah sepenuhnya di refund tidak bisa Mengubah Nominal 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('ongkir', 'amount_refund') + def _onchange_refund_fields(self): + for rec in self: + uang_masuk = rec.uang_masuk or 0.0 + ongkir = rec.ongkir or 0.0 + refund_input = rec.amount_refund or 0.0 + + total_refund_with_ongkir = refund_input + ongkir + if total_refund_with_ongkir > uang_masuk: + raise UserError("❌ Refund + Ongkir tidak boleh melebihi Uang Masuk.") + remaining = uang_masuk - refund_input + rec.remaining_refundable = remaining @api.depends('status_payment', 'status') def _compute_is_locked(self): @@ -362,6 +438,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): @@ -374,14 +481,12 @@ class RefundSaleOrder(models.Model): line_vals.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, + 'product_from': line.order_id.name, 'reason': '', })) self.line_ids = line_vals - if self.refund_type == 'barang_kosong_sebagian' and self.sale_order_ids: - self.uang_masuk = sum(self.sale_order_ids.mapped('amount_total')) + sum(self.sale_order_ids.mapped('delivery_amt')) - elif self.refund_type in ['retur', 'retur_half'] and self.sale_order_ids: line_vals = [] StockPicking = self.env['stock.picking'] @@ -476,6 +581,7 @@ class RefundSaleOrder(models.Model): line_vals.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, + 'product_from': line.order_id.name, 'reason': '', })) res['line_ids'] = line_vals @@ -671,7 +777,52 @@ 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) class RefundSaleOrderLine(models.Model): _name = 'refund.sale.order.line' @@ -682,3 +833,4 @@ class RefundSaleOrderLine(models.Model): product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Qty') reason = fields.Char(string='Reason') + product_from = fields.Char(string='Product Reference') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index aa534d0c..2acee890 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -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', @@ -3197,9 +3196,35 @@ class SaleOrder(models.Model): def button_refund(self): self.ensure_one() + 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_so = sum(self.mapped('amount_total')) + total_refunded = sum(self.refund_ids.mapped('amount_refund')) or 0.0 + 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', @@ -3210,9 +3235,9 @@ class SaleOrder(models.Model): 'context': { 'default_sale_order_ids': [(6, 0, [self.id])], 'default_invoice_ids': [(6, 0, invoice_ids.ids)], - 'default_uang_masuk': total_so, + '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': '', @@ -3231,15 +3256,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_signed')) - total_so = sum(self.mapped('amount_total')) return { 'type': 'ir.actions.act_window', @@ -3250,7 +3303,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_so, + 'default_uang_masuk': total_uang_masuk, 'default_ongkir': delivery_total, 'default_bank': '', 'default_account_name': '', @@ -3259,11 +3312,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/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml index 27c5feec..dd47c2ab 100644 --- a/indoteknik_custom/views/refund_sale_order.xml +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -14,6 +14,7 @@ + - + + + @@ -133,7 +136,8 @@ - + + @@ -168,6 +172,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 1d51fd69..8f8e4288 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -39,8 +39,7 @@ + + - + -- cgit v1.2.3 From 3286de7f2d48f913ad1aa477b49155047ebb742c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 27 Aug 2025 13:11:17 +0700 Subject: Regenerate midtrans via website --- indoteknik_api/controllers/api_v1/sale_order.py | 128 +++++++++--------------- 1 file changed, 49 insertions(+), 79 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 374b49a2..d4038a64 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -116,14 +116,12 @@ class SaleOrder(controller.Controller): if not params['valid']: return self.response(code=400, description=params) - partner_child_ids = self.get_partner_child_ids( - params['value']['partner_id']) + partner_child_ids = self.get_partner_child_ids(params['value']['partner_id']) domain = [('partner_id', 'in', partner_child_ids)] context = params['value']['context'] if context == 'quotation': - domain += ["|", "|", ("state", "=", "draft"), - ("state", "=", "sent"), ("state", "=", "cancel")] + domain += ["|", "|", ("state", "=", "draft"), ("state", "=", "sent"), ("state", "=", "cancel")] if not context: domain += ["|", ("state", "=", "sale"), ("state", "=", "done")] @@ -135,39 +133,28 @@ class SaleOrder(controller.Controller): ('product_id.name', 'ilike', name), ('product_id.default_code', 'ilike', name), ]) - sale_order_ids_from_lines = order_lines.mapped('order_id.id') - domain += ['|', '|', ('name', 'ilike', name), ('partner_purchase_order_name', 'ilike', name), ('id', 'in', sale_order_ids_from_lines) ] - + if params['value']['site']: site = params['value']['site'].replace(' ', '%') - domain += [ - ('partner_id.site_id.name', 'ilike', '%' + site + '%') - ] + domain += [('partner_id.site_id.name', 'ilike', '%' + site + '%')] status = params['value'].get('status') if status: if status == 'quotation': domain += [('state', '=', 'draft')] domain += [('approval_status', '=', False)] - elif status == 'cancel': domain += [('state', '=', 'cancel')] - elif status == 'diproses': - domain += [ - ('state', '=', 'draft'), - ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), - ] - + domain += [('state', '=', 'draft'), ('approval_status', 'in', ['pengajuan1', 'pengajuan2'])] elif status in ['dikemas', 'dikirim', 'selesai', 'partial']: domain += [('state', '=', 'sale')] - elif status == 'all': domain += [] @@ -179,7 +166,7 @@ class SaleOrder(controller.Controller): elif params['value']['sort'] == 'desc': order = 'amount_total desc' - # Filter berdasarkan tanggal order + # Filter tanggal try: if params['value']['startDate'] and params['value']['endDate']: start_date = datetime.strptime(params['value']['startDate'], '%d/%m/%Y').strftime('%Y-%m-%d 00:00:00') @@ -190,22 +177,17 @@ class SaleOrder(controller.Controller): domain.append(('date_order', '>=', start_date)) domain.append(('date_order', '<=', end_date)) - except ValueError: return self.response(code=400, description="Invalid date format. Use 'DD/MM/YYYY'.") + sale_orders = request.env['sale.order'].search(domain, order=order) - - sale_orders = request.env['sale.order'].search( - domain, order=order) + # Filter status pengiriman (tetap sama) status = params['value'].get('status') if status in ['dikemas', 'dikirim', 'selesai', 'partial']: filtered_orders = [] for sale_order in sale_orders: - bu_pickings = [ - p for p in sale_order.picking_ids - if p.picking_type_id and p.picking_type_id.id == 29 - ] + bu_pickings = [p for p in sale_order.picking_ids if p.picking_type_id and p.picking_type_id.id == 29] total = len(bu_pickings) done_pickings = [p for p in bu_pickings if p.state == 'done'] done_with_driver = [p for p in done_pickings if p.sj_return_date] @@ -217,69 +199,57 @@ class SaleOrder(controller.Controller): filtered_orders.append(sale_order) elif status == 'selesai' and len(done_pickings) == total and len(done_pickings) > 0 and len(done_with_driver) == total: filtered_orders.append(sale_order) - elif status == 'partial' and ( - len(done_pickings) != total or - (done_with_driver and done_without_driver) - ): + elif status == 'partial' and (len(done_pickings) != total or (done_with_driver and done_without_driver)): filtered_orders.append(sale_order) else: filtered_orders = sale_orders filtered_orders_paginated = filtered_orders[offset: offset + limit] + # === Tambahan: payment summary per SO === + CBD_PAYMENT_TERM_ID = 26 + ALLOWED_UNSETTLED = {'', 'expire', 'pending', 'challenge', 'cancel'} # belum settlement + def _is_website_order(so): + # Sesuaikan kalau perlu spesifik id + # return bool(so.source_id and so.source_id.id == 59) + name = (so.source_id and so.source_id.name or '').lower() + return 'web' in name or 'site' in name or bool(so.source_id) + + sale_orders_payload = [] + for so in filtered_orders_paginated: + item = request.env['sale.order'].api_v1_single_response(so) + + so_state = so.state or '' + pay_term_id = so.payment_term_id.id if so.payment_term_id else None + pay_status = (getattr(so, 'payment_status', '') or '').strip().lower() + from_web = _is_website_order(so) + + is_quotation = so_state in ('draft', 'sent') + is_cbd = (pay_term_id == CBD_PAYMENT_TERM_ID) + ok_status = (pay_status in ALLOWED_UNSETTLED) + + eligible = bool(is_quotation and is_cbd and ok_status and from_web) + + # tambahkan field baru (camelCase biar enak dipakai di Next) + item.update({ + 'eligibleContinue': eligible, + 'paymentSummary': { + 'eligible': eligible, + 'soState': so_state, + 'paymentTermId': pay_term_id, + 'paymentStatus': pay_status, + 'sourceId': so.source_id.id if so.source_id else None, + 'redirectUrl': getattr(so, 'payment_link_midtrans', '') or '', + } + }) + sale_orders_payload.append(item) + data = { 'sale_order_total': len(filtered_orders), - 'sale_orders': [request.env['sale.order'].api_v1_single_response(x) for x in filtered_orders_paginated] + 'sale_orders': sale_orders_payload } - return self.response(data) - @http.route(PREFIX_PARTNER + 'sale_order/', auth='public', method=['GET', 'OPTIONS']) - @controller.Controller.must_authorized() - def partner_get_sale_order_detail(self, **kw): - params = self.get_request_params(kw, { - 'partner_id': ['number'], - 'id': ['number'], - }) - if not params['valid']: - return self.response(code=400, description=params) - - partner_child_ids = self.get_partner_child_ids(params['value']['partner_id']) - domain = [('id', '=', params['value']['id']), ('partner_id', 'in', partner_child_ids)] - data = {} - sale_order = request.env['sale.order'].search(domain) - if sale_order: - data = request.env['sale.order'].api_v1_single_response(sale_order, context='with_detail') - if sale_order.expected_ready_to_ship: - bulan_id = [ - "Januari", "Februari", "Maret", "April", "Mei", "Juni", - "Juli", "Agustus", "September", "Oktober", "November", "Desember" - ] - tanggal = sale_order.expected_ready_to_ship.day - bulan = bulan_id[sale_order.expected_ready_to_ship.month - 1] - tahun = sale_order.expected_ready_to_ship.year - data['expected_ready_to_ship'] = f"{tanggal} {bulan} {tahun}" - if sale_order.eta_date_start: - bulan_id = [ - "Januari", "Februari", "Maret", "April", "Mei", "Juni", - "Juli", "Agustus", "September", "Oktober", "November", "Desember" - ] - tanggal = sale_order.eta_date_start.day - bulan = bulan_id[sale_order.eta_date_start.month - 1] - tahun = sale_order.eta_date_start.year - data['eta_date_start'] = f"{tanggal} {bulan} {tahun}" - - if sale_order.eta_date: - bulan_id = [ - "Januari", "Februari", "Maret", "April", "Mei", "Juni", - "Juli", "Agustus", "September", "Oktober", "November", "Desember" - ] - tanggal = sale_order.eta_date.day - bulan = bulan_id[sale_order.eta_date.month - 1] - tahun = sale_order.eta_date.year - data['eta_date_end'] = f"{tanggal} {bulan} {tahun}" - - return self.response(data) @http.route(PREFIX_PARTNER + 'sale_order//checkout', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized(private=True, private_key='partner_id') -- cgit v1.2.3 From 3a1c84158be32915ab9a30877e26c6e48733ba2b Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 28 Aug 2025 10:02:36 +0700 Subject: push --- indoteknik_api/controllers/api_v1/sale_order.py | 44 +++++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index d4038a64..fbbb9479 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -206,40 +206,47 @@ class SaleOrder(controller.Controller): filtered_orders_paginated = filtered_orders[offset: offset + limit] - # === Tambahan: payment summary per SO === + # === Tambahan ringkasan + (opsional) auto-regenerate link Midtrans === CBD_PAYMENT_TERM_ID = 26 - ALLOWED_UNSETTLED = {'', 'expire', 'pending', 'challenge', 'cancel'} # belum settlement + ALLOWED_UNSETTLED = {'', 'expire', 'cancel', 'null', '[null]'} # sesuai syaratmu + def _is_website_order(so): - # Sesuaikan kalau perlu spesifik id - # return bool(so.source_id and so.source_id.id == 59) - name = (so.source_id and so.source_id.name or '').lower() - return 'web' in name or 'site' in name or bool(so.source_id) + # kalau source website fix id=59, ini paling aman + return bool(so.source_id and so.source_id.id == 59) sale_orders_payload = [] for so in filtered_orders_paginated: item = request.env['sale.order'].api_v1_single_response(so) - so_state = so.state or '' - pay_term_id = so.payment_term_id.id if so.payment_term_id else None - pay_status = (getattr(so, 'payment_status', '') or '').strip().lower() - from_web = _is_website_order(so) + # ---- 4 syarat kamu ---- + approval_ok = (so.approval_status in ('pengajuan1', 'pengajuan2')) + source_ok = _is_website_order(so) + term_ok = bool(so.payment_term_id and so.payment_term_id.id == CBD_PAYMENT_TERM_ID) + pay_status = (getattr(so, 'payment_status', '') or '').strip().lower() + payment_ok = (pay_status in ALLOWED_UNSETTLED) - is_quotation = so_state in ('draft', 'sent') - is_cbd = (pay_term_id == CBD_PAYMENT_TERM_ID) - ok_status = (pay_status in ALLOWED_UNSETTLED) + eligible = bool(approval_ok and source_ok and term_ok and payment_ok) - eligible = bool(is_quotation and is_cbd and ok_status and from_web) + # Jika eligible & link kosong, auto-generate SEKARANG (tanpa endpoint baru) + redirect_url = getattr(so, 'payment_link_midtrans', '') or '' + if eligible and not redirect_url: + try: + so.sudo().generate_payment_link_midtrans_sales_order() + redirect_url = getattr(so, 'payment_link_midtrans', '') or '' + except Exception as e: + # jangan gagalkan list; cukup lanjut tanpa link + _logger.warning(f'Autogenerate Midtrans gagal untuk SO {so.id}: {e}') - # tambahkan field baru (camelCase biar enak dipakai di Next) + # sisipkan ke payload (pakai camelCase agar enak di FE) item.update({ 'eligibleContinue': eligible, 'paymentSummary': { 'eligible': eligible, - 'soState': so_state, - 'paymentTermId': pay_term_id, + 'approvalStatus': so.approval_status or '', 'paymentStatus': pay_status, + 'paymentTermId': so.payment_term_id.id if so.payment_term_id else None, 'sourceId': so.source_id.id if so.source_id else None, - 'redirectUrl': getattr(so, 'payment_link_midtrans', '') or '', + 'redirectUrl': redirect_url, } }) sale_orders_payload.append(item) @@ -251,6 +258,7 @@ class SaleOrder(controller.Controller): return self.response(data) + @http.route(PREFIX_PARTNER + 'sale_order//checkout', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized(private=True, private_key='partner_id') def partner_checkout_sale_order_by_id(self, **kw): -- cgit v1.2.3 From 576c49b3260e9d34c86e2da04461dff4bb16e0de Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 28 Aug 2025 10:46:34 +0700 Subject: Push --- indoteknik_api/controllers/api_v1/sale_order.py | 48 ++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index fbbb9479..b48c5a34 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -208,7 +208,7 @@ class SaleOrder(controller.Controller): # === Tambahan ringkasan + (opsional) auto-regenerate link Midtrans === CBD_PAYMENT_TERM_ID = 26 - ALLOWED_UNSETTLED = {'', 'expire', 'cancel', 'null', '[null]'} # sesuai syaratmu + ALLOWED_UNSETTLED = {'', 'expire', 'cancel'} # sesuai syaratmu def _is_website_order(so): # kalau source website fix id=59, ini paling aman @@ -256,7 +256,53 @@ class SaleOrder(controller.Controller): 'sale_orders': sale_orders_payload } return self.response(data) + + @http.route(PREFIX_PARTNER + 'sale_order/', auth='public', method=['GET', 'OPTIONS']) + @controller.Controller.must_authorized() + def partner_get_sale_order_detail(self, **kw): + params = self.get_request_params(kw, { + 'partner_id': ['number'], + 'id': ['number'], + }) + if not params['valid']: + return self.response(code=400, description=params) + partner_child_ids = self.get_partner_child_ids(params['value']['partner_id']) + domain = [('id', '=', params['value']['id']), ('partner_id', 'in', partner_child_ids)] + data = {} + sale_order = request.env['sale.order'].search(domain) + if sale_order: + data = request.env['sale.order'].api_v1_single_response(sale_order, context='with_detail') + if sale_order.expected_ready_to_ship: + bulan_id = [ + "Januari", "Februari", "Maret", "April", "Mei", "Juni", + "Juli", "Agustus", "September", "Oktober", "November", "Desember" + ] + tanggal = sale_order.expected_ready_to_ship.day + bulan = bulan_id[sale_order.expected_ready_to_ship.month - 1] + tahun = sale_order.expected_ready_to_ship.year + data['expected_ready_to_ship'] = f"{tanggal} {bulan} {tahun}" + if sale_order.eta_date_start: + bulan_id = [ + "Januari", "Februari", "Maret", "April", "Mei", "Juni", + "Juli", "Agustus", "September", "Oktober", "November", "Desember" + ] + tanggal = sale_order.eta_date_start.day + bulan = bulan_id[sale_order.eta_date_start.month - 1] + tahun = sale_order.eta_date_start.year + data['eta_date_start'] = f"{tanggal} {bulan} {tahun}" + + if sale_order.eta_date: + bulan_id = [ + "Januari", "Februari", "Maret", "April", "Mei", "Juni", + "Juli", "Agustus", "September", "Oktober", "November", "Desember" + ] + tanggal = sale_order.eta_date.day + bulan = bulan_id[sale_order.eta_date.month - 1] + tahun = sale_order.eta_date.year + data['eta_date_end'] = f"{tanggal} {bulan} {tahun}" + + return self.response(data) @http.route(PREFIX_PARTNER + 'sale_order//checkout', auth='public', method=['POST', 'OPTIONS'], csrf=False) -- cgit v1.2.3 From c6f2111d3872604c6cdd0901c1ee2fde5edb59de Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 28 Aug 2025 13:39:55 +0700 Subject: Done --- indoteknik_api/controllers/api_v1/sale_order.py | 13 ++++++++++++- indoteknik_api/models/sale_order.py | 14 +++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index b48c5a34..2d697ae1 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -151,8 +151,19 @@ class SaleOrder(controller.Controller): domain += [('approval_status', '=', False)] elif status == 'cancel': domain += [('state', '=', 'cancel')] + elif status == 'belum_bayar': + domain += [ + ('state', '=', 'draft'), + ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), + ('payment_status', 'in', [False, None, '', 'pending' ,'expire']) + ] elif status == 'diproses': - domain += [('state', '=', 'draft'), ('approval_status', 'in', ['pengajuan1', 'pengajuan2'])] + domain += [ + ('state', '=', 'draft'), + ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), + ('payment_status', '!=', False), + ('payment_status', 'not in', ['', 'pending', 'expire']), + ] elif status in ['dikemas', 'dikirim', 'selesai', 'partial']: domain += [('state', '=', 'sale')] elif status == 'all': diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py index 9be03927..c59dead9 100644 --- a/indoteknik_api/models/sale_order.py +++ b/indoteknik_api/models/sale_order.py @@ -60,13 +60,17 @@ class SaleOrder(models.Model): # 'tracking_number': picking.delivery_tracking_no or '', # 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False or picking.sj_return_date != False, }) + if sale_order.state == 'cancel': data['status'] = 'cancel' - if sale_order.state == 'draft' and sale_order.approval_status == False: - data['status'] = 'draft' - if sale_order.state == 'draft' and sale_order.approval_status in ['pengajuan1', 'pengajuan2']: - data['status'] = 'waiting' - + elif sale_order.state == 'draft': + if not sale_order.approval_status: + data['status'] = 'draft' + elif sale_order.approval_status in ('pengajuan1', 'pengajuan2'): + if sale_order.payment_status in ('', 'pending', False, None, 'expire'): + data['status'] = 'belum_bayar' + elif sale_order.payment_status not in ['', 'pending', False, None, 'expire']: + data['status'] = 'waiting' if sale_order.state == 'sale': bu_pickings = [ -- cgit v1.2.3 From d672d8745f5157d4cf3ff17907a3ca0881b68901 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 28 Aug 2025 14:24:48 +0700 Subject: Show bayar sekarang in detail transaction --- indoteknik_api/controllers/api_v1/sale_order.py | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 2d697ae1..16fcd449 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -284,6 +284,43 @@ class SaleOrder(controller.Controller): sale_order = request.env['sale.order'].search(domain) if sale_order: data = request.env['sale.order'].api_v1_single_response(sale_order, context='with_detail') + + CBD_PAYMENT_TERM_ID = 26 + ALLOWED_UNSETTLED = {'', 'expire', 'pending'} # kalau mau ikutkan 'pending', tinggal tambahkan di set ini + + def _is_website_order(so): + # aman kalau source website fix id=59 + return bool(so.source_id and so.source_id.id == 59) + + pay_status = (getattr(sale_order, 'payment_status', '') or '').strip().lower() + eligible = ( + sale_order.approval_status in ('pengajuan1', 'pengajuan2') and + _is_website_order(sale_order) and + sale_order.payment_term_id and sale_order.payment_term_id.id == CBD_PAYMENT_TERM_ID and + pay_status in ALLOWED_UNSETTLED + ) + + redirect_url = getattr(sale_order, 'payment_link_midtrans', '') or '' + if eligible and not redirect_url: + try: + sale_order.sudo().generate_payment_link_midtrans_sales_order() + redirect_url = getattr(sale_order, 'payment_link_midtrans', '') or '' + except Exception as e: + _logger.warning(f'Autogenerate Midtrans gagal untuk SO {sale_order.id}: {e}') + + # sisipkan ke payload (snake_case; FE kamu sudah biasa auto-camelCase) + data.update({ + 'eligible_continue': eligible, + 'payment_summary': { + 'eligible': eligible, + 'approval_status': sale_order.approval_status or '', + 'payment_status': pay_status, + 'payment_term_id': sale_order.payment_term_id.id if sale_order.payment_term_id else None, + 'source_id': sale_order.source_id.id if sale_order.source_id else None, + 'redirect_url': redirect_url, + } + }) + if sale_order.expected_ready_to_ship: bulan_id = [ "Januari", "Februari", "Maret", "April", "Mei", "Juni", -- cgit v1.2.3 From 3139c592dd1f74b7c6a6c4917419628895296406 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Fri, 29 Aug 2025 14:23:18 +0700 Subject: refund abis testing aman --- indoteknik_custom/models/refund_sale_order.py | 306 ++++++++++++++++++++------ indoteknik_custom/models/sale_order.py | 36 ++- indoteknik_custom/models/tukar_guling.py | 61 ++--- indoteknik_custom/views/refund_sale_order.xml | 76 ++++++- 4 files changed, 374 insertions(+), 105 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 0b4d2893..086c7a81 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -10,7 +10,7 @@ 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) @@ -49,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") @@ -63,8 +64,19 @@ class RefundSaleOrder(models.Model): ], string='Refund Type', required=True) tukar_guling_ids = fields.One2many( - 'tukar.guling', 'refund_id', - string="Tukar Guling" + '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", + help="Pilih transaksi salah transfer dari jurnal Uang Muka (journal_id=11) yang tidak terkait SO." ) tukar_guling_count = fields.Integer( @@ -72,6 +84,11 @@ class RefundSaleOrder(models.Model): 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') @@ -152,6 +169,34 @@ class RefundSaleOrder(models.Model): 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): @@ -176,6 +221,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( @@ -188,10 +236,10 @@ 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', 'retur']: + 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 jika ada invoice") - if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian']: + 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: @@ -206,7 +254,7 @@ class RefundSaleOrder(models.Model): 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 + 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.") @@ -215,21 +263,37 @@ class RefundSaleOrder(models.Model): 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 == 'barang_kosong_sebagian' and so_ids: - sale_orders = self.env['sale.order'].browse(so_ids) - vals['uang_masuk'] = sum(sale_orders.mapped('amount_total')) - - moves = self.env['account.move'].search([ - ('sale_id', 'in', so_ids), - ('journal_id', '=', 11), - ('state', '=', 'posted'), + 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'), ]) - total_uang_muka = sum(moves.mapped('amount_total_signed')) + 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 - uang_masuk = total_uang_muka if moves else sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) - vals['uang_masuk'] = uang_masuk - total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 + existing_refunds = self.env['refund.sale.order'].search([ + ('sale_order_ids', 'in', so_ids) + ], order='id desc', limit=1) + if existing_refunds: + sisa_uang_masuk = existing_refunds.remaining_refundable + else: + sisa_uang_masuk = total_pembayaran + + if sisa_uang_masuk < 0: + raise UserError("❌ Tidak ada sisa transaksi untuk di-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: @@ -240,16 +304,13 @@ class RefundSaleOrder(models.Model): 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: - so = self.env['sale.order'].browse(so_ids[0]) - existing_refunds = self.search([('sale_order_ids', 'in', so_ids)]) - total_refunded = sum(existing_refunds.mapped('amount_refund')) + amount_refund - remaining = uang_masuk - total_refunded + 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) + return super().create(vals) def write(self, vals): @@ -276,6 +337,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) @@ -294,15 +358,12 @@ class RefundSaleOrder(models.Model): refund_type = vals.get('refund_type', rec.refund_type) 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) + 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 != 'lainnya': - raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.") - - if refund_type == 'barang_kosong_sebagian' and sale_orders: - vals['uang_masuk'] = sum(sale_orders.mapped('amount_total')) + 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) @@ -320,8 +381,12 @@ class RefundSaleOrder(models.Model): 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']: - raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Hanya Bisa dipilih Jika Ada Invoice") + 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 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')) @@ -349,15 +414,23 @@ class RefundSaleOrder(models.Model): @api.onchange('ongkir', 'amount_refund') def _onchange_refund_fields(self): for rec in self: - uang_masuk = rec.uang_masuk or 0.0 - ongkir = rec.ongkir or 0.0 refund_input = rec.amount_refund or 0.0 - total_refund_with_ongkir = refund_input + ongkir - if total_refund_with_ongkir > uang_masuk: - raise UserError("❌ Refund + Ongkir tidak boleh melebihi Uang Masuk.") - remaining = uang_masuk - refund_input - rec.remaining_refundable = remaining + # ambil refund terakhir untuk SO + existing_refund = self.env['refund.sale.order'].search([ + ('sale_order_ids', 'in', rec.sale_order_ids.ids) + ], order='id desc', limit=1) + + if existing_refund: + sisa_uang_masuk = existing_refund.remaining_refundable + else: + sisa_uang_masuk = rec.uang_masuk or 0.0 + + # update field uang_masuk supaya form menampilkan sisa aktual + rec.uang_masuk = sisa_uang_masuk + + # hitung remaining setelah input refund + rec.remaining_refundable = sisa_uang_masuk - refund_input @api.depends('status_payment', 'status') def _compute_is_locked(self): @@ -399,6 +472,8 @@ class RefundSaleOrder(models.Model): all_invoices = self.env['account.move'] total_invoice = 0.0 + so_ids = self.sale_order_ids.ids + for so in self.sale_order_ids: self.ongkir += so.delivery_amt or 0.0 valid_invoices = so.invoice_ids.filtered( @@ -407,6 +482,15 @@ class RefundSaleOrder(models.Model): all_invoices |= valid_invoices total_invoice += sum(valid_invoices.mapped('amount_total_signed')) + 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 + self.invoice_ids = all_invoices self.total_invoice = total_invoice @@ -464,9 +548,15 @@ class RefundSaleOrder(models.Model): line_vals.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, - 'ref_id': line.order_id.id, + '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 @@ -495,33 +585,57 @@ class RefundSaleOrder(models.Model): ]) for picking in product_out: for move in picking.move_lines: - line_vals.append((0, 0, { - 'product_id': move.product_id.id, - 'ref_id': picking.id, - 'quantity': move.product_uom_qty, - 'reason': '', - })) + 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: - line_vals.append((0, 0, { - 'product_id': move.product_id.id, - 'ref_id': picking.ref_id, - 'quantity': move.product_uom_qty, - 'reason': '', - })) + 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: - line_vals.append((0, 0, { - 'product_id': move.product_id.id, - 'ref_id': picking.ref_id, - 'quantity': move.product_uom_qty, - 'reason': '', - })) - self.line_ids = line_vals + 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') @@ -602,6 +716,10 @@ class RefundSaleOrder(models.Model): 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' @@ -856,6 +974,41 @@ class RefundSaleOrder(models.Model): ]) 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']: @@ -897,8 +1050,7 @@ class RefundSaleOrder(models.Model): 'origin_so': refund.sale_order_ids.id, 'operations': picking.id, 'return_type': 'revisi_so', - 'is_has_invoice': bool(refund.invoice_ids.ids), - 'invoice_id': refund.invoice_ids.ids if refund.invoice_ids.ids else None, + 'invoice_id': [(6, 0, refund.invoice_ids.ids)], 'refund_id': refund.id, 'line_ids': line_vals, 'mapping_koli_ids': koli_lines @@ -933,10 +1085,38 @@ class RefundSaleOrder(models.Model): 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='Product Reference') + 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/sale_order.py b/indoteknik_custom/models/sale_order.py index a357eb70..998363ef 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -3196,6 +3196,16 @@ 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), @@ -3220,7 +3230,7 @@ class SaleOrder(models.Model): "(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')) or 0.0 + total_refunded = sum(self.refund_ids.mapped('amount_refund')) sisa_uang_muka = total_uang_muka - total_refunded if sisa_uang_muka <= 0: @@ -3249,6 +3259,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.") diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 1309fcfe..ff4edc84 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -96,38 +96,39 @@ 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), - ] - - if rec.partner_id: - domain.append(('partner_id', '=', rec.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 = 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: @@ -1009,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/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml index a2538f42..ae0861e2 100644 --- a/indoteknik_custom/views/refund_sale_order.xml +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -90,6 +90,11 @@ attrs="{'invisible': [('show_return_alert', '=', False)]}"> ⚠️ SO belum melakukan retur barang. Silakan buat pengajuan retur. + +
@@ -101,12 +106,24 @@ attrs="{'invisible': [('journal_refund_move_id', '=', False)]}"> - + +
- - - - - + + + + + + +
- - + + + @@ -157,9 +179,17 @@ - + + + + + + + + + @@ -176,6 +206,7 @@ +
@@ -230,11 +261,34 @@ + + + + + + + + + + + + + + + + +
+
-- cgit v1.2.3 From fd2d67cacb3015e346b9656b7329606066c2f95a Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 29 Aug 2025 15:01:45 +0700 Subject: fix return selain SO PO --- indoteknik_custom/models/stock_picking_return.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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): -- cgit v1.2.3 From 0298605049e29ef436a5e6984b743f89fed712b3 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Sat, 30 Aug 2025 09:23:35 +0700 Subject: last logic unlink --- indoteknik_custom/models/refund_sale_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 086c7a81..51907083 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -660,10 +660,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') -- cgit v1.2.3 From 19e63a5715369a740f209c49115a90967a6aa7d6 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Sat, 30 Aug 2025 09:58:22 +0700 Subject: fix validation approval --- indoteknik_custom/models/refund_sale_order.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 51907083..6d0b5741 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -751,19 +751,19 @@ class RefundSaleOrder(models.Model): if not rec.status or rec.status == 'draft': rec.status = 'pengajuan1' - elif rec.status == 'pengajuan1': + elif rec.status == 'pengajuan1' and self.env.user.id == 19: rec.status = 'pengajuan2' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_sales = now rec.position_sales = 'Sales Manager' - elif rec.status == 'pengajuan2': + elif rec.status == 'pengajuan2' and self.env.user.id == 688: rec.status = 'pengajuan3' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_ar = now rec.position_ar = 'AR' - elif rec.status == 'pengajuan3': + elif rec.status == 'pengajuan3' and self.env.user.id == 7: rec.status = 'refund' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_pimpinan = now -- cgit v1.2.3 From 7186c4657cb85d2175222b4bebc55456e6970a3a Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 30 Aug 2025 10:09:06 +0700 Subject: unlink val vcm ccm --- indoteknik_custom/models/tukar_guling.py | 4 ++-- indoteknik_custom/models/tukar_guling_po.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 699ee670..e9cbf246 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -435,9 +435,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() 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() -- cgit v1.2.3 From f41e635755bb8092d9a85ccc0a0cb675afc7af00 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sun, 31 Aug 2025 12:17:35 +0700 Subject: oke --- indoteknik_api/controllers/api_v1/sale_order.py | 159 ++++++++++++------------ indoteknik_custom/models/sale_order.py | 21 ++-- 2 files changed, 94 insertions(+), 86 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 16fcd449..339fee4d 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -22,7 +22,7 @@ class SaleOrder(controller.Controller): }) sale_order_line = request.env['sale.order.line'].search([ - ('product_id', '=', product_id), + ('product_id', '=', product_id), ('order_id', '=', so_id) ], limit=1) @@ -41,7 +41,7 @@ class SaleOrder(controller.Controller): return self.response('work') else: return self.response('Sale order line not found', status=404) - + @http.route(prefix + "sale_order_number", auth='public', method=['GET', 'OPTIONS']) @controller.Controller.must_authorized() def get_number_sale_order(self, **kw): @@ -135,10 +135,10 @@ class SaleOrder(controller.Controller): ]) sale_order_ids_from_lines = order_lines.mapped('order_id.id') domain += ['|', '|', - ('name', 'ilike', name), - ('partner_purchase_order_name', 'ilike', name), - ('id', 'in', sale_order_ids_from_lines) - ] + ('name', 'ilike', name), + ('partner_purchase_order_name', 'ilike', name), + ('id', 'in', sale_order_ids_from_lines) + ] if params['value']['site']: site = params['value']['site'].replace(' ', '%') @@ -155,7 +155,7 @@ class SaleOrder(controller.Controller): domain += [ ('state', '=', 'draft'), ('approval_status', 'in', ['pengajuan1', 'pengajuan2']), - ('payment_status', 'in', [False, None, '', 'pending' ,'expire']) + ('payment_status', 'in', [False, None, '', 'pending', 'expire']) ] elif status == 'diproses': domain += [ @@ -193,7 +193,7 @@ class SaleOrder(controller.Controller): sale_orders = request.env['sale.order'].search(domain, order=order) - # Filter status pengiriman (tetap sama) + # Filter status pengiriman status = params['value'].get('status') if status in ['dikemas', 'dikirim', 'selesai', 'partial']: filtered_orders = [] @@ -206,49 +206,39 @@ class SaleOrder(controller.Controller): if status == 'dikemas' and len(done_pickings) == 0: filtered_orders.append(sale_order) - elif status == 'dikirim' and len(done_pickings) == total and len(done_pickings) > 0 and len(done_without_driver) == total: + elif status == 'dikirim' and len(done_pickings) == total and len(done_pickings) > 0 and len( + done_without_driver) == total: filtered_orders.append(sale_order) - elif status == 'selesai' and len(done_pickings) == total and len(done_pickings) > 0 and len(done_with_driver) == total: + elif status == 'selesai' and len(done_pickings) == total and len(done_pickings) > 0 and len( + done_with_driver) == total: filtered_orders.append(sale_order) - elif status == 'partial' and (len(done_pickings) != total or (done_with_driver and done_without_driver)): + elif status == 'partial' and ( + len(done_pickings) != total or (done_with_driver and done_without_driver)): filtered_orders.append(sale_order) else: filtered_orders = sale_orders filtered_orders_paginated = filtered_orders[offset: offset + limit] - # === Tambahan ringkasan + (opsional) auto-regenerate link Midtrans === + # === Ringkasan (tanpa auto-generate) === CBD_PAYMENT_TERM_ID = 26 - ALLOWED_UNSETTLED = {'', 'expire', 'cancel'} # sesuai syaratmu + ALLOWED_CONTINUE = {'', 'pending', 'capture', 'expire', 'cancel'} # boleh munculkan "Bayar Sekarang" def _is_website_order(so): - # kalau source website fix id=59, ini paling aman return bool(so.source_id and so.source_id.id == 59) sale_orders_payload = [] for so in filtered_orders_paginated: item = request.env['sale.order'].api_v1_single_response(so) - # ---- 4 syarat kamu ---- approval_ok = (so.approval_status in ('pengajuan1', 'pengajuan2')) - source_ok = _is_website_order(so) - term_ok = bool(so.payment_term_id and so.payment_term_id.id == CBD_PAYMENT_TERM_ID) - pay_status = (getattr(so, 'payment_status', '') or '').strip().lower() - payment_ok = (pay_status in ALLOWED_UNSETTLED) - - eligible = bool(approval_ok and source_ok and term_ok and payment_ok) + source_ok = _is_website_order(so) + term_ok = bool(so.payment_term_id and so.payment_term_id.id == CBD_PAYMENT_TERM_ID) + pay_status = (getattr(so, 'payment_status', '') or '').strip().lower() + eligible = bool(approval_ok and source_ok and term_ok and pay_status in ALLOWED_CONTINUE) - # Jika eligible & link kosong, auto-generate SEKARANG (tanpa endpoint baru) redirect_url = getattr(so, 'payment_link_midtrans', '') or '' - if eligible and not redirect_url: - try: - so.sudo().generate_payment_link_midtrans_sales_order() - redirect_url = getattr(so, 'payment_link_midtrans', '') or '' - except Exception as e: - # jangan gagalkan list; cukup lanjut tanpa link - _logger.warning(f'Autogenerate Midtrans gagal untuk SO {so.id}: {e}') - # sisipkan ke payload (pakai camelCase agar enak di FE) item.update({ 'eligibleContinue': eligible, 'paymentSummary': { @@ -267,13 +257,14 @@ class SaleOrder(controller.Controller): 'sale_orders': sale_orders_payload } return self.response(data) - + @http.route(PREFIX_PARTNER + 'sale_order/', auth='public', method=['GET', 'OPTIONS']) @controller.Controller.must_authorized() def partner_get_sale_order_detail(self, **kw): params = self.get_request_params(kw, { 'partner_id': ['number'], 'id': ['number'], + 'ensure_payment_link': [], # optional flag }) if not params['valid']: return self.response(code=400, description=params) @@ -281,34 +272,47 @@ class SaleOrder(controller.Controller): partner_child_ids = self.get_partner_child_ids(params['value']['partner_id']) domain = [('id', '=', params['value']['id']), ('partner_id', 'in', partner_child_ids)] data = {} + sale_order = request.env['sale.order'].search(domain) if sale_order: data = request.env['sale.order'].api_v1_single_response(sale_order, context='with_detail') CBD_PAYMENT_TERM_ID = 26 - ALLOWED_UNSETTLED = {'', 'expire', 'pending'} # kalau mau ikutkan 'pending', tinggal tambahkan di set ini + ALLOWED_CONTINUE = {'', 'pending', 'capture', 'expire', 'cancel'} def _is_website_order(so): - # aman kalau source website fix id=59 return bool(so.source_id and so.source_id.id == 59) pay_status = (getattr(sale_order, 'payment_status', '') or '').strip().lower() eligible = ( - sale_order.approval_status in ('pengajuan1', 'pengajuan2') and - _is_website_order(sale_order) and - sale_order.payment_term_id and sale_order.payment_term_id.id == CBD_PAYMENT_TERM_ID and - pay_status in ALLOWED_UNSETTLED + sale_order.approval_status in ('pengajuan1', 'pengajuan2') and + _is_website_order(sale_order) and + sale_order.payment_term_id and sale_order.payment_term_id.id == CBD_PAYMENT_TERM_ID and + pay_status in ALLOWED_CONTINUE ) redirect_url = getattr(sale_order, 'payment_link_midtrans', '') or '' - if eligible and not redirect_url: - try: - sale_order.sudo().generate_payment_link_midtrans_sales_order() - redirect_url = getattr(sale_order, 'payment_link_midtrans', '') or '' - except Exception as e: - _logger.warning(f'Autogenerate Midtrans gagal untuk SO {sale_order.id}: {e}') - # sisipkan ke payload (snake_case; FE kamu sudah biasa auto-camelCase) + ensure_raw = params['value'].get('ensure_payment_link') + ensure_flag = False + if ensure_raw is not None: + s = str(ensure_raw).strip().lower() + ensure_flag = s not in ('', '0', 'false', 'no', 'none') + + if ensure_flag and eligible: + should_generate = False + if not redirect_url: + should_generate = True + elif pay_status in ('expire', 'cancel'): + should_generate = True + + if should_generate: + try: + sale_order.sudo().generate_payment_link_midtrans_sales_order() + redirect_url = getattr(sale_order, 'payment_link_midtrans', '') or '' + except Exception as e: + _logger.warning(f'Generate Midtrans gagal untuk SO {sale_order.id}: {e}') + data.update({ 'eligible_continue': eligible, 'payment_summary': { @@ -321,6 +325,7 @@ class SaleOrder(controller.Controller): } }) + # formatting tanggal (tetap sama) if sale_order.expected_ready_to_ship: bulan_id = [ "Januari", "Februari", "Maret", "April", "Mei", "Juni", @@ -330,6 +335,7 @@ class SaleOrder(controller.Controller): bulan = bulan_id[sale_order.expected_ready_to_ship.month - 1] tahun = sale_order.expected_ready_to_ship.year data['expected_ready_to_ship'] = f"{tanggal} {bulan} {tahun}" + if sale_order.eta_date_start: bulan_id = [ "Januari", "Februari", "Maret", "April", "Mei", "Juni", @@ -352,7 +358,6 @@ class SaleOrder(controller.Controller): return self.response(data) - @http.route(PREFIX_PARTNER + 'sale_order//checkout', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized(private=True, private_key='partner_id') def partner_checkout_sale_order_by_id(self, **kw): @@ -379,12 +384,12 @@ class SaleOrder(controller.Controller): sale_order.approval_status = 'pengajuan2' elif sale_order._requires_approval_margin_manager(): sale_order.approval_status = 'pengajuan1' - + data = request.env['sale.order'].api_v1_single_response( sale_order, context='with_detail') return self.response(data) - + @http.route(PREFIX_PARTNER + 'sale_order//approve', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized(private=True, private_key='partner_id') def partner_approve_sale_order_by_id(self, **kw): @@ -392,27 +397,27 @@ class SaleOrder(controller.Controller): 'partner_id': ['number'], 'id': ['number'] }) - + if not params['valid']: return self.response(code=400, description=params) - + value = params['value'] partner_child_ids = self.get_partner_child_ids(value['partner_id']) - + sale_order = request.env['sale.order'].search([ ('id', '=', value['id']), ('partner_id', 'in', partner_child_ids) ]) if not sale_order: return self.response(code=404, description='Sale Order not found') - + partner = request.env['res.partner'].browse(value['partner_id']) if not partner.web_role: return self.response(code=400, description='Unauthorized') - + if partner.web_role: sale_order.web_approval = 'cust_%s' % partner.web_role - + if sale_order.web_approval in ['cust_procurement', 'cust_director']: if sale_order._requires_approval_margin_leader(): sale_order.approval_status = 'pengajuan2' @@ -420,7 +425,7 @@ class SaleOrder(controller.Controller): sale_order.approval_status = 'pengajuan1' return self.response('success') - + @http.route(PREFIX_PARTNER + 'sale_order//reject', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized(private=True, private_key='partner_id') def partner_reject_sale_order_by_id(self, **kw): @@ -428,27 +433,27 @@ class SaleOrder(controller.Controller): 'partner_id': ['number'], 'id': ['number'] }) - + if not params['valid']: return self.response(code=400, description=params) - + value = params['value'] partner_child_ids = self.get_partner_child_ids(value['partner_id']) - + sale_order = request.env['sale.order'].search([ ('id', '=', value['id']), ('partner_id', 'in', partner_child_ids) ]) if not sale_order: return self.response(code=404, description='Sale Order not found') - + partner = request.env['res.partner'].browse(value['partner_id']) if not partner.web_role: return self.response(code=400, description='Unauthorized') - + if partner.web_role: sale_order.web_approval = 'cust_%s' % partner.web_role - + sale_order.state = 'cancel' sale_order.approval_status = False @@ -519,7 +524,7 @@ class SaleOrder(controller.Controller): sale_order = request.env['sale.order'].sudo().search_read([('id', '=', id)], ['name']) # pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'quotation_so_new')]).render_jasper([id], {}) pdf, type = request.env['ir.actions.report'].sudo().search([('report_name', '=', 'indoteknik_custom.report_saleorder_website')])._render_qweb_pdf([id]) - + if pdf and len(sale_order) > 0: return rest_api.response_attachment({ 'content': pdf, @@ -550,27 +555,27 @@ class SaleOrder(controller.Controller): sale_order.state = 'cancel' data = sale_order.id return self.response(data) - + @http.route(prefix + 'user//sale_order/checkout', auth='public', method=['GET', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized(private=True, private_key='user_id') def get_user_checkout_so(self, user_id, **kw): m_voucher = request.env['voucher'] m_cart = request.env['website.user.cart'] - + voucher_code = kw.get('voucher') voucher_shipping_code = kw.get('voucher_shipping') source = kw.get('source') - + voucher = m_voucher.search([('code', '=', voucher_code), ('apply_type', 'in', ['all', 'brand'])], limit=1) voucher_shipping = m_voucher.search([('code', '=', voucher_shipping_code), ('apply_type', '=', 'shipping')], limit=1) result = m_cart.with_context(price_for="web").get_user_checkout( - user_id, - voucher=voucher, - voucher_shipping=voucher_shipping, + user_id, + voucher=voucher, + voucher_shipping=voucher_shipping, source=source ) return self.response(result) - + @http.route(PREFIX_PARTNER + 'sale_order/checkout', auth='public', method=['POST', 'OPTIONS'], csrf=False) @controller.Controller.must_authorized() def create_partner_sale_order(self, **kw): @@ -616,7 +621,7 @@ class SaleOrder(controller.Controller): main_partner = partner_invoice.get_main_parent() _logger.info( f"Partner Info - Sales: {sales_partner.id}, Invoice: {partner_invoice.id}, Main: {main_partner.id}") - + def _get_request_context(params, kw): # 1) kw (querystring di route) ctx = kw.get('context') @@ -654,7 +659,7 @@ class SaleOrder(controller.Controller): f"form={repr(request.httprequest.form.get('context'))}, " f"json={(request.httprequest.get_json(force=False, silent=True) or {}).get('context') if hasattr(request.httprequest,'get_json') else None}, " f"normalized={ctx}") - + payment_term_id_value = 26 ctx = str((kw.get('context') or '')) @@ -679,7 +684,7 @@ class SaleOrder(controller.Controller): payment_term_id_value = term.id except Exception as e: _logger.warning(f"Gagal resolve payment term dari user: {e}") - + parameters = { 'warehouse_id': 8, 'carrier_id': 1, @@ -839,7 +844,7 @@ class SaleOrder(controller.Controller): except Exception as e: _logger.error(f"Error in create_partner_sale_order: {str(e)}", exc_info=True) return self.response(code=500, description=str(e)) - + @http.route(PREFIX_PARTNER + 'sale-order//awb', auth='public', methods=['GET', 'OPTIONS']) @controller.Controller.must_authorized(private=True, private_key='partner_id') def get_airway_bill_by_sale_order_id(self, **kw): @@ -854,8 +859,8 @@ class SaleOrder(controller.Controller): for airway_bill in airway_bills: data = airway_bill.decode_response() delivery_order = airway_bill.do_id - result = data['rajaongkir']['result'] - + result = data['rajaongkir']['result'] + manifests_data = [] for manifest in airway_bill.manifest_ids: manifest_data = { @@ -865,7 +870,7 @@ class SaleOrder(controller.Controller): 'city': manifest.city, } manifests_data.append(manifest_data) - + airways.append({ 'delivery_order': { 'name': delivery_order.name, @@ -873,7 +878,7 @@ class SaleOrder(controller.Controller): 'receiver_name': airway_bill.receiver_name, 'receiver_city': airway_bill.receiver_city, }, - 'delivered': result['delivered'], + 'delivered': result['delivered'], 'waybill_number': result['summary']['waybill_number'], 'delivery_status': result['delivery_status'], 'manifests': manifests_data @@ -881,7 +886,7 @@ class SaleOrder(controller.Controller): response = {'airways': airways} return self.response(response) - + @http.route('/api/sale_order/invoiced', auth='public', methods=['GET']) def get_sale_order_invoiced_by_partner_id(self, **kw): if not self.authenticate(): diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index ffb53dce..8cb169a6 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1900,26 +1900,30 @@ class SaleOrder(models.Model): # raise UserError('Kelurahan Real Delivery Address harus diisi') def generate_payment_link_midtrans_sales_order(self): - # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox - # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox - midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production - midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production + # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox + # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox + midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production + midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production + so_number = self.name so_number = so_number.replace('/', '-') so_grandtotal = math.floor(self.grand_total) + headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': midtrans_auth, } - check_url = f'https://api.midtrans.com/v2/{so_number}/status' - check_response = requests.get(check_url, headers=headers) + # ==== ENV ==== + # check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox + check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production + # ============================================= + check_response = requests.get(check_url, headers=headers) if check_response.status_code == 200: status_response = check_response.json() - if status_response.get('transaction_status') == 'expire' or status_response.get( - 'transaction_status') == 'cancel': + if status_response.get('transaction_status') in ('expire', 'cancel'): so_number = so_number + '-cpl' json_data = { @@ -1951,7 +1955,6 @@ class SaleOrder(models.Model): buffer = BytesIO() img.save(buffer, format="PNG") qr_code_img = base64.b64encode(buffer.getvalue()).decode() - self.payment_qr_code = qr_code_img @api.model -- cgit v1.2.3 From d91498b1aae363a3c51023a0fc8bc8086a294838 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 1 Sep 2025 09:40:59 +0700 Subject: --- indoteknik_custom/security/ir.model.access.csv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 866a7140..3a320510 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -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 -- cgit v1.2.3 From d7a5e47f741211e9750c21ac148331d6e29bdc16 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 1 Sep 2025 10:00:32 +0700 Subject: (andri) add sum grand total tree dunning run --- indoteknik_custom/views/dunning_run.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/views/dunning_run.xml b/indoteknik_custom/views/dunning_run.xml index 210f7917..f624c42e 100644 --- a/indoteknik_custom/views/dunning_run.xml +++ b/indoteknik_custom/views/dunning_run.xml @@ -13,7 +13,7 @@ - + -- cgit v1.2.3 From 8cf34c73b9276f88499b056c0a7070a3464f3367 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 1 Sep 2025 14:54:15 +0700 Subject: fix finish transactions --- indoteknik_api/controllers/api_v1/sale_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 339fee4d..accc7531 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -87,8 +87,8 @@ class SaleOrder(controller.Controller): 'amount_tax': sale.amount_tax, 'amount_total': sale.amount_total, 'expected_ready_to_ship': f"{sale.expected_ready_to_ship.day} {INDONESIAN_MONTHS[sale.expected_ready_to_ship.month]} {sale.expected_ready_to_ship.year}", - 'eta_date_start': f"{sale.eta_date_start.day} {INDONESIAN_MONTHS[sale.eta_date_start.month]} {sale.eta_date_start.year}", - 'eta_date_end': f"{sale.eta_date.day} {INDONESIAN_MONTHS[sale.eta_date.month]} {sale.eta_date.year}", + # 'eta_date_start': f"{sale.eta_date_start.day} {INDONESIAN_MONTHS[sale.eta_date_start.month]} {sale.eta_date_start.year}", + # 'eta_date_end': f"{sale.eta_date.day} {INDONESIAN_MONTHS[sale.eta_date.month]} {sale.eta_date.year}", 'product_name': product_name, 'product_not_in_id': product_not_in_id, 'details': [request.env['sale.order.line'].api_single_response(x, context='with_detail') for x in sale.order_line] -- cgit v1.2.3 From 77539c2e9ee720c7e731d1b5138041263394a978 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Tue, 2 Sep 2025 11:12:22 +0700 Subject: fix refund ongkir --- indoteknik_custom/models/refund_sale_order.py | 29 ++++++++------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 6d0b5741..737419e7 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -392,6 +392,7 @@ class RefundSaleOrder(models.Model): total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) vals['total_invoice'] = total_invoice uang_masuk = rec.uang_masuk + amount_refund = vals.get('amount_refund', rec.amount_refund) if amount_refund <= 0: @@ -401,36 +402,22 @@ class RefundSaleOrder(models.Model): ('sale_order_ids', 'in', so_ids), ('id', '!=', rec.id) ]) - total_refunded = sum(existing_refunds.mapped('amount_refund')) + amount_refund - remaining = uang_masuk - total_refunded + total_refunded = sum(existing_refunds.mapped('amount_refund')) + if existing_refunds: + remaining = uang_masuk - total_refunded + else: + remaining = uang_masuk - amount_refund - if remaining < 0: - raise ValidationError("❌ Dana uang masuk telah sepenuhnya di refund tidak bisa Mengubah Nominal Refund") vals['remaining_refundable'] = remaining return super().write(vals) - @api.onchange('ongkir', 'amount_refund') + @api.onchange('amount_refund') def _onchange_refund_fields(self): for rec in self: refund_input = rec.amount_refund or 0.0 - - # ambil refund terakhir untuk SO - existing_refund = self.env['refund.sale.order'].search([ - ('sale_order_ids', 'in', rec.sale_order_ids.ids) - ], order='id desc', limit=1) - - if existing_refund: - sisa_uang_masuk = existing_refund.remaining_refundable - else: - sisa_uang_masuk = rec.uang_masuk or 0.0 - - # update field uang_masuk supaya form menampilkan sisa aktual - rec.uang_masuk = sisa_uang_masuk - - # hitung remaining setelah input refund - rec.remaining_refundable = sisa_uang_masuk - refund_input + rec.remaining_refundable = (rec.uang_masuk or 0.0) - refund_input @api.depends('status_payment', 'status') def _compute_is_locked(self): -- cgit v1.2.3 From 71f13fcc278b01133d07d56103bbd45afd967d48 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 2 Sep 2025 13:43:43 +0700 Subject: fix finish Checkout --- indoteknik_api/controllers/api_v1/sale_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 374b49a2..65894d8a 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -87,8 +87,8 @@ class SaleOrder(controller.Controller): 'amount_tax': sale.amount_tax, 'amount_total': sale.amount_total, 'expected_ready_to_ship': f"{sale.expected_ready_to_ship.day} {INDONESIAN_MONTHS[sale.expected_ready_to_ship.month]} {sale.expected_ready_to_ship.year}", - 'eta_date_start': f"{sale.eta_date_start.day} {INDONESIAN_MONTHS[sale.eta_date_start.month]} {sale.eta_date_start.year}", - 'eta_date_end': f"{sale.eta_date.day} {INDONESIAN_MONTHS[sale.eta_date.month]} {sale.eta_date.year}", + # 'eta_date_start': f"{sale.eta_date_start.day} {INDONESIAN_MONTHS[sale.eta_date_start.month]} {sale.eta_date_start.year}", + # 'eta_date_end': f"{sale.eta_date.day} {INDONESIAN_MONTHS[sale.eta_date.month]} {sale.eta_date.year}", 'product_name': product_name, 'product_not_in_id': product_not_in_id, 'details': [request.env['sale.order.line'].api_single_response(x, context='with_detail') for x in sale.order_line] -- cgit v1.2.3 From ddaedc0eeaf53b47786a7747664fc99ae4dc1a7a Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Tue, 2 Sep 2025 14:46:57 +0700 Subject: fix refund validasi --- indoteknik_custom/models/refund_sale_order.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 737419e7..731d8e41 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -408,6 +408,8 @@ class RefundSaleOrder(models.Model): else: remaining = uang_masuk - amount_refund + if remaining < 0: + raise ValidationError("Semua dana sudah dikembalikan, tidak bisa mengajukan refund") vals['remaining_refundable'] = remaining -- cgit v1.2.3 From 461020919518ac0bde4024de482303734fd314dc Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 2 Sep 2025 14:51:29 +0700 Subject: Fix cancel invoice ccm --- indoteknik_custom/models/tukar_guling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index d2e00781..6e839bf0 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -134,7 +134,7 @@ class TukarGuling(models.Model): 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 -- cgit v1.2.3 From f6537c3987112c351a2a099475b1e32a9e8de76e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 4 Sep 2025 09:39:08 +0700 Subject: Fix singleton mathces SO --- indoteknik_custom/models/purchase_order_sales_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.3 From 18df26a9a413261a63cf0531078b51f92f0594fc Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 4 Sep 2025 09:41:21 +0700 Subject: fix finance note open lock --- indoteknik_custom/views/refund_sale_order.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml index ae0861e2..20a07aa3 100644 --- a/indoteknik_custom/views/refund_sale_order.xml +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -214,12 +214,12 @@ - + - - - + + + -- cgit v1.2.3 From 14d3ad7bbb8a6bd9c1272685aed4b3712ab1f33d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 4 Sep 2025 11:14:27 +0700 Subject: (andri) fix reminder --- indoteknik_custom/models/account_move.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index c44cad78..c93cfb76 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -179,9 +179,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( -- cgit v1.2.3 From 31a7f35e3c179b7f7ed3dcf764c7e834af559283 Mon Sep 17 00:00:00 2001 From: it-fixcomart Date: Thu, 4 Sep 2025 11:21:39 +0700 Subject: fix finance note open lock --- indoteknik_custom/views/refund_sale_order.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml index 20a07aa3..0c6cd371 100644 --- a/indoteknik_custom/views/refund_sale_order.xml +++ b/indoteknik_custom/views/refund_sale_order.xml @@ -218,8 +218,8 @@ - - + + -- cgit v1.2.3 From 0a1f22cac507ee7e9975f1a68f79bccdf179c9fd Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 5 Sep 2025 09:07:23 +0700 Subject: add image --- indoteknik_custom/models/stock_move.py | 1 + indoteknik_custom/views/stock_picking.xml | 100 ++++++++++++++++++------------ 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index 90ab30a4..d4106830 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -15,6 +15,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_128 = fields.Image('Product Image', related='product_id.image_1920') # @api.model_create_multi # def create(self, vals_list): diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index b3f0ce9f..e230bccc 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -8,7 +8,7 @@ final_seq asc - + @@ -20,9 +20,11 @@ - + - + @@ -50,11 +52,11 @@ type="object" attrs="{'invisible': ['|', ('state', 'in', ['done']), ('approval_receipt_status', '=', 'pengajuan1')]}" /> - - - - - + + + + + + + + @@ -105,7 +112,8 @@ - + @@ -133,7 +141,7 @@ 1 - + @@ -141,7 +149,8 @@ - + + - - + + - - + + - + - - + + - + @@ -220,7 +235,8 @@ - - -- cgit v1.2.3 From 4bd34e21d8d4dc3b2f688e181fb05c6359ac1f43 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 6 Sep 2025 11:29:08 +0700 Subject: fix manufacture in stock move line & done --- indoteknik_custom/models/stock_move.py | 3 +-- indoteknik_custom/views/stock_move_line.xml | 9 +++++---- indoteknik_custom/views/stock_picking.xml | 7 ++++++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index 9ec36fcd..24d405a6 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -18,8 +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.Image('Product Image', related='product_id.image_1920') - + product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly = True) # @api.model_create_multi # def create(self, vals_list): # moves = super(StockMove, self).create(vals_list) 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 @@ Stock Move Line stock.move.line - + + - + - + Stock Move Line stock.move.line - + diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index c8aa6454..50c5f261 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -99,7 +99,7 @@ attrs="{'invisible': [('state_approve_md', 'not in', ['waiting'])]}" /> - @@ -140,6 +140,11 @@ 1 + + + + -- cgit v1.2.3 From f2adf9ad9ed7491a6ad5422d1236761e7d42c82f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 8 Sep 2025 12:30:59 +0700 Subject: remove image in tree view --- indoteknik_custom/views/stock_picking.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 50c5f261..fc8be790 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -99,11 +99,11 @@ attrs="{'invisible': [('state_approve_md', 'not in', ['waiting'])]}" /> - - + --> -- cgit v1.2.3 From 6188090965c5381fa120268a024a486e62056505 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 8 Sep 2025 15:05:19 +0700 Subject: hilangin validasi sementara --- indoteknik_custom/models/tukar_guling_po.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index f2f37606..c65ac286 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -441,8 +441,8 @@ class TukarGulingPO(models.Model): ('state', '!=', 'cancel'), ], limit=1) - if existing_tukar_guling: - raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) + # if existing_tukar_guling: + # raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) picking = self.operations pick_id = self.operations.picking_type_id.id -- cgit v1.2.3 From 7a34ad0965e08d1428c700f09a49f30f17c1996c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 8 Sep 2025 15:08:54 +0700 Subject: Balikin --- indoteknik_custom/models/tukar_guling_po.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index c65ac286..f2f37606 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -441,8 +441,8 @@ class TukarGulingPO(models.Model): ('state', '!=', 'cancel'), ], limit=1) - # if existing_tukar_guling: - # raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) + if existing_tukar_guling: + raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) picking = self.operations pick_id = self.operations.picking_type_id.id -- cgit v1.2.3 From 69c9579636182d81b1b6da010f29f5efda6f88a7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 8 Sep 2025 15:13:38 +0700 Subject: Fix oauth google --- indoteknik_api/controllers/api_v1/user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py index dde30fec..c75e4954 100644 --- a/indoteknik_api/controllers/api_v1/user.py +++ b/indoteknik_api/controllers/api_v1/user.py @@ -89,7 +89,8 @@ 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) -- cgit v1.2.3 From 03db5f5bfd027fceeb0733bbb51e95e03c43d2cd Mon Sep 17 00:00:00 2001 From: Miqdad Date: Mon, 8 Sep 2025 16:20:28 +0700 Subject: copy false PO refund --- indoteknik_custom/models/purchase_order.py | 2 +- indoteknik_custom/models/refund_sale_order.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 731d8e41..9ab18f27 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -76,6 +76,7 @@ class RefundSaleOrder(models.Model): 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." ) -- cgit v1.2.3 From 142f1a37b0d73ea847345428acc4224ff6e49419 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 11 Sep 2025 08:57:04 +0700 Subject: purchase report --- indoteknik_custom/__manifest__.py | 2 + indoteknik_custom/report/purchase_report.xml | 162 +++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 indoteknik_custom/report/purchase_report.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 31685005..09a3aa6f 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -166,6 +166,7 @@ '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', @@ -177,6 +178,7 @@ '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/report/purchase_report.xml b/indoteknik_custom/report/purchase_report.xml new file mode 100644 index 00000000..9d7f4028 --- /dev/null +++ b/indoteknik_custom/report/purchase_report.xml @@ -0,0 +1,162 @@ + + + + + + Purchase Order (Website) + purchase.order + qweb-pdf + indoteknik_custom.report_purchaseorder_website + indoteknik_custom.report_purchaseorder_website + + ('PO - %s - %s' % (object.partner_id.name, object.name)) + + + report + + + + + + + + + + -- cgit v1.2.3