1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
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"<b>Partial Delivery Created:</b> <a href=# data-oe-model='stock.picking' data-oe-id='{new_picking.id}'>{new_picking.name}</a> "
f"oleh {self.env.user.name}",
message_type="comment",
subtype_xmlid="mail.mt_note",
)
# --- 📝 Log di DO baru ---
new_picking.message_post(
body=f"<b>Partial Picking created</b> 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")
|