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 @api.onchange('picking_id') def _onchange_picking_id(self): """Generate lines whenever picking_id is changed""" lines = [] if self.picking_id: moves = self.picking_id.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, })) self.line_ids = lines 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_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 not selected_lines: raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya.")) if selected_lines.selected_qty > selected_lines.reserved_qty: raise UserError(_("Jumlah produk yang dipilih melebihi jumlah reserved.")) 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: move = line.move_id move._do_unreserve() # kalau cuma selected tanpa isi qty, otomatis set selected_qty = reserved_qty if line.selected and not line.selected_qty: line.selected_qty = line.reserved_qty # MODE 1 → Prioritas kalau ada selected_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 # split move new_move = move.copy(default={ 'product_uom_qty': line.selected_qty, 'picking_id': new_picking.id, 'partial': True, }) move.write({'product_uom_qty': qty_to_keep}) else: # full pindah move.write({'picking_id': new_picking.id, 'partial': True}) # Confirm & assign DO baru new_picking.action_confirm() new_picking.action_assign() # Reassign DO lama biar sisa qty ke-update picking.action_assign() # --- 🔢 Rename picking baru dengan format "/(Nomor urut)" --- existing_partials = self.env['stock.picking'].search([ ('origin', '=', picking.origin), ('state_reserve', '=', 'partial'), ('id', '!=', new_picking.id), ], order='name asc') suffix_number = len(existing_partials) if suffix_number == 0: suffix_number = 1 else: suffix_number += 1 new_name = f"{picking.name}/{suffix_number}" new_picking.name = new_name # --- 💬 Post message ke SO --- 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", ) # --- 📝 Log di DO baru --- 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")