from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError from datetime import datetime, timedelta, timezone, time import logging, random, string, requests, math, json, re, qrcode, base64 _logger = logging.getLogger(__name__) class PartialDeliveryWizard(models.TransientModel): _name = 'partial.delivery.wizard' _description = 'Partial Delivery Wizard' sale_id = fields.Many2one('sale.order') picking_ids = fields.Many2many('stock.picking') picking_id = fields.Many2one( 'stock.picking', string='Delivery Order', domain="[('id','in',picking_ids), ('state', 'not in', ('done', 'cancel')), ('name', 'like', 'BU/PICK/%')]" ) line_ids = fields.One2many('partial.delivery.wizard.line', 'wizard_id') # @api.model # def default_get(self, fields_list): # res = super().default_get(fields_list) # picking_ids_ctx = self.env.context.get('default_picking_ids') # lines = [] # if picking_ids_ctx: # if isinstance(picking_ids_ctx, list) and picking_ids_ctx and isinstance(picking_ids_ctx[0], tuple): # picking_ids = picking_ids_ctx[0][2] # else: # picking_ids = picking_ids_ctx # pickings = self.env['stock.picking'].browse(picking_ids) # moves = pickings.move_ids_without_package.filtered(lambda m: m.reserved_availability > 0) # for move in moves: # lines.append((0, 0, { # 'product_id': move.product_id.id, # 'reserved_qty': move.reserved_availability, # 'move_id': move.id, # })) # res['line_ids'] = lines # return res def action_select_all(self): if len(self.line_ids) == 0: raise UserError(_("Tidak ada produk yang dipilih.")) for line in self.line_ids: line.selected = True return { 'type': 'ir.actions.act_window', 'res_model': self._name, 'view_mode': 'form', 'res_id': self.id, 'target': 'new', } def action_unselect_all(self): if len(self.line_ids) == 0: raise UserError(_("Tidak ada produk yang dipilih.")) for line in self.line_ids: line.selected = False return { 'type': 'ir.actions.act_window', 'res_model': self._name, 'view_mode': 'form', 'res_id': self.id, 'target': 'new', } @api.onchange('picking_id') def _onchange_picking_id(self): """Generate lines whenever picking_id is changed""" if not self.picking_id: self.line_ids = [(5, 0, 0)] return if self.line_ids: self.line_ids.unlink() moves = self.picking_id.move_lines or self.picking_id.move_ids_without_package moves = moves.filtered(lambda m: m.product_id and m.reserved_availability > 0) if not moves: _logger.warning(f"[PartialDeliveryWizard] Tidak ada move line di picking {self.picking_id.name}") return for move in moves: reserved_qty = move.reserved_availability or 0.0 ordered_qty = move.sale_line_id.product_uom_qty if move.sale_line_id else 0.0 self.env['partial.delivery.wizard.line'].create({ 'wizard_id': self.id, 'product_id': move.product_id.id, 'reserved_qty': reserved_qty, # 'selected_qty': reserved_qty, 'move_id': move.id, 'sale_line_id': move.sale_line_id.id if move.sale_line_id else False, }) _logger.info( f"[PartialDeliveryWizard] ✅ Created line for {move.product_id.display_name} " f"(reserved={reserved_qty}, move_id={move.id})" ) def action_confirm_partial_delivery(self): self.ensure_one() StockPicking = self.env['stock.picking'] picking = self.picking_id if not picking: raise UserError(_("Tidak ada picking yang dipilih.")) if picking.state != "assigned": raise UserError(_("Picking harus dalam status Ready (assigned).")) lines_by_qty = self.line_ids.filtered(lambda l: l.selected_qty > 0) lines_validation = self.line_ids.filtered(lambda l: l.selected_qty > l.reserved_qty) lines_by_selected = self.line_ids.filtered(lambda l: l.selected and not l.selected_qty) selected_lines = lines_by_qty | lines_by_selected # gabung dua domain hasil filter if lines_validation: raise UserError(_("Jumlah yang dipilih melebihi jumlah yang terdapat di DO.")) if not selected_lines: raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya.")) # 🧠 Cek apakah semua move di DO sudah muncul di wizard dan semua dipilih picking_move_ids = picking.move_ids_without_package.ids wizard_move_ids = self.line_ids.mapped('move_id').ids # Semua move DO muncul di wizard, dan semua baris dipilih full_selected = ( set(picking_move_ids) == set(wizard_move_ids) and len(selected_lines) == len(self.line_ids) and all( (line.selected_qty or line.reserved_qty) >= line.reserved_qty for line in selected_lines ) ) if full_selected: # 💡 Gak perlu bikin picking baru, langsung ubah state_reserve picking.write({'state_reserve': 'partial'}) picking.message_post( body=f"Full Picking Confirmed via wizard partial delivery oleh {self.env.user.name} (tanpa DO baru)", message_type="comment", subtype_xmlid="mail.mt_note", ) return { "type": "ir.actions.act_window", "res_model": "stock.picking", "view_mode": "form", "res_id": picking.id, "target": "current", "effect": { "fadeout": "slow", "message": f"✅ Semua produk dari DO ini dikirim penuh — tidak dibuat DO baru.", "type": "rainbow_man", }, } # 🧩 Kalau bukan full selected, lanjut bikin DO baru new_picking = StockPicking.create({ 'origin': picking.origin, 'partner_id': picking.partner_id.id, 'picking_type_id': picking.picking_type_id.id, 'location_id': picking.location_id.id, 'location_dest_id': picking.location_dest_id.id, 'company_id': picking.company_id.id, 'state_reserve': 'partial', }) for line in selected_lines: if line.selected_qty > line.reserved_qty: raise UserError(_("Jumlah produk %s yang dipilih melebihi jumlah reserved.") % line.product_id.display_name) move = line.move_id move._do_unreserve() # 🔹 Kalau cuma selected tanpa qty → anggap kirim semua reserved qty if line.selected and not line.selected_qty: line.selected_qty = line.reserved_qty if line.selected_qty > 0: if line.selected_qty > move.product_uom_qty: raise UserError(_( f"Qty kirim ({line.selected_qty}) untuk {move.product_id.display_name} melebihi qty move ({move.product_uom_qty})." )) if line.selected_qty < move.product_uom_qty: qty_to_keep = move.product_uom_qty - line.selected_qty new_move = move.copy(default={ 'product_uom_qty': line.selected_qty, 'picking_id': new_picking.id, 'partial': True, }) if move.move_dest_ids: for dest_move in move.move_dest_ids: # dest_move.write({'move_orig_ids': [(4, new_move.id)]}) new_move.write({'move_dest_ids': [(4, dest_move.id)]}) move.write({'product_uom_qty': qty_to_keep}) else: move.write({'picking_id': new_picking.id, 'partial': True}) new_picking.action_confirm() new_picking.action_assign() picking.action_assign() origin_name = picking.name existing_siblings = self.env['stock.picking'].search([ ('name', 'like', f"{origin_name}/%"), ('id', '!=', new_picking.id), ]) suffixes = [] for p in existing_siblings: match = re.search(r'/(\d+)$', p.name) if match: suffixes.append(int(match.group(1))) next_suffix = max(suffixes) + 1 if suffixes else 1 new_picking.name = f"{origin_name}/{next_suffix}" if picking.origin: sale_order = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1) if sale_order: sale_order.message_post( body=f"Partial Delivery Created: {new_picking.name} " f"oleh {self.env.user.name}", message_type="comment", subtype_xmlid="mail.mt_note", ) new_picking.message_post( body=f"Partial Picking created dari {picking.name} oleh {self.env.user.name}", message_type="comment", subtype_xmlid="mail.mt_note", ) return { "type": "ir.actions.act_window", "res_model": "stock.picking", "view_mode": "form", "res_id": new_picking.id, "target": "current", "effect": { "fadeout": "slow", "message": f"🚚 Partial Delivery {new_picking.name} berhasil dibuat!", "type": "rainbow_man", }, } class PartialDeliveryWizardLine(models.TransientModel): _name = 'partial.delivery.wizard.line' _description = 'Partial Delivery Wizard Line' wizard_id = fields.Many2one('partial.delivery.wizard') product_id = fields.Many2one('product.product', string="Product") reserved_qty = fields.Float(string="Reserved Qty") selected_qty = fields.Float(string="Send Qty") move_id = fields.Many2one('stock.move') selected = fields.Boolean(string="Select") sale_line_id = fields.Many2one('sale.order.line', string="SO Line", related='move_id.sale_line_id') ordered_qty = fields.Float(related='sale_line_id.product_uom_qty', string="Ordered Qty") @api.onchange('selected') def onchange_selected(self): if self.selected: self.selected_qty = self.reserved_qty