summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/tukar_guling_po.py
blob: 1ee1067950b74847620e650cd44ed88b925d9477 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
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')
                ])

                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"📦 <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])]
                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 = _(
                    "📦 <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