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 == 'aproved' 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')
])
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, 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])]
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