diff options
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 3 | ||||
| -rwxr-xr-x | indoteknik_custom/models/__init__.py | 2 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 76 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking_return.py | 145 | ||||
| -rw-r--r-- | indoteknik_custom/models/tukar_guling.py | 570 | ||||
| -rw-r--r-- | indoteknik_custom/models/tukar_guling_po.py | 537 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 5 | ||||
| -rw-r--r-- | indoteknik_custom/views/ir_sequence.xml | 16 | ||||
| -rw-r--r-- | indoteknik_custom/views/stock_picking.xml | 10 | ||||
| -rw-r--r-- | indoteknik_custom/views/tukar_guling.xml | 116 | ||||
| -rw-r--r-- | indoteknik_custom/views/tukar_guling_po.xml | 116 | ||||
| -rw-r--r-- | indoteknik_custom/views/tukar_guling_return_views.xml | 20 |
12 files changed, 1553 insertions, 63 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 21afc26f..2af13816 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -170,6 +170,9 @@ 'views/public_holiday.xml', 'views/stock_inventory.xml', 'views/sale_order_delay.xml', + 'views/tukar_guling.xml', + # 'views/tukar_guling_return_views.xml' + 'views/tukar_guling_po.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index b815b472..84f2e15a 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -153,3 +153,5 @@ from . import sale_order_delay from . import approval_invoice_date from . import approval_payment_term # from . import patch +from . import tukar_guling +from . import tukar_guling_po diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 0efffd2f..868a96c1 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])]) @@ -2538,4 +2548,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..a9781d3c 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,38 +1,133 @@ -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): +class StockReturnPicking(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(StockReturnPicking, 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.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..7e857d02 --- /dev/null +++ b/indoteknik_custom/models/tukar_guling.py @@ -0,0 +1,570 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +import logging + +_logger = logging.getLogger(__name__) + + +class TukarGuling(models.Model): + _name = 'tukar.guling' + _description = 'Tukar Guling' + _order = 'date desc, id desc' + _rec_name = 'name' + + 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' + ) + 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) + state = fields.Selection(string='Status', selection=[ + ('draft', 'Draft'), + ('approval_sales', ' Approval Sales'), + ('approval_logistic', 'Approval Logistic'), + ('approval_finance', 'Approval Finance'), + ('done', 'Done'), + ('cancel', 'Canceled') + ], default='draft', tracking=True, required=True) + + line_ids = fields.One2many('tukar.guling.line', 'tukar_guling_id', string='Product Lines') + + @api.onchange('operations') + def _onchange_operations(self): + """Auto-populate lines ketika operations dipilih""" + 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 + return + + # Clear existing lines hanya jika tidak dari return picking + 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 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': + sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling')], limit=1) + if sequence: + vals['name'] = sequence.next_by_id() + else: + # Fallback jika sequence belum dibuat + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') or 'PTG-00001' + + # 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 + + return super(TukarGuling, self).create(vals) + + 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() + 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.") + 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 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!") + + # 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_logistic' + + 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 = 'approval_finance' + + 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 = 'done' + rec._create_pickings() + else: + raise UserError("Status ini tidak bisa di-approve.") + + def action_cancel(self): + self.ensure_one() + # picking = self.env['stock.picking'] + 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): + for record in self: + if not record.operations: + raise UserError("BU/OUT dari field operations tidak ditemukan.") + + bu_pick_to_return = record.operations.konfirm_koli_lines.pick_id + bu_out_to_return = record.operations + + if not bu_pick_to_return and not bu_out_to_return: + raise UserError("Tidak ada BU/PICK atau BU/OUT yang selesai untuk diretur.") + + created_returns = [] + + # Picking types & locations + 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) + + def _create_return_from_picking(picking): + if not picking: + return None + grup = record.operations.group_id + + PARTNER_LOCATION_ID = 5 + BU_OUTPUT_LOCATION_ID = 60 + BU_STOCK_LOCATION_ID = 57 + + if picking.picking_type_id.id == 30: + # BU/PICK → ORT + return_type = ort_type + default_location_id = BU_OUTPUT_LOCATION_ID + default_location_dest_id = BU_STOCK_LOCATION_ID + elif picking.picking_type_id.id == 74: + # ORT → BU/PICK + return_type = bu_pick_type + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_OUTPUT_LOCATION_ID + elif picking.picking_type_id.id == 29: + # BU/OUT → SRT + return_type = srt_type + default_location_id = PARTNER_LOCATION_ID + default_location_dest_id = BU_OUTPUT_LOCATION_ID + elif picking.picking_type_id.id == 73: + # SRT → BU/OUT + return_type = bu_out_type + default_location_id = BU_OUTPUT_LOCATION_ID + default_location_dest_id = PARTNER_LOCATION_ID + else: + return None + + 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 = [] + # 🔥 Hanya pakai qty dari tukar guling line + for line in record.line_ids: + move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) + if move: + return_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'move_id': move[0].id, + })) + else: + raise UserError( + _("Tidak ditemukan move line di picking %s untuk produk %s") + % (picking.name, line.product_id.display_name) + ) + + if not return_lines: + raise UserError(_("Tidak ada product line valid untuk retur picking %s") % picking.name) + + return_wizard.product_return_moves = return_lines + return_vals = return_wizard.create_returns() + return_id = return_vals.get('res_id') + return_picking = self.env['stock.picking'].browse(return_id) + + if not return_picking: + raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) + + return_picking.write({ + 'location_dest_id': default_location_dest_id, + 'location_id': default_location_id, + 'group_id': grup.id, + 'tukar_guling_id': record.id, + }) + + return return_picking + + # === PERBAIKI URUTAN === + srt = _create_return_from_picking(bu_out_to_return) + if srt: + created_returns.append(srt) + + picks = record.operations.konfirm_koli_lines.pick_id + for picking in picks: + ort = _create_return_from_picking(picking) + if ort: + created_returns.append(ort) + if record.return_type == 'tukar_guling': + bu_pick = _create_return_from_picking(ort) + if bu_pick: + created_returns.append(bu_pick) + + if record.return_type == 'tukar_guling' and srt: + bu_out = _create_return_from_picking(srt) + if bu_out: + created_returns.append(bu_out) + + if not created_returns: + raise UserError("Tidak ada dokumen retur yang berhasil dibuat.") + + +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.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') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py new file mode 100644 index 00000000..6d7d7335 --- /dev/null +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -0,0 +1,537 @@ +from email.policy import default + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +import logging + +_logger = logging.getLogger(__name__) + + +class TukarGulingPO(models.Model): + _name = 'tukar.guling.po' + _description = 'Tukar Guling PO' + + 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) + operations = fields.Many2one( + 'stock.picking', + string='Operations', + domain=[ + ('picking_type_id.id', 'in', [75, 28]), + ('state', '=', 'done') + ], help='Nomor BU INPUT atau BU PUT' + ) + ba_num = fields.Char('Nomor BA') + return_type = fields.Selection([ + ('revisi_po', 'Revisi PO'), + ('tukar_guling', 'Tukar Guling'), + ], string='Return Type', required=True) + notes = fields.Text('Notes') + 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') + state = fields.Selection([ + ('draft', 'Draft'), + ('approval_purchase', 'Approval Purchasing'), + ('approval_logistic', 'Approval Logistic'), + ('approval_finance', 'Approval Finance'), + ('done', 'Done'), + ('cancel', 'Cancel'), + ], string='Status', default='draft') + + @api.model + def create(self, vals): + # Generate sequence number + if not vals.get('name') or vals['name'] == 'New': + sequence = self.env['ir.sequence'].search([('code', '=', 'tukar.guling.po')], limit=1) + if sequence: + vals['name'] = sequence.next_by_id() + else: + # Fallback jika sequence belum dibuat + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'new' + + # 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 + + return super(TukarGulingPO, self).create(vals) + + @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: + 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 + + # Clear existing lines hanya jika tidak dari return picking + 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_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 + + 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): + 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 and self.operations.picking_type_id.id == 28 and self.return_type == '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), + ('state', '=', 'done') + ], limit=1) + + if bu_put: + raise UserError("❌ Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") + + picking = self.operations + if picking.picking_type_id.id == 75: + if picking.state != 'done': + raise UserError("BU/PUT belum Done!") + + if picking.picking_type_id.id != 75 or picking.picking_type_id.id != 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!") + + # 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_logistic' + + 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 = 'approval_finance' + + 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 = 'done' + rec._create_pickings() + 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") + 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 = [] + + group = record.operations.group_id + bu_inputs = bu_puts = self.env['stock.picking'] + + 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.") + + PARTNER_LOCATION_ID = 4 + BU_INPUT_LOCATION_ID = 58 + BU_STOCK_LOCATION_ID = 57 + + def _create_return_from_picking(picking): + if not picking: + return None + + grup = record.operations.group_id + + # Mapping lokasi sesuai picking type + if picking.picking_type_id.id == 28: + default_location_id = BU_INPUT_LOCATION_ID + default_location_dest_id = PARTNER_LOCATION_ID + elif picking.picking_type_id.id == 75: + default_location_id = BU_STOCK_LOCATION_ID + default_location_dest_id = BU_INPUT_LOCATION_ID + elif picking.picking_type_id.id == 77: + default_location_id = BU_INPUT_LOCATION_ID + default_location_dest_id = BU_STOCK_LOCATION_ID + elif picking.picking_type_id.id == 76: + default_location_id = PARTNER_LOCATION_ID + default_location_dest_id = BU_INPUT_LOCATION_ID + else: + return None + + 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 + }) + + # Sesuai line tukar guling + return_lines = [] + for line in record.line_ids: + move = picking.move_lines.filtered(lambda m: m.product_id == line.product_id) + if move: + return_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'move_id': move[0].id, + })) + else: + raise UserError( + _("Tidak ditemukan move line di picking %s untuk produk %s") + % (picking.name, line.product_id.display_name) + ) + + if not return_lines: + return None + + 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')) + + if not return_picking: + raise UserError("Retur gagal dibuat. Hasil create_returns: %s" % str(return_vals)) + + 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, + }) + + # Paksa lokasi di move lines juga + for move in return_picking.move_lines: + move.write({ + 'location_id': default_location_id, + 'location_dest_id': default_location_dest_id, + }) + + return return_picking + + # === Eksekusi pembuatan picking === + if record.operations.picking_type_id.id == 28: + # Kalau dari BU INPUT → hanya PRT + prt = _create_return_from_picking(record.operations) + if prt: + created_returns.append(prt) + else: + # 1. Dari BU PUT buat VRT + for bu_put in bu_puts: + vrt = _create_return_from_picking(bu_put) + if vrt: + created_returns.append(vrt) + + # 2. Dari BU INPUT buat PRT + for bu_input in bu_inputs: + prt = _create_return_from_picking(bu_input) + if prt: + created_returns.append(prt) + + # 3. Kalau tukar guling buat lanjut INPUT & PUT + 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) + if bu_input: + created_returns.append(bu_input) + + for vrt in created_returns.filtered(lambda p: p.picking_type_id.id == 77): + bu_put = _create_return_from_picking(vrt) + if bu_put: + created_returns.append(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') + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO Ref') diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 2b970cfd..85781524 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -183,3 +183,8 @@ access_production_purchase_match,access.production.purchase.match,model_producti access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1 access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1 access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,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
\ No newline at end of file diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index f2b42c3b..3f0ba0b6 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -200,5 +200,21 @@ <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="prefix">CCM/</field> + <field name="padding">5</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="prefix">VCM/</field> + <field name="padding">5</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + </record> </data> </odoo>
\ No newline at end of file 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..41e9a18d --- /dev/null +++ b/indoteknik_custom/views/tukar_guling.xml @@ -0,0 +1,116 @@ +<?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.menu_sale_report" + sequence="3" + 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="date"/> + <field name="operations" string="Operations"/> + <field name="ba_num" string="Nomor BA"/> + <field name="return_type" string="Return Type"/> + <field name="state" widget="badge" + decoration-info="state in ('draft', 'approval_sales', 'approval_logistic','approval_finance')" + decoration-success="state == 'done'" + decoration-muted="state == 'cancel'" + /> + </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_logistic', 'approval_finance'])]}"/> + <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_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="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_so', '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"/> + </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> + </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..0e29e965 --- /dev/null +++ b/indoteknik_custom/views/tukar_guling_po.xml @@ -0,0 +1,116 @@ +<?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="date"/> + <field name="operations" string="Operations"/> + <field name="ba_num" string="Nomor BA"/> + <field name="return_type" string="Return Type"/> + <field name="state" widget="badge" + decoration-info="state in ('draft', 'approval_purchase', 'approval_logistic','approval_finance')" + decoration-success="state == 'done'" + decoration-muted="state == 'cancel'" + /> + </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_logistic', 'approval_finance'])]}"/> + <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="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"/> + </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> + </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 |
