diff options
| -rwxr-xr-x | indoteknik_custom/models/__init__.py | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 2 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 44 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 19 | ||||
| -rw-r--r-- | indoteknik_custom/models/sale_order_line.py | 24 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_immediate_transfer.py | 36 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 205 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/views/sale_order.xml | 8 | ||||
| -rw-r--r-- | indoteknik_custom/views/stock_picking.xml | 25 |
10 files changed, 339 insertions, 26 deletions
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index b698b14c..bb223a31 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -134,4 +134,5 @@ from . import find_page from . import approval_retur_picking from . import va_multi_approve from . import va_multi_reject +from . import stock_immediate_transfer from . import coretax_fatur diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 9007dd71..5bedae13 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -416,7 +416,7 @@ class ProductProduct(models.Model): box_size=5, border=4, ) - qr.add_data(rec.display_name) + qr.add_data(rec.default_code) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") 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/sale_order.py b/indoteknik_custom/models/sale_order.py index f5e7e8a1..7b2d9bf8 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1031,8 +1031,9 @@ class SaleOrder(models.Model): if self.have_outstanding_invoice: raise UserError("Invoice harus di Cancel dahulu") - elif self.have_outstanding_picking: - raise UserError("DO harus di Cancel dahulu") + for line in self.order_line: + if line.qty_delivered > 0: + raise UserError("DO harus di-cancel terlebih dahulu.") if not self.web_approval: self.web_approval = 'company' @@ -1410,4 +1411,16 @@ class SaleOrder(models.Model): 'npwp': partner.npwp, 'email': partner.email, 'customer_type': partner.customer_type, - })
\ No newline at end of file + }) + + def write(self, vals): + for order in self: + if order.state in ['sale', 'cancel']: + if 'order_line' in vals: + new_lines = vals.get('order_line', []) + for command in new_lines: + if command[0] == 0: # A new line is being added + raise UserError( + "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") + res = super(SaleOrder, self).write(vals) + return res
\ No newline at end of file diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index a31ff569..29a046fa 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -38,6 +38,14 @@ class SaleOrderLine(models.Model): md_vendor_id = fields.Many2one('res.partner', string='MD Vendor', readonly=True) margin_md = fields.Float(string='Margin MD') qty_free_bu = fields.Float(string='Free BU', compute='_get_qty_free_bandengan') + desc_updatable = fields.Boolean(string='desc boolean', default=False, compute='_get_desc_updatable') + + def _get_desc_updatable(self): + for line in self: + if line.product_id.id != 417724: + line.desc_updatable = False + else: + line.desc_updatable = True def _get_qty_free_bandengan(self): for line in self: @@ -272,6 +280,10 @@ class SaleOrderLine(models.Model): (line.product_id.short_spesification if line.product_id.short_spesification else '') line.name = line_name line.weight = line.product_id.weight + if line.product_id.id != 417724: + line.desc_updatable = False + else: + line.desc_updatable = True @api.constrains('vendor_id') def _check_vendor_id(self): @@ -378,4 +390,14 @@ class SaleOrderLine(models.Model): if not line.product_id.product_tmpl_id.sale_ok: raise UserError('Product %s belum bisa dijual, harap hubungi finance' % line.product_id.display_name) if not line.vendor_id or not line.purchase_price and not line.display_type == 'line_note': - raise UserError(_('Isi Vendor dan Harga Beli sebelum Request Approval'))
\ No newline at end of file + raise UserError(_('Isi Vendor dan Harga Beli sebelum Request Approval')) + + @api.depends('state') + def _compute_product_updatable(self): + for line in self: + if line.state == 'draft': + line.product_updatable = True + # line.desc_updatable = True + else: + line.product_updatable = False + # line.desc_updatable = False diff --git a/indoteknik_custom/models/stock_immediate_transfer.py b/indoteknik_custom/models/stock_immediate_transfer.py new file mode 100644 index 00000000..4be0dff2 --- /dev/null +++ b/indoteknik_custom/models/stock_immediate_transfer.py @@ -0,0 +1,36 @@ +from odoo import models, api, _ +from odoo.exceptions import UserError + +class StockImmediateTransfer(models.TransientModel): + _inherit = 'stock.immediate.transfer' + + def process(self): + """Override process method to add send_mail_bills logic.""" + pickings_to_do = self.env['stock.picking'] + pickings_not_to_do = self.env['stock.picking'] + + for line in self.immediate_transfer_line_ids: + if line.to_immediate is True: + pickings_to_do |= line.picking_id + else: + pickings_not_to_do |= line.picking_id + + for picking in pickings_to_do: + picking.send_mail_bills() + # If still in draft => confirm and assign + if picking.state == 'draft': + picking.action_confirm() + if picking.state != 'assigned': + picking.action_assign() + if picking.state != 'assigned': + raise UserError(_("Could not reserve all requested products. Please use the 'Mark as Todo' button to handle the reservation manually.")) + for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']): + for move_line in move.move_line_ids: + move_line.qty_done = move_line.product_uom_qty + + pickings_to_validate = self.env.context.get('button_validate_picking_ids') + if pickings_to_validate: + pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate) + pickings_to_validate = pickings_to_validate - pickings_not_to_do + return pickings_to_validate.with_context(skip_immediate=True).button_validate() + return True diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index e6506a0b..2feb9c72 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,7 +789,7 @@ 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() @@ -715,6 +797,59 @@ class StockPicking(models.Model): self.date_done = datetime.datetime.utcnow() self.state_reserve = 'done' return res + + + def send_mail_bills(self): + if self.picking_type_code == 'incoming' and self.purchase_id: + 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', + }) + + # Siapkan data untuk mail.compose.message + compose_values = { + 'subject': "Pengiriman Email Invoice", + 'body': rendered_body, + 'attachment_ids': [(4, attachment.id)], + 'res_id': self.purchase_id.id, + 'model': 'purchase.order', + } + + # Buat mail.compose.message + compose_message = self.env['mail.compose.message'].create(compose_values) + + # Kirim pesan melalui wizard + compose_message.action_send_mail() + + return True + 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 +993,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/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 8e4d3a71..6093636c 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -148,4 +148,5 @@ 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_stock_immediate_transfer,access.stock.immediate.transfer,model_stock_immediate_transfer,,1,1,1,1 access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1 diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 703b4d49..3539dbf3 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -107,6 +107,14 @@ {'readonly': [('state', 'in', ('done','cancel'))]} </attribute> </xpath> + <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree" position="inside"> + <field name="desc_updatable" invisible="1"/> + </xpath> + <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='name']" position="attributes"> + <attribute name="attrs"> + {'readonly': [('desc_updatable', '=', False)]} + </attribute> + </xpath> <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" position="attributes"> <attribute name="attrs"> { diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index fab83885..882e5a29 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"/> @@ -79,6 +84,9 @@ <field name="partner_id" position="after"> <field name="real_shipping_id"/> </field> + <field name="product_uom_qty" position="attributes"> + <attribute name="attrs">{'readonly': [('parent.picking_type_code', '=', 'incoming')]}</attribute> + </field> <field name="date_done" position="after"> <field name="arrival_time"/> </field> @@ -150,7 +158,7 @@ <field name="sj_documentation" widget="image" /> <field name="paket_documentation" widget="image" /> </group> - <group> + <group attrs="{'invisible': [('carrier_id', '!=', 151)]}"> <field name="envio_id" invisible="1"/> <field name="envio_code"/> <field name="envio_ref_code"/> @@ -168,6 +176,17 @@ <field name="envio_latest_longitude" invisible="1"/> <field name="tracking_by" invisible="1"/> </group> + <group attrs="{'invisible': [('carrier_id', '!=', 9)]}"> + <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"> @@ -182,8 +201,10 @@ <field name="name">check.product.tree</field> <field name="model">check.product</field> <field name="arch" type="xml"> - <tree editable="bottom"> + <tree editable="bottom" decoration-warning="status == 'Pending'" decoration-success="status == 'Done'"> <field name="product_id"/> + <field name="quantity"/> + <field name="status"/> </tree> </field> </record> --> |
