from email.policy import default from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging from datetime import datetime _logger = logging.getLogger(__name__) class TukarGulingPO(models.Model): _name = 'tukar.guling.po' _description = 'Pengajuan Retur PO' _inherit = ['mail.thread', 'mail.activity.mixin'] vendor_id = fields.Many2one('res.partner', string='Vendor Name', readonly=True) origin = fields.Char(string='Origin PO') is_po = fields.Boolean('Is PO', default=True) is_so = fields.Boolean('Is SO', default=False) name = fields.Char(string='Name', required=True) po_picking_ids = fields.One2many( 'stock.picking', 'tukar_guling_po_id', string='Picking Reference', ) name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) date_purchase = fields.Datetime('Date Approve Purchase', readonly=True) date_finance = fields.Datetime('Date Approve Finance', readonly=True) date_logistic = fields.Datetime('Date Approve Logistic', readonly=True) operations = fields.Many2one( 'stock.picking', string='Operations', domain=[ ('picking_type_id.id', 'in', [75, 28]), ('state', '=', 'done') ], help='Nomor BU INPUT atau BU PUT', tracking=3 ) ba_num = fields.Char('Nomor BA', tracking=3) return_type = fields.Selection([ ('retur_po', 'Retur PO'), ('tukar_guling', 'Tukar Guling'), ], string='Return Type', required=True, tracking=3, help='Retur PO (VRT-PRT),\n Tukar Guling (VRT-PRT-INPUT-PUT') notes = fields.Text('Notes', tracking=3) tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade') line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines', tracking=3) state = fields.Selection([ ('draft', 'Draft'), ('approval_purchase', 'Approval Purchasing'), ('approval_finance', 'Approval Finance'), ('approval_logistic', 'Approval Logistic'), ('approved', 'Waiting for Operations'), ('done', 'Done'), ('cancel', 'Cancel'), ], string='Status', default='draft', tracking=3) val_bil_opt = fields.Selection([ ('tanpa_cancel', 'Tanpa Cancel Bill'), ('cancel_bill', 'Cancel Bill'), ], tracking=3, string='Bill Option') is_has_bill = fields.Boolean('Has Bill?', compute='_compute_is_has_bill', readonly=True, default=False) bill_id = fields.Many2many('account.move', string='Bill Ref', readonly=True) origin_po = fields.Many2one('purchase.order', string='Origin PO', compute='_compute_origin_po') @api.depends('origin', 'operations') def _compute_origin_po(self): for rec in self: rec.origin_po = False origin_str = rec.origin or rec.operations.origin if origin_str: so = self.env['purchase.order'].search([('name', '=', origin_str)], limit=1) rec.origin_po = so.id if so else False @api.depends('origin', 'origin_po', 'vendor_id', 'line_ids.product_id') def _compute_is_has_bill(self): Move = self.env['account.move'] for rec in self: # reset rec.is_has_bill = False rec.bill_id = [(5, 0, 0)] product_ids = rec.line_ids.mapped('product_id').ids if not product_ids: continue # dasar: bill atau vendor credit note yang linennya mengandung produk TG domain = [ ('move_type', 'in', ['in_invoice', 'in_refund']), ('state', 'not in', ['draft', 'cancel']), ('invoice_line_ids.product_id', 'in', product_ids), ] # batasi ke vendor sama (kalau ada) if rec.vendor_id: domain.append(('partner_id', '=', rec.vendor_id.id)) # bantu pembatasan ke asal dokumen extra = [] if rec.origin: extra.append(('invoice_origin', 'ilike', rec.origin)) if rec.origin_po: # di Odoo 14, invoice line biasanya link ke purchase.line lewat purchase_line_id extra.append(('invoice_line_ids.purchase_line_id.order_id', '=', rec.origin_po.id)) # OR-kan semua extra filter jika ada if extra: domain = domain + ['|'] * (len(extra) - 1) + extra bills = Move.search(domain).with_context(active_test=False) # --- Opsi 1: minimal salah satu produk TG muncul di bill (default) --- rec.bill_id = [(6, 0, bills.ids)] rec.is_has_bill = bool(bills) def set_opt(self): if not self.val_bil_opt and self.is_has_bill == True: raise UserError("Kalau sudah ada bill Return Bill Option harus diisi!") for rec in self: if rec.val_bil_opt == 'cancel_bill' and self.is_has_bill == True: raise UserError("Tidak bisa mengubah Return karena sudah ada bill dan belum di cancel.") elif rec.val_bil_opt == 'tanpa_cancel' and self.is_has_bill == True: continue @api.model def create(self, vals): # Generate sequence number # ven_name = self.origin.search([('name', 'ilike', vals['origin'])]) if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') # Auto-fill origin from operations if not vals.get('origin') and vals.get('operations'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin if picking.group_id.id: vals['vendor_id'] = picking.group_id.partner_id.id res = super(TukarGulingPO, self).create(vals) res.message_post(body=_("VCM Created By %s") % self.env.user.name) return res # def _check_bill_on_retur_po(self): # for record in self: # if record.return_type == 'retur_po' and record.origin: # bills = self.env['account.move'].search([ # ('invoice_origin', 'ilike', record.origin), # ('move_type', '=', 'in_invoice'), # hanya vendor bill # ('state', 'not in', ['draft', 'cancel']) # ]) # if bills: # raise ValidationError( # _("Tidak bisa memilih Return Type 'Retur PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin # ) @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" if self.operations.picking_type_id.id not in [75, 28]: raise UserError("❌ Picking type harus BU/INPUT atau BU/PUT") if self.operations: from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) if self.line_ids and from_return_picking: # Hanya update origin, jangan ubah lines if self.operations.origin: self.origin = self.operations.origin self.origin_po = self.operations.group_id.id return if from_return_picking: # Gunakan qty dari context (stock return wizard) default_lines = self.env.context.get('default_line_ids', []) parsed_lines = [] sequence = 10 for line_data in default_lines: if isinstance(line_data, (list, tuple)) and len(line_data) == 3: vals = line_data[2] parsed_lines.append((0, 0, { 'sequence': sequence, 'product_id': vals.get('product_id'), 'product_uom_qty': vals.get('quantity'), 'product_uom': self.env['product.product'].browse(vals.get('product_id')).uom_id.id, 'name': self.env['product.product'].browse(vals.get('product_id')).display_name, })) sequence += 10 self.line_ids = parsed_lines return else: self.line_ids = [(5, 0, 0)] # Set origin dari operations if self.operations.origin: self.origin = self.operations.origin # Auto-populate lines dari move_ids operations lines_data = [] sequence = 10 # Untuk Odoo 14, gunakan move_ids_without_package atau move_lines moves_to_check = [] # 1. move_ids_without_package (standard di Odoo 14) if hasattr(self.operations, 'move_ids_without_package') and self.operations.move_ids_without_package: moves_to_check = self.operations.move_ids_without_package # 2. move_lines (backup untuk versi lama) elif hasattr(self.operations, 'move_lines') and self.operations.move_lines: moves_to_check = self.operations.move_lines for move in moves_to_check: _logger.info( f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, Qty: {move.product_uom_qty}, State: {move.state}") # Ambil semua move yang ada quantity if move.product_id and move.product_uom_qty > 0: lines_data.append((0, 0, { 'sequence': sequence, 'product_id': move.product_id.id, 'product_uom_qty': move.product_uom_qty, 'product_uom': move.product_uom.id, 'name': move.name or move.product_id.display_name, })) sequence += 10 if lines_data: self.line_ids = lines_data _logger.info(f"Created {len(lines_data)} lines") else: _logger.info("No lines created - no valid moves found") else: # Clear lines jika operations dikosongkan, kecuali dari return picking from_return_picking = self.env.context.get('from_return_picking', False) or \ self.env.context.get('default_line_ids', False) if not from_return_picking: self.line_ids = [(5, 0, 0)] self.origin = False def _check_not_allow_tukar_guling_on_bu_input(self, return_type=None): operasi = self.operations.picking_type_id.id tipe = return_type or self.return_type if operasi == 28 and self.operations.linked_manual_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah done") if operasi == 28 and tipe == 'tukar_guling': raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling") def action_populate_lines(self): """Manual button untuk populate lines - sebagai alternatif""" self.ensure_one() if not self.operations: raise UserError("Pilih BU/OUT atau BU/PICK terlebih dahulu!") # Clear existing lines self.line_ids = [(5, 0, 0)] lines_data = [] sequence = 10 # Ambil semua stock moves dari operations for move in self.operations.move_ids: if move.product_uom_qty > 0: lines_data.append((0, 0, { 'sequence': sequence, 'product_id': move.product_id.id, 'product_uom_qty': move.product_uom_qty, 'product_uom': move.product_uom.id, 'name': move.name or move.product_id.display_name, })) sequence += 10 if lines_data: self.line_ids = lines_data else: raise UserError("Tidak ditemukan barang di BU/OUT yang dipilih!") @api.constrains('return_type', 'operations') def _check_required_bu_fields(self): for record in self: if record.return_type in ['retur_po', 'tukar_guling'] and not record.operations: raise ValidationError("Operations harus diisi") @api.constrains('line_ids', 'state') def _check_product_lines(self): """Constraint: Product lines harus ada jika state bukan draft""" for record in self: if record.state in ('approval_purchase', 'approval_finance', 'approval_logistic', 'done') and not record.line_ids: raise ValidationError("Product lines harus diisi sebelum submit atau approve!") def _validate_product_lines(self): """Helper method untuk validasi product lines""" self.ensure_one() # Check ada product lines if not self.line_ids: raise UserError("Belum ada product lines yang ditambahkan!") # Check product sudah diisi empty_lines = self.line_ids.filtered(lambda line: not line.product_id) if empty_lines: raise UserError("Ada product lines yang belum diisi productnya!") # Check quantity > 0 zero_qty_lines = self.line_ids.filtered(lambda line: line.product_uom_qty <= 0) if zero_qty_lines: raise UserError("Quantity product tidak boleh kosong atau 0!") return True # def _is_already_returned(self, picking): # return self.env['stock.picking'].search_count([ # ('origin', '=', 'Return of %s' % picking.name), # # ('returned_from_id', '=', picking.id), # ('state', 'not in', ['cancel', 'draft']), # ]) > 0 def copy(self, default=None): if default is None: default = {} # Generate new sequence untuk duplicate sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling.po')], limit=1) if sequence: default['name'] = sequence.next_by_id() else: default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'copy' default.update({ 'state': 'draft', 'date': fields.Datetime.now(), }) new_record = super(TukarGulingPO, self).copy(default) # Re-sequence lines if new_record.line_ids: for i, line in enumerate(new_record.line_ids): line.sequence = (i + 1) * 10 return new_record def write(self, vals): if self.operations.picking_type_id.id not in [75, 28]: raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!") # self._check_bill_on_retur_po() tipe = vals.get('return_type', self.return_type) # if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': # group = self.operations.group_id # if group: # # Cari BU/PUT dalam group yang sama # bu_put = self.env['stock.picking'].search([ # ('group_id', '=', group.id), # ('picking_type_id.id', '=', 75), # 75 = ID BU/PUT # ('state', '=', 'done') # ], limit=1) # # if bu_put: # raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") # if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': # raise UserError("❌ BU/INPUT tidak boleh di retur tukar guling") # if self.operations.picking_type_id.id != 28: # if self._is_already_returned(self.operations): # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if 'operations' in vals and not vals.get('origin'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin return super(TukarGulingPO, self).write(vals) def unlink(self): for record in self: 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() return super(TukarGulingPO, self).unlink() def action_view_picking(self): self.ensure_one() # picking_origin = f"Return of {self.operations.name}" returs = self.env['stock.picking'].search([ ('tukar_guling_po_id', '=', self.id), ]) if not returs: raise UserError("Doc Retrun Not Found") return { 'type': 'ir.actions.act_window', 'name': 'Delivery Pengajuan Retur PO', 'res_model': 'stock.picking', 'view_mode': 'tree,form', 'domain': [('id', 'in', returs.ids)], 'target': 'current', } def action_draft(self): """Reset to draft state""" for record in self: if record.state == 'cancel': record.write({'state': 'draft'}) else: raise UserError("Hanya record yang di-cancel yang bisa dikembalikan ke draft") def action_submit(self): self.ensure_one() # self._check_bill_on_retur_po() self._validate_product_lines() self._check_not_allow_tukar_guling_on_bu_input() if self.operations.picking_type_id.id == 28: group = self.operations.group_id if group: # Cari BU/PUT dalam group yang sama bu_put = self.env['stock.picking'].search([ ('group_id', '=', group.id), ('picking_type_id.id', '=', 75), ('state', '=', 'done') ], limit=1) if bu_put: raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") existing_tukar_guling = self.env['tukar.guling.po'].search([ ('operations', '=', self.operations.id), ('id', '!=', self.id), ('state', '!=', 'cancel'), ], limit=1) # if existing_tukar_guling: # raise UserError("BU ini sudah pernah diretur oleh dokumen %s." % existing_tukar_guling.name) picking = self.operations pick_id = self.operations.picking_type_id.id if pick_id == 75: if picking.state != 'done': raise UserError("BU/PUT belum Done!") if pick_id not in [75, 28]: raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!") # if self._is_already_returned(self.operations): # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") self.state = 'approval_purchase' def action_approve(self): self.ensure_one() self._validate_product_lines() # self._check_bill_on_retur_po() self._check_not_allow_tukar_guling_on_bu_input() if not self.operations: raise UserError("Operations harus diisi!") if not self.return_type: raise UserError("Return Type harus diisi!") now = datetime.now() # Cek hak akses berdasarkan state for rec in self: if rec.state == 'approval_purchase': if not rec.env.user.has_group('indoteknik_custom.group_role_purchasing'): raise UserError("Hanya Purchasing yang boleh approve tahap ini.") rec.state = 'approval_finance' rec.date_purchase = now elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance yang boleh approve tahap ini.") # rec._check_bill_on_retur_po() rec.set_opt() rec.state = 'approval_logistic' rec.date_finance = now elif rec.state == 'approval_logistic': if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic yang boleh approve tahap ini.") rec.state = 'approved' rec._create_pickings() rec.date_logistic = now else: raise UserError("Status ini tidak bisa di-approve.") def update_doc_state(self): # bu input rev po if self.operations.picking_type_id.id == 28 and self.return_type == 'retur_po': prt = self.env['stock.picking'].search([ ('tukar_guling_po_id', '=', self.id), ('state', '=', 'done'), ('picking_type_id.id', '=', 76) ]) if self.state == 'approved' and prt: self.state = 'done' # bu put rev po elif self.operations.picking_type_id.id == 75 and self.return_type == 'retur_po': total_prt = self.env['stock.picking'].search_count([ ('tukar_guling_po_id', '=', self.id), ('picking_type_id.id', '=', 76) ]) prt = self.env['stock.picking'].search_count([ ('tukar_guling_po_id', '=', self.id), ('state', '=', 'done'), ('picking_type_id.id', '=', 76) ]) if self.state == 'approved' and total_prt > 0 and prt == total_prt: self.state = 'done' # bu put tukar guling elif self.operations.picking_type_id.id == 75 and self.return_type == 'tukar_guling': total_put = self.env['stock.picking'].search_count([ ('tukar_guling_po_id', '=', self.id), ('picking_type_id.id', '=', 75) ]) put = self.env['stock.picking'].search_count([ ('tukar_guling_po_id', '=', self.id), ('state', '=', 'done'), ('picking_type_id.id', '=', 75) ]) if self.state == 'approved' and total_put > 0 and put == total_put: self.state = 'done' def action_cancel(self): self.ensure_one() # if self.state == 'done': # raise UserError("Tidak bisa cancel jika sudah done") user = self.env.user if not ( user.has_group('indoteknik_custom.group_role_purchasing') or user.has_group('indoteknik_custom.group_role_fat') or user.has_group('indoteknik_custom.group_role_logistic') ): raise UserWarning('Anda tidak memiliki Permission untuk cancel document') bu_done = self.po_picking_ids.filtered(lambda p: p.state == 'done') if bu_done: raise UserError("Dokuemn BU sudah Done, tidak bisa di cancel") ongoing_bu = self.po_picking_ids.filtered(lambda p: p.state != 'done') for picking in ongoing_bu: picking.action_cancel() self.state = 'cancel' def _create_pickings(self): for record in self: if not record.operations: raise UserError("BU Operations belum dipilih.") created_returns = self.env['stock.picking'] group = record.operations.group_id bu_inputs = bu_puts = self.env['stock.picking'] # Buat qty map awal dari line_ids bu_input_qty_map = { line.product_id.id: line.product_uom_qty for line in record.line_ids if line.product_id and line.product_uom_qty > 0 } bu_put_qty_map = bu_input_qty_map.copy() if group: po_pickings = self.env['stock.picking'].search([ ('group_id', '=', group.id), ('state', '=', 'done') ]) product_ids = set(record.line_ids.mapped("product_id").ids) _logger.info("TG product_ids: %s", product_ids) def _get_moves(picking): return picking.move_ids_without_package if picking.move_ids_without_package else picking.move_lines bu_inputs = po_pickings.filtered( lambda p: p.picking_type_id.id == 28 and any( m.product_id.id in product_ids for m in _get_moves(p) ) ) _logger.info("BU INPUT dengan product sama: %s", bu_inputs.mapped("name")) bu_puts = po_pickings.filtered(lambda p: p.picking_type_id.id == 75) else: raise UserError("Group ID tidak ditemukan pada BU Operations.") def _create_return_from_picking(picking, qty_map): if not picking: return self.env['stock.picking'] grup = record.operations.group_id # Tentukan lokasi PARTNER_LOCATION_ID = 4 BU_INPUT_LOCATION_ID = 58 BU_STOCK_LOCATION_ID = 57 picking_type = picking.picking_type_id.id if picking_type == 28: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID elif picking_type == 75: default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID elif picking_type == 77: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID elif picking_type == 76: default_location_id = PARTNER_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID else: return self.env['stock.picking'] return_context = dict(self.env.context) return_context.update({ 'active_id': picking.id, 'default_location_id': default_location_id, 'default_location_dest_id': default_location_dest_id, 'from_ui': False, }) return_wizard = self.env['stock.return.picking'].with_context(return_context).create({ 'picking_id': picking.id, 'location_id': default_location_dest_id, 'original_location_id': default_location_id }) return_lines = [] moves = getattr(picking, 'move_ids_without_package', False) or picking.move_lines for move in moves: product = move.product_id if not product: continue pid = product.id available_qty = qty_map.get(pid, 0.0) move_qty = move.product_uom_qty allocate_qty = min(available_qty, move_qty) if allocate_qty <= 0: continue return_lines.append((0, 0, { 'product_id': pid, 'quantity': allocate_qty, 'move_id': move.id, })) qty_map[pid] -= allocate_qty _logger.info(f"📦 Alokasi {allocate_qty} untuk {product.display_name} | Sisa: {qty_map[pid]}") if not return_lines: # Tukar Guling lanjut dari PRT/VRT if picking.picking_type_id.id in [76, 77]: for move in moves: if move.product_uom_qty > 0: return_lines.append((0, 0, { 'product_id': move.product_id.id, 'quantity': move.product_uom_qty, 'move_id': move.id, })) _logger.info( f"🔁 TG lanjutan: Alokasi {move.product_uom_qty} untuk {move.product_id.display_name}") else: _logger.warning( f"⏭️ Skipped return picking {picking.name}, tidak ada qty yang bisa dialokasikan.") return self.env['stock.picking'] return_wizard.product_return_moves = return_lines return_vals = return_wizard.create_returns() return_picking = self.env['stock.picking'].browse(return_vals.get('res_id')) return_picking.write({ 'location_id': default_location_id, 'location_dest_id': default_location_dest_id, 'group_id': grup.id, 'tukar_guling_po_id': record.id, }) record.message_post( body=f"📦 {return_picking.name} " f"{return_picking.picking_type_id.display_name} " f"Created by {self.env.user.name} " f"status {return_picking.state} " f"at {fields.Datetime.now().strftime('%d/%m/%Y %H:%M')}", message_type="comment", subtype_id=self.env.ref("mail.mt_note").id, ) return return_picking # ============================ # Eksekusi utama return logic # ============================ if record.operations.picking_type_id.id == 28: # Dari BU INPUT langsung buat PRT prt = _create_return_from_picking(record.operations, bu_input_qty_map) if prt: created_returns |= prt else: # ✅ Pairing BU PUT ↔ BU INPUT # Temukan index dari BU PUT yang dipilih user try: bu_put_index = sorted(bu_puts, key=lambda p: p.name).index(record.operations) except ValueError: raise UserError("Dokumen BU PUT yang dipilih tidak ditemukan dalam daftar BU PUT.") # Ambil pasangannya di BU INPUT (asumsi urutan sejajar) sorted_bu_puts = sorted(bu_puts, key=lambda p: p.name) # sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name) # if bu_put_index >= len(sorted_bu_inputs): # raise UserError("Tidak ditemukan pasangan BU INPUT untuk BU PUT yang dipilih.") # paired = [(sorted_bu_puts[bu_put_index], sorted_bu_inputs[bu_put_index])] sorted_bu_inputs = sorted(bu_inputs, key=lambda p: p.name) if not sorted_bu_inputs: raise UserError( "Tidak ditemukan BU INPUT yang memiliki product TG." ) paired = [(record.operations, sorted_bu_inputs[0])] _logger.info( "🔗 Pairing BU PUT %s dengan BU INPUT %s", record.operations.name, sorted_bu_inputs[0].name ) for bu_put, bu_input in paired: vrt = _create_return_from_picking(bu_put, bu_put_qty_map) if vrt: created_returns |= vrt prt = _create_return_from_picking(bu_input, bu_input_qty_map) if prt: created_returns |= prt # 🌀 Tukar Guling: buat dokumen baru dari PRT & VRT if record.return_type == 'tukar_guling': for prt in created_returns.filtered(lambda p: p.picking_type_id.id == 76): bu_input = _create_return_from_picking(prt, bu_input_qty_map) if bu_input: created_returns |= bu_input for vrt in created_returns.filtered(lambda p: p.picking_type_id.id == 77): bu_put = _create_return_from_picking(vrt, bu_put_qty_map) if bu_put: created_returns |= bu_put if not created_returns: raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") class TukarGulingLinePO(models.Model): _name = 'tukar.guling.line.po' _description = 'Tukar Guling PO Line' sequence = fields.Integer('Sequence', default=10, copy=False) product_id = fields.Many2one('product.product', string='Product', required=True) tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade') product_uom_qty = fields.Float('Quantity', digits='Product Unit of Measure', required=True, default=1.0) product_uom = fields.Many2one('uom.uom', string='Unit of Measure') name = fields.Text('Description') @api.constrains('product_uom_qty') def _check_qty_change_allowed(self): for rec in self: if rec.tukar_guling_po_id and rec.tukar_guling_po_id.state not in ['draft', 'cancel']: raise ValidationError("Tidak bisa mengubah Quantity karena status dokumen bukan Draft atau Cancel.") def unlink(self): for rec in self: if rec.tukar_guling_po_id and rec.tukar_guling_po_id.state not in ['draft', 'cancel']: raise UserError("Tidak bisa menghapus data karena status dokumen bukan Draft atau Cancel.") return super(TukarGulingLinePO, self).unlink() class StockPicking(models.Model): _inherit = 'stock.picking' tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref') def button_validate(self): res = super(StockPicking, self).button_validate() for picking in self: if picking.tukar_guling_po_id: message = _( "📦 %s Validated by %s Status Changed %s at %s." ) % ( picking.name, # picking.picking_type_id.name, picking.env.user.name, picking.state, fields.Datetime.now().strftime("%d/%m/%Y %H:%M") ) picking.tukar_guling_po_id.message_post(body=message) return res