From 3553599efb4046ad4788e1d2b48a3081f246c85b Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 3 Jan 2025 11:01:39 +0700 Subject: export xml faktur --- indoteknik_custom/__manifest__.py | 3 +- indoteknik_custom/models/__init__.py | 1 + indoteknik_custom/models/coretax_fatur.py | 93 ++++++++++++++++++++++++++ indoteknik_custom/security/ir.model.access.csv | 1 + indoteknik_custom/views/coretax_faktur.xml | 49 ++++++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 indoteknik_custom/models/coretax_fatur.py create mode 100644 indoteknik_custom/views/coretax_faktur.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 89f62524..255fbd3d 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -8,7 +8,7 @@ 'author': 'Rafi Zadanly', 'website': '', 'images': ['assets/favicon.ico'], - 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan'], + 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur' ], 'data': [ 'security/ir.model.access.csv', 'views/group_partner.xml', @@ -155,6 +155,7 @@ 'report/report_invoice.xml', 'report/report_picking.xml', 'report/report_sale_order.xml', + 'views/coretax_faktur.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index ad6d75dd..b698b14c 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -134,3 +134,4 @@ from . import find_page from . import approval_retur_picking from . import va_multi_approve from . import va_multi_reject +from . import coretax_fatur diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py new file mode 100644 index 00000000..1c3af6a4 --- /dev/null +++ b/indoteknik_custom/models/coretax_fatur.py @@ -0,0 +1,93 @@ +from odoo import models, fields +import xml.etree.ElementTree as ET +from xml.dom import minidom +import base64 + +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 generate_xml(self): + # Buat root XML + root = ET.Element('TaxInvoiceBulk', { + 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", + 'xsi:noNamespaceSchemaLocation': "TaxInvoice.xsd" + }) + ET.SubElement(root, 'TIN').text = 'xxxxxxxxxxxxxxxx' + + # 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')]) + for invoice in invoices: + tax_invoice = ET.SubElement(list_of_tax_invoice, 'TaxInvoice') + + # 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, 'TaxInvoiceOpt').text = 'Normal' + ET.SubElement(tax_invoice, 'TrxCode').text = '01' + ET.SubElement(tax_invoice, 'AddInfo') + ET.SubElement(tax_invoice, 'CustomDoc') + ET.SubElement(tax_invoice, 'RefDesc') + ET.SubElement(tax_invoice, 'FacilityStamp') + ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0000000000000000000000' + ET.SubElement(tax_invoice, 'BuyerTin').text = 'xxxxxxxxxxxxxxxx' + ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' + ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IND' + ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' + ET.SubElement(tax_invoice, 'BuyerIDTKU').text = '0000000000000000000000' + + # Tambahkan elemen ListOfGoodService + list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService') + for line in invoice.invoice_line_ids: + 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.0001' + ET.SubElement(good_service, 'Price').text = str(line.price_unit) + ET.SubElement(good_service, 'Qty').text = str(line.quantity) + ET.SubElement(good_service, 'TotalDiscount').text = '100000' + ET.SubElement(good_service, 'TaxBase').text = str(line.price_subtotal) + ET.SubElement(good_service, 'OtherTaxBase').text = str(line.price_subtotal) + ET.SubElement(good_service, 'VATRate').text = '11' + ET.SubElement(good_service, 'VAT').text = str(line.price_total * 0.11) + ET.SubElement(good_service, 'STLGRate').text = '20' + ET.SubElement(good_service, 'STLG').text = '580000' + + # Pretty print XML + xml_str = ET.tostring(root, encoding='utf-8') + xml_pretty = minidom.parseString(xml_str).toprettyxml(indent=" ") + return xml_pretty + + def export_to_download(self): + # Generate XML content + xml_content = self.generate_xml() + + # Encode content to Base64 + xml_encoded = base64.b64encode(xml_content.encode('utf-8')) + + # Buat attachment untuk XML + attachment = self.env['ir.attachment'].create({ + 'name': 'Faktur_XML.xml', + 'type': 'binary', + 'datas': xml_encoded, + 'mimetype': 'application/xml', + 'store_fname': 'faktur_xml.xml', + }) + + # Kembalikan URL untuk download dengan header 'Content-Disposition' + return { + 'type': 'ir.actions.act_url', + 'url': '/web/content/{}'.format(attachment.id), + 'target': 'self', + 'params': {'download': True}, # Menambahkan parameter untuk memastikan file didownload + } diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 2375df9d..8e4d3a71 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -148,3 +148,4 @@ access_sales_order_fulfillment_v2,access.sales.order.fulfillment.v2,model_sales_ access_v_move_outstanding,access.v.move.outstanding,model_v_move_outstanding,,1,1,1,1 access_va_multi_approve,access.va.multi.approve,model_va_multi_approve,,1,1,1,1 access_va_multi_reject,access.va.multi.reject,model_va_multi_reject,,1,1,1,1 +access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1 diff --git a/indoteknik_custom/views/coretax_faktur.xml b/indoteknik_custom/views/coretax_faktur.xml new file mode 100644 index 00000000..45eea23f --- /dev/null +++ b/indoteknik_custom/views/coretax_faktur.xml @@ -0,0 +1,49 @@ + + + + + Export Faktur Pajak Keluaran XML Version + ir.actions.act_window + coretax.faktur + form + new + + + + view coretax faktur + coretax.faktur + form + +
+

