diff options
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 |
