summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIndoteknik . <it@fixcomart.co.id>2025-07-26 11:58:32 +0700
committerIndoteknik . <it@fixcomart.co.id>2025-07-26 11:58:32 +0700
commite09e59db22d65fa144c2a43cd8f6959fada49484 (patch)
tree0567b1a8f0c8c57efb2d0d38ae489da26a55c245
parentfff5507cf7c0f360a5c68068e77fa84b1d6340c5 (diff)
parent77e76f376b78733cad58aa196e3ead3c6a04ff42 (diff)
(andri) fix conflict
-rwxr-xr-xindoteknik_custom/__manifest__.py6
-rwxr-xr-xindoteknik_custom/models/__init__.py2
-rwxr-xr-xindoteknik_custom/models/purchase_order.py7
-rw-r--r--indoteknik_custom/models/refund_sale_order.py31
-rwxr-xr-xindoteknik_custom/models/sale_order.py47
-rw-r--r--indoteknik_custom/models/shipment_group.py2
-rw-r--r--indoteknik_custom/models/stock_picking.py78
-rw-r--r--indoteknik_custom/models/stock_picking_return.py164
-rw-r--r--indoteknik_custom/models/tukar_guling.py843
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py662
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv9
-rw-r--r--indoteknik_custom/views/account_move.xml6
-rw-r--r--indoteknik_custom/views/ir_sequence.xml18
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml7
-rw-r--r--indoteknik_custom/views/refund_sale_order.xml199
-rwxr-xr-xindoteknik_custom/views/sale_order.xml25
-rw-r--r--indoteknik_custom/views/stock_picking.xml10
-rw-r--r--indoteknik_custom/views/tukar_guling.xml129
-rw-r--r--indoteknik_custom/views/tukar_guling_po.xml127
-rw-r--r--indoteknik_custom/views/tukar_guling_return_views.xml20
20 files changed, 2092 insertions, 300 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index f609acbf..e2f7659c 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -172,7 +172,11 @@
'views/sale_order_delay.xml',
'views/down_payment.xml',
'views/down_payment_realization.xml',
- 'views/refund_sale_order.xml',
+ # 'views/refund_sale_order.xml',
+ 'views/tukar_guling.xml',
+ # 'views/tukar_guling_return_views.xml'
+ 'views/tukar_guling_po.xml',
+ # 'views/refund_sale_order.xml',
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index d855b64d..87310614 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -155,3 +155,5 @@ from . import approval_payment_term
from . import refund_sale_order
# from . import patch
from . import down_payment
+from . import tukar_guling
+from . import tukar_guling_po
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 5b9e1acb..45134939 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -17,6 +17,7 @@ _logger = logging.getLogger(__name__)
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
+ vcm_id = fields.Many2one('tukar.guling.po', string='Doc VCM', readonly=True, compute='_has_vcm', copy=False)
order_sales_match_line = fields.One2many('purchase.order.sales.match', 'purchase_order_id', string='Sales Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
procurement_status = fields.Char(string='Procurement Status', compute='get_procurement_status', readonly=True)
@@ -99,6 +100,10 @@ class PurchaseOrder(models.Model):
)
manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders')
+ def _has_vcm(self):
+ if self.id:
+ self.vcm_id = self.env['tukar.guling.po'].search([('origin', '=', self.name)], limit=1)
+
@api.depends('name')
def _compute_bu_related_count(self):
StockPicking = self.env['stock.picking']
@@ -1043,7 +1048,7 @@ class PurchaseOrder(models.Model):
for line in self.order_line:
if line.taxes_id != reference_taxes:
- raise UserError("PPN harus sama untuk semua baris pada line.")
+ raise UserError(f"PPN harus sama untuk semua baris pada line {line.product_id.name}")
def check_data_vendor(self):
vendor = self.partner_id
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 559ca07a..11bfd07f 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -17,7 +17,7 @@ class RefundSaleOrder(models.Model):
note_refund = fields.Text(string='Note Refund')
sale_order_ids = fields.Many2many('sale.order', string='Sales Order Numbers')
uang_masuk = fields.Float(string='Uang Masuk', required=True)
- total_invoice = fields.Float(string='Total Invoice', compute='_compute_total_invoice', readonly=True)
+ total_invoice = fields.Float(string='Total Invoice')
ongkir = fields.Float(string='Ongkir', required=True, default=0.0)
amount_refund = fields.Float(string='Total Refund', required=True)
amount_refund_text = fields.Char(string='Total Refund Text', compute='_compute_refund_text')
@@ -105,6 +105,10 @@ class RefundSaleOrder(models.Model):
)
is_locked = fields.Boolean(string="Locked", compute="_compute_is_locked")
+ sale_order_names_jasper = fields.Char(string='Sales Order List', compute='_compute_order_invoice_names')
+ invoice_names_jasper = fields.Char(string='Invoice List', compute='_compute_order_invoice_names')
+
+
@api.depends('refund_type')
def _compute_refund_type_display(self):
@@ -147,10 +151,10 @@ class RefundSaleOrder(models.Model):
invoice_ids_data = vals.get('invoice_ids', [])
invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else []
- if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'retur_half']:
+ if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice")
- if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']:
+ if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice")
@@ -241,10 +245,10 @@ class RefundSaleOrder(models.Model):
else:
invoice_ids = rec.invoice_ids.ids
- if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'retur_half']:
+ if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice")
- if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']:
+ if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice")
if refund_type in ['retur', 'retur_half'] and so_ids:
@@ -279,11 +283,12 @@ class RefundSaleOrder(models.Model):
def _compute_is_locked(self):
for rec in self:
rec.is_locked = rec.status_payment in ['done', 'reject']
-
- @api.depends('invoice_ids.amount_total')
- def _compute_total_invoice(self):
+
+ @api.depends('sale_order_ids.name', 'invoice_ids.name')
+ def _compute_order_invoice_names(self):
for rec in self:
- rec.total_invoice = sum(inv.amount_total for inv in rec.invoice_ids)
+ rec.sale_order_names_jasper = ', '.join(rec.sale_order_ids.mapped('name')) or ''
+ rec.invoice_names_jasper = ', '.join(rec.invoice_ids.mapped('name')) or ''
@api.depends('sale_order_ids')
def _compute_advance_move_names(self):
@@ -428,14 +433,6 @@ class RefundSaleOrder(models.Model):
pengurangan = rec.total_invoice + rec.ongkir
refund = rec.uang_masuk - pengurangan
rec.amount_refund = refund if refund > 0 else 0.0
-
- if rec.uang_masuk and rec.uang_masuk <= pengurangan:
- return {
- 'warning': {
- 'title': 'Uang Masuk Kurang',
- 'message': 'Uang masuk harus lebih besar dari total invoice + ongkir untuk dapat melakukan refund.'
- }
- }
@api.model
def default_get(self, fields_list):
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 995cafba..7be0e8ff 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -125,6 +125,7 @@ class SaleOrderLine(models.Model):
class SaleOrder(models.Model):
_inherit = "sale.order"
+ ccm_id = fields.Many2one('tukar.guling', string='Doc. CCM', readonly=True, compute='_has_ccm', copy=False)
ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True,
tracking=3)
@@ -364,6 +365,21 @@ class SaleOrder(models.Model):
compute='_compute_advance_payment_move',
string='Advance Payment Move',
)
+ advance_payment_move_ids = fields.Many2many(
+ 'account.move',
+ compute='_compute_advance_payment_moves',
+ string='All Advance Payment Moves',
+ )
+
+ advance_payment_move_count = fields.Integer(
+ string='Jumlah Jurnal Uang Muka',
+ compute='_compute_advance_payment_moves',
+ store=False
+ )
+
+ def _has_ccm(self):
+ if self.id:
+ self.ccm_id = self.env['tukar.guling'].search([('origin', 'ilike', self.name)], limit=1)
@api.depends('order_line.product_id', 'date_order')
def _compute_et_products(self):
@@ -3191,15 +3207,38 @@ class SaleOrder(models.Model):
('state', '=', 'posted'),
], limit=1, order="id desc")
order.advance_payment_move_id = move
+
+ @api.depends('invoice_ids')
+ def _compute_advance_payment_moves(self):
+ for order in self:
+ moves = self.env['account.move'].search([
+ ('sale_id', '=', order.id),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ])
+ order.advance_payment_move_ids = moves
+
+ @api.depends('invoice_ids')
+ def _compute_advance_payment_moves(self):
+ for order in self:
+ moves = self.env['account.move'].search([
+ ('sale_id', '=', order.id),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ])
+ order.advance_payment_move_ids = moves
+ order.advance_payment_move_count = len(moves)
- def action_open_advance_payment_move(self):
+ def action_open_advance_payment_moves(self):
self.ensure_one()
- if not self.advance_payment_move_id:
+ moves = self.advance_payment_move_ids
+ if not moves:
return
return {
'type': 'ir.actions.act_window',
+ 'name': 'Journals Sales Order',
'res_model': 'account.move',
- 'res_id': self.advance_payment_move_id.id,
- 'view_mode': 'form',
+ 'view_mode': 'tree,form',
+ 'domain': [('id', 'in', moves.ids)],
'target': 'current',
} \ No newline at end of file
diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py
index 4969c35a..7203b566 100644
--- a/indoteknik_custom/models/shipment_group.py
+++ b/indoteknik_custom/models/shipment_group.py
@@ -36,6 +36,8 @@ class ShipmentGroup(models.Model):
def create(self, vals):
vals['number'] = self.env['ir.sequence'].next_by_code('shipment.group') or '0'
result = super(ShipmentGroup, self).create(vals)
+ if result.shipment_line and result.shipment_line[0].partner_id:
+ result.partner_id = result.shipment_line[0].partner_id.id
return result
class ShipmentGroupLine(models.Model):
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 0efffd2f..3e152f10 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -28,6 +28,10 @@ biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lI
class StockPicking(models.Model):
_inherit = 'stock.picking'
_order = 'final_seq ASC'
+ tukar_guling_id = fields.Many2one(
+ 'tukar.guling',
+ string='Tukar Guling Reference'
+ )
konfirm_koli_lines = fields.One2many('konfirm.koli', 'picking_id', string='Konfirm Koli', auto_join=True,
copy=False)
scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True, copy=False)
@@ -1094,38 +1098,40 @@ class StockPicking(models.Model):
self.approval_receipt_status = 'pengajuan1'
def ask_return_approval(self):
- for pick in self:
- if self.env.user.is_accounting:
- pick.approval_return_status = 'approved'
- continue
- else:
- pick.approval_return_status = 'pengajuan1'
-
- action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard')
-
- if self.picking_type_code == 'outgoing':
- if self.env.user.id in [3988, 3401, 20] or (
- self.env.user.has_group(
- 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
- ):
- action['context'] = {'picking_ids': [x.id for x in self]}
- return action
- elif not self.env.user.has_group(
- 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
- raise UserError('Harus Purchasing yang Ask Return')
- else:
- raise UserError('Harus Sales Admin yang Ask Return')
-
- elif self.picking_type_code == 'incoming':
- if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or (
- self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
- ):
- action['context'] = {'picking_ids': [x.id for x in self]}
- return action
- elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin:
- raise UserError('Harus Sales Admin yang Ask Return')
- else:
- raise UserError('Harus Purchasing yang Ask Return')
+ pass
+ raise UserError("Bisa langsung Validate")
+ # for pick in self:
+ # if self.env.user.is_accounting:
+ # pick.approval_return_status = 'approved'
+ # continue
+ # else:
+ # pick.approval_return_status = 'pengajuan1'
+ #
+ # action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard')
+ #
+ # if self.picking_type_code == 'outgoing':
+ # if self.env.user.id in [3988, 3401, 20] or (
+ # self.env.user.has_group(
+ # 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
+ # ):
+ # action['context'] = {'picking_ids': [x.id for x in self]}
+ # return action
+ # elif not self.env.user.has_group(
+ # 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
+ # raise UserError('Harus Purchasing yang Ask Return')
+ # else:
+ # raise UserError('Harus Sales Admin yang Ask Return')
+ #
+ # elif self.picking_type_code == 'incoming':
+ # if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or (
+ # self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
+ # ):
+ # action['context'] = {'picking_ids': [x.id for x in self]}
+ # return action
+ # elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin:
+ # raise UserError('Harus Sales Admin yang Ask Return')
+ # else:
+ # raise UserError('Harus Purchasing yang Ask Return')
def calculate_line_no(self):
@@ -1220,6 +1226,10 @@ class StockPicking(models.Model):
def button_validate(self):
self.check_invoice_date()
+ _logger.info("Kode Picking: %s", self.picking_type_id.code)
+ _logger.info("Group ID: %s", self.group_id)
+ _logger.info("Group ID ID: %s", self.group_id.id if self.group_id else None)
+ _logger.info("Is Internal Use: %s", self.is_internal_use)
threshold_datetime = waktu(2025, 4, 11, 6, 26)
group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
@@ -2513,6 +2523,8 @@ class KonfirmKoli(models.Model):
copy=False,
)
pick_id = fields.Many2one('stock.picking', string='Pick')
+ product_id = fields.Many2one('product.product', string='Product')
+ qty_done = fields.Float(string='Qty Done')
@api.constrains('pick_id')
def _check_duplicate_pick_id(self):
@@ -2538,4 +2550,4 @@ class WarningModalWizard(models.TransientModel):
def action_continue(self):
if self.picking_id:
return self.picking_id.with_context(skip_koli_check=True).button_validate()
- return {'type': 'ir.actions.act_window_close'}
+ return {'type': 'ir.actions.act_window_close'} \ No newline at end of file
diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py
index a683d80e..1fc8d088 100644
--- a/indoteknik_custom/models/stock_picking_return.py
+++ b/indoteknik_custom/models/stock_picking_return.py
@@ -1,38 +1,154 @@
-from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round
+from odoo import models, fields, api, _
+import logging
+_logger = logging.getLogger(__name__)
class ReturnPicking(models.TransientModel):
_inherit = 'stock.return.picking'
- @api.model
- def default_get(self, fields):
- res = super(ReturnPicking, self).default_get(fields)
-
- stock_picking = self.env['stock.picking'].search([
- ('id', '=', res['picking_id']),
- ])
-
- # sale_id = stock_picking.group_id.sale_id
- if not stock_picking.approval_return_status == 'approved':
- raise UserError('Harus Approval Accounting AR untuk melakukan Retur')
-
- # purchase = self.env['purchase.order'].search([
- # ('name', '=', stock_picking.group_id.name),
- # ])
- # if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids:
- # raise UserError('Harus Approval Accounting AP untuk melakukan Retur')
-
- return res
-
+ # return_type = fields.Selection([
+ # ('revisi_so', 'Revisi SO'),
+ # ('tukar_guling', 'Tukar Guling')
+ # ], string='Jenis Retur', default='revisi_so')
+
+
+ def create_returns(self):
+ picking = self.picking_id
+ # guling = self.env['tukar.guling']
+ # if guling._is_already_returned(picking):
+ # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
+ # if self._is_already_returned(picking):
+ # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
+ # if picking.picking_type_id.id == 30 and picking.linked_manual_bu_out.state == 'done':
+ # raise UserError("❌ BU/PICK tidak dapat di retur karena BU/OUT Sudah Done")
+
+ if self._context.get('from_ui', True):
+ return self._redirect_to_tukar_guling()
+ return super(ReturnPicking, self).create_returns()
+
+ def _redirect_to_tukar_guling(self):
+ """Redirect ke Tukar Guling SO atau PO form dengan pre-filled data"""
+ self.ensure_one()
+ picking = self.picking_id
+
+ # Ambil lines valid
+ valid_lines = []
+ self.env.cr.execute("SELECT id FROM stock_return_picking_line WHERE wizard_id = %s", (self.id,))
+ line_ids = [row[0] for row in self.env.cr.fetchall()]
+ if line_ids:
+ existing_lines = self.env['stock.return.picking.line'].sudo().browse(line_ids)
+ for line in existing_lines:
+ if line.exists() and line.quantity > 0:
+ valid_lines.append(line)
+
+ if not valid_lines:
+ for line in self.product_return_moves:
+ if hasattr(line, 'quantity') and line.quantity > 0:
+ valid_lines.append(line)
+
+ if not valid_lines:
+ raise UserError(_("Tidak ada produk yang bisa diretur. Pastikan ada produk dengan quantity > 0."))
+
+ # Siapkan context
+ context = {
+ 'default_operations': picking.id,
+ 'default_date': fields.Datetime.now(),
+ 'default_state': 'draft',
+ 'default_notes': _('Retur dari %s') % picking.name,
+ 'from_return_picking': True,
+ }
+ if picking.origin:
+ context['default_origin'] = picking.origin
+ if picking.partner_id:
+ context['default_partner_id'] = picking.partner_id.id
+ if hasattr(picking, 'real_shipping_id') and picking.real_shipping_id:
+ context['default_real_shipping_id'] = picking.real_shipping_id.id
+ elif picking.partner_id:
+ context['default_real_shipping_id'] = picking.partner_id.id
+
+ # Siapkan product lines
+ line_vals = []
+ sequence = 10
+ for line in valid_lines:
+ quantity = getattr(line, 'quantity', 0)
+ if quantity <= 0:
+ continue
+ product = getattr(line, 'product_id', None)
+ if not product:
+ continue
+ line_vals.append((0, 0, {
+ 'sequence': sequence,
+ 'product_id': product.id,
+ 'product_uom_qty': quantity,
+ 'product_uom': product.uom_id.id,
+ 'name': product.display_name,
+ }))
+ sequence += 10
+ if line_vals:
+ context['default_line_ids'] = line_vals
+
+ if picking.picking_type_id.id == 29:
+ mapping_koli_vals = []
+ sequence = 10
+ returned_product_ids = set()
+
+ # Ambil move lines dari BU/PICK
+ for move_line in picking.move_line_ids_without_package:
+ # Cek apakah produk ini ada di daftar retur dan qty_done > 0
+ if move_line.product_id.id in returned_product_ids and move_line.qty_done > 0:
+ mapping_koli_vals.append((0, 0, {
+ 'sequence': sequence,
+ 'pick_id': picking.id, # ID BU/PICK itu sendiri
+ 'product_id': move_line.product_id.id,
+ 'qty_done': move_line.qty_done,
+ 'qty_return': move_line.qty_done,
+ }))
+ sequence += 10
+
+ if mapping_koli_vals:
+ context['default_mapping_koli_ids'] = mapping_koli_vals
+
+ if picking.purchase_id or 'PO' in picking.origin:
+ _logger.info("Redirect ke Tukar Guling PO via purchase_id / origin")
+ return {
+ 'name': _('Tukar Guling PO'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'tukar.guling.po',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': context,
+ }
+ else:
+ _logger.info("This picking is NOT from a PO, fallback to SO.")
+ return {
+ 'name': _('Tukar Guling SO'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'tukar.guling',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': context,
+ }
+
+
class ReturnPickingLine(models.TransientModel):
_inherit = 'stock.return.picking.line'
@api.onchange('quantity')
def _onchange_quantity(self):
+ """Validate quantity against done quantity"""
for rec in self:
- qty_done = rec.move_id.quantity_done
+ if rec.move_id and rec.quantity > 0:
+ # Get quantity done from the move
+ qty_done = rec.move_id.quantity_done
+
+ # If quantity_done is 0, use product_uom_qty as fallback
+ if qty_done == 0:
+ qty_done = rec.move_id.product_uom_qty
- if rec.quantity > qty_done:
- raise UserError(f"Quantity yang Anda masukkan tidak boleh melebihi quantity done yaitu: {qty_done}") \ No newline at end of file
+ if rec.quantity > qty_done:
+ raise UserError(
+ _("Quantity yang Anda masukkan (%.2f) tidak boleh melebihi quantity done yaitu: %.2f untuk produk %s")
+ % (rec.quantity, qty_done, rec.product_id.name)
+ )
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
new file mode 100644
index 00000000..43bc156e
--- /dev/null
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -0,0 +1,843 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError, ValidationError
+import logging
+from datetime import datetime
+
+_logger = logging.getLogger(__name__)
+
+#TODO
+# 1. tracking status dokumen BU [X]
+# 2. ganti nama dokumen
+# 3. Tracking ketika create dokumen [X]
+# 4. Tracking ketika ganti field operations, date approval (sales, finance, logistic) [X]
+# 5. Ganti proses approval ke Sales, Finance, Logistic [X]
+# 6. Make sure bu pick dan out tidak bisa diedit ketika ort dan srt blm done
+# 7. change approval
+
+class TukarGuling(models.Model):
+ _name = 'tukar.guling'
+ _description = 'Pengajuan Retur SO'
+ _order = 'date desc, id desc'
+ _rec_name = 'name'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
+ origin = fields.Char(string='Origin SO')
+ if_so = fields.Boolean('Is SO', default=True)
+ if_po = fields.Boolean('Is PO', default=False)
+ real_shipping_id = fields.Many2one('res.partner', string='Shipping Address')
+ picking_ids = fields.One2many(
+ 'stock.picking',
+ 'tukar_guling_id',
+ string='Transfers'
+ )
+ # origin_so = fields.Many2one('sale.order', string='Origin SO')
+ name = fields.Char('Number', required=True, copy=False, readonly=True, default='New')
+ date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
+ operations = fields.Many2one(
+ 'stock.picking',
+ string='Operations',
+ domain=[
+ '|',
+ # BU/OUT
+ '&',
+ ('picking_type_id.id', '=', 29),
+ ('state', '=', 'done'),
+ '&',
+ '&',
+ ('picking_type_id.id', '=', 30),
+ ('state', '=', 'done'),
+ ('linked_manual_bu_out', '!=', 'done'),
+ ],
+ help='Nomor BU/OUT atau BU/PICK', tracking=3,
+ required=True
+ )
+ 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')], required=True, tracking=3)
+ state = fields.Selection(string='Status', selection=[
+ ('draft', 'Draft'),
+ ('approval_sales', ' Approval Sales'),
+ ('approval_finance', 'Approval Finance'),
+ ('approval_logistic', 'Approval Logistic'),
+ ('done', 'Done'),
+ ('cancel', 'Canceled')
+ ], default='draft', tracking=True, required=True)
+
+ line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines')
+ mapping_koli_ids = fields.One2many('tukar.guling.mapping.koli', 'tukar_guling_id', string='Mapping Koli')
+ date_finance = fields.Datetime('Approved Date Finance', tracking=3, readonly=True)
+ date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True)
+ date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True)
+
+ # @api.onchange('operations')
+ # def get_partner_id(self):
+ # if self.operations and self.operations.partner_id and self.operations.partner_id.name:
+ # self.partner_id == self.operations.partner_id.name
+
+ def _check_mapping_koli(self):
+ for record in self:
+ if record.operations.picking_type_id.id == 29: # Only for BU/OUT
+ if not record.mapping_koli_ids:
+ raise UserError("❌ Mapping Koli belum diisi")
+
+ # Calculate totals
+ total_mapping_qty = sum(int(mapping.qty_return) for mapping in record.mapping_koli_ids)
+ total_line_qty = sum(int(line.product_uom_qty) for line in record.line_ids)
+
+ if total_mapping_qty != total_line_qty:
+ raise UserError(
+ "❌ Total quantity return di mapping koli (%d) tidak sama dengan quantity retur product lines (%d)" %
+ (total_mapping_qty, total_line_qty)
+ )
+ else:
+ _logger.info("✅ Qty mapping koli sesuai dengan product lines")
+
+ @api.onchange('operations')
+ def _onchange_operations(self):
+ """Auto-populate lines ketika operations dipilih"""
+ if self.operations.picking_type_id.id not in [29,30]:
+ raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
+ for rec in self:
+ if rec.operations and rec.operations.picking_type_id.id == 30:
+ rec.return_type = 'revisi_so'
+
+ 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
+ _logger.info("📌 Menggunakan product lines dari return wizard, tidak populate ulang.")
+
+ # 🚀 Tapi tetap populate mapping koli jika BU/OUT
+ if self.operations.picking_type_id.id == 29:
+ mapping_koli_data = []
+ sequence = 10
+ tg_product_ids = self.line_ids.mapped('product_id.id')
+
+ for koli_line in self.operations.konfirm_koli_lines:
+ for move in koli_line.pick_id.move_line_ids_without_package:
+ if move.product_id.id in tg_product_ids:
+ mapping_koli_data.append((0, 0, {
+ 'sequence': sequence,
+ 'pick_id': koli_line.pick_id.id,
+ 'product_id': move.product_id.id,
+ 'qty_done': move.qty_done,
+ 'qty_return': 0
+ }))
+ sequence += 10
+
+ self.mapping_koli_ids = mapping_koli_data
+ _logger.info(f"✅ Created {len(mapping_koli_data)} mapping koli lines (from return wizard)")
+ return # keluar supaya tidak populate ulang lines
+
+ # Clear existing lines hanya jika tidak dari return picking
+ self.line_ids = [(5, 0, 0)]
+ self.mapping_koli_ids = [(5, 0, 0)] # Clear existing mapping koli juga
+
+ # 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 = []
+ if hasattr(self.operations, 'move_ids_without_package') and self.operations.move_ids_without_package:
+ moves_to_check = self.operations.move_ids_without_package
+ elif hasattr(self.operations, 'move_lines') and self.operations.move_lines:
+ moves_to_check = self.operations.move_lines
+
+ # Collect product data
+ product_data = {}
+ for move in moves_to_check:
+ if move.product_id and move.product_uom_qty > 0:
+ product_id = move.product_id.id
+ if product_id not in product_data:
+ product_data[product_id] = {
+ 'product': move.product_id,
+ 'qty': move.product_uom_qty,
+ 'uom': move.product_uom.id,
+ 'name': move.name or move.product_id.display_name
+ }
+
+ # Buat lines_data
+ for product_id, data in product_data.items():
+ lines_data.append((0, 0, {
+ 'sequence': sequence,
+ 'product_id': product_id,
+ 'product_uom_qty': data['qty'],
+ 'product_uom': data['uom'],
+ 'name': data['name'],
+ }))
+ sequence += 10
+
+ if lines_data:
+ self.line_ids = lines_data
+ _logger.info(f"✅ Created {len(lines_data)} product lines")
+
+ # Prepare mapping koli jika BU/OUT
+ mapping_koli_data = []
+ sequence = 10
+
+ if self.operations.picking_type_id.id == 29:
+ tg_product_ids = [p for p in product_data]
+ for koli_line in self.operations.konfirm_koli_lines:
+ for move in koli_line.pick_id.move_line_ids_without_package:
+ if move.product_id.id in tg_product_ids:
+ mapping_koli_data.append((0, 0, {
+ 'sequence': sequence,
+ 'pick_id': koli_line.pick_id.id,
+ 'product_id': move.product_id.id,
+ 'qty_done': move.qty_done
+ }))
+ sequence += 10
+
+ if mapping_koli_data:
+ self.mapping_koli_ids = mapping_koli_data
+ _logger.info(f"✅ Created {len(mapping_koli_data)} mapping koli lines")
+ else:
+ _logger.info("⚠️ No mapping koli lines created")
+ else:
+ _logger.info("⚠️ No product lines created - no valid moves found")
+ else:
+ 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.mapping_koli_ids = [(5, 0, 0)]
+
+ self.origin = False
+
+
+ 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_so', '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_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
+
+ def _is_already_returned(self, picking):
+ return self.env['stock.picking'].search_count([
+ ('origin', '=', 'Return of %s' % picking.name),
+ ('state', '!=', 'cancel')
+ ]) > 0
+
+ @api.constrains('return_type', 'operations')
+ def _check_invoice_on_revisi_so(self):
+ for record in self:
+ if record.return_type == 'revisi_so' and record.origin:
+ invoices = self.env['account.move'].search([
+ ('invoice_origin', 'ilike', record.origin),
+ ('state', 'not in', ['draft', 'cancel'])
+ ])
+ if invoices:
+ raise ValidationError(
+ _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin
+ )
+
+ @api.model
+ def create(self, vals):
+ # Generate sequence number
+ if not vals.get('name') or vals['name'] == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling')
+
+ # 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.partner_id:
+ vals['partner_id'] = picking.partner_id.id
+
+ res = super(TukarGuling, self).create(vals)
+ res.message_post(body=_("CCM Created By %s") % self.env.user.name)
+ return res
+
+ def copy(self, default=None):
+ if default is None:
+ default = {}
+
+ # Generate new sequence untuk duplicate
+ sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling')], limit=1)
+ if sequence:
+ default['name'] = sequence.next_by_id()
+ else:
+ default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'copy'
+
+ 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 write(self, vals):
+ self.ensure_one()
+ if self.operations.picking_type_id.id not in [29,30]:
+ raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
+ self._check_invoice_on_revisi_so()
+ operasi = self.operations.picking_type_id.id
+ tipe = self.return_type
+ pp = vals.get('return_type', tipe)
+
+ if not self.operations:
+ raise UserError("Operations harus diisi!")
+
+ if not self.return_type:
+ raise UserError("Return Type harus diisi!")
+
+ if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
+ raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
+ if operasi == 30 and pp == 'tukar_guling':
+ raise UserError("❌ BU/PICK tidak boleh di retur tukar guling")
+ # else:
+ # _logger.info("hehhe")
+
+ 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(TukarGuling, self).write(vals)
+
+ def unlink(self):
+ # if self.state == 'done':
+ # raise UserError ("Tidak Boleh delete ketika sudahh done")
+ for record in self:
+ if record.state == 'done':
+ raise UserError(
+ "Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu jika ingin menghapus")
+ ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'done')
+ for picking in ongoing_bu:
+ picking.action_cancel()
+ return super(TukarGuling, self).unlink()
+
+ 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 _check_not_allow_tukar_guling_on_bu_pick(self, return_type=None):
+ operasi = self.operations.picking_type_id.id
+ tipe = return_type or self.return_type
+
+ if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
+ raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
+ if operasi == 30 and tipe == 'tukar_guling':
+ raise UserError("❌ BU/PICK tidak boleh di retur tukar guling")
+
+ def action_submit(self):
+ self.ensure_one()
+ self._check_not_allow_tukar_guling_on_bu_pick()
+
+ existing_tukar_guling = self.env['tukar.guling'].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
+ if picking.picking_type_id.id == 30 and self.return_type == 'tukar_guling':
+ raise UserError("❌ BU/PICK tidak boleh di retur tukar guling")
+ if picking.picking_type_id.id == 29:
+ if picking.state != 'done':
+ raise UserError("BU/OUT belum Done!")
+ elif picking.picking_type_id.id == 30:
+ linked_bu_out = picking.linked_manual_bu_out
+ if linked_bu_out and linked_bu_out.state == 'done':
+ raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT suda Done!")
+ if self._is_already_returned(self.operations):
+ raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
+
+ if self.operations.picking_type_id.id == 29:
+ for line in self.line_ids:
+ mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
+ total_qty = sum(l.qty_return for l in mapping_lines)
+ if total_qty != line.product_uom_qty:
+ raise UserError(
+ _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name)
+
+ self._check_invoice_on_revisi_so()
+ self._validate_product_lines()
+
+ if self.state != 'draft':
+ raise UserError("Submit hanya bisa dilakukan dari Draft.")
+ self.state = 'approval_sales'
+
+ def action_approve(self):
+ self.ensure_one()
+ self._validate_product_lines()
+ self._check_invoice_on_revisi_so()
+ self._check_not_allow_tukar_guling_on_bu_pick()
+
+ operasi = self.operations.picking_type_id.id
+ tipe = self.return_type
+
+ if self.operations.picking_type_id.id == 29:
+ for line in self.line_ids:
+ mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
+ total_qty = sum(l.qty_return for l in mapping_lines)
+ if total_qty != line.product_uom_qty:
+ raise UserError(
+ _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name)
+
+ if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
+ raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
+ if operasi == 30 and tipe == 'tukar_guling':
+ raise UserError("❌ BU/PICK tidak boleh di retur tukar guling")
+ # else:
+ # _logger.info("hehhe")
+
+ 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_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_finance'
+ rec.date_sales = 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()
+ # picking = self.env['stock.picking']
+
+ 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.picking_ids.filtered(lambda p: p.state == 'done')
+ if bu_done:
+ raise UserError("Dokuemen BU sudah Done, tidak bisa di cancel")
+ ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'done')
+ for picking in ongoing_bu:
+ picking.action_cancel()
+
+ # if self.state == 'done':
+ # raise UserError("Tidak bisa cancel jika sudah done")
+ self.state = 'cancel'
+
+ def _create_pickings(self):
+ _logger.info("🛠 Starting _create_pickings()")
+ for record in self:
+ if not record.operations:
+ raise UserError("BU/OUT dari field operations tidak ditemukan.")
+
+ bu_out = record.operations
+ mapping_koli = record.mapping_koli_ids
+
+ # Constants
+ PARTNER_LOCATION_ID = 5
+ BU_OUTPUT_LOCATION_ID = 60
+ BU_STOCK_LOCATION_ID = 57
+
+ # Picking Types
+ srt_type = self.env['stock.picking.type'].browse(73)
+ ort_type = self.env['stock.picking.type'].browse(74)
+ bu_pick_type = self.env['stock.picking.type'].browse(30)
+ bu_out_type = self.env['stock.picking.type'].browse(29)
+
+ created_returns = []
+
+ ### ======== SRT dari BU/OUT =========
+ srt_return_lines = []
+ for prod in mapping_koli.mapped('product_id'):
+ qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
+ move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
+ if not move:
+ raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}")
+ srt_return_lines.append((0, 0, {
+ 'product_id': prod.id,
+ 'quantity': qty_total,
+ 'move_id': move.id,
+ }))
+ _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
+
+ srt_picking = None
+ if srt_return_lines:
+ srt_wizard = self.env['stock.return.picking'].with_context({
+ 'active_id': bu_out.id,
+ 'default_location_id': PARTNER_LOCATION_ID,
+ 'default_location_dest_id': BU_OUTPUT_LOCATION_ID,
+ 'from_ui': False,
+ }).create({
+ 'picking_id': bu_out.id,
+ 'location_id': PARTNER_LOCATION_ID,
+ 'original_location_id': BU_OUTPUT_LOCATION_ID,
+ 'product_return_moves': srt_return_lines
+ })
+ srt_vals = srt_wizard.create_returns()
+ srt_picking = self.env['stock.picking'].browse(srt_vals['res_id'])
+ srt_picking.write({
+ 'location_id': PARTNER_LOCATION_ID,
+ 'location_dest_id': BU_OUTPUT_LOCATION_ID,
+ 'group_id': bu_out.group_id.id,
+ 'tukar_guling_id': record.id,
+ 'sale_order': record.origin
+ })
+ created_returns.append(srt_picking)
+ _logger.info(f"✅ SRT created: {srt_picking.name}")
+ record.message_post(
+ body=f"📦 <b>{srt_picking.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{srt_picking.state}</b>)")
+
+ ### ======== ORT dari BU/PICK =========
+ ort_pickings = []
+ is_retur_from_bu_pick = record.operations.picking_type_id.id == 30
+ picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id') or line.product_uom_qty
+
+ for pick in picks_to_return:
+ ort_return_lines = []
+ if is_retur_from_bu_pick:
+ # Ambil dari tukar.guling.line
+ for line in record.line_ids:
+ move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id)
+ if not move:
+ raise UserError(
+ f"Move tidak ditemukan di BU/PICK {pick.name} untuk {line.product_id.display_name}")
+ ort_return_lines.append((0, 0, {
+ 'product_id': line.product_id.id,
+ 'quantity': line.product_uom_qty,
+ 'move_id': move.id,
+ }))
+ _logger.info(f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}")
+ else:
+ # Ambil dari mapping koli
+ for mk in mapping_koli.filtered(lambda m: m.pick_id == pick):
+ move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id)
+ if not move:
+ raise UserError(
+ f"Move tidak ditemukan di BU/PICK {pick.name} untuk {mk.product_id.display_name}")
+ ort_return_lines.append((0, 0, {
+ 'product_id': mk.product_id.id,
+ 'quantity': mk.qty_return,
+ 'move_id': move.id,
+ }))
+ _logger.info(f"📟 ORT (mapping koli) | {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}")
+
+ if ort_return_lines:
+ ort_wizard = self.env['stock.return.picking'].with_context({
+ 'active_id': pick.id,
+ 'default_location_id': BU_OUTPUT_LOCATION_ID,
+ 'default_location_dest_id': BU_STOCK_LOCATION_ID,
+ 'from_ui': False,
+ }).create({
+ 'picking_id': pick.id,
+ 'location_id': BU_OUTPUT_LOCATION_ID,
+ 'original_location_id': BU_STOCK_LOCATION_ID,
+ 'product_return_moves': ort_return_lines
+ })
+ ort_vals = ort_wizard.create_returns()
+ ort_picking = self.env['stock.picking'].browse(ort_vals['res_id'])
+ ort_picking.write({
+ 'location_id': BU_OUTPUT_LOCATION_ID,
+ 'location_dest_id': BU_STOCK_LOCATION_ID,
+ 'group_id': bu_out.group_id.id,
+ 'tukar_guling_id': record.id,
+ 'sale_order': record.origin
+ })
+ created_returns.append(ort_picking)
+ ort_pickings.append(ort_picking)
+ _logger.info(f"✅ ORT created: {ort_picking.name}")
+ record.message_post(
+ body=f"📦 <b>{ort_picking.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{ort_picking.state}</b>)")
+
+ ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ========
+ if record.return_type == 'tukar_guling':
+
+ # BU/PICK Baru dari ORT
+ for ort_p in ort_pickings:
+ return_lines = []
+ for move in ort_p.move_lines:
+ 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"🔁 BU/PICK baru dari ORT {ort_p.name} | {move.product_id.display_name} | qty={move.product_uom_qty}")
+
+ if not return_lines:
+ _logger.warning(f"❌ Tidak ada qty > 0 di ORT {ort_p.name}, dilewati.")
+ continue
+
+ bu_pick_wizard = self.env['stock.return.picking'].with_context({
+ 'active_id': ort_p.id,
+ 'default_location_id': BU_STOCK_LOCATION_ID,
+ 'default_location_dest_id': BU_OUTPUT_LOCATION_ID,
+ 'from_ui': False,
+ }).create({
+ 'picking_id': ort_p.id,
+ 'location_id': BU_STOCK_LOCATION_ID,
+ 'original_location_id': BU_OUTPUT_LOCATION_ID,
+ 'product_return_moves': return_lines
+ })
+ bu_pick_vals = bu_pick_wizard.create_returns()
+ new_pick = self.env['stock.picking'].browse(bu_pick_vals['res_id'])
+ new_pick.write({
+ 'location_id': BU_STOCK_LOCATION_ID,
+ 'location_dest_id': BU_OUTPUT_LOCATION_ID,
+ 'group_id': bu_out.group_id.id,
+ 'tukar_guling_id': record.id,
+ 'sale_order': record.origin
+ })
+ new_pick.action_assign() # Penting agar bisa trigger check koli
+ new_pick.action_confirm()
+ created_returns.append(new_pick)
+ _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}")
+ record.message_post(
+ body=f"📦 <b>{new_pick.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{new_pick.state}</b>)")
+
+ # BU/OUT Baru dari SRT
+ if srt_picking:
+ return_lines = []
+ for move in srt_picking.move_lines:
+ 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"🔁 BU/OUT baru dari SRT | {move.product_id.display_name} | qty={move.product_uom_qty}")
+
+ bu_out_wizard = self.env['stock.return.picking'].with_context({
+ 'active_id': srt_picking.id,
+ 'default_location_id': BU_OUTPUT_LOCATION_ID,
+ 'default_location_dest_id': PARTNER_LOCATION_ID,
+ 'from_ui': False,
+ }).create({
+ 'picking_id': srt_picking.id,
+ 'location_id': BU_OUTPUT_LOCATION_ID,
+ 'original_location_id': PARTNER_LOCATION_ID,
+ 'product_return_moves': return_lines
+ })
+ bu_out_vals = bu_out_wizard.create_returns()
+ new_out = self.env['stock.picking'].browse(bu_out_vals['res_id'])
+ new_out.write({
+ 'location_id': BU_OUTPUT_LOCATION_ID,
+ 'location_dest_id': PARTNER_LOCATION_ID,
+ 'group_id': bu_out.group_id.id,
+ 'tukar_guling_id': record.id,
+ 'sale_order': record.origin
+ })
+ created_returns.append(new_out)
+ _logger.info(f"✅ BU/OUT Baru dari SRT created: {new_out.name}")
+ record.message_post(
+ body=f"📦 <b>{new_out.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{new_out.state}</b>)")
+
+ if not created_returns:
+ raise UserError("Tidak ada dokumen retur berhasil dibuat.")
+
+ _logger.info("✅ Finished _create_pickings(). Created %s returns: %s",
+ len(created_returns),
+ ", ".join([p.name for p in created_returns]))
+
+
+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.constrains('product_uom_qty')
+ def _check_qty_change_allowed(self):
+ for rec in self:
+ if rec.tukar_guling_id and rec.tukar_guling_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_id and rec.tukar_guling_id.state not in ['draft', 'cancel']:
+ raise UserError("Tidak bisa menghapus data karena status dokumen bukan Draft atau Cancel.")
+ return super(TukarGulingLine, self).unlink()
+
+ @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 StockPicking(models.Model):
+ _inherit = 'stock.picking'
+
+ tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling Ref')
+
+ def button_validate(self):
+ res = super(StockPicking, self).button_validate()
+
+ for picking in self:
+ if picking.tukar_guling_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_id.message_post(body=message)
+
+ return res
+
+
+
+class TukarGulingMappingKoli(models.Model):
+ _name = 'tukar.guling.mapping.koli'
+ _description = 'Mapping Koli di Tukar Guling'
+
+ tukar_guling_id = fields.Many2one('tukar.guling', string='Tukar Guling')
+ pick_id = fields.Many2one('stock.picking', string='BU PICK')
+ product_id = fields.Many2one('product.product', string='Product')
+ qty_done = fields.Float(string='Qty Done BU PICK')
+ qty_return = fields.Float(string='Qty diretur')
+ sequence = fields.Integer(string='Sequence', default=10)
+ @api.constrains('qty_return')
+ def _check_qty_return_editable(self):
+ for rec in self:
+ if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']:
+ raise ValidationError("Tidak Bisa ubah qty retur jika status sudah approval atau done.")
+
+ def unlink(self):
+ for rec in self:
+ if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']:
+ raise UserError("Tidak bisa menghapus Mapping Koli karena status Tukar Guling bukan Draft atau Cancel.")
+ return super(TukarGulingMappingKoli, self).unlink() \ No newline at end of file
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
new file mode 100644
index 00000000..14f2cc96
--- /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 = '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([
+ ('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_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 \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 3c958bda..6b9ac164 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -189,6 +189,11 @@ access_realization_down_payment_line,access.realization.down.payment.line,model_
access_realization_down_payment_use_line,access.realization.down.payment.use.line,model_realization_down_payment_use_line,,1,1,1,1
access_down_payment_ap_only,access.down.payment.ap.only,model_down_payment_ap_only,,1,1,1,1
access_reject_reason_downpayment,access.reject.reason.downpayment,model_reject_reason_downpayment,,1,1,1,1
-access_refund_sale_order,access.refund.sale.order,model_refund_sale_order,base.group_user,1,1,1,1
-access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1
+
access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1
+
+access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1
+access_tukar_guling_line_all_users,tukar.guling.line.all.users,model_tukar_guling_line,base.group_user,1,1,1,1
+access_tukar_guling_po_all_users,tukar.guling.po.all.users,model_tukar_guling_po,base.group_user,1,1,1,1
+access_tukar_guling_line_po_all_users,tukar.guling.line.po.all.users,model_tukar_guling_line_po,base.group_user,1,1,1,1
+access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,model_tukar_guling_mapping_koli,base.group_user,1,1,1,1 \ No newline at end of file
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index ae944a4a..9b1c791b 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -36,9 +36,9 @@
<!-- <field name="purchase_order_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/> -->
</field>
<field name="ref" position="after">
- <field name="sale_id" readonly="1" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', True)]}"/>
- <field name="refund_so_links" readonly="1" widget="html" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/>
- <field name="has_refund_so" invisible="1"/>
+ <field name="sale_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'entry')]}"/>
+ <!-- <field name="refund_so_links" readonly="1" widget="html" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/>
+ <field name="has_refund_so" invisible="1"/> -->
</field>
<field name="reklas_misc_id" position="after">
<field name="purchase_order_id" context="{'form_view_ref': 'purchase.purchase_order_form'}" options="{'no_create': True}"/>
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index 8c054fed..5fa3d2dd 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -200,6 +200,24 @@
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
+ <record id="seq_tukar_guling" model="ir.sequence">
+ <field name="name">Pengajuan Return SO</field>
+ <field name="code">tukar.guling</field>
+ <field name="active">TRUE</field>
+ <field name="prefix">CCM/%(year)s/%(month)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ </record>
+ <record id="seq_tukar_guling_po" model="ir.sequence">
+ <field name="name">Pengajuan Return PO</field>
+ <field name="code">tukar.guling.po</field>
+ <field name="active">TRUE</field>
+ <field name="prefix">VCM/%(year)s/%(month)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ </record>
<record id="sequence_down_payment" model="ir.sequence">
<field name="name">Down Payment Sequence</field>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index dae23eed..ff223125 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -189,6 +189,13 @@
<field name="order_sales_match_line"/>
</page>
</xpath>
+ <xpath expr="//form/sheet/notebook/page[@name='purchase_delivery_invoice']" position="after">
+ <page string="Other Info" name="purchase_order_sales_matches_lines">
+ <group string="Return Doc">
+ <field name="vcm_id"/>
+ </group>
+ </page>
+ </xpath>
</field>
</record>
</data>
diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml
deleted file mode 100644
index 3b348730..00000000
--- a/indoteknik_custom/views/refund_sale_order.xml
+++ /dev/null
@@ -1,199 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<odoo>
- <!-- Tree View -->
- <record id="view_refund_sale_order_tree" model="ir.ui.view">
- <field name="name">refund.sale.order.tree</field>
- <field name="model">refund.sale.order</field>
- <field name="arch" type="xml">
- <tree string="Refund Sales Orders">
- <field name="name" readonly="1"/>
- <field name="created_date" readonly="1"/>
- <field name="partner_id" readonly="1"/>
- <field name="sale_order_ids" widget="many2many_tags" readonly="1"/>
- <field name="uang_masuk" readonly="1"/>
- <field name="ongkir" readonly="1"/>
- <field name="total_invoice" readonly="1"/>
- <field name="amount_refund" readonly="1"/>
- <field name="status"
- decoration-info="status == 'draft'"
- decoration-danger="status == 'reject'"
- decoration-success="status == 'refund'"
- decoration-warning="status == 'pengajuan1' or status == 'pengajuan2' or status == 'pengajuan3'"
- widget="badge"
- readonly="1"/>
- <field name="status_payment"
- decoration-info="status_payment == 'pending'"
- decoration-danger="status_payment == 'reject'"
- decoration-success="status_payment == 'done'"
- widget="badge"
- readonly="1"/>
- <field name="refund_date" readonly="1"/>
- <field name="amount_refund_text" readonly="1" optional="hide"/>
- <field name="invoice_ids" readonly="1" optional="hide"/>
- <field name="refund_type" readonly="1" optional="hide"/>
- <field name="user_ids" readonly="1" optional="hide"/>
- </tree>
- </field>
- </record>
-
- <!-- Form View -->
- <record id="view_refund_sale_order_form" model="ir.ui.view">
- <field name="name">refund.sale.order.form</field>
- <field name="model">refund.sale.order</field>
- <field name="arch" type="xml">
- <form string="Refund Sales Order">
- <header>
- <button name="action_ask_approval"
- type="object"
- string="Ask Approval"
- attrs="{'invisible': [('status', '!=', 'draft')]}"/>
-
- <button name="action_approve_flow"
- type="object"
- string="Approve"
- class="oe_highlight"
- attrs="{'invisible': [('status', 'in', ['refund', 'reject', 'draft'])]}"/>
- <button name="action_trigger_cancel"
- type="object"
- string="Cancel"
- attrs="{'invisible': ['|', ('status_payment', '!=', 'pending'), ('status', '=', 'reject')]}" />
- <button name="action_confirm_refund"
- type="object"
- string="Confirm Refund"
- class="btn-primary"
- attrs="{'invisible': ['|', ('status', 'not in', ['pengajuan3','refund']), ('status_payment', '!=', 'pending')]}"/>
- <button name="action_create_journal_refund"
- string="Journal Refund"
- type="object"
- class="oe_highlight"
- attrs="{'invisible': ['|', ('status', 'not in', ['pengajuan3','refund']), ('journal_refund_state', '=', 'posted')]}"/>
-
- <field name="status"
- widget="statusbar"
- statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,reject"
- attrs="{'invisible': [('status', '!=', 'reject')]}" />
-
- <field name="status"
- widget="statusbar"
- statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,refund"
- attrs="{'invisible': [('status', '=', 'reject')]}" />
- </header>
- <sheet>
- <div class="oe_button_box" name="button_box">
- <button name="action_open_journal_refund"
- type="object"
- class="oe_stat_button"
- icon="fa-book"
- width="250px"
- attrs="{'invisible': ['|', ('journal_refund_move_id', '=', False), ('journal_refund_state', '!=', 'posted')]}">
- <field name="journal_refund_move_id" string="Journal Refund" widget="statinfo"/>
- </button>
- </div>
- <widget name="web_ribbon"
- title="PAID"
- bg_color="bg-success"
- attrs="{'invisible': [('status_payment', '!=', 'done')]}"/>
-
- <widget name="web_ribbon"
- title="CANCEL"
- bg_color="bg-danger"
- attrs="{'invisible': [('status_payment', '!=', 'reject')]}"/>
- <h1>
- <field name="name" readonly="1"/>
- </h1>
- <group col="2">
- <group>
- <field name="is_locked" invisible="1"/>
- <field name="status_payment" invisible="1"/>
- <field name="journal_refund_state" invisible="1"/>
-
- <field name="partner_id" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="sale_order_ids" widget="many2many_tags" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="invoice_ids" widget="many2many_tags" readonly="1"/>
- <field name="invoice_names" widget="html" readonly="1"/>
- <field name="so_names" widget="html" readonly="1"/>
- <field name="advance_move_names" widget="html" readonly="1"/>
- <field name="refund_type" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="note_refund" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- </group>
- <group>
- <field name="uang_masuk" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="total_invoice" readonly="1"/>
- <field name="ongkir" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="amount_refund" readonly="1"/>
- <field name="amount_refund_text" readonly="1"/>
- <field name="uang_masuk_type" required="1" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="bukti_uang_masuk_image" widget="image"
- attrs="{'invisible': [('uang_masuk_type', '=', 'pdf')], 'readonly': [('is_locked', '=', True)]}"/>
- <field name="bukti_uang_masuk_pdf" widget="pdf_viewer"
- attrs="{'invisible': [('uang_masuk_type', '=', 'image')], 'readonly': [('is_locked', '=', True)]}"/>
- </group>
- </group>
-
- <notebook>
- <page string="Produk Line">
- <field name="line_ids" attrs="{'readonly': [('is_locked', '=', True)]}">
- <tree editable="bottom">
- <field name="product_id"/>
- <field name="quantity"/>
- <field name="reason"/>
- </tree>
- </field>
- </page>
-
- <page string="Other Info">
- <group col="2">
- <group>
- <field name="user_ids" widget="many2many_tags" readonly="1"/>
- <field name="created_date" readonly="1"/>
- <field name="refund_date" attrs="{'readonly': [('status', 'not in', ['pengajuan3','refund'])]}"/>
- </group>
- <group>
- <field name="bank" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="account_name" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="account_no" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- </group>
- </group>
- </page>
-
- <page string="Finance Note">
- <group col="2">
- <group>
- <field name="finance_note" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- </group>
- <group>
- <field name="bukti_refund_type" reqiured="1" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="bukti_transfer_refund_pdf" widget="pdf_viewer" attrs="{'invisible': [('bukti_refund_type', '=', 'image')]}"/>
- <field name="bukti_transfer_refund_image" widget="image" attrs="{'invisible': [('bukti_refund_type', '=', 'pdf')]}"/>
- </group>
- </group>
- </page>
-
- <page string="Cancel Reason" attrs="{'invisible': [('status', '=', 'refund')]}">
- <group>
- <field name="reason_reject"/>
- </group>
- </page>
- </notebook>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
- </form>
- </field>
- </record>
- <!-- Action -->
- <record id="action_refund_sale_order" model="ir.actions.act_window">
- <field name="name">Refund Sales Order</field>
- <field name="res_model">refund.sale.order</field>
- <field name="view_mode">tree,form</field>
- </record>
-
- <!-- Menu -->
- <menuitem id="menu_refund_sale_order"
- name="Refund"
- parent="sale.sale_order_menu"
- sequence="10"
- action="action_refund_sale_order"/>
-</odoo>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index bb8bdc08..c1f1fe61 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -35,30 +35,30 @@
string="UangMuka"
type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/>
</button>
- <xpath expr="//header" position="inside">
+ <!-- <xpath expr="//header" position="inside">
<button name="button_refund"
type="object"
string="Refund"
class="btn-primary"
attrs="{'invisible': ['|', ('state', 'not in', ['sale', 'done']), ('has_refund', '=', True)]}" />
- </xpath>
+ </xpath> -->
<div class="oe_button_box" name="button_box">
- <button name="action_open_advance_payment_move"
+ <field name="advance_payment_move_ids" invisible="1"/>
+ <button name="action_open_advance_payment_moves"
type="object"
class="oe_stat_button"
icon="fa-book"
- width="250px"
- attrs="{'invisible': [('advance_payment_move_id','=',False)]}">
- <field name="advance_payment_move_id" string="Journal Uang Muka" widget="statinfo"/>
+ attrs="{'invisible': [('advance_payment_move_ids', '=', [])]}">
+ <field name="advance_payment_move_count" widget="statinfo" string="Journals"/>
</button>
- <button type="object"
+ <!-- <button type="object"
name="action_view_related_refunds"
class="oe_stat_button"
icon="fa-refresh"
attrs="{'invisible': [('refund_count', '=', 0)]}">
<field name="refund_count" widget="statinfo" string="Refund"/>
- </button>
+ </button> -->
</div>
<field name="payment_term_id" position="after">
<field name="create_uid" invisible="1"/>
@@ -176,7 +176,10 @@
<field name="expected_ready_to_ship"/>
<field name="eta_date_start"/>
<field name="eta_date" readonly="1"/>
- <field name="has_refund" readonly="1"/>
+ <!-- <field name="has_refund" readonly="1"/> -->
+ </group>
+ <group string="Return Doc">
+ <field name="ccm_id" readonly="1"/>
</group>
</xpath>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']"
@@ -658,7 +661,7 @@
</record>
</data>
- <data>
+ <!-- <data>
<record id="sale_order_multi_create_refund_ir_actions_server" model="ir.actions.server">
<field name="name">Refund</field>
<field name="model_id" ref="sale.model_sale_order"/>
@@ -666,7 +669,7 @@
<field name="state">code</field>
<field name="code">action = records.open_form_multi_create_refund()</field>
</record>
- </data>
+ </data> -->
<data>
<record id="mail_template_sale_order_notification_to_salesperson" model="mail.template">
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 5e33e4c7..f9200dfa 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -50,11 +50,11 @@
type="object"
attrs="{'invisible': ['|', ('state', 'in', ['done']), ('approval_receipt_status', '=', 'pengajuan1')]}"
/>
- <button name="ask_return_approval"
- string="Ask Return/Acc"
- type="object"
- attrs="{'invisible': [('state', 'in', ['draft', 'cancel', 'assigned'])]}"
- />
+<!-- <button name="ask_return_approval"-->
+<!-- string="Ask Return/Acc"-->
+<!-- type="object"-->
+<!-- attrs="{'invisible': [('state', 'in', ['draft', 'cancel', 'assigned'])]}"-->
+<!-- />-->
<button name="action_create_invoice_from_mr"
string="Create Bill"
type="object"
diff --git a/indoteknik_custom/views/tukar_guling.xml b/indoteknik_custom/views/tukar_guling.xml
new file mode 100644
index 00000000..fa3db0d2
--- /dev/null
+++ b/indoteknik_custom/views/tukar_guling.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <data>
+ <!-- Action -->
+ <record id="action_pengajuan_tukar_guling" model="ir.actions.act_window">
+ <field name="name">Pengajuan Return SO</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">tukar.guling</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <!-- Menu -->
+ <menuitem
+ id="menu_pengajuan_tukar_guling"
+ name="Pengajuan Return SO"
+ parent="sale.sale_order_menu"
+ sequence="7"
+ action="action_pengajuan_tukar_guling"
+ />
+ <!-- Tree View -->
+ <record id="pengajuan_tukar_guling_tree" model="ir.ui.view">
+ <field name="name">pengajuan.tukar.guling.tree</field>
+ <field name="model">tukar.guling</field>
+ <field name="arch" type="xml">
+ <tree create="1" delete="1" default_order="create_date desc">
+ <field name="name"/>
+ <field name="partner_id" string="Customer"/>
+ <field name="origin" string="SO Number"/>
+ <field name="operations" string="Operations"/>
+ <field name="return_type" string="Return Type"/>
+ <field name="state" widget="badge"
+ decoration-info="state in ('draft', 'approval_sales', 'approval_finance','approval_logistic')"
+ decoration-success="state == 'done'"
+ decoration-muted="state == 'cancel'"
+ />
+ <field name="ba_num" string="Nomor BA"/>
+ <field name="date"/>
+ <field name="date_logistic" string="Approved Date"/>
+ </tree>
+ </field>
+ </record>
+ <!-- Form View -->
+ <record id="pengajuan_tukar_guling_form" model="ir.ui.view">
+ <field name="name">pengajuan.tukar.guling.form</field>
+ <field name="model">tukar.guling</field>
+ <field name="arch" type="xml">
+ <form>
+ <header>
+ <button name="action_submit" string="Submit" type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('state', '!=', 'draft')]}"/>
+ <button name="action_approve" string="Approve" type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('state', 'not in', ['approval_sales', 'approval_finance', 'approval_logistic'])]}"/>
+ <button name="action_cancel" string="Cancel" type="object"
+ class="btn-secondary"
+ attrs="{'invisible': [('state', '=', 'cancel')]}"/>
+ <button name="action_draft" string="Set to Draft" type="object"
+ class="btn-secondary"
+ attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ <field name="state" widget="statusbar" readonly="1"
+ statusbar_visible="draft,approval_sales,approval_logistic,approval_finance,done"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box">
+ <button name="action_view_picking"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-truck"
+ attrs="{'invisible': [('picking_ids', '=', False), ('state', 'in', ['draft', 'approval_sales', 'approval_logistic', 'approval_finance'])]}">
+ <field name="picking_ids" widget="statinfo" string="Delivery"/>
+ </button>
+ </div>
+ <div class="oe_title">
+ <h1>
+ <field name="name" readonly="1" class="oe_inline"/>
+ </h1>
+ </div>
+ <group>
+ <group>
+ <field name="date" string="Date" readonly="1"/>
+ <field name="partner_id" readonly="1"/>
+ <field name="return_type" attrs="{'readonly': [('state', 'not in', 'draft')]}"/>
+ <field name="operations"
+ attrs="{'readonly': [('state', 'not in', 'draft')]}"/>
+ <field name="origin" readonly="1"/>
+ </group>
+ <group>
+ <field name="ba_num" string="Nomor BA"/>
+ <field name="notes"/>
+ <field name="date_sales" readonly="1"/>
+ <field name="date_finance" readonly="1"/>
+ <field name="date_logistic" readonly="1"/>
+ </group>
+ </group>
+ <notebook>
+ <page string="Product Lines" name="product_lines">
+ <field name="line_ids">
+ <tree string="Product Lines" editable="top" create="0" delete="1">
+ <field name="sequence" widget="handle"/>
+ <field name="product_id" required="0"
+ options="{'no_create': True, 'no_create_edit': True}" readonly="0"/>
+ <field name="name" force_save="0" readonly="1"/>
+ <field name="product_uom_qty" string="Quantity"/>
+ <field name="product_uom" string="UoM"
+ options="{'no_create': True, 'no_create_edit': True}"/>
+ </tree>
+ </field>
+ </page>
+ <page string="Mapping Koli" name="mapping_koli">
+ <field name="mapping_koli_ids">
+ <tree editable="top" create="0" delete="1">
+ <field name="pick_id" readonly="1" force_save="1"/>
+ <field name="product_id" readonly="1" force_save="1"/>
+ <field name="qty_done" force_save="1" readonly="1"/>
+ <field name="qty_return"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/tukar_guling_po.xml b/indoteknik_custom/views/tukar_guling_po.xml
new file mode 100644
index 00000000..26c0a0d4
--- /dev/null
+++ b/indoteknik_custom/views/tukar_guling_po.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <data>
+ <!-- Action -->
+ <record id="action_pengajuan_tukar_guling_po" model="ir.actions.act_window">
+ <field name="name">Pengajuan Return PO</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">tukar.guling.po</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <!-- Menu -->
+ <menuitem
+ id="menu_pengajuan_tukar_guling_po"
+ name="Pengajuan Return PO"
+ parent="purchase.menu_procurement_management"
+ sequence="3"
+ action="action_pengajuan_tukar_guling_po"
+ />
+ <!-- Tree View -->
+ <record id="pengajuan_tukar_guling_po_tree" model="ir.ui.view">
+ <field name="name">pengajuan.tukar.guling.po.tree</field>
+ <field name="model">tukar.guling.po</field>
+ <field name="arch" type="xml">
+ <tree create="1" delete="1" default_order="create_date desc">
+ <field name="name"/>
+ <field name="vendor_id" string="Customer"/>
+ <field name="origin" string="SO Number"/>
+ <field name="operations" string="Operations"/>
+ <field name="return_type" string="Return Type"/>
+ <field name="state" widget="badge"
+ decoration-info="state in ('draft', 'approval_purchase', 'approval_finance','approval_logistic')"
+ decoration-success="state == 'done'"
+ decoration-muted="state == 'cancel'"
+ />
+ <field name="ba_num" string="Nomor BA"/>
+ <field name="date"/>
+ <field name="date_logistic" string="Approved Date"/>
+ </tree>
+ </field>
+ </record>
+ <!-- Form View -->
+ <record id="pengajuan_tukar_guling_po_form" model="ir.ui.view">
+ <field name="name">pengajuan.tukar.guling.po.form</field>
+ <field name="model">tukar.guling.po</field>
+ <field name="arch" type="xml">
+ <form>
+ <header>
+ <button name="action_submit" string="Submit" type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('state', '!=', 'draft')]}"/>
+ <button name="action_approve" string="Approve" type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('state', 'not in', ['approval_purchase', 'approval_finance', 'approval_logistic'])]}"/>
+ <button name="action_cancel" string="Cancel" type="object"
+ class="btn-secondary"
+ attrs="{'invisible': [('state', '=', 'cancel')]}"
+ confirm="Are you sure you want to cancel this record?"/>
+ <button name="action_draft" string="Set to Draft" type="object"
+ class="btn-secondary"
+ attrs="{'invisible': [('state', '!=', 'cancel')]}"
+ confirm="Are you sure you want to reset this record to draft?"/>
+ <field name="state" widget="statusbar" readonly="1"
+ statusbar_visible="draft,approval_purchase,approval_logistic,approval_finance,done"/>
+ </header>
+ <sheet>
+ <div class="oe_button_box">
+ <button name="action_view_picking"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-truck"
+ attrs="{'invisible': [('po_picking_ids', '=', False)]}">
+ <field name="po_picking_ids" widget="statinfo" string="Delivery"/>
+ </button>
+ </div>
+ <div class="oe_title">
+ <h1>
+ <field name="name" readonly="1" class="oe_inline"/>
+ </h1>
+ </div>
+ <group>
+ <group>
+ <field name="vendor_id" readonly="1"/>
+ <field name="date" string="Date" readonly="1"/>
+ <field name="return_type"/>
+ <!-- <field name="ort_num" readonly="1"/>-->
+ <!-- <field name="srt_num" readonly="1"/>-->
+ <field name="operations" string="Operations"
+ attrs="{
+ 'required': [('return_type', 'in', ['revisi_po', 'tukar_guling'])]
+ }"/>
+ <field name="origin" readonly="1"/>
+ <!-- <field name="origin_so" readonly="1"/>-->
+ </group>
+ <group>
+ <field name="ba_num" string="Nomor BA"/>
+ <field name="notes"/>
+ <field name="date_purchase" readonly="1"/>
+ <field name="date_finance" readonly="1"/>
+ <field name="date_logistic" readonly="1"/>
+ </group>
+ </group>
+ <!-- Product Lines -->
+ <notebook>
+ <page string="Product Lines" name="product_lines" create="0" edit="0">
+ <field name="line_ids" delete="1" readonly="1">
+ <tree string="Product Lines">
+ <field name="sequence" widget="handle"/>
+ <field name="product_id" required="1"
+ options="{'no_create': True, 'no_create_edit': True}"/>
+ <field name="name" force_save="1"/>
+ <field name="product_uom_qty" string="Quantity"/>
+ <field name="product_uom" string="UoM"
+ options="{'no_create': True, 'no_create_edit': True}"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/tukar_guling_return_views.xml b/indoteknik_custom/views/tukar_guling_return_views.xml
new file mode 100644
index 00000000..9312005a
--- /dev/null
+++ b/indoteknik_custom/views/tukar_guling_return_views.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <!-- Inherit the return picking form view -->
+ <record id="view_stock_return_picking_form_inherit" model="ir.ui.view">
+ <field name="name">stock.return.picking.form.inherit.tukar.guling</field>
+ <field name="model">stock.return.picking</field>
+ <field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
+ <field name="priority" eval="20"/> <!-- Higher than stock_account -->
+ <field name="arch" type="xml">
+ <!-- Add fields above the product moves table -->
+ <xpath expr="//field[@name='product_return_moves']" position="before">
+ <div class="row mb-3">
+ <div class="col-12">
+ <field name="return_type" class="oe_inline"/>
+ </div>
+ </div>
+ </xpath>
+ </field>
+ </record>
+</odoo> \ No newline at end of file