From 3c170c77e4913313ca28169172dbad8c8726ad5c Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 15 May 2025 13:19:22 +0700 Subject: (andri) add button sync price to SO & penyesuaian readonly pada orderline PO --- indoteknik_custom/models/purchase_order.py | 228 ++++++++++++++++++++++++ indoteknik_custom/models/purchase_order_line.py | 54 ++++++ indoteknik_custom/views/purchase_order.xml | 20 ++- 3 files changed, 297 insertions(+), 5 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 98b367d0..d5c08660 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -2,6 +2,7 @@ from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta +from odoo.tools import float_compare import logging from pytz import timezone, utc import io @@ -89,6 +90,233 @@ class PurchaseOrder(models.Model): store_name = fields.Char(string='Nama Toko') purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') + + + def sync_price_to_so(self): + updated_lines = [] + skipped_lines = [] # Untuk melacak line yang dilewati karena harga sudah sama + affected_so_ids = set() # Untuk melacak SO mana saja yang terkena dampak + + for order in self: + # Filter hanya line-line yang ditandai + marked_lines = order.order_line.filtered(lambda l: l.mark_po_line) + + if not marked_lines: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Info'), + 'message': _('Tidak ada item yang ditandai untuk disinkronkan'), + 'sticky': False, + 'type': 'info', + } + } + + # Cek apakah ada referensi ke sale order + if not order.sale_order_id and not order.order_sales_match_line: + raise UserError(_("Tidak ada Sales Order yang terkait dengan Purchase Order ini!")) + + # Jika PO dibuat dari SO langsung + if order.sale_order_id: + affected_so_ids.add(order.sale_order_id.id) + for po_line in marked_lines: + # Cari SO line yang terkait + so_line = po_line.so_line_id + if not so_line and po_line.product_id: + # Coba cari berdasarkan product jika tidak ada referensi langsung + so_line = self.env['sale.order.line'].search([ + ('product_id', '=', po_line.product_id.id), + ('order_id', '=', order.sale_order_id.id) + ], limit=1) + + if so_line: + old_price = so_line.purchase_price + + # Cek apakah harga sudah sama + if float_compare(old_price, po_line.price_unit, precision_digits=2) == 0: + skipped_lines.append({ + 'product': po_line.product_id.display_name, + 'so_name': so_line.order_id.name, + 'so_id': so_line.order_id.id, + 'price': old_price, + 'currency': order.currency_id or so_line.order_id.currency_id + }) + else: + so_line.purchase_price = po_line.price_unit + updated_lines.append({ + 'product': po_line.product_id.display_name, + 'so_line_id': so_line.id, + 'so_name': so_line.order_id.name, + 'so_id': so_line.order_id.id, + 'old_price': old_price, + 'new_price': po_line.price_unit, + 'currency': order.currency_id or so_line.order_id.currency_id + }) + + # Hapus tanda setelah sinkronisasi + po_line.mark_po_line = False + + # Jika PO terkait dengan beberapa SO (melalui order_sales_match_line) + elif order.order_sales_match_line: + for po_line in marked_lines: + # Cari match lines yang sesuai dengan product di PO line + match_lines = order.order_sales_match_line.filtered( + lambda m: m.product_id.id == po_line.product_id.id + ) + + for match_line in match_lines: + if match_line.sale_id: + affected_so_ids.add(match_line.sale_id.id) + + so_line = match_line.sale_line_id + if so_line: + old_price = so_line.purchase_price + + # Cek apakah harga sudah sama + if float_compare(old_price, po_line.price_unit, precision_digits=2) == 0: + skipped_lines.append({ + 'product': po_line.product_id.display_name, + 'so_name': so_line.order_id.name, + 'so_id': so_line.order_id.id, + 'price': old_price, + 'currency': order.currency_id or so_line.order_id.currency_id + }) + else: + so_line.purchase_price = po_line.price_unit + updated_lines.append({ + 'product': po_line.product_id.display_name, + 'so_line_id': so_line.id, + 'so_name': so_line.order_id.name, + 'so_id': so_line.order_id.id, + 'old_price': old_price, + 'new_price': po_line.price_unit, + 'currency': order.currency_id or so_line.order_id.currency_id + }) + + # Hapus tanda setelah sinkronisasi + po_line.mark_po_line = False + + # Ambil data SO yang terkena dampak + affected_sales_orders = self.env['sale.order'].browse(list(affected_so_ids)) + + # Buat pesan untuk log + message_body = "" + + # Jika ada line yang diupdate + if updated_lines: + # Kelompokkan perubahan berdasarkan SO + changes_by_so = {} + for line in updated_lines: + so_id = line['so_id'] + if so_id not in changes_by_so: + changes_by_so[so_id] = [] + changes_by_so[so_id].append(line) + + # Buat pesan untuk line yang diupdate + message_body += f"

Harga Purchase pada Sales Order telah diperbarui dari {self.name}:

" + + for so_id, lines in changes_by_so.items(): + so = self.env['sale.order'].browse(so_id) + # Buat link ke SO yang bisa diklik + message_body += f"

Sales Order: {so.name}

" + + # Jika ada line yang dilewati karena harga sudah sama + if skipped_lines: + # Kelompokkan berdasarkan SO + skipped_by_so = {} + for line in skipped_lines: + so_id = line['so_id'] + if so_id not in skipped_by_so: + skipped_by_so[so_id] = [] + skipped_by_so[so_id].append(line) + + # Tambahkan pesan untuk line yang dilewati + if message_body: + message_body += "

Item berikut dilewati karena harga sudah sama:

" + else: + message_body += f"

Tidak ada perubahan harga untuk PO {self.name}:

" + message_body += "

Item berikut sudah memiliki harga yang sama di SO:

" + + for so_id, lines in skipped_by_so.items(): + so = self.env['sale.order'].browse(so_id) + message_body += f"

Sales Order: {so.name}

" + + # Posting log message jika ada isi + if message_body: + subject = "Price Sync to SO" if updated_lines else "Price Sync - Harga Sama" + self.message_post(body=message_body, subject=subject) + + # Update juga log di setiap SO yang terkena dampak + if updated_lines: + for so in affected_sales_orders: + so_lines = [line for line in updated_lines if line['so_id'] == so.id] + if so_lines: + # Buat link ke PO yang bisa diklik + so_message = f"

Harga Purchase diperbarui dari {self.name}:

" + so.message_post(body=so_message, subject=f"Price Updated from PO {self.name}") + + # Recalculate margins + if updated_lines and hasattr(self, 'compute_total_margin'): + self.compute_total_margin() + + # Recalculate margins di SO juga + if updated_lines: + for so in affected_sales_orders: + if hasattr(so, 'compute_total_margin'): + so.compute_total_margin() + + # Tentukan pesan notifikasi dan tipe + if updated_lines and skipped_lines: + message = f"{len(updated_lines)} item diperbarui dan {len(skipped_lines)} item dilewati karena harga sudah sama" + title = "Sukses!" + notification_type = "success" + elif updated_lines: + message = f"{len(updated_lines)} item telah diperbarui harganya di {len(affected_so_ids)} Sales Order" + title = "Sukses!" + notification_type = "success" + elif skipped_lines: + message = f"item tersebut ({len(skipped_lines)}) sudah memiliki harga yang sama" + title = "Info" + notification_type = "info" + else: + message = "Tidak ada line yang berhasil diperbarui" + title = "Info" + notification_type = "info" + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _(title), + 'message': _(message), + 'sticky': False, + 'type': notification_type, + } + } # cek payment term def _check_payment_term(self): _logger.info("Check Payment Term Terpanggil") diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 033469b8..766e304b 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -51,6 +51,8 @@ class PurchaseOrderLine(models.Model): contribution_cost_service = fields.Float(string='Contribution Cost Service', compute='_compute_doc_delivery_amt') ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt') + mark_po_line = fields.Boolean(string=' ', default=False, help='centang jika anda ingin menandai PO line ini') + def _compute_doc_delivery_amt(self): for line in self: # Inisialisasi nilai default untuk field computed @@ -385,3 +387,55 @@ class PurchaseOrderLine(models.Model): line.delivery_amt_line = delivery_amt * contribution else: line.delivery_amt_line = 0 + + # @api.model + # def create(self, vals): + # """ + # Override method create untuk mencegah penambahan line baru + # jika status PO sudah bukan draft + # """ + # # Cek apakah ada order_id di vals + # if vals.get('order_id'): + # order = self.env['purchase.order'].browse(vals['order_id']) + # # Jika status PO bukan draft, tolak pembuatan line baru + # if order.state in ['purchase', 'done', 'cancel']: + # raise UserError(_("Tidak bisa menambahkan item baru karena PO sudah dikonfirmasi atau selesai.")) + + # return super(PurchaseOrderLine, self).create(vals) + + # def write(self, vals): + # """ + # Override method write untuk membatasi field yang bisa diubah + # jika status PO sudah bukan draft + # """ + # for line in self: + # if line.order_id.state in ['purchase', 'done', 'cancel']: + # # Hanya izinkan mengubah mark_po_line + # if 'mark_po_line' in vals and len(vals) == 1: + # # Izinkan perubahan jika hanya field mark_po_line yang diubah + # return super(PurchaseOrderLine, self).write(vals) + # else: + # _logger.info("Fields being updated: %s", vals.keys()) + # raise UserError(_("Tidak bisa mengubah item karena PO sudah dikonfirmasi atau selesai.")) + + # return super(PurchaseOrderLine, self).write(vals) + + # def toggle_mark_po_line(self): + # """ + # Method khusus untuk toggle mark_po_line, aman digunakan dalam semua status PO + # """ + # for line in self: + # line.mark_po_line = not line.mark_po_line + # return True + + # def unlink(self): + # """ + # Override method unlink untuk mencegah penghapusan line + # jika status PO sudah bukan draft + # """ + # for line in self: + # if line.order_id.state in ['purchase', 'done', 'cancel']: + # raise UserError(_("Tidak bisa menghapus item karena PO sudah dikonfirmasi atau selesai.")) + + # return super(PurchaseOrderLine, self).unlink() + diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index b58139c6..bd842a76 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -36,7 +36,9 @@ + @@ -140,19 +142,27 @@ - {'readonly': ['|', ('state', 'in', ['purchase', 'done', 'cancel']), ('has_active_invoice', '=', True)]} + {'readonly': ['|', ('state', 'in', ['done', 'cancel']), ('has_active_invoice', '=', True)]} + + + + + + + {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])]} + - {'readonly': [], 'required': True} + {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])], 'required': True} - {'readonly': []} + {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])]} - {'required': True} + {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])],'required': True} -- cgit v1.2.3