diff options
| author | Miqdad <ahmadmiqdad27@gmail.com> | 2025-06-19 10:33:02 +0700 |
|---|---|---|
| committer | Miqdad <ahmadmiqdad27@gmail.com> | 2025-06-19 10:33:02 +0700 |
| commit | 1542b2373ef4cff98ded7c9bbf426e18b5524162 (patch) | |
| tree | dbe7867e87eaf8009a194c38435be9150901b160 | |
| parent | ff9b0cb54bb2f9c99773cfc7c8c5b612bb5d7444 (diff) | |
<miqdad> push
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 2 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking_return.py | 432 | ||||
| -rw-r--r-- | indoteknik_custom/models/tukar_guling.py | 148 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 2 |
4 files changed, 399 insertions, 185 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index b9365ba9..f58acbfb 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -169,7 +169,7 @@ 'views/stock_inventory.xml', 'views/sale_order_delay.xml', 'views/tukar_guling.xml', - 'views/tukar_guling_po.xml', + # 'views/tukar_guling_po.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py index a683d80e..341383e8 100644 --- a/indoteknik_custom/models/stock_picking_return.py +++ b/indoteknik_custom/models/stock_picking_return.py @@ -1,38 +1,402 @@ -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 +from odoo.exceptions import UserError, ValidationError +import logging +_logger = logging.getLogger(__name__) -class ReturnPicking(models.TransientModel): - _inherit = 'stock.return.picking' + +class TukarGuling(models.Model): + _name = 'tukar.guling' + _description = 'Tukar Guling' + _order = 'date desc, id desc' + _rec_name = 'name' + + origin = fields.Char(string='Origin SO') + real_shipping_id = fields.Many2one('res.partner', string='Shipping Address') + picking_ids = fields.One2many('stock.picking', 'tukar_guling_id', string='Transfers') + + 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', 'Operations', + domain=[('picking_type_id.code', '=', 'outgoing')], + 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'), + ('revisi_so', 'Revisi SO')]) + + 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""" + if self.operations: + # Clear existing lines + self.line_ids = [(5, 0, 0)] + + # Set origin dari operations + if self.operations.origin: + self.origin = self.operations.origin + + # Set shipping address + if self.operations.real_shipping_id: + self.real_shipping_id = self.operations.real_shipping_id.id + + # Auto-populate lines dari move_ids operations + lines_data = [] + sequence = 10 + + # Ambil moves yang sudah done/delivered + moves_to_check = self.operations.move_ids_without_package.filtered( + lambda m: m.state == 'done' and m.quantity_done > 0 + ) + + _logger.info(f"BU/OUT: {self.operations.name}, State: {self.operations.state}") + _logger.info(f"Total moves found: {len(moves_to_check)}") + + for move in moves_to_check: + _logger.info( + f"Move: {move.name}, Product: {move.product_id.name if move.product_id else 'No Product'}, " + f"Qty Done: {move.quantity_done}, State: {move.state}" + ) + + # Hanya ambil yang sudah done dengan quantity_done > 0 + if move.product_id and move.quantity_done > 0: + lines_data.append((0, 0, { + 'sequence': sequence, + 'product_id': move.product_id.id, + 'product_uom_qty': move.quantity_done, # Gunakan quantity_done + '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 + self.line_ids = [(5, 0, 0)] + self.origin = False + self.real_shipping_id = False + + def _create_pickings(self): + """Improved picking creation with proper move handling""" + if not self.operations: + raise UserError("BU/Out harus diisi terlebih dahulu.") + + origin_so = self.operations.origin + if not origin_so: + raise UserError("BU/OUT tidak memiliki origin (SO), tidak bisa cari BU/PICK.") + + # Cari BU/PICK dari SO yang sama + pick_picking = self.env['stock.picking'].search([ + ('origin', '=', origin_so), + ('picking_type_id.code', '=', 'internal') + ], limit=1) + + if not pick_picking: + raise UserError(f"BU/PICK dengan origin {origin_so} tidak ditemukan.") + + # Ambil group_id dari operations + group_id = self.operations.group_id.id if self.operations.group_id else False + + Picking = self.env['stock.picking'] + StockMove = self.env['stock.move'] + + # Cari picking types + srt_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'SRT') + ], limit=1) + + ort_type = self.env['stock.picking.type'].search([ + ('sequence_code', '=', 'ORT') + ], limit=1) + + if not srt_type or not ort_type: + raise UserError("Picking type SRT atau ORT tidak ditemukan!") + + # Lokasi + location_dest_id = srt_type.default_location_dest_id.id + location_dest_id_ort = ort_type.default_location_dest_id.id + location_customer = self.operations.location_dest_id + + # 1. Create BU/SRT: return dari customer ke gudang + srt_picking = Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': srt_type.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'origin': f"Retur {self.operations.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + }) + + # Create moves untuk SRT + srt_moves = [] + for line in self.line_ids: + move_vals = { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_customer.id, + 'location_dest_id': location_dest_id, + 'picking_id': srt_picking.id, + 'group_id': group_id, + 'state': 'draft', + } + move = StockMove.create(move_vals) + srt_moves.append(move) + + # Confirm SRT picking + srt_picking.action_confirm() + + # 2. Create BU/ORT: return dari gudang ke supplier/vendor + ort_picking = Picking.create({ + 'partner_id': self.operations.partner_id.id, + 'real_shipping_id': self.operations.real_shipping_id.id, + 'picking_type_id': ort_type.id, + 'location_id': location_dest_id, + 'location_dest_id': location_dest_id_ort, + 'origin': f"Retur {pick_picking.name}", + 'tukar_guling_id': self.id, + 'group_id': group_id, + }) + + # Create moves untuk ORT + ort_moves = [] + for line in self.line_ids: + move_vals = { + 'name': line.name or line.product_id.name, + 'product_id': line.product_id.id, + 'product_uom_qty': line.product_uom_qty, + 'product_uom': line.product_uom.id, + 'location_id': location_dest_id, + 'location_dest_id': location_dest_id_ort, + 'picking_id': ort_picking.id, + 'group_id': group_id, + 'state': 'draft', + } + move = StockMove.create(move_vals) + ort_moves.append(move) + + # Confirm ORT picking + ort_picking.action_confirm() + ort_picking.action_assign() + + # Log creation + _logger.info(f"Created SRT picking: {srt_picking.name} with {len(srt_moves)} moves") + _logger.info(f"Created ORT picking: {ort_picking.name} with {len(ort_moves)} moves") + + return { + 'srt_picking': srt_picking, + 'ort_picking': ort_picking + } + + def action_approve(self): + self.ensure_one() + + if not self.operations: + raise UserError("BU/Out harus diisi!") + + if not self.return_type: + raise UserError("Return Type harus diisi!") + + # Validasi product lines + self._validate_product_lines() + + # Cek hak akses berdasarkan state + if self.state == 'approval_sales': + if not self.env.user.has_group('indoteknik_custom.group_role_sales'): + raise UserError("Hanya Sales Manager yang boleh approve tahap ini.") + self.state = 'approval_logistic' + + elif self.state == 'approval_logistic': + if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): + raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") + self.state = 'approval_finance' + + elif self.state == 'approval_finance': + if not self.env.user.has_group('indoteknik_custom.group_role_fat'): + raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + self.state = 'done' + # Create pickings saat final approval + result = self._create_pickings() + if result: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Success', + 'message': f"Berhasil membuat BU/SRT: {result['srt_picking'].name} dan BU/ORT: {result['ort_picking'].name}", + 'type': 'success', + 'sticky': False, + } + } + else: + raise UserError("Status ini tidak bisa di-approve.") + + # ... (rest of the methods remain the same) + @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("BU/Out 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 @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 - -class ReturnPickingLine(models.TransientModel): - _inherit = 'stock.return.picking.line' - - @api.onchange('quantity') - def _onchange_quantity(self): - for rec in self: - qty_done = rec.move_id.quantity_done - - 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 + def create(self, vals): + if not vals.get('name') or vals['name'] == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') 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(TukarGuling, self).create(vals) + + def action_submit(self): + self.ensure_one() + if self.state != 'draft': + raise UserError("Submit hanya bisa dilakukan dari Draft.") + + # Validasi sebelum submit + self._validate_product_lines() + + self.state = 'approval_sales' + + def action_cancel(self): + self.ensure_one() + # Cek apakah ada picking yang sudah dibuat + if self.picking_ids: + done_pickings = self.picking_ids.filtered(lambda p: p.state == 'done') + if done_pickings: + raise UserError("Tidak bisa cancel karena ada transfer yang sudah selesai!") + + self.state = 'cancel' + + 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 + else: + raise UserError("Belum ada transfer yang dibuat!") + return action + + +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 Reference') + + def action_create_tukar_guling(self): + """Action untuk membuat Tukar Guling dari picking""" + self.ensure_one() + + # Cek apakah picking sudah done + if self.state != 'done': + raise UserError("Hanya bisa membuat Tukar Guling dari delivery yang sudah selesai!") + + # Cek apakah sudah ada tukar guling untuk picking ini + existing_tukar_guling = self.env['tukar.guling'].search([ + ('operations', '=', self.id) + ]) + + if existing_tukar_guling: + raise UserError(f"Sudah ada Tukar Guling untuk delivery ini: {existing_tukar_guling.name}") + + # Create tukar guling baru + tukar_guling = self.env['tukar.guling'].create({ + 'operations': self.id, + 'return_type': 'tukar_guling', # default value + }) + + return { + 'type': 'ir.actions.act_window', + 'name': 'Tukar Guling', + 'res_model': 'tukar.guling', + 'res_id': tukar_guling.id, + 'view_mode': 'form', + 'target': 'current', + }
\ No newline at end of file diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 186cff97..08b862a7 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -355,137 +355,6 @@ class TukarGuling(models.Model): ort_picking.action_confirm() ort_picking.action_assign() -class TukarGulingPO(models.Model): - _name = 'tukar.guling.po' - _description = 'Tukar Guling PO' - - 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', 'Nomor BU/Out', - domain=[('picking_type_id.code', '=', 'outgoing')]) - ba_num = fields.Text('Nomor BA') - notes = fields.Text('Notes') - state = fields.Selection(string='Status', selection=[ - ('draft', 'Draft'), - ('approval_purchase', ' Approval Purchase'), - ('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.po', 'tukar_guling_po_id', string='Product Lines') - tukar_guling_po_id = fields.Many2one('tukar.guling.po', 'Tukar Guling PO') - - return_type = fields.Selection([ - ('tukar_guling', 'Tukar Guling'), - ('revisi_po', 'Revisi PO'), - ('debit_memo', 'Debit Memo'), - ], string='Return Type', required=True) - - @api.constrains('return_type', 'operations') - def _check_required_bu_fields(self): - for record in self: - if record.return_type in ['tukar_guling', 'revisi_po', 'debit_memo'] and not record.operations: - raise ValidationError("BU/Out 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 - - @api.model - def create(self, vals): - if not vals.get('name') or vals['name'] in ('New', False): - vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' - return super(TukarGulingPO, self).create(vals) - def copy(self, default=None): - if default is None: - default = {} - - # Generate sequence satu-satunya di sini - default['name'] = self.env['ir.sequence'].next_by_code('tukar.guling.po') or 'New' - default['state'] = 'draft' - default['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 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() - - if self.state != 'draft': - raise UserError("Submit hanya bisa dilakukan dari Draft.") - self.state = 'approval_purchase' - - def action_approve(self): - self.ensure_one() - - if not self.operations: - raise UserError("BU/Out harus diisi!") - - if not self.return_type: - raise UserError("Return Type harus diisi!") - - if self.state == 'approval_purchase': - if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'): - raise UserError("Hanya Purchasing yang boleh approve tahap ini.") - self.state = 'approval_logistic' - - elif self.state == 'approval_logistic': - if not self.env.user.has_group('indoteknik_custom.group_role_logistic'): - raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.") - self.state = 'approval_finance' - - elif self.state == 'approval_finance': - if not self.env.user.has_group('indoteknik_custom.group_role_fat'): - raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - self.state = 'done' - - 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") - self.state = 'cancel' - class TukarGulingLine(models.Model): _name = 'tukar.guling.line' _description = 'Tukar Guling Line' @@ -520,23 +389,6 @@ class TukarGulingLine(models.Model): self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id -class TukarGulingLinePO(models.Model): - _name = 'tukar.guling.line.po' - _description = 'Tukar Guling Line (PO)' - _order = 'sequence, id' - - tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', required=True, ondelete='cascade') - sequence = fields.Integer('Sequence', default=10, copy=False) - 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.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' diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 63e7b53a..a6d0acaa 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -183,6 +183,4 @@ 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_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,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_all_users,tukar.guling.line.all.users,model_tukar_guling_line,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 |