+ Klik tombol Export di bawah untuk mulai export Faktur Pajak Keluaran. + Data yang diexport adalah Customer Invoice yang berstatus Open dan belum diexport ke E-Faktur. +

+ +

+ Setelah proses export Faktur Pajak Keluaran selesai dilakukan, + download file ini: +

+ + + + +

+ Lalu import ke program E-Faktur DJP melalui menu Referensi - Pajak Keluaran - Import +

+ +
+
+
+
+
+ + +
+
\ No newline at end of file -- cgit v1.2.3 From 2a3adac9333c2bd5cb9f0dca3ba080cd3e6dfce8 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Fri, 3 Jan 2025 11:31:46 +0700 Subject: automatic download --- indoteknik_custom/models/coretax_fatur.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 1c3af6a4..a0820fee 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -85,9 +85,10 @@ class CoretaxFaktur(models.Model): }) # Kembalikan URL untuk download dengan header 'Content-Disposition' - return { + response = { 'type': 'ir.actions.act_url', - 'url': '/web/content/{}'.format(attachment.id), + 'url': '/web/content/{}?download=true'.format(attachment.id), 'target': 'self', - 'params': {'download': True}, # Menambahkan parameter untuk memastikan file didownload } + + return response -- cgit v1.2.3 From 992f530f9da44716361b27e62458c82a8aca78e1 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 3 Jan 2025 14:43:27 +0700 Subject: push --- indoteknik_custom/models/coretax_fatur.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index a0820fee..426fb737 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -16,17 +16,17 @@ class CoretaxFaktur(models.Model): 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", 'xsi:noNamespaceSchemaLocation': "TaxInvoice.xsd" }) - ET.SubElement(root, 'TIN').text = 'xxxxxxxxxxxxxxxx' + ET.SubElement(root, 'TIN').text = '74.226.022.7-086.000' # 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), + invoices = inv_obj.search([('is_efaktur_exported','=',True), ('state','=','posted'), ('efaktur_id','!=', False), - ('move_type','=','out_invoice')]) + ('move_type','=','out_invoice')], limit = 5) for invoice in invoices: tax_invoice = ET.SubElement(list_of_tax_invoice, 'TaxInvoice') @@ -39,7 +39,7 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'RefDesc') ET.SubElement(tax_invoice, 'FacilityStamp') ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0000000000000000000000' - ET.SubElement(tax_invoice, 'BuyerTin').text = 'xxxxxxxxxxxxxxxx' + ET.SubElement(tax_invoice, 'BuyerTin').text = invoice.partner_id.npwp or '' ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IND' ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' @@ -50,9 +50,9 @@ class CoretaxFaktur(models.Model): for line in invoice.invoice_line_ids: 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, 'Code').text = line.product_id.default_code ET.SubElement(good_service, 'Name').text = line.name - ET.SubElement(good_service, 'Unit').text = 'UM.0001' + ET.SubElement(good_service, 'Unit').text = 'UM.0018' ET.SubElement(good_service, 'Price').text = str(line.price_unit) ET.SubElement(good_service, 'Qty').text = str(line.quantity) ET.SubElement(good_service, 'TotalDiscount').text = '100000' -- cgit v1.2.3 From bef3ba4c18d585bc0980942d91b5fcb4d44427e1 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 3 Jan 2025 15:27:56 +0700 Subject: push --- indoteknik_custom/models/coretax_fatur.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 426fb737..32082774 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -36,32 +36,32 @@ class CoretaxFaktur(models.Model): ET.SubElement(tax_invoice, 'TrxCode').text = '01' ET.SubElement(tax_invoice, 'AddInfo') ET.SubElement(tax_invoice, 'CustomDoc') - ET.SubElement(tax_invoice, 'RefDesc') + ET.SubElement(tax_invoice, 'RefDesc').text = invoice.name ET.SubElement(tax_invoice, 'FacilityStamp') - ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0000000000000000000000' + ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000' ET.SubElement(tax_invoice, 'BuyerTin').text = invoice.partner_id.npwp or '' ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IND' ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or '' - ET.SubElement(tax_invoice, 'BuyerIDTKU').text = '0000000000000000000000' + ET.SubElement(tax_invoice, 'BuyerIDTKU').text = '' # Tambahkan elemen ListOfGoodService list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService') for line in invoice.invoice_line_ids: good_service = ET.SubElement(list_of_good_service, 'GoodService') - ET.SubElement(good_service, 'Opt').text = 'A' + ET.SubElement(good_service, 'Opt').text = 'B' if line.product_id.type == 'service' else 'A' ET.SubElement(good_service, 'Code').text = line.product_id.default_code ET.SubElement(good_service, 'Name').text = line.name ET.SubElement(good_service, 'Unit').text = 'UM.0018' ET.SubElement(good_service, 'Price').text = str(line.price_unit) ET.SubElement(good_service, 'Qty').text = str(line.quantity) - ET.SubElement(good_service, 'TotalDiscount').text = '100000' + ET.SubElement(good_service, 'TotalDiscount').text = str(line.discount) ET.SubElement(good_service, 'TaxBase').text = str(line.price_subtotal) ET.SubElement(good_service, 'OtherTaxBase').text = str(line.price_subtotal) - ET.SubElement(good_service, 'VATRate').text = '11' + ET.SubElement(good_service, 'VATRate').text = '0,11' ET.SubElement(good_service, 'VAT').text = str(line.price_total * 0.11) - ET.SubElement(good_service, 'STLGRate').text = '20' - ET.SubElement(good_service, 'STLG').text = '580000' + ET.SubElement(good_service, 'STLGRate').text = '' + ET.SubElement(good_service, 'STLG').text = '' # Pretty print XML xml_str = ET.tostring(root, encoding='utf-8') -- cgit v1.2.3 From 02d27a0e871dd4949c9382000843cd35dd3db3f8 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 8 Jan 2025 09:48:07 +0700 Subject: automatic email bill, api lalamove --- indoteknik_custom/models/purchase_order.py | 44 +++++-- indoteknik_custom/models/stock_picking.py | 195 +++++++++++++++++++++++++++-- indoteknik_custom/views/stock_picking.xml | 16 +++ 3 files changed, 236 insertions(+), 19 deletions(-) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 0e39d12a..799c4db0 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -75,6 +75,28 @@ class PurchaseOrder(models.Model): exclude_incoming = fields.Boolean(string='Exclude Incoming', default=False, help='Centang jika tidak mau masuk perhitungan Incoming Qty') not_update_purchasepricelist = fields.Boolean(string='Not Update Purchase Pricelist?') + # total_cost_service = fields.Float(string='Total Cost Service') + # total_delivery_amt = fields.Float(string='Total Delivery Amt') + + # @api.onchange('total_cost_service') + # def _onchange_total_cost_service(self): + # for order in self: + # lines = order.order_line + # if lines: + # # Hitung nilai rata-rata cost_service + # per_line_cost_service = order.total_cost_service / len(lines) + # for line in lines: + # line.cost_service = per_line_cost_service + + # @api.onchange('total_delivery_amt') + # def _onchange_total_delivery_amt(self): + # for order in self: + # lines = order.order_line + # if lines: + # # Hitung nilai rata-rata delivery_amt + # per_line_delivery_amt = order.total_delivery_amt / len(lines) + # for line in lines: + # line.delivery_amt = per_line_delivery_amt def _compute_total_margin_match(self): for purchase in self: @@ -115,6 +137,7 @@ class PurchaseOrder(models.Model): 'ref': self.name, 'invoice_date': current_date, 'date': current_date, + 'invoice_origin': self.name, 'move_type': 'in_invoice' } @@ -165,6 +188,11 @@ class PurchaseOrder(models.Model): self.bills_pelunasan_id = bills.id + lognote_message = ( + f"Vendor bill created from: {self.name} ({self.partner_ref})" + ) + bills.message_post(body=lognote_message) + return { 'name': _('Account Move'), 'view_mode': 'tree,form', @@ -174,12 +202,10 @@ class PurchaseOrder(models.Model): 'domain': [('id', '=', bills.id)] } - - def create_bill_dp(self): if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa bikin bill dp') - + current_date = datetime.utcnow() data_bills = { 'partner_id': self.partner_id.id, @@ -187,8 +213,8 @@ class PurchaseOrder(models.Model): 'ref': self.name, 'invoice_date': current_date, 'date': current_date, + 'invoice_origin': self.name, 'move_type': 'in_invoice' - } bills = self.env['account.move'].create([data_bills]) @@ -197,14 +223,13 @@ class PurchaseOrder(models.Model): data_line_bills = { 'move_id': bills.id, - 'product_id': product_dp.id, #product down payment - 'account_id': 401, #Uang Muka persediaan barang dagang + 'product_id': product_dp.id, # product down payment + 'account_id': 401, # Uang Muka persediaan barang dagang 'quantity': 1, 'product_uom_id': 1, 'tax_ids': [line[0].taxes_id.id for line in self.order_line], } - bills_line = self.env['account.move.line'].create([data_line_bills]) self.bills_dp_id = bills.id @@ -213,6 +238,11 @@ class PurchaseOrder(models.Model): move_line.name = '[IT.121456] Down Payment' move_line.partner_id = self.partner_id.id + lognote_message = ( + f"Vendor bill created from: {self.name} ({self.partner_ref})" + ) + bills.message_post(body=lognote_message) + return { 'name': _('Account Move'), 'view_mode': 'tree,form', diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index e6506a0b..51899de9 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -6,10 +6,17 @@ from itertools import groupby import pytz, requests, json, requests from dateutil import parser import datetime - +import hmac +import hashlib +import base64 +import requests +import time +import logging +_logger = logging.getLogger(__name__) class StockPicking(models.Model): _inherit = 'stock.picking' + # check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True) is_internal_use = fields.Boolean('Internal Use', help='flag which is internal use or not') account_id = fields.Many2one('account.account', string='Account') efaktur_id = fields.Many2one('vit.efaktur', string='Faktur Pajak') @@ -134,6 +141,86 @@ class StockPicking(models.Model): envio_latest_longitude = fields.Float(string="Log Longitude", readonly=True) tracking_by = fields.Many2one('res.users', string='Tracking By', readonly=True, tracking=True) + # Lalamove Section + lalamove_order_id = fields.Char(string="Lalamove Order ID", copy=False) + lalamove_address = fields.Char(string="Lalamove Address") + lalamove_name = fields.Char(string="Lalamove Name") + lalamove_phone = fields.Char(string="Lalamove Phone") + lalamove_status = fields.Char(string="Lalamove Status") + lalamove_delivered_at = fields.Datetime(string="Lalamove Delivered At") + lalamove_data = fields.Text(string="Lalamove Data", readonly=True) + lalamove_image_url = fields.Char(string="Lalamove Image URL") + lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html") + + def _compute_lalamove_image_html(self): + for record in self: + if record.lalamove_image_url: + record.lalamove_image_html = f'' + else: + record.lalamove_image_html = "No image available." + + def action_fetch_lalamove_order(self): + pickings = self.env['stock.picking'].search([ + ('picking_type_code', '=', 'outgoing'), + ('state', '=', 'done'), + ('carrier_id', '=', 9) + ]) + for picking in pickings: + try: + order_id = picking.lalamove_order_id + apikey = self.env['ir.config_parameter'].sudo().get_param('lalamove.apikey') + secret = self.env['ir.config_parameter'].sudo().get_param('lalamove.secret') + market = self.env['ir.config_parameter'].sudo().get_param('lalamove.market', default='ID') + + order_data = picking.get_lalamove_order(order_id, apikey, secret, market) + picking.lalamove_data = order_data + except Exception as e: + _logger.error(f"Error fetching Lalamove order for picking {picking.id}: {str(e)}") + continue + + def get_lalamove_order(self, order_id, apikey, secret, market): + timestamp = str(int(time.time() * 1000)) + message = f"{timestamp}\r\nGET\r\n/v3/orders/{order_id}\r\n\r\n" + signature = hmac.new(secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest() + + headers = { + "Content-Type": "application/json", + "Authorization": f"hmac {apikey}:{timestamp}:{signature}", + "Market": market + } + + url = f"https://rest.lalamove.com/v3/orders/{order_id}" + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + stops = data.get("data", {}).get("stops", []) + + for stop in stops: + pod = stop.get("POD", {}) + if pod.get("status") == "DELIVERED": + image_url = pod.get("image") # Sesuaikan jika key berbeda + self.lalamove_image_url = image_url + + address = stop.get("address") + name = stop.get("name") + phone = stop.get("phone") + delivered_at = pod.get("deliveredAt") + + delivered_at_dt = self._convert_to_datetime(delivered_at) + + self.lalamove_address = address + self.lalamove_name = name + self.lalamove_phone = phone + self.lalamove_status = pod.get("status") + self.lalamove_delivered_at = delivered_at_dt + return data + + raise UserError("No delivered data found in Lalamove response.") + else: + raise UserError(f"Error {response.status_code}: {response.text}") + + def _convert_to_wib(self, date_str): """ Mengonversi string waktu ISO 8601 ke format waktu Indonesia (WIB) @@ -661,13 +748,13 @@ class StockPicking(models.Model): if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking': if self.origin and 'Return of' in self.origin: raise UserError("Button ini hanya untuk Logistik") - + if self.picking_type_code == 'internal': self.check_qty_done_stock() if self._name != 'stock.picking': return super(StockPicking, self).button_validate() - + if not self.picking_code: self.picking_code = self.env['ir.sequence'].next_by_code('stock.picking.code') or '0' @@ -681,15 +768,10 @@ class StockPicking(models.Model): raise UserError("Harus di Approve oleh Accounting") if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver: - raise UserError("Harus di Approve oleh Logistik") + raise UserError("Harus di Approve oleh Logistik") if self.location_dest_id.id == 47 and not self.env.user.is_purchasing_manager: - raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara") - - # if self.group_id.sale_id: - # if self.group_id.sale_id.payment_link_midtrans: - # if self.group_id.sale_id.payment_status != 'settlement' and self.group_id.sale_id.state == 'draft': - # raise UserError('Uang belum masuk (settlement), mohon konfirmasi ke sales atau finance') + raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara") if self.is_internal_use: self.approval_status = 'approved' @@ -707,14 +789,57 @@ class StockPicking(models.Model): if not self.date_reserved: current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.date_reserved = current_time - + self.validation_minus_onhand_quantity() self.responsible = self.env.user.id res = super(StockPicking, self).button_validate() self.calculate_line_no() self.date_done = datetime.datetime.utcnow() self.state_reserve = 'done' + + template = self.env.ref('indoteknik_custom.mail_template_invoice_po_document') + if template and self.purchase_id: + # Render email body + email_values = template.sudo().generate_email( + res_ids=[self.purchase_id.id], + fields=['body_html'] + ) + rendered_body = email_values.get(self.purchase_id.id, {}).get('body_html', '') + + # Render report dengan XML ID + report = self.env.ref('purchase.action_report_purchase_order') # Gunakan XML ID laporan + if not report: + raise UserError("Laporan dengan XML ID 'purchase.action_report_purchase_order' tidak ditemukan.") + + # Render laporan ke PDF + pdf_content, _ = report._render_qweb_pdf([self.purchase_id.id]) + report_content = base64.b64encode(pdf_content).decode('utf-8') + + # Kirim email menggunakan template + email_sent = template.sudo().send_mail(self.purchase_id.id, force_send=True) + + if email_sent: + # Buat attachment untuk laporan + attachment = self.env['ir.attachment'].create({ + 'name': self.purchase_id.name or "Laporan Invoice.pdf", + 'type': 'binary', + 'datas': report_content, + 'res_model': 'purchase.order', + 'res_id': self.purchase_id.id, + 'mimetype': 'application/pdf', + }) + + # Tambahkan isi email dan laporan ke log note + self.purchase_id.message_post( + body=rendered_body, + subject="Pengiriman Email Invoice", + message_type='comment', + subtype_xmlid="mail.mt_note", + attachment_ids=[attachment.id], + ) + return res + def action_cancel(self): if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking': if self.origin and 'Return of' in self.origin: @@ -858,4 +983,50 @@ class StockPicking(models.Model): formatted_fastest_eta = fastest_eta.strftime(format_time_fastest) formatted_longest_eta = longest_eta.strftime(format_time) - return f'{formatted_fastest_eta} - {formatted_longest_eta}' \ No newline at end of file + return f'{formatted_fastest_eta} - {formatted_longest_eta}' + +# class CheckProduct(models.Model): +# _name = 'check.product' +# _description = 'Check Product' +# _order = 'picking_id, id' + +# picking_id = fields.Many2one('stock.picking', string='Picking Reference', required=True, ondelete='cascade', index=True, copy=False) +# product_id = fields.Many2one('product.product', string='Product') + + +# @api.constrains('product_id') +# def check_product_validity(self): +# """ +# Validate if the product exists in the related stock.picking's move_ids_without_package +# and ensure that the product's quantity does not exceed the available product_uom_qty. +# """ +# for record in self: +# if not record.picking_id or not record.product_id: +# continue + +# # Filter move lines in the related picking for the selected product +# moves = record.picking_id.move_ids_without_package.filtered( +# lambda move: move.product_id.id == record.product_id.id +# ) + +# if not moves: +# raise UserError(( +# "The product '%s' is not available in the related stock picking's moves. " +# "Please check and try again." +# ) % record.product_id.display_name) + +# # Calculate the total entries for the product in check.product for the same picking +# product_entries_count = self.search_count([ +# ('picking_id', '=', record.picking_id.id), +# ('product_id', '=', record.product_id.id) +# ]) + +# # Sum the product_uom_qty for all relevant moves +# total_qty_in_moves = sum(moves.mapped('product_uom_qty')) + +# # Compare the count of entries against the available quantity +# if product_entries_count > total_qty_in_moves: +# raise UserError(( +# "The product '%s' exceeds the allowable quantity (%s) in the related stock picking's moves. " +# "You can only add it %s times." +# ) % (record.product_id.display_name, total_qty_in_moves, total_qty_in_moves)) diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index fab83885..9d22903e 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -61,6 +61,11 @@ type="object" attrs="{'invisible': [('carrier_id', '!=', 151)]}" /> + @@ -168,6 +173,17 @@ + + + + + + + + + + + -- cgit v1.2.3 From c5f049fd628ce1f8c6b7c65282fbfd4bb2739ebc Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Sat, 11 Jan 2025 14:44:04 +0700 Subject: delete temporary --- indoteknik_custom/views/stock_picking.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index be281983..30ef0f7a 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -84,9 +84,6 @@ - - 1 - -- cgit v1.2.3 From 20b7432d8566d25371e3a3277ed3931aa59cdf0c Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Sat, 11 Jan 2025 15:56:13 +0700 Subject: cr attrs product uom qty --- indoteknik_custom/views/stock_picking.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index be281983..e38dd0c7 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -85,7 +85,7 @@ - 1 + {'readonly': [('picking_type_code', '=', 'incoming')]} -- cgit v1.2.3 From 88a54aebe579809dcaf8d8fd9640d5c4db83f2d6 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Sat, 11 Jan 2025 16:15:08 +0700 Subject: fix bug --- indoteknik_custom/views/stock_picking.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index e38dd0c7..882e5a29 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -85,7 +85,7 @@ - {'readonly': [('picking_type_code', '=', 'incoming')]} + {'readonly': [('parent.picking_type_code', '=', 'incoming')]} -- cgit v1.2.3 From fcefddc96acc561ec13d52e734eaf04b041d4a0b Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 13 Jan 2025 11:24:08 +0700 Subject: update export xml by selected invoice --- indoteknik_custom/models/account_move.py | 32 ++++++++++++++ indoteknik_custom/models/coretax_fatur.py | 69 ++++++++++++++++++++----------- indoteknik_custom/views/account_move.xml | 8 ++++ 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 725b3c2d..42678847 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -325,3 +325,35 @@ class AccountMove(models.Model): # if rec.statement_line_id and not rec.statement_line_id.statement_id.is_edit and rec.statement_line_id.statement_id.state == 'confirm': # raise UserError('Bank Statement di Lock, Minta admin reconcile untuk unlock') # return res + + def validate_faktur_for_export(self): + invoices = self.filtered(lambda inv: not inv.is_efaktur_exported and + inv.state == 'posted' and + inv.move_type == 'out_invoice') + + invalid_invoices = self - invoices + if invalid_invoices: + invalid_ids = ", ".join(str(inv.id) for inv in invalid_invoices) + raise UserError(_( + "Faktur dengan ID berikut tidak valid untuk diekspor: {}.\n" + "Pastikan faktur dalam status 'posted', belum diekspor, dan merupakan 'out_invoice'.".format(invalid_ids) + )) + + return invoices + + def export_faktur_to_xml(self): + + valid_invoices = self.validate_faktur_for_export() + + # Panggil model coretax.faktur untuk menghasilkan XML + coretax_faktur = self.env['coretax.faktur'].create({}) + response = coretax_faktur.export_to_download(invoices=valid_invoices) + + current_time = datetime.utcnow() + # Tandai faktur sebagai sudah diekspor + valid_invoices.write({ + 'is_efaktur_exported': True, + 'date_efaktur_exported': current_time, # Set tanggal ekspor + }) + + return response diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 32082774..00a8f5ab 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -2,6 +2,7 @@ from odoo import models, fields import xml.etree.ElementTree as ET from xml.dom import minidom import base64 +import re class CoretaxFaktur(models.Model): _name = 'coretax.faktur' @@ -9,68 +10,88 @@ class CoretaxFaktur(models.Model): export_file = fields.Binary(string="Export File", ) export_filename = fields.Char(string="Export File", ) + + def validate_and_format_number(slef, input_number): + # Hapus semua karakter non-digit + cleaned_number = re.sub(r'\D', '', input_number) + + # Hitung jumlah digit + digit_count = len(cleaned_number) + + # Jika jumlah digit kurang dari 15, tambahkan nol di depan + if digit_count < 16: + cleaned_number = cleaned_number.zfill(16) + + return cleaned_number - def generate_xml(self): + def generate_xml(self, invoices=None): # Buat root XML root = ET.Element('TaxInvoiceBulk', { 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", 'xsi:noNamespaceSchemaLocation': "TaxInvoice.xsd" }) - ET.SubElement(root, 'TIN').text = '74.226.022.7-086.000' + ET.SubElement(root, 'TIN').text = '0742260227086000' # 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) + # 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) if invoice.partner_id.npwp else '0000000000000000' + buyerIDTKU = buyerTIN.ljust(len(buyerTIN) + 6, '0') if invoice.partner_id.npwp 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, 'TaxInvoiceOpt').text = 'Normal' - ET.SubElement(tax_invoice, 'TrxCode').text = '01' + ET.SubElement(tax_invoice, 'TrxCode').text = '04' ET.SubElement(tax_invoice, 'AddInfo') ET.SubElement(tax_invoice, 'CustomDoc') ET.SubElement(tax_invoice, 'RefDesc').text = invoice.name ET.SubElement(tax_invoice, 'FacilityStamp') ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000' - ET.SubElement(tax_invoice, 'BuyerTin').text = invoice.partner_id.npwp or '' - ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' - ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IND' + ET.SubElement(tax_invoice, 'BuyerTin').text = buyerTIN + ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if invoice.partner_id.npwp else 'Other ID' + ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IDN' + ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' + 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 = '' + ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU # 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 good_service = ET.SubElement(list_of_good_service, 'GoodService') - ET.SubElement(good_service, 'Opt').text = 'B' if line.product_id.type == 'service' else 'A' - ET.SubElement(good_service, 'Code').text = line.product_id.default_code + ET.SubElement(good_service, 'Opt').text = 'A' + ET.SubElement(good_service, 'Code') ET.SubElement(good_service, 'Name').text = line.name ET.SubElement(good_service, 'Unit').text = 'UM.0018' - ET.SubElement(good_service, 'Price').text = str(line.price_unit) + 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 = str(line.discount) - ET.SubElement(good_service, 'TaxBase').text = str(line.price_subtotal) - ET.SubElement(good_service, 'OtherTaxBase').text = str(line.price_subtotal) - ET.SubElement(good_service, 'VATRate').text = '0,11' - ET.SubElement(good_service, 'VAT').text = str(line.price_total * 0.11) - ET.SubElement(good_service, 'STLGRate').text = '' - ET.SubElement(good_service, 'STLG').text = '' + 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, '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, 'STLGRate').text = '0' + ET.SubElement(good_service, 'STLG').text = '0' # Pretty print XML xml_str = ET.tostring(root, encoding='utf-8') xml_pretty = minidom.parseString(xml_str).toprettyxml(indent=" ") return xml_pretty - def export_to_download(self): + def export_to_download(self, invoices): # Generate XML content - xml_content = self.generate_xml() + xml_content = self.generate_xml(invoices) # Encode content to Base64 xml_encoded = base64.b64encode(xml_content.encode('utf-8')) diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index 2863af57..4cc35b6d 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -163,5 +163,13 @@ code action = records.open_form_multi_create_reklas_penjualan() + + + Export Faktur ke XML + + + code + action = records.export_faktur_to_xml() + \ No newline at end of file -- cgit v1.2.3 From 14a677c77defd6f5c92af2db6f128e3f7227ddf6 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 13 Jan 2025 14:27:36 +0700 Subject: update customer non pkp --- indoteknik_custom/models/coretax_fatur.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 00a8f5ab..2f85a97f 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -44,8 +44,8 @@ class CoretaxFaktur(models.Model): for invoice in invoices: tax_invoice = ET.SubElement(list_of_tax_invoice, 'TaxInvoice') - buyerTIN = self.validate_and_format_number(invoice.partner_id.npwp) if invoice.partner_id.npwp else '0000000000000000' - buyerIDTKU = buyerTIN.ljust(len(buyerTIN) + 6, '0') if invoice.partner_id.npwp else '000000' + buyerTIN = self.validate_and_format_number(invoice.partner_id.npwp) if invoice.partner_id.customer_type == 'pkp' else '0000000000000000' + buyerIDTKU = buyerTIN.ljust(len(buyerTIN) + 6, '0') if invoice.partner_id.customer_type == 'pkp' else '000000' # Tambahkan elemen faktur ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime('%Y-%m-%d') if invoice.invoice_date else '' -- cgit v1.2.3 From 8579a74d62ea1c730159fc6969fabdb44285b288 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 13 Jan 2025 15:01:43 +0700 Subject: feedback buyrTIN --- indoteknik_custom/models/coretax_fatur.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 2f85a97f..74f9c8b7 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -14,6 +14,10 @@ class CoretaxFaktur(models.Model): def validate_and_format_number(slef, 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' # Hitung jumlah digit digit_count = len(cleaned_number) @@ -44,8 +48,8 @@ class CoretaxFaktur(models.Model): for invoice in invoices: tax_invoice = ET.SubElement(list_of_tax_invoice, 'TaxInvoice') - buyerTIN = self.validate_and_format_number(invoice.partner_id.npwp) if invoice.partner_id.customer_type == 'pkp' else '0000000000000000' - buyerIDTKU = buyerTIN.ljust(len(buyerTIN) + 6, '0') if invoice.partner_id.customer_type == 'pkp' else '000000' + buyerTIN = self.validate_and_format_number(invoice.partner_id.npwp) + buyerIDTKU = buyerTIN.ljust(len(buyerTIN) + 6, '0') 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 '' -- cgit v1.2.3 From 7c6f97ba1b4e8fb54204697a3835bcb78a1ddc6b Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 13 Jan 2025 15:25:44 +0700 Subject: udpate other id --- indoteknik_custom/models/coretax_fatur.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index 74f9c8b7..d8b9dc72 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -61,7 +61,7 @@ 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 invoice.partner_id.npwp 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 = '-' ET.SubElement(tax_invoice, 'BuyerName').text = invoice.partner_id.nama_wajib_pajak or '' -- cgit v1.2.3 From efefae8a0bdb05f36e0aeee9d06927a55fe648a6 Mon Sep 17 00:00:00 2001 From: trisusilo48 Date: Mon, 13 Jan 2025 15:41:04 +0700 Subject: udpate buyer doc number --- indoteknik_custom/models/coretax_fatur.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py index d8b9dc72..ff8606b1 100644 --- a/indoteknik_custom/models/coretax_fatur.py +++ b/indoteknik_custom/models/coretax_fatur.py @@ -63,7 +63,7 @@ class CoretaxFaktur(models.Model): 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, 'BuyerCountry').text = 'IDN' - ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' + ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = 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 '' -- cgit v1.2.3