summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/tukar_guling_po.py
diff options
context:
space:
mode:
Diffstat (limited to 'indoteknik_custom/models/tukar_guling_po.py')
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py662
1 files changed, 662 insertions, 0 deletions
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
new file mode 100644
index 00000000..7c9680f8
--- /dev/null
+++ b/indoteknik_custom/models/tukar_guling_po.py
@@ -0,0 +1,662 @@
+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 = 'Tukar Guling 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([
+ ('revisi_po', 'Revisi PO'),
+ ('tukar_guling', 'Tukar Guling'),
+ ], string='Return Type', required=True, tracking=3)
+ 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'),
+ ('done', 'Done'),
+ ('cancel', 'Cancel'),
+ ], string='Status', default='draft', tracking=3)
+
+ @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
+
+ @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.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
+ 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 ['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_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_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!")
+
+ 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_sales'):
+ raise UserError("Hanya Sales Manager 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 Manager yang boleh approve tahap ini.")
+ 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 Manager yang boleh approve tahap ini.")
+ rec.state = 'done'
+ rec._create_pickings()
+ rec.date_logistic = now
+ 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")
+
+ user = self.env.user
+ if not (
+ user.has_group('indoteknik_custom.group_role_sales') 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"📦 <b>{return_picking.name}</b> "
+ f"<b>{return_picking.picking_type_id.display_name}</b> "
+ f"Created by <b>{self.env.user.name}</b> "
+ f"status <b>{return_picking.state}</b> "
+ f"at <b>{fields.Datetime.now().strftime('%d/%m/%Y %H:%M')}</b>",
+ 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_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 = _(
+ "📦 <b>%s</b> Validated by <b>%s</b> Status Changed <b>%s</b> at <b>%s</b>."
+ ) % (
+ 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 \ No newline at end of file