from odoo import models, fields, api from odoo.exceptions import UserError, ValidationError class TukarGuling(models.Model): _name = 'tukar.guling' _description = 'Tukar Guling' _order = 'date desc, id desc' _rec_name = 'name' real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') picking_ids = fields.One2many( 'stock.picking', 'tukar_guling_id', string='Transfers' ) name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', domain=[('picking_type_id.code', '=', 'outgoing')]) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama ('revisi_so', 'Revisi SO'), # -> ganti barang ? ('credit_memo', 'Credit Memo')]) # -> dijadiin credit memo state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('approval_sales', ' Approval Sales'), ('approval_logistic', 'Approval Logistic'), ('approval_finance', 'Approval Finance'), ('done', 'Done'), ('cancel', 'Canceled') ], default='draft', tracking=True, required=True) line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') @api.constrains('return_type', 'out_num') def _check_required_bu_fields(self): for record in self: if record.return_type in ['revisi_so', 'credit_memo', 'tukar_guling'] and not record.out_num: raise ValidationError("BU/Out 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_sales', '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 @api.model def create(self, vals): if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'New' return super(TukarGuling, self).create(vals) def copy(self, default=None): if default is None: default = {} if 'name' not in default: default.update({ 'name': self.env['ir.sequence'].next_by_code(self._name) or 'New', }) default.update({ 'state': 'draft', 'date': fields.Datetime.now(), }) new_record = super(TukarGuling, 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 action_view_picking(self): self.ensure_one() action = self.env.ref('stock.action_picking_tree_all').read()[0] pickings = self.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() if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") self.state = 'approval_sales' def action_approve(self): self.ensure_one() if not self.out_num: raise UserError("BU/Out 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_sales': 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") self.state = 'cancel' def _create_pickings(self): if not self.out_num: raise UserError("BU/Out harus diisi terlebih dahulu.") group_id = self.out_num.group_id.id if self.out_num.group_id else False Picking = self.env['stock.picking'] srt_type = self.env['stock.picking.type'].search([ ('sequence_code', '=', 'SRT') ], limit=1) ort_type = self.env['stock.picking.type'].search([ ('sequence_code', '=', 'ORT') ], limit=1) # Lokasi location_dest_id = srt_type.default_location_dest_id.id location_customer = self.out_num.location_dest_id # 1. BU/SRT: retur dari out_num srt_picking = Picking.create({ 'partner_id': self.out_num.partner_id.id, 'picking_type_id': srt_type.id, 'location_id': location_customer.id, 'location_dest_id': location_dest_id, 'origin': f"Retur {self.out_num.name}", 'tukar_guling_id': self.id, 'group_id': group_id, 'move_ids_without_package': [ (0, 0, { 'name': line.name or line.product_id.name, 'product_id': line.product_id.id, 'product_uom_qty': line.product_uom_qty, 'product_uom': line.product_uom.id, 'location_id': location_customer.id, 'location_dest_id': location_dest_id, }) for line in self.line_ids ] }) srt_picking.action_confirm() # 2. Cari BU/PICK dari SO yang sama origin_so = self.out_num.origin if not origin_so: raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") pick = Picking.search([ ('origin', '=', origin_so), ('picking_type_id.code', '=', 'internal') ], limit=1) if not pick: raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") # 3. BU/ORT: retur dari BU/PICK ort_picking = Picking.create({ 'partner_id': self.out_num.partner_id.id, 'picking_type_id': ort_type.id, 'location_id': location_dest_id, 'location_dest_id': location_customer.id, 'origin': f"Retur {pick.name}", 'tukar_guling_id': self.id, 'group_id': group_id, 'move_ids_without_package': [ (0, 0, { 'name': line.name or line.product_id.name, 'product_id': line.product_id.id, 'product_uom_qty': line.product_uom_qty, 'product_uom': line.product_uom.id, 'location_id': location_dest_id, 'location_dest_id': location_customer.id, }) for line in self.line_ids ] }) ort_picking.action_confirm() class TukarGulingPO(models.Model): _name = 'tukar.guling.po' _description = 'Tukar Guling PO' name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) out_num = fields.Many2one('stock.picking', 'Nomor BU/Out', domain=[('picking_type_id.code', '=', 'outgoing')]) ba_num = fields.Text('Nomor BA') notes = fields.Text('Notes') state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('approval_purchase', ' Approval Purchase'), ('approval_logistic', 'Approval Logistic'), ('approval_finance', 'Approval Finance'), ('done', 'Done'), ('cancel', 'Canceled') ], default='draft', tracking=True, required=True) line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines') tukar_guling_po_id = fields.Many2one('tukar.guling.po', 'Tukar Guling PO') return_type = fields.Selection([ ('tukar_guling', 'Tukar Guling'), ('revisi_po', 'Revisi PO'), ('debit_memo', 'Debit Memo'), ], string='Return Type', required=True) @api.constrains('return_type', 'out_num') def _check_required_bu_fields(self): for record in self: if record.return_type in ['tukar_guling', 'revisi_po', 'debit_memo'] and not record.out_num: raise ValidationError("BU/Out 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 @api.model def create(self, vals): if not vals.get('name') or vals['name'] in ('New', False): vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' return super(TukarGulingPO, self).create(vals) def copy(self, default=None): if default is None: default = {} # Generate sequence satu-satunya di sini default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' default['state'] = 'draft' default['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 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() if self.state != 'draft': raise UserError("Submit hanya bisa dilakukan dari Draft.") self.state = 'approval_purchase' def action_approve(self): self.ensure_one() if not self.out_num: raise UserError("BU/Out harus diisi!") if not self.return_type: raise UserError("Return Type harus diisi!") if self.state == 'approval_purchase': if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'): raise UserError("Hanya Purchasing yang boleh approve tahap ini.") self.state = 'approval_logistic' elif self.state == 'approval_logistic': if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") self.state = 'approval_finance' elif self.state == 'approval_finance': if not self.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") self.state = 'done' 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") self.state = 'cancel' class TukarGulingLine(models.Model): _name = 'tukar.guling.line' _description = 'Tukar Guling Line' _order = 'sequence, id' sequence = fields.Integer('Sequence', default=10, copy=False) tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling', required=True, ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', required=True) 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.model_create_multi def create(self, vals_list): """Override create to auto-assign sequence""" for vals in vals_list: if 'sequence' not in vals or vals.get('sequence', 0) <= 0: # Get max sequence untuk tukar_guling yang sama tukar_guling_id = vals.get('tukar_guling_id') if tukar_guling_id: max_seq = self.search([ ('tukar_guling_id', '=', tukar_guling_id) ], order='sequence desc', limit=1) vals['sequence'] = (max_seq.sequence or 0) + 10 else: vals['sequence'] = 10 return super(TukarGulingLine, self).create(vals_list) @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id class TukarGulingLinePO(models.Model): _name = 'tukar.guling.line.po' _description = 'Tukar Guling Line (PO)' _order = 'sequence, id' tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', required=True, ondelete='cascade') sequence = fields.Integer('Sequence', default=10, copy=False) product_id = fields.Many2one('product.product', string='Product', required=True) 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.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id class StockPicking(models.Model): _inherit = 'stock.picking' origin_tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref')