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 From 351934c0b2107782557998feb68c71cc5ba024e0 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 15 May 2025 13:21:53 +0700 Subject: (andri) hapus method yang tidak diperlukan/comment --- indoteknik_custom/models/purchase_order_line.py | 49 ------------------------- 1 file changed, 49 deletions(-) diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 766e304b..4802ebea 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -388,54 +388,5 @@ class PurchaseOrderLine(models.Model): 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() -- cgit v1.2.3 From d75cb8d198eeca1296aa467b0d8e3fd9db9c571f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 15 May 2025 15:09:09 +0700 Subject: Add Sales Discount in XML Faktur Pajak --- indoteknik_custom/models/coretax_fatur.py | 82 +++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index b4bffbd2..ffc2813f 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -4,19 +4,20 @@ from xml.dom import minidom import base64 import re + class CoretaxFaktur(models.Model): _name = 'coretax.faktur' _description = 'Export Faktur ke XML' - - export_file = fields.Binary(string="Export File", ) - export_filename = fields.Char(string="Export File", ) - - def validate_and_format_number(slef, input_number): + + export_file = fields.Binary(string="Export File", ) + export_filename = fields.Char(string="Export File", ) + + def validate_and_format_number(self, input_number): # Hapus semua karakter non-digit cleaned_number = re.sub(r'\D', '', input_number) - + total_sum = sum(int(char) for char in cleaned_number) - if total_sum == 0 : + if total_sum == 0: return '0000000000000000' # Hitung jumlah digit @@ -39,22 +40,16 @@ class CoretaxFaktur(models.Model): # Tambahkan elemen ListOfTaxInvoice list_of_tax_invoice = ET.SubElement(root, 'ListOfTaxInvoice') - # Dapatkan data faktur - # inv_obj = self.env['account.move'] - # invoices = inv_obj.search([('is_efaktur_exported','=',True), - # ('state','=','posted'), - # ('efaktur_id','!=', False), - # ('move_type','=','out_invoice')], limit = 5) - for invoice in invoices: tax_invoice = ET.SubElement(list_of_tax_invoice, 'TaxInvoice') buyerTIN = self.validate_and_format_number(invoice.partner_id.npwp) nitku = invoice.partner_id.nitku formula = nitku if nitku else buyerTIN.ljust(len(buyerTIN) + 6, '0') - buyerIDTKU = formula if sum(int(char) for char in buyerTIN) > 0 else '000000' + buyerIDTKU = formula if sum(int(char) for char in buyerTIN) > 0 else '000000' # Tambahkan elemen faktur - ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime('%Y-%m-%d') if invoice.invoice_date else '' + ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime( + '%Y-%m-%d') if invoice.invoice_date else '' ET.SubElement(tax_invoice, 'TaxInvoiceOpt').text = 'Normal' ET.SubElement(tax_invoice, 'TrxCode').text = '04' ET.SubElement(tax_invoice, 'AddInfo') @@ -64,30 +59,67 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'FacilityStamp') ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000' ET.SubElement(tax_invoice, 'BuyerTin').text = buyerTIN - ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum(int(char) for char in buyerTIN) > 0 else 'Other ID' + ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum( + int(char) for char in buyerTIN) > 0 else 'Other ID' ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IDN' - ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum(int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id) + ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum( + int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id) ET.SubElement(tax_invoice, 'BuyerName').text = invoice.partner_id.nama_wajib_pajak or '' ET.SubElement(tax_invoice, 'BuyerAdress').text = invoice.partner_id.alamat_lengkap_text or '' ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU + # Find all product lines (exclude discounts and other adjustments) + product_lines = invoice.invoice_line_ids.filtered( + lambda l: not l.display_type and l.product_id and 'Diskon' not in l.account_id.name + ) + + # Find all discount lines + discount_lines = invoice.invoice_line_ids.filtered( + lambda l: not l.display_type and ('Diskon' in l.account_id.name or l.name and 'Diskon' in l.name) + ) + + # Calculate total product amount (before discount) + total_product_amount = sum(line.price_subtotal for line in product_lines) + if total_product_amount == 0: + total_product_amount = 1 # Avoid division by zero + + # Calculate total discount amount + total_discount_amount = abs(sum(line.price_subtotal for line in discount_lines)) + # Tambahkan elemen ListOfGoodService list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService') - for line in invoice.invoice_line_ids: - otherTaxBase = round(line.price_subtotal * (11/12)) if line.price_subtotal else 0 + + for line in product_lines: + # Calculate prorated discount + line_proportion = line.price_subtotal / total_product_amount + line_discount = total_discount_amount * line_proportion + + # unit_price = line.price_unit + subtotal = line.price_subtotal + quantity = line.quantity + total_discount = round(line_discount, 2) + + # Calculate tax base after discount + price_after_discount = line.price_subtotal - line_discount + + # Calculate other tax values + otherTaxBase = round(price_after_discount * (11 / 12), 2) if price_after_discount else 0 + vat_amount = round(otherTaxBase * 0.12, 2) + + # Create the line in XML good_service = ET.SubElement(list_of_good_service, 'GoodService') ET.SubElement(good_service, 'Opt').text = 'A' ET.SubElement(good_service, 'Code').text = '000000' ET.SubElement(good_service, 'Name').text = line.name ET.SubElement(good_service, 'Unit').text = 'UM.0018' - ET.SubElement(good_service, 'Price').text = str(round(line.price_subtotal/line.quantity, 2)) if line.price_subtotal else '0' - ET.SubElement(good_service, 'Qty').text = str(line.quantity) - ET.SubElement(good_service, 'TotalDiscount').text = '0' - ET.SubElement(good_service, 'TaxBase').text = str(round(line.price_subtotal)) if line.price_subtotal else '0' + ET.SubElement(good_service, 'Price').text = str(round(subtotal / quantity, 2)) if subtotal else '0' + ET.SubElement(good_service, 'Qty').text = str(quantity) + ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount) + ET.SubElement(good_service, 'TaxBase').text = str(round(price_after_discount)) ET.SubElement(good_service, 'OtherTaxBase').text = str(otherTaxBase) ET.SubElement(good_service, 'VATRate').text = '12' - ET.SubElement(good_service, 'VAT').text = str(round(otherTaxBase * 0.12, 2)) + ET.SubElement(good_service, 'VAT').text = str(vat_amount) ET.SubElement(good_service, 'STLGRate').text = '0' ET.SubElement(good_service, 'STLG').text = '0' -- cgit v1.2.3 From caa3b18640a1cffb80677701da5598f42a795a7d Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 15 May 2025 15:30:11 +0700 Subject: (andri) revisi --- indoteknik_custom/views/purchase_order.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index bd842a76..e1ff099b 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -38,7 +38,7 @@ @@ -146,7 +146,7 @@ - + -- cgit v1.2.3 From 3ecfd3cbf9e3257644c388801f18870960ef3ac0 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 15 May 2025 21:58:34 +0700 Subject: prevent export error when state is in draft and add filter based on account_id. --- indoteknik_custom/models/coretax_fatur.py | 47 +++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index ffc2813f..c0558e5b 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -12,11 +12,20 @@ class CoretaxFaktur(models.Model): export_file = fields.Binary(string="Export File", ) export_filename = fields.Char(string="Export File", ) - def validate_and_format_number(self, input_number): + def validate_and_format_number(slef, input_number): + # mencegah error ketika mau cetak xml ketika masih di draft + if input_number is None: + return '0000000000000000' + + # ubah ke str kalau blm + if not isinstance(input_number, str): + input_number = str(input_number) + # Hapus semua karakter non-digit cleaned_number = re.sub(r'\D', '', input_number) total_sum = sum(int(char) for char in cleaned_number) + if total_sum == 0: return '0000000000000000' @@ -69,18 +78,36 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU - # Find all product lines (exclude discounts and other adjustments) + # initiate diskon id + # ACCOUNT_DISCOUNT_IDS = [463, 464, 467] + # product_lines = invoice.invoice_line_ids.filtered( + # lambda l: not l.display_type and l.product_id and + # hasattr(l, 'account_id') and l.account_id and + # l.account_id.id not in self.ACCOUNT_DISCOUNT_IDS and + # l.quantity != -1 + # ) + # discount_lines = invoice.invoice_line_ids.filtered( + # lambda l: not l.display_type and ( + # (hasattr(l, 'account_id') and l.account_id and + # l.account_id.id in self.ACCOUNT_DISCOUNT_IDS) or + # (l.quantity == -1) + # ) + # ) + # discount_id = self.env['account.account'].search([('name', '=', 'Diskon')]) + + # cari product dari inovoice line product_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and l.product_id and 'Diskon' not in l.account_id.name + lambda l: not l.display_type and l.product_id and 'Diskon' not in l.account_id.id ) - # Find all discount lines + # cari diskon/potongan discount_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and ('Diskon' in l.account_id.name or l.name and 'Diskon' in l.name) - ) + lambda l: not l.display_type and ('Diskon' in l.account_id.id or l.name and 'Diskon' in l.name) + ) # ini ke account.id # Calculate total product amount (before discount) total_product_amount = sum(line.price_subtotal for line in product_lines) + if total_product_amount == 0: total_product_amount = 1 # Avoid division by zero @@ -100,11 +127,8 @@ class CoretaxFaktur(models.Model): quantity = line.quantity total_discount = round(line_discount, 2) - # Calculate tax base after discount - price_after_discount = line.price_subtotal - line_discount - # Calculate other tax values - otherTaxBase = round(price_after_discount * (11 / 12), 2) if price_after_discount else 0 + otherTaxBase = round(subtotal * (11 / 12), 2) if subtotal else 0 vat_amount = round(otherTaxBase * 0.12, 2) # Create the line in XML @@ -116,7 +140,8 @@ class CoretaxFaktur(models.Model): ET.SubElement(good_service, 'Price').text = str(round(subtotal / quantity, 2)) if subtotal else '0' ET.SubElement(good_service, 'Qty').text = str(quantity) ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount) - ET.SubElement(good_service, 'TaxBase').text = str(round(price_after_discount)) + ET.SubElement(good_service, 'TaxBase').text = str( + round(subtotal)) if subtotal else '0' ET.SubElement(good_service, 'OtherTaxBase').text = str(otherTaxBase) ET.SubElement(good_service, 'VATRate').text = '12' ET.SubElement(good_service, 'VAT').text = str(vat_amount) -- cgit v1.2.3 From a6a26665953d36578c62b6f4d5608b716d9fac88 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 15 May 2025 23:42:20 +0700 Subject: fix not iterable account_id --- indoteknik_custom/models/coretax_fatur.py | 40 ++++++++----------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index c0558e5b..f2a1f793 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -13,14 +13,6 @@ class CoretaxFaktur(models.Model): export_filename = fields.Char(string="Export File", ) def validate_and_format_number(slef, input_number): - # mencegah error ketika mau cetak xml ketika masih di draft - if input_number is None: - return '0000000000000000' - - # ubah ke str kalau blm - if not isinstance(input_number, str): - input_number = str(input_number) - # Hapus semua karakter non-digit cleaned_number = re.sub(r'\D', '', input_number) @@ -78,36 +70,24 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU - # initiate diskon id - # ACCOUNT_DISCOUNT_IDS = [463, 464, 467] - # product_lines = invoice.invoice_line_ids.filtered( - # lambda l: not l.display_type and l.product_id and - # hasattr(l, 'account_id') and l.account_id and - # l.account_id.id not in self.ACCOUNT_DISCOUNT_IDS and - # l.quantity != -1 - # ) - # discount_lines = invoice.invoice_line_ids.filtered( - # lambda l: not l.display_type and ( - # (hasattr(l, 'account_id') and l.account_id and - # l.account_id.id in self.ACCOUNT_DISCOUNT_IDS) or - # (l.quantity == -1) - # ) - # ) - # discount_id = self.env['account.account'].search([('name', '=', 'Diskon')]) - - # cari product dari inovoice line + # cari product product_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and l.product_id and 'Diskon' not in l.account_id.id + lambda l: not l.display_type and l.product_id and + hasattr(l, 'account_id') and l.account_id and + 'Diskon' not in (l.account_id.name or '') ) # cari diskon/potongan discount_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and ('Diskon' in l.account_id.id or l.name and 'Diskon' in l.name) - ) # ini ke account.id + lambda l: not l.display_type and ( + (hasattr(l, 'account_id') and l.account_id and 'Diskon' in (l.account_id.name or '')) or + (l.name and isinstance(l.name, str) and 'Diskon' in l.name) or + (l.quantity == -1) + ) + ) # Calculate total product amount (before discount) total_product_amount = sum(line.price_subtotal for line in product_lines) - if total_product_amount == 0: total_product_amount = 1 # Avoid division by zero -- cgit v1.2.3 From 24c52892c3143dc2474e5a2ba3c99d08ee1f7d29 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 16 May 2025 08:55:53 +0700 Subject: (andri) add field date hold/unhold SO --- indoteknik_custom/models/sale_order.py | 11 ++++++++++- indoteknik_custom/views/sale_order.xml | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 0d4fc6c3..0ae61914 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -4,6 +4,7 @@ from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError from datetime import datetime, timedelta import logging, random, string, requests, math, json, re, qrcode, base64 +import pytz from io import BytesIO from collections import defaultdict @@ -302,6 +303,12 @@ class SaleOrder(models.Model): ('hold', 'Hold'), ('approve', 'Approve') ], tracking=True, string='State Cancel', copy=False) + hold_outgoing_datetime = fields.Datetime( + string='Date Hold/Unhold', + tracking=True, + readonly=True, + help='Waktu terakhir ketika status Hold Outgoing SO berubah' + ) def _compute_total_margin_excl_third_party(self): for order in self: @@ -340,11 +347,13 @@ class SaleOrder(models.Model): } } - def hold_unhold_qty_outgoing_so(self): + def hold_unhold_qty_outgoing_so(self): if self.hold_outgoing == True: self.hold_outgoing = False else: self.hold_outgoing = True + + self.hold_outgoing_datetime = fields.Datetime.now() def _validate_uniform_taxes(self): for order in self: diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index e0085eeb..e57aeabb 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -94,6 +94,7 @@ + @@ -402,6 +403,7 @@ + -- cgit v1.2.3 From f8811bdf897fe0176921cc01b0ee8c9b98c883d3 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 16 May 2025 09:35:23 +0700 Subject: filter using account_id --- indoteknik_custom/models/coretax_fatur.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index f2a1f793..92ff1a72 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -12,6 +12,8 @@ class CoretaxFaktur(models.Model): export_file = fields.Binary(string="Export File", ) export_filename = fields.Char(string="Export File", ) + DISCOUNT_ACCOUNT_ID = 463 + def validate_and_format_number(slef, input_number): # Hapus semua karakter non-digit cleaned_number = re.sub(r'\D', '', input_number) @@ -70,18 +72,19 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU - # cari product + # Filter product product_lines = invoice.invoice_line_ids.filtered( - lambda l: not l.display_type and l.product_id and - hasattr(l, 'account_id') and l.account_id and - 'Diskon' not in (l.account_id.name or '') + lambda l: not l.display_type and hasattr(l, 'account_id') and + l.account_id and l.product_id and + l.account_id.id != self.DISCOUNT_ACCOUNT_ID and + l.quantity != -1 ) - # cari diskon/potongan + # Filter discount discount_lines = invoice.invoice_line_ids.filtered( lambda l: not l.display_type and ( - (hasattr(l, 'account_id') and l.account_id and 'Diskon' in (l.account_id.name or '')) or - (l.name and isinstance(l.name, str) and 'Diskon' in l.name) or + (hasattr(l, 'account_id') and l.account_id and + l.account_id.id == self.DISCOUNT_ACCOUNT_ID) or (l.quantity == -1) ) ) -- cgit v1.2.3 From 76d3d70b10a46e4143d8a2e5e6952341d661fdf2 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 16 May 2025 11:07:18 +0700 Subject: (andri) rev field datetime hold/unhold menjadi 2 field berbeda --- indoteknik_custom/models/sale_order.py | 13 ++++++------- indoteknik_custom/views/sale_order.xml | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 0ae61914..17115908 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -303,11 +303,9 @@ class SaleOrder(models.Model): ('hold', 'Hold'), ('approve', 'Approve') ], tracking=True, string='State Cancel', copy=False) - hold_outgoing_datetime = fields.Datetime( - string='Date Hold/Unhold', - tracking=True, - readonly=True, - help='Waktu terakhir ketika status Hold Outgoing SO berubah' + date_hold = fields.Datetime(string='Date Hold', tracking=True, readonly=True, help='Waktu ketika SO di Hold' + ) + date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold' ) def _compute_total_margin_excl_third_party(self): @@ -350,10 +348,11 @@ class SaleOrder(models.Model): def hold_unhold_qty_outgoing_so(self): if self.hold_outgoing == True: self.hold_outgoing = False + self.date_unhold = fields.Datetime.now() else: self.hold_outgoing = True - - self.hold_outgoing_datetime = fields.Datetime.now() + self.date_hold = fields.Datetime.now() + def _validate_uniform_taxes(self): for order in self: diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index e57aeabb..3418deaf 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -94,7 +94,8 @@ - + + @@ -403,7 +404,6 @@ - -- cgit v1.2.3 From c61cb9fcd3d01b4ffafaa6446f9fd68b09a88ff7 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 16 May 2025 11:17:25 +0700 Subject: (andri) hapus perubahan terkait sync price to SO pada branch ini --- indoteknik_custom/models/purchase_order.py | 228 ------------------------ indoteknik_custom/models/purchase_order_line.py | 2 - indoteknik_custom/views/purchase_order.xml | 19 +- 3 files changed, 4 insertions(+), 245 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index d5c08660..98b367d0 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -2,7 +2,6 @@ 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 @@ -90,233 +89,6 @@ 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}

    " - for line in lines: - # Format harga dalam format mata uang - currency = line['currency'] - old_price_formatted = self.env['ir.qweb.field.monetary'].value_to_html(line['old_price'], {'display_currency': currency}) - new_price_formatted = self.env['ir.qweb.field.monetary'].value_to_html(line['new_price'], {'display_currency': currency}) - - message_body += f"
  • {line['product']}:
    {old_price_formatted} → {new_price_formatted}
  • " - message_body += "
" - - # 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}

    " - for line in lines: - # Format harga dalam format mata uang - currency = line['currency'] - price_formatted = self.env['ir.qweb.field.monetary'].value_to_html(line['price'], {'display_currency': currency}) - - message_body += f"
  • {line['product']}: {price_formatted}
  • " - message_body += "
" - - # 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}:

    " - for line in so_lines: - # Format harga dalam format mata uang - currency = line['currency'] - old_price_formatted = self.env['ir.qweb.field.monetary'].value_to_html(line['old_price'], {'display_currency': currency}) - new_price_formatted = self.env['ir.qweb.field.monetary'].value_to_html(line['new_price'], {'display_currency': currency}) - - so_message += f"
  • {line['product']}:
    {old_price_formatted} → {new_price_formatted}
  • " - so_message += "
" - 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 4802ebea..315795d5 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -51,8 +51,6 @@ 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 diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index e1ff099b..0fbbb5e7 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -37,9 +37,6 @@ - @@ -142,27 +139,19 @@ - {'readonly': ['|', ('state', 'in', ['done', 'cancel']), ('has_active_invoice', '=', True)]} + {'readonly': ['|', ('state', 'in', ['purchase', 'done', 'cancel']), ('has_active_invoice', '=', True)]} - - - - - - - {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])]} - - {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])], 'required': True} + {'readonly': [], 'required': True} - {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])]} + {'readonly': []} - {'readonly': [('parent.state', 'in', ['purchase', 'done', 'cancel'])],'required': True} + {'required': True} -- cgit v1.2.3