from email.policy import default from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging _logger = logging.getLogger(__name__) class TukarGulingPO(models.Model): _name = 'tukar.guling.po' _description = 'Tukar Guling PO' 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) operations = fields.Many2one( 'stock.picking', string='Operations', domain=[ ('picking_type_id.id', 'in', [75, 28]), ('state', '=', 'done') ], help='Nomor BU INPUT atau BU PUT' ) ba_num = fields.Char('Nomor BA') return_type = fields.Selection([ ('revisi_po', 'Revisi PO'), ('tukar_guling', 'Tukar Guling'), ], string='Return Type', required=True) notes = fields.Text('Notes') 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') state = fields.Selection([ ('draft', 'Draft'), ('approval_purchase', 'Approval Purchasing'), ('approval_logistic', 'Approval Logistic'), ('approval_finance', 'Approval Finance'), ('done', 'Done'), ('cancel', 'Cancel'), ], string='Status', default='draft') @api.model def create(self, vals): # Generate sequence number if not vals.get('name') or vals['name'] == 'New': sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling.po')], limit=1) if sequence: vals['name'] = sequence.next_by_id() else: # Fallback jika sequence belum dibuat vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'new' # 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 return super(TukarGulingPO, self).create(vals) @api.constrains('return_type', 'operations') def _check_bill_on_revisi_po(self): for record in self: if record.return_type == 'revisi_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 'Revisi PO' karena PO %s sudah dibuat vendor bill.") % record.origin ) @api.onchange('operations') def _onchange_operations(self): """Auto-populate lines ketika operations dipilih""" 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 return # Clear existing lines hanya jika tidak dari return picking 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 ['revisi_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_logistic', 'approval_finance', '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), ('state', '!=', 'cancel') ]) > 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): self._check_bill_on_revisi_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 == 'done': raise UserError("Tidak bisa hapus pengajuan jika sudah done, set ke draft 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() action = self.env.ref('stock.action_picking_tree_all').read()[0] pickings = self.po_picking_ids if len(pickings) > 1: action['domain'] = [('id', 'in', pickings.ids)] elif pickings: action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] action['res_id'] = pickings.id return action 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_revisi_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!") 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_revisi_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!") # 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_sales'): raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") rec.state = 'approval_logistic' elif rec.state == 'approval_logistic': if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") rec.state = 'approval_finance' elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") rec.state = 'done' rec._create_pickings() else: raise UserError("Status ini tidak bisa di-approve.") def action_cancel(self): self.ensure_one() # if self.state == 'done': # raise UserError("Tidak bisa cancel jika sudah done") 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'] if group: po_pickings = self.env['stock.picking'].search([ ('group_id', '=', group.id), ('state', '=', 'done') ]) bu_inputs = po_pickings.filtered(lambda p: p.picking_type_id.id == 28) 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): if not picking: return self.env['stock.picking'] grup = record.operations.group_id # Tentukan location PARTNER_LOCATION_ID = 4 BU_INPUT_LOCATION_ID = 58 BU_STOCK_LOCATION_ID = 57 if picking.picking_type_id.id == 28: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = PARTNER_LOCATION_ID elif picking.picking_type_id.id == 75: default_location_id = BU_STOCK_LOCATION_ID default_location_dest_id = BU_INPUT_LOCATION_ID elif picking.picking_type_id.id == 77: default_location_id = BU_INPUT_LOCATION_ID default_location_dest_id = BU_STOCK_LOCATION_ID elif picking.picking_type_id.id == 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 = [] for line in record.line_ids: move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) if move: return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, 'move_id': move[0].id, })) else: raise UserError( _("Tidak ditemukan move line di picking %s untuk produk %s") % (picking.name, line.product_id.display_name) ) if not return_lines: raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) 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, }) for move in return_picking.move_lines: move.write({ 'location_id': default_location_id, 'location_dest_id': default_location_dest_id, }) return return_picking # === Eksekusi pembuatan picking === if record.operations.picking_type_id.id == 28: # Kalau dari BU INPUT → hanya PRT prt = _create_return_from_picking(record.operations) if prt: created_returns |= prt else: # 1. Dari BU PUT buat VRT for bu_put in bu_puts: vrt = _create_return_from_picking(bu_put) if vrt: created_returns |= vrt # 2. Dari BU INPUT buat PRT for bu_input in bu_inputs: prt = _create_return_from_picking(bu_input) if prt: created_returns |= prt # 3. Kalau tukar guling buat lanjut INPUT & PUT 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) 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) 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') class StockPicking(models.Model): _inherit = 'stock.picking' tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref')