From c5642f4f6c4f0969475d863bee7243a83b9290dc Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 8 Oct 2025 14:55:04 +0700 Subject: partial --- indoteknik_custom/models/partial_delivery.py | 189 +++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 indoteknik_custom/models/partial_delivery.py (limited to 'indoteknik_custom/models/partial_delivery.py') diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py new file mode 100644 index 00000000..c9d2ba5c --- /dev/null +++ b/indoteknik_custom/models/partial_delivery.py @@ -0,0 +1,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"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") + -- cgit v1.2.3 From a9aff3725c86ae6e864e8b5e2b45596ef7dff6e0 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 8 Oct 2025 17:15:05 +0700 Subject: fix bug --- indoteknik_custom/models/partial_delivery.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models/partial_delivery.py') diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py index c9d2ba5c..c9b188ea 100644 --- a/indoteknik_custom/models/partial_delivery.py +++ b/indoteknik_custom/models/partial_delivery.py @@ -74,9 +74,6 @@ class PartialDeliveryWizard(models.TransientModel): 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, @@ -89,6 +86,8 @@ class PartialDeliveryWizard(models.TransientModel): }) 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() -- cgit v1.2.3 From fd3ce46f21aa78d9b9caa6ffdd1b9f61d89dfa65 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 10 Oct 2025 10:42:39 +0700 Subject: select all and unselect all --- indoteknik_custom/models/partial_delivery.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'indoteknik_custom/models/partial_delivery.py') diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py index c9b188ea..1204089b 100644 --- a/indoteknik_custom/models/partial_delivery.py +++ b/indoteknik_custom/models/partial_delivery.py @@ -41,6 +41,32 @@ class PartialDeliveryWizard(models.TransientModel): # res['line_ids'] = lines # return res + def action_select_all(self): + for line in self.line_ids: + line.selected = True + # return action supaya wizard gak nutup + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'view_mode': 'form', + 'res_id': self.id, + 'target': 'new', + } + + def action_unselect_all(self): + for line in self.line_ids: + line.selected = False + # juga reload biar tetap di wizard + 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""" -- cgit v1.2.3 From 23a2d929b209c6121d3bf4e3b35e6bfec4a99e8d Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 13 Oct 2025 11:09:37 +0700 Subject: fix bug --- indoteknik_custom/models/partial_delivery.py | 85 +++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models/partial_delivery.py') diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py index 1204089b..83fe9981 100644 --- a/indoteknik_custom/models/partial_delivery.py +++ b/indoteknik_custom/models/partial_delivery.py @@ -42,6 +42,8 @@ class PartialDeliveryWizard(models.TransientModel): # 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 action supaya wizard gak nutup @@ -54,6 +56,8 @@ class PartialDeliveryWizard(models.TransientModel): } 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 # juga reload biar tetap di wizard @@ -65,21 +69,41 @@ class PartialDeliveryWizard(models.TransientModel): 'target': 'new', } - - @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 + if not self.picking_id: + self.line_ids = [(5, 0, 0)] + return + + # ๐Ÿงน hapus line lama dulu + 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, # biar langsung keisi default + '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): @@ -92,7 +116,6 @@ class PartialDeliveryWizard(models.TransientModel): 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) @@ -101,6 +124,37 @@ class PartialDeliveryWizard(models.TransientModel): if not selected_lines: raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya.")) + # ๐Ÿง  Tambahan: kalau semua line dipilih (full delivery) + all_selected = len(selected_lines) == len(self.line_ids) + full_selected = all_selected 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'}) + + # Tambahin log aja biar ada jejak + picking.message_post( + body=f"Full Picking Confirmed dari wizard partial delivery 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": picking.id, + "target": "current", + "effect": { + "fadeout": "slow", + "message": f"โœ… Semua produk dikirim penuh โ€” tidak dibuat DO baru.", + "type": "rainbow_man", + }, + } + new_picking = StockPicking.create({ 'origin': picking.origin, 'partner_id': picking.partner_id.id, @@ -212,3 +266,8 @@ class PartialDeliveryWizardLine(models.TransientModel): 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 + -- cgit v1.2.3 From 724d7ed6d85ecc3acadbcf56a98aead0512af01d Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 14 Oct 2025 14:11:18 +0700 Subject: push --- indoteknik_custom/models/partial_delivery.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) (limited to 'indoteknik_custom/models/partial_delivery.py') diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py index 83fe9981..977cceed 100644 --- a/indoteknik_custom/models/partial_delivery.py +++ b/indoteknik_custom/models/partial_delivery.py @@ -46,7 +46,6 @@ class PartialDeliveryWizard(models.TransientModel): raise UserError(_("Tidak ada produk yang dipilih.")) for line in self.line_ids: line.selected = True - # return action supaya wizard gak nutup return { 'type': 'ir.actions.act_window', 'res_model': self._name, @@ -60,7 +59,6 @@ class PartialDeliveryWizard(models.TransientModel): raise UserError(_("Tidak ada produk yang dipilih.")) for line in self.line_ids: line.selected = False - # juga reload biar tetap di wizard return { 'type': 'ir.actions.act_window', 'res_model': self._name, @@ -76,7 +74,6 @@ class PartialDeliveryWizard(models.TransientModel): self.line_ids = [(5, 0, 0)] return - # ๐Ÿงน hapus line lama dulu if self.line_ids: self.line_ids.unlink() @@ -95,7 +92,7 @@ class PartialDeliveryWizard(models.TransientModel): 'wizard_id': self.id, 'product_id': move.product_id.id, 'reserved_qty': reserved_qty, - 'selected_qty': reserved_qty, # biar langsung keisi default + # 'selected_qty': reserved_qty, 'move_id': move.id, 'sale_line_id': move.sale_line_id.id if move.sale_line_id else False, }) @@ -119,12 +116,11 @@ class PartialDeliveryWizard(models.TransientModel): 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 + selected_lines = lines_by_qty | lines_by_selected if not selected_lines: raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya.")) - # ๐Ÿง  Tambahan: kalau semua line dipilih (full delivery) all_selected = len(selected_lines) == len(self.line_ids) full_selected = all_selected and all( (line.selected_qty or line.reserved_qty) >= line.reserved_qty @@ -132,10 +128,8 @@ class PartialDeliveryWizard(models.TransientModel): ) if full_selected: - # ๐Ÿ’ก Gak perlu bikin picking baru, langsung ubah state_reserve picking.write({'state_reserve': 'partial'}) - # Tambahin log aja biar ada jejak picking.message_post( body=f"Full Picking Confirmed dari wizard partial delivery oleh {self.env.user.name}", message_type="comment", @@ -171,11 +165,9 @@ class PartialDeliveryWizard(models.TransientModel): 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(_( @@ -184,7 +176,6 @@ class PartialDeliveryWizard(models.TransientModel): 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, @@ -192,19 +183,13 @@ class PartialDeliveryWizard(models.TransientModel): }) 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'), @@ -220,7 +205,6 @@ class PartialDeliveryWizard(models.TransientModel): 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: @@ -231,7 +215,6 @@ class PartialDeliveryWizard(models.TransientModel): 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", -- cgit v1.2.3 From b41c2d160e5e114bf805baed573d791fbac3feac Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Tue, 14 Oct 2025 14:12:23 +0700 Subject: push --- indoteknik_custom/models/partial_delivery.py | 37 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) (limited to 'indoteknik_custom/models/partial_delivery.py') diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py index 977cceed..4df7da1e 100644 --- a/indoteknik_custom/models/partial_delivery.py +++ b/indoteknik_custom/models/partial_delivery.py @@ -116,22 +116,31 @@ class PartialDeliveryWizard(models.TransientModel): 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 + 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.")) - all_selected = len(selected_lines) == len(self.line_ids) - full_selected = all_selected and all( - (line.selected_qty or line.reserved_qty) >= line.reserved_qty - for line in selected_lines + # ๐Ÿง  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 dari wizard partial delivery oleh {self.env.user.name}", + 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", ) @@ -144,11 +153,12 @@ class PartialDeliveryWizard(models.TransientModel): "target": "current", "effect": { "fadeout": "slow", - "message": f"โœ… Semua produk dikirim penuh โ€” tidak dibuat DO baru.", + "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, @@ -187,7 +197,6 @@ class PartialDeliveryWizard(models.TransientModel): new_picking.action_confirm() new_picking.action_assign() - picking.action_assign() existing_partials = self.env['stock.picking'].search([ @@ -196,14 +205,8 @@ class PartialDeliveryWizard(models.TransientModel): ('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 + 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) @@ -234,8 +237,6 @@ class PartialDeliveryWizard(models.TransientModel): }, } - - class PartialDeliveryWizardLine(models.TransientModel): _name = 'partial.delivery.wizard.line' _description = 'Partial Delivery Wizard Line' -- cgit v1.2.3