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()
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) + 1
new_picking.name = f"{picking.name}/{suffix_number}"
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