diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2025-01-08 09:48:07 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2025-01-08 09:48:07 +0700 |
| commit | 02d27a0e871dd4949c9382000843cd35dd3db3f8 (patch) | |
| tree | de4dbbd21bae07a9507a60b562420f7346e8914f | |
| parent | d35c2dce88a87bc05d30c4935d51d7d58aa5d37d (diff) | |
automatic email bill, api lalamove
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 44 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 195 | ||||
| -rw-r--r-- | 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'<img src="{record.lalamove_image_url}" width="300" height="300"/>' + 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)]}" /> + <button name="action_fetch_lalamove_order" + string="Tracking Lalamove" + type="object" + attrs="{'invisible': [('carrier_id', '!=', 9)]}" + /> </button> <field name="backorder_id" position="after"> <field name="summary_qty_detail"/> @@ -168,6 +173,17 @@ <field name="envio_latest_longitude" invisible="1"/> <field name="tracking_by" invisible="1"/> </group> + <group> + <field name="lalamove_data" invisible="1"/> + <field name="lalamove_order_id"/> + <field name="lalamove_address"/> + <field name="lalamove_name"/> + <field name="lalamove_phone"/> + <field name="lalamove_status"/> + <field name="lalamove_delivered_at"/> + <field name="lalamove_image_url" invisible="1"/> + <field name="lalamove_image_html"/> + </group> </group> </page> <!-- <page string="Check Product" name="check_product"> |
