diff options
| author | Miqdad <ahmadmiqdad27@gmail.com> | 2025-07-14 13:09:29 +0700 |
|---|---|---|
| committer | Miqdad <ahmadmiqdad27@gmail.com> | 2025-07-14 13:09:29 +0700 |
| commit | 6bd30cc76c8d822840a6063959bfe50c6625fd12 (patch) | |
| tree | 890fd4b46c8697fdb4763e4fbdcbc47a9713de9c | |
| parent | 717dbfa7070e94c0af2bb39e2cebb4dc71d123b9 (diff) | |
| parent | b70acc8a458e9544f672ecc9549eb1fc3606bc35 (diff) | |
<miqdad> merge
21 files changed, 893 insertions, 179 deletions
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py index c5a4f7ed..85b0fbba 100644 --- a/indoteknik_api/controllers/api_v1/stock_picking.py +++ b/indoteknik_api/controllers/api_v1/stock_picking.py @@ -1,10 +1,14 @@ from .. import controller from odoo import http -from odoo.http import request +from odoo.http import request, Response from pytz import timezone from datetime import datetime import json +import logging +_logger = logging.getLogger(__name__) + +_logger = logging.getLogger(__name__) class StockPicking(controller.Controller): prefix = '/api/v1/' @@ -143,54 +147,104 @@ class StockPicking(controller.Controller): 'name': picking_data.name }) - # @http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False) - # def udpate_status_from_bitehsip(self, **kw): - # try: - # if not request.jsonrequest: - # return "ok" + @http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False) + def update_status_from_biteship(self, **kw): + _logger.info("Biteship Webhook: Request received at controller start (type='json').") + + try: + # Karena type='json', Odoo secara otomatis akan mem-parsing JSON untuk Anda. + # 'data' akan berisi dictionary Python dari payload JSON Biteship. + data = request.jsonrequest + + # Log ini akan menunjukkan payload yang diterima (sudah dalam bentuk dict) + _logger.info(f"Biteship Webhook: Parsed JSON data from request.jsonrequest: {json.dumps(data)}") + + event = data.get('event') + if event: + _logger.info(f"Biteship Webhook: Processing event: {event}") + if event == "order.status": + self.process_order_status(data) + elif event == "order.price": + self.process_order_price(data) + elif event == "order.waybill_id": + self.process_order_waybill(data) + # Tambahkan logika untuk event lain jika ada + else: + _logger.info("Biteship Webhook: No specific event in payload. Likely an installation/verification ping or unknown event type.") + + # Untuk route type='json', Anda cukup mengembalikan dictionary Python. + # Odoo akan secara otomatis mengonversinya menjadi respons JSON yang valid. + return {'status': 'ok'} + + except Exception as e: + _logger.error(f"Biteship Webhook: Unhandled error during processing: {e}", exc_info=True) + # Untuk error, kembalikan dictionary error juga, Odoo akan mengonversinya ke JSON + return {'status': 'error', 'message': str(e)} - # data = request.jsonrequest # Ambil data JSON dari request - # event = data.get('event') + def process_order_status(self, data): + picking = request.env['stock.picking'].sudo().search([ + ('biteship_id', '=', data.get('order_id')) + ], limit=1) - # # Handle Event Berdasarkan Jenisnya - # if event == "order.status": - # self.process_order_status(data) - # elif event == "order.price": - # self.process_order_price(data) - # elif event == "order.waybill_id": - # self.process_order_waybill(data) + if not picking: + _logger.warning(f"[Webhook] Tidak ditemukan picking untuk order_id {data.get('order_id')}") + return - # return {'success': True, 'message': f'Webhook {event} received'} - # except Exception as e: - # return {'success': False, 'message': str(e)} + status = data.get('status') + timestamp = data.get('updated_at') or datetime.utcnow().isoformat() + + description = picking._get_biteship_status_description(status, { + "courier": {"company": data.get("courier_company", "")}, + "destination": {"contact_name": picking.partner_id.name or ""} + }) + + # Tambahkan extra data dari webhook + extra_data = { + "courier_driver_name": data.get("courier_driver_name"), + "courier_driver_phone": data.get("courier_driver_phone"), + "courier_driver_plate_number": data.get("courier_driver_plate_number"), + "courier_link": data.get("courier_link"), + "order_price": data.get("order_price"), + "status": data.get("status"), + } + + picking.log_biteship_event_from_webhook(status, timestamp, description, extra_data=extra_data) - # @http.route(prefix + 'webhook/biteship', auth='public', methods=['POST'], csrf=False) - # def udpate_status_from_bitehsip(self, **kw): - # return "ok" - def process_order_status(self, data): - picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], - limit=1) - if data.get('status') == 'picked': - picking_model.write({'driver_departure_date': datetime.utcnow()}) - elif data.get('status') == 'delivered': - picking_model.write({'driver_arrival_date': datetime.utcnow()}) def process_order_price(self, data): - picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], - limit=1) - order = request.env['sale.order'].sudo().search([('name', '=', picking_model.sale_id.name)], limit=1) - if order: - order.write({ - 'delivery_amt': data.get('price') - }) + picking = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], limit=1) + + if not picking: + _logger.warning(f"Tidak ditemukan picking untuk order_id {data.get('order_id')}") + return + + picking.log_biteship_event_from_webhook( + status='order.price', + timestamp=data.get('updated_at') or datetime.utcnow().isoformat(), + description='Biaya pengiriman telah diperbarui berdasarkan informasi terbaru dari Biteship.', + extra_data={ + "order_price": data.get("price") + } + ) + def process_order_waybill(self, data): - picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], - limit=1) - if picking_model: - picking_model.write({ - 'biteship_waybill_id': data.get('courier_waybill_id'), - 'delivery_tracking_no': data.get('courier_waybill_id'), - 'biteship_tracking_id': data.get('courier_tracking_id') - }) + picking = request.env['stock.picking'].sudo().search([ + ('biteship_id', '=', data.get('order_id')) + ], limit=1) + + if not picking: + _logger.warning(f"Tidak ditemukan picking untuk order_id {data.get('order_id')}") + return + + picking.log_biteship_event_from_webhook( + status='order.waybill_id', + timestamp=data.get('updated_at') or datetime.utcnow().isoformat(), + description="Nomor waybill dan tracking diperbarui melalui Biteship.", + extra_data={ + "tracking_id": data.get("courier_tracking_id"), + "waybill_id": data.get("courier_waybill_id") + } + ) + diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index cf5556d1..2af13816 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -97,6 +97,7 @@ 'views/mail_template_po.xml', 'views/mail_template_efaktur.xml', 'views/mail_template_invoice_po.xml', + 'views/mail_template_invoice_reminder.xml', 'views/price_group.xml', 'views/mrp_production.xml', 'views/apache_solr.xml', @@ -156,6 +157,7 @@ 'views/stock_backorder_confirmation_views.xml', 'views/barcoding_product.xml', 'views/project_views.xml', + 'views/approval_payment_term.xml', 'report/report.xml', 'report/report_banner_banner.xml', 'report/report_banner_banner2.xml', diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 903c0745..84f2e15a 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -151,5 +151,7 @@ from . import account_payment_register from . import stock_inventory from . import sale_order_delay from . import approval_invoice_date +from . import approval_payment_term +# from . import patch from . import tukar_guling from . import tukar_guling_po diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index af24f93e..72ac5452 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -8,12 +8,15 @@ import PyPDF2 import os import re from terbilang import Terbilang +from collections import defaultdict +from odoo.tools.misc import formatLang _logger = logging.getLogger(__name__) class AccountMove(models.Model): _inherit = 'account.move' + _description = 'Account Move' invoice_day_to_due = fields.Integer(string="Day to Due", compute="_compute_invoice_day_to_due") bill_day_to_due = fields.Date(string="Day to Due", compute="_compute_bill_day_to_due") date_send_fp = fields.Datetime(string="Tanggal Kirim Faktur Pajak") @@ -72,21 +75,103 @@ class AccountMove(models.Model): bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') down_payment = fields.Boolean('Down Payments?') + def send_due_invoice_reminder(self): + today = fields.Date.today() + target_dates = [ + today - timedelta(days=7), + today - timedelta(days=3), + today, + today + timedelta(days=3), + today + timedelta(days=7), + ] + + partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1) + if not partner: + _logger.info("Partner tidak ditemukan.") + return + + invoices = self.env['account.move'].search([ + ('move_type', '=', 'out_invoice'), + ('state', '=', 'posted'), + ('payment_state', 'not in', ['paid','in_payment', 'reversed']), + ('invoice_date_due', 'in', target_dates), + ('partner_id', '=', partner.id), + ]) + + _logger.info(f"Invoices tahap 1: {invoices}") + + invoices = invoices.filtered( + lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() + ) + _logger.info(f"Invoices tahap 2: {invoices}") + + if not invoices: + _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") + return + + grouped = {} + for inv in invoices: + grouped.setdefault(inv.partner_id, []).append(inv) + + template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') + + for partner, invs in grouped.items(): + if not partner.email: + _logger.info(f"Partner {partner.name} tidak memiliki email") + continue + + invoice_table_rows = "" + for inv in invs: + days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 + invoice_table_rows += f""" + <tr> + <td>{inv.name}</td> + <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td> + <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td> + <td>{days_to_due}</td> + <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td> + <td>{inv.ref or '-'}</td> + </tr> + """ + + subject = f"Reminder Invoice Due - {partner.name}" + body_html = re.sub( + r"<tbody[^>]*>.*?</tbody>", + f"<tbody>{invoice_table_rows}</tbody>", + template.body_html, + flags=re.DOTALL + ).replace('${object.name}', partner.name) \ + .replace('${object.partner_id.name}', partner.name) + # .replace('${object.email}', partner.email or '') + + values = { + 'subject': subject, + 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi + 'email_from': 'finance@indoteknik.co.id', + 'body_html': body_html, + 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id', + } + + _logger.info(f"VALUES: {values}") + + template.send_mail(invs[0].id, force_send=True, email_values=values) + + # Default System User + user_system = self.env['res.users'].browse(25) + system_id = user_system.partner_id.id if user_system else False + _logger.info(f"System User: {user_system.name} ({user_system.id})") + _logger.info(f"System User ID: {system_id}") + + for inv in invs: + inv.message_post( + subject=subject, + body=body_html, + subtype_id=self.env.ref('mail.mt_note').id, + author_id=system_id, + ) + + _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") - # def name_get(self): - # result = [] - # for move in self: - # if move.move_type == 'entry': - # # Jika masih draft, tampilkan 'Draft CAB' - # if move.state == 'draft': - # label = 'Draft CAB' - # else: - # label = move.name - # result.append((move.id, label)) - # else: - # # Untuk invoice dan lainnya, pakai default - # result.append((move.id, move.display_name)) - # return result @api.onchange('invoice_date') def _onchange_invoice_date(self): @@ -99,12 +184,27 @@ class AccountMove(models.Model): self.invoice_date = self.date + # def compute_length_of_payment(self): + # for rec in self: + # payment_term = rec.invoice_payment_term_id.line_ids[0].days + # terima_faktur = rec.date_terima_tukar_faktur + # payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) + + # if payment and terima_faktur: + # date_diff = terima_faktur - payment.date + # rec.length_of_payment = date_diff.days + payment_term + # else: + # rec.length_of_payment = 0 + def compute_length_of_payment(self): for rec in self: - payment_term = rec.invoice_payment_term_id.line_ids[0].days + payment_term = 0 + if rec.invoice_payment_term_id and rec.invoice_payment_term_id.line_ids: + payment_term = rec.invoice_payment_term_id.line_ids[0].days + terima_faktur = rec.date_terima_tukar_faktur payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) - + if payment and terima_faktur: date_diff = terima_faktur - payment.date rec.length_of_payment = date_diff.days + payment_term diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py new file mode 100644 index 00000000..025f9ed4 --- /dev/null +++ b/indoteknik_custom/models/approval_payment_term.py @@ -0,0 +1,187 @@ +from odoo import models, api, fields +from odoo.exceptions import AccessError, UserError, ValidationError +from datetime import timedelta, date, datetime +import logging + +_logger = logging.getLogger(__name__) + +class ApprovalPaymentTerm(models.Model): + _name = "approval.payment.term" + _description = "Approval Payment Term" + _inherit = ['mail.thread'] + _rec_name = 'number' + + number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True) + partner_id = fields.Many2one('res.partner', string='Partner', copy=False) + property_payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', copy=False, tracking=True) + parent_id = fields.Many2one('res.partner', string='Related Company', copy=False) + blocking_stage = fields.Float(string='Blocking Amount', + help="Cannot make sales once the selected " + "customer is crossed blocking amount." + "Set its value to 0.00 to disable " + "this feature", tracking=True, copy=False) + warning_stage = fields.Float(string='Warning Amount', + help="A warning message will appear once the " + "selected customer is crossed warning " + "amount. Set its value to 0.00 to" + " disable this feature", tracking=True, copy=False) + active_limit = fields.Boolean('Active Credit Limit', copy=False, tracking=True) + approve_sales_manager = fields.Boolean('Approve Sales Manager', tracking=True, copy=False) + approve_finance = fields.Boolean('Approve Finance', tracking=True, copy=False) + approve_leader = fields.Boolean('Approve Pimpinan', tracking=True, copy=False) + reason = fields.Text('Reason', tracking=True) + approve_date = fields.Datetime('Approve Date') + state = fields.Selection([ + ('waiting_approval_sales_manager', 'Waiting Approval Sales Manager'), + ('waiting_approval_finance', 'Waiting Approval Finance'), + ('waiting_approval_leader', 'Waiting Approval Leader'), + ('approved', 'Approved'), + ('rejected', 'Rejected')], + default='waiting_approval_sales_manager', tracking=True) + reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) + sale_order_ids = fields.Many2many( + 'sale.order', + string='Sale Orders', + copy=False, + tracking=True + ) + + total = fields.Char( + string='Sale Order Totals', + compute='_compute_total' + ) + + grand_total = fields.Float(string='Grand Total', compute="_compute_grand_total") + + change_log_688 = fields.Text(string="Change Log", readonly=True, copy=False) + + def write(self, vals): + # Ambil nilai lama sebelum perubahan + old_values_dict = { + rec.id: rec.read(vals.keys())[0] + for rec in self + } + + res = super().write(vals) + + self._track_changes_for_user_688(vals, old_values_dict) + return res + + def _track_changes_for_user_688(self, vals, old_values_dict): + if self.env.user.id != 688: + return + + for rec in self: + changes = [] + old_values = old_values_dict.get(rec.id, {}) + + for field_name, new_value in vals.items(): + if field_name not in rec._fields or field_name == 'change_log_688': + continue + + field = rec._fields[field_name] + old_value = old_values.get(field_name) + + field_label = field.string # Ambil label user-friendly + + # Relational field + if field.type == 'many2one': + old_id = old_value[0] if old_value else False + is_different = old_id != new_value + if is_different: + old_display = old_value[1] if old_value else 'False' + new_display = rec.env[field.comodel_name].browse(new_value).display_name if new_value else 'False' + changes.append(f"[{field_label}] dari '{old_display}' ke '{new_display}'") + + else: + # Float khusus + if field.type == 'float': + is_different = not self._float_equal(old_value, new_value) + else: + is_different = old_value != new_value + + if is_different: + changes.append(f"[{field_label}] dari '{old_value}' ke '{new_value}'") + + if changes: + timestamp = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S') + rec.change_log_688 = f"{timestamp} - Perubahan oleh Widya:\n" + "\n".join(changes) + + + @staticmethod + def _float_equal(val1, val2, eps=1e-6): + try: + return abs(float(val1 or 0.0) - float(val2 or 0.0)) < eps + except Exception: + return False + + def _compute_grand_total(self): + for rec in self: + grand_total = sum(order.amount_total for order in rec.sale_order_ids) + rec.grand_total = grand_total + + def _compute_total(self): + for rec in self: + totals_list = [] + for order in rec.sale_order_ids: + formatted_total = "{:,.2f}".format(order.amount_total) + totals_list.append(f"{order.name}: {formatted_total}") + + rec.total = "\n".join(totals_list) if totals_list else "No Sale Orders" + + + @api.constrains('partner_id') + def constrains_partner_id(self): + if self.partner_id: + self.parent_id = self.partner_id.parent_id.id if self.partner_id.parent_id else None + self.blocking_stage = self.partner_id.blocking_stage + self.warning_stage = self.partner_id.warning_stage + self.active_limit = self.partner_id.active_limit + self.property_payment_term_id = self.partner_id.property_payment_term_id.id + + def button_approve(self): + user = self.env.user + is_it = user.has_group('indoteknik_custom.group_role_it') + + if (not user.id ==7 and user.id == 19 and not self.approve_sales_manager) or is_it: + self.approve_sales_manager = True + self.state = 'waiting_approval_finance' + return + + if (not user.id ==7 and user.id == 688 and not self.approve_finance) or is_it: + self.approve_finance = True + self.state = 'waiting_approval_leader' + return + + if (user.id == 7 and self.approve_finance) or is_it: + self.approve_leader = True + + if not self.approve_finance and not is_it: + raise UserError('Harus Approval Finance!!') + if not self.approve_leader and not is_it: + raise UserError('Harus Approval Pimpinan!!') + + if user.id == 7: + if not self.approve_finance: + raise UserError('Belum Di Approve Oleh Finance') + + if self.approve_leader == True: + self.partner_id.write({ + 'blocking_stage': self.blocking_stage, + 'warning_stage': self.warning_stage, + 'active_limit': self.active_limit, + 'property_payment_term_id': self.property_payment_term_id.id + }) + self.approve_date = datetime.utcnow() + self.state = 'approved' + + def button_reject(self): + if self.env.user.id not in [688, 7]: + raise UserError("Hanya Finance atau Pimpinan Yang Bisa Reject") + self.state = 'rejected' + + @api.model + def create(self, vals): + vals['number'] = self.env['ir.sequence'].next_by_code('approval.payment.term') or '0' + result = super(ApprovalPaymentTerm, self).create(vals) + return result diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 97184cdb..26b5df37 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -193,6 +193,7 @@ class CustomerCommision(models.Model): commision_amt_text = fields.Char(string='Amount Text', compute='compute_delivery_amt_text') total_cashback_text = fields.Char(string='Cashback Text', compute='compute_total_cashback_text') total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp') + biaya_lain_lain = fields.Float(string='Biaya Lain-lain') commision_type = fields.Selection([ ('fee', 'Fee'), ('cashback', 'Cashback'), @@ -363,13 +364,13 @@ class CustomerCommision(models.Model): else: self.cashback = 0 self.total_commision = 0 - + def _compute_total_dpp(self): for data in self: total_dpp = 0 for line in data.commision_lines: total_dpp = total_dpp + line.dpp - data.total_dpp = total_dpp + data.total_dpp = total_dpp - data.biaya_lain_lain @api.model def create(self, vals): diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index bb53fc0c..5a6aebac 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -92,10 +92,23 @@ class DunningRun(models.Model): ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('partner_id', '=', partner.id), - # ('amount_residual_signed', '>', 0), ('date_kirim_tukar_faktur', '=', False), ] - invoices = self.env['account.move'].search(query, order='invoice_date') + invoices = self.env['account.move'].search(query) + + # sort full berdasarkan tahun, bulan, nomor + def invoice_key(x): + try: + parts = x.name.split('/') + tahun = int(parts[1]) + bulan = int(parts[2]) + nomor = int(parts[3]) + return (tahun, bulan, nomor) + except Exception: + return (0, 0, 0) + + invoices = sorted(invoices, key=invoice_key) + count = 0 for invoice in invoices: self.env['dunning.run.line'].create([{ @@ -123,8 +136,9 @@ class DunningRunLine(models.Model): _name = 'dunning.run.line' _description = 'Dunning Run Line' # _order = 'dunning_id, id' - _order = 'invoice_id desc, id' + _order = 'invoice_number asc, id' + invoice_number = fields.Char('Invoice Number', related='invoice_id.name') dunning_id = fields.Many2one('dunning.run', string='Dunning Ref', required=True, ondelete='cascade', index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Customer') invoice_id = fields.Many2one('account.move', string='Invoice') diff --git a/indoteknik_custom/models/patch/__init__.py b/indoteknik_custom/models/patch/__init__.py new file mode 100644 index 00000000..051b6537 --- /dev/null +++ b/indoteknik_custom/models/patch/__init__.py @@ -0,0 +1 @@ +from . import http_override
\ No newline at end of file diff --git a/indoteknik_custom/models/patch/http_override.py b/indoteknik_custom/models/patch/http_override.py new file mode 100644 index 00000000..6bec1343 --- /dev/null +++ b/indoteknik_custom/models/patch/http_override.py @@ -0,0 +1,45 @@ +import odoo.http +import json +import logging +from werkzeug.exceptions import BadRequest +import functools + +_logger = logging.getLogger(__name__) + +class CustomJsonRequest(odoo.http.JsonRequest): + def __init__(self, httprequest): + super(odoo.http.JsonRequest, self).__init__(httprequest) + + self.params = {} + request_data_raw = self.httprequest.get_data().decode(self.httprequest.charset) + + self.jsonrequest = {} + if request_data_raw.strip(): + try: + self.jsonrequest = json.loads(request_data_raw) + except ValueError: + msg = 'Invalid JSON data: %r' % (request_data_raw,) + _logger.info('%s: %s (Handled by CustomJsonRequest)', self.httprequest.path, msg) + raise BadRequest(msg) + else: + _logger.info("CustomJsonRequest: Received empty or whitespace-only JSON body. Treating as empty JSON for webhook.") + + self.params = dict(self.jsonrequest.get("params", {})) + self.context = self.params.pop('context', dict(self.session.context)) + + +_original_get_request = odoo.http.Root.get_request + +@functools.wraps(_original_get_request) +def _get_request_override(self, httprequest): + _logger.info("--- DEBUG: !!! _get_request_override IS CALLED !!! ---") + _logger.info(f"--- DEBUG: Request Mimetype: {httprequest.mimetype}, Path: {httprequest.path} ---") + + if httprequest.mimetype in ("application/json", "application/json-rpc"): + _logger.debug("Odoo HTTP: Using CustomJsonRequest for mimetype: %s", httprequest.mimetype) + return CustomJsonRequest(httprequest) + else: + _logger.debug("Odoo HTTP: Using original get_request for mimetype: %s", httprequest.mimetype) + return _original_get_request(self, httprequest) + +odoo.http.Root.get_request = _get_request_override
\ No newline at end of file diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index f5347bea..52947128 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -168,12 +168,17 @@ class ResPartner(models.Model): payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3) payment_history_url = fields.Text(string='Payment History URL') + # no compute + # payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3) + + # tidak terpakai @api.depends('parent_id.payment_difficulty') def _compute_payment_difficulty(self): for partner in self: if partner.parent_id: partner.payment_difficulty = partner.parent_id.payment_difficulty + # tidak terpakai def _inverse_payment_difficulty(self): for partner in self: if not partner.parent_id: diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 591951ca..e197a6af 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -3048,6 +3048,10 @@ class SaleOrder(models.Model): if picking.state == 'assigned': picking.carrier_id = vals['carrier_id'] + for picking in order.picking_ids: + if picking.state not in ['done', 'cancel', 'assigned']: + picking.write({'carrier_id': vals['carrier_id']}) + try: helper_ids = self._get_helper_ids() if str(self.env.user.id) in helper_ids: diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 291940ed..2a0160e8 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta class SaleOrderLine(models.Model): _inherit = 'sale.order.line' + item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', help="Total Margin in Sales Order Header") diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 1827c489..f3d7c2b9 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -20,8 +20,10 @@ _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" + + # biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" - + class StockPicking(models.Model): _inherit = 'stock.picking' @@ -123,7 +125,7 @@ class StockPicking(models.Model): waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill') purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', string="Purchase Representative") - carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method') + carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3) shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status") date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False) status_printed = fields.Selection([ @@ -275,16 +277,29 @@ class StockPicking(models.Model): # Biteship Section biteship_id = fields.Char(string="Biteship Respon ID") - biteship_tracking_id = fields.Char(string="Biteship Trackcking ID") + biteship_tracking_id = fields.Char(string="Biteship Tracking ID") biteship_waybill_id = fields.Char(string="Biteship Waybill ID") + biteship_driver_name = fields.Char('Biteship Driver Name') + biteship_driver_phone = fields.Char('Biteship Driver Phone') + biteship_driver_plate_number = fields.Char('Biteship Driver Plate Number') + biteship_courier_link = fields.Char('Biteship Courier Link') + biteship_shipping_status = fields.Char('Biteship Shipping Status', help="Status pengiriman dari Biteship") + biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', + help="Harga pengiriman dari Biteship") + currency_id = fields.Many2one('res.currency', related='sale_id.currency_id', string='Currency', readonly=True) final_seq = fields.Float(string='Remaining Time') - shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', related='sale_id.carrier_id') - shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option SO', related='sale_id.shipping_option_id') + shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', + help="Shipping Method yang digunakan di SO", tracking=3) + shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', + related='sale_id.shipping_option_id', + help="Shipping Option yang digunakan di SO", tracking=3) select_shipping_option_so = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), - ], string='Shipping Type SO', related='sale_id.select_shipping_option') - state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') + ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO", + tracking=3) + state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], + string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False) update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD') @@ -295,7 +310,7 @@ class StockPicking(models.Model): if not self.name or not self.origin: return False return f"{self.name}" - + def _download_pod_photo(self, url): """Mengunduh foto POD dari URL""" try: @@ -304,7 +319,7 @@ class StockPicking(models.Model): return base64.b64encode(response.content) except Exception as e: raise UserError(f"Gagal mengunduh foto POD: {str(e)}") - + def _parse_datetime(self, dt_str): """Parse datetime string dari format KGX""" try: @@ -315,37 +330,37 @@ class StockPicking(models.Model): return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S') except ValueError: return False - + def action_get_kgx_pod(self, shipment=False): self.ensure_one() - + awb_number = shipment or self._get_kgx_awb_number() if not awb_number: raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin") - + url = "https://kgx.co.id/get_detail_awb" headers = {'Content-Type': 'application/json'} - payload = {"params" : {'awb_number': awb_number}} - + payload = {"params": {'awb_number': awb_number}} + try: response = requests.post(url, headers=headers, data=json.dumps(payload)) response.raise_for_status() data = response.json() - + if data.get('result', {}).get('data', []): pod_data = data['result']['data'][0].get('connote_pod', {}) photo_url = pod_data.get('photo') - + self.kgx_pod_photo_url = photo_url self.kgx_pod_signature = pod_data.get('signature') self.kgx_pod_receiver = pod_data.get('receiver') self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive')) self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive')) - + return data else: raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}") - + except requests.exceptions.RequestException as e: raise UserError(f"Gagal mengambil data POD: {str(e)}") @@ -692,8 +707,9 @@ class StockPicking(models.Model): raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") if self.sale_id.select_shipping_option == 'custom': - raise UserError("Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.") - + raise UserError( + "Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.") + def is_courier_need_coordinates(service_code): return service_code in [ "instant", "same_day", "instant_car", @@ -790,11 +806,11 @@ class StockPicking(models.Model): self.message_post( body=f"Biteship berhasil dilakukan.<br/>" - f"Kurir: {self.carrier_id.name}<br/>" - f"Tracking ID: {self.biteship_tracking_id or '-'}<br/>" - f"Resi: {waybill_id or '-'}<br/>" - f"Reference: {self.name}<br/>" - f"SO: {self.sale_id.name}", + f"Kurir: {self.carrier_id.name}<br/>" + f"Tracking ID: {self.biteship_tracking_id or '-'}<br/>" + f"Resi: {waybill_id or '-'}<br/>" + f"Reference: {self.name}<br/>" + f"SO: {self.sale_id.name}", message_type="comment" ) @@ -936,6 +952,9 @@ class StockPicking(models.Model): pending_section = None # Invoice values. invoice_vals = order._prepare_invoice() + invoice_date = self.date_done + invoice_vals['date'] = invoice_date + invoice_vals['invoice_date'] = invoice_date # Invoice line values (keep only necessary sections). for line in self.move_ids_without_package: po_line = self.env['purchase.order.line'].search( @@ -1307,7 +1326,6 @@ class StockPicking(models.Model): ) ) - self.validation_minus_onhand_quantity() self.responsible = self.env.user.id # self.send_koli_to_so() @@ -1358,7 +1376,7 @@ class StockPicking(models.Model): if 'BU/PUT' in self.name: self.automatic_reserve_product() return res - + def automatic_reserve_product(self): if self.state == 'done': po = self.env['purchase.order'].search([ @@ -1376,11 +1394,12 @@ class StockPicking(models.Model): continue invoice = self.env['account.move'].search( - [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), ('move_type', '=', 'out_invoice')], limit=1) + [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), + ('move_type', '=', 'out_invoice')], limit=1) if not invoice: continue - + if not picking.so_lama and invoice and (not picking.date_doc_kirim or not invoice.invoice_date): raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!") @@ -1664,7 +1683,7 @@ class StockPicking(models.Model): self.ensure_one() order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) - + sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', order.name)], limit=1) product_shipped = [] @@ -1679,11 +1698,12 @@ class StockPicking(models.Model): 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '-', - 'service' : order.delivery_service_type or '-', + 'service': order.delivery_service_type or '-', 'receiver_name': '', 'receiver_city': '' }, - 'delivered_date': self.driver_departure_date.strftime('%d %b %Y') if self.driver_departure_date != False else '-', + 'delivered_date': self.driver_departure_date.strftime( + '%d %b %Y') if self.driver_departure_date != False else '-', 'delivered': False, 'status': self.shipping_status, 'waybill_number': self.delivery_tracking_no or '-', @@ -1706,7 +1726,7 @@ class StockPicking(models.Model): elif sale_order_delay.status == 'early': day_start = day_start - sale_order_delay.days_delayed day_end = day_end - sale_order_delay.days_delayed - + eta_start = order.date_order + timedelta(days=day_start) eta_end = order.date_order + timedelta(days=day_end) formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" @@ -1735,7 +1755,7 @@ class StockPicking(models.Model): "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - + manifests = [] try: @@ -1743,15 +1763,15 @@ class StockPicking(models.Model): response = requests.get(_biteship_url + '/trackings/' + self.biteship_tracking_id, headers=headers, json=manifests) result = response.json() - description = { - 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', - 'allocated' : 'Kurir akan melakukan pick-up pesanan', - 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', - 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), - 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', - 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', - 'delivered' : f'Pesanan telah sampai dan diterima oleh <span style="color:#DC2626;">{result.get("destination", {}).get("contact_name", "")}</span>' - } + # description = { + # 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', + # 'allocated' : 'Kurir akan melakukan pick-up pesanan', + # 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', + # 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("company", ""), + # 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', + # 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + # 'delivered' : f'Pesanan telah sampai dan diterima oleh <span style="color:#DC2626;">{result.get("destination", {}).get("contact_name", "")}</span>' + # } if (result.get('success') == True): history = result.get("history", []) status = result.get("status", "") @@ -1760,7 +1780,7 @@ class StockPicking(models.Model): manifests.append({ "status": re.sub(r'[^a-zA-Z0-9\s]', ' ', entry["status"]).lower().capitalize(), "datetime": self._convert_to_local_time(entry["updated_at"]), - "description": description[entry["status"]], + "description": self._get_biteship_status_description(entry["status"], result), }) return { @@ -1772,57 +1792,163 @@ class StockPicking(models.Model): "manifests": [], "delivered": False } - except Exception as e : + except Exception as e: _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") - return { 'error': str(e) } - - def action_sync_biteship_tracking(self): - for picking in self: - if not picking.biteship_id: - raise UserError("Tracking Biteship tidak tersedia.") - - histori = picking.get_manifest_biteship() - updated_fields = {} - seen_logs = set() - - manifests = sorted(histori.get("manifests", []), key=lambda m: m.get("datetime") or "") - - for manifest in manifests: - status = manifest.get("status", "").lower() - dt_str = manifest.get("datetime") - desc = manifest.get("description") - dt = False - - try: - dt = picking._convert_to_utc_datetime(dt_str) - _logger.info(f"[Biteship Sync] Berhasil parse datetime: {dt_str} -> {dt}") - except Exception as e: - _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") - continue + return {'error': str(e)} + + # ACTION GET TRACKING MANUAL BITESHIP + # def action_sync_biteship_tracking(self): + # for picking in self: + # if not picking.biteship_id: + # raise UserError("Tracking Biteship tidak tersedia.") + + # histori = picking.get_manifest_biteship() + # updated_fields = {} + # seen_logs = set() + + # manifests = sorted(histori.get("manifests", []), key=lambda m: m.get("datetime") or "") + + # for manifest in manifests: + # status = manifest.get("status", "").lower() + # dt_str = manifest.get("datetime") + # desc = manifest.get("description") + # dt = False + + # try: + # dt = picking._convert_to_utc_datetime(dt_str) + # _logger.info(f"[Biteship Sync] Berhasil parse datetime: {dt_str} -> {dt}") + # except Exception as e: + # _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") + # continue + + # # Update tanggal ke field (pastikan naive datetime UTC) + # if status == "picked" and dt and not picking.driver_departure_date: + # updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) + + # if status == "delivered" and dt and not picking.driver_arrival_date: + # updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) + + # # Buat log unik dengan waktu lokal Asia/Jakarta + # if dt and desc: + # try: + # dt_local = parser.parse(dt_str).replace(tzinfo=None) + # except Exception as e: + # _logger.warning(f"[Biteship Sync] Gagal parse dt_str untuk log: {e}") + # dt_local = dt # fallback + + # desc_clean = ' '.join(desc.strip().split()) + # log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" + # if not picking._has_existing_log(log_line): + # picking.message_post(body=log_line) + # seen_logs.add(log_line) + + # if updated_fields: + # picking.write(updated_fields) + + def action_open_biteship_tracking(self): + self.ensure_one() + if not self.biteship_courier_link: + raise UserError("Biteship tracking link tidak tersedia.") + return { + 'type': 'ir.actions.act_url', + 'url': self.biteship_courier_link, + 'target': 'new', + } - # Update tanggal ke field (pastikan naive datetime UTC) - if status == "picked" and dt and not picking.driver_departure_date: - updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) + def _get_biteship_status_description(self, status, data=None): + data = data or {} + courier = data.get("courier", {}).get("company", "") + contact_name = data.get("destination", {}).get("contact_name", "") + + description_map = { + 'confirmed': 'Indoteknik telah melakukan permintaan pick-up', + 'allocated': 'Kurir akan melakukan pick-up pesanan', + 'picking_up': 'Kurir sedang dalam perjalanan menuju lokasi pick-up', + 'picked': f'Pesanan sudah di pick-up kurir {courier}', + 'dropping_off': 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + 'delivered': f'Pesanan telah sampai dan diterima oleh <span style="color:#DC2626;">{contact_name}</span>', + 'return_in_transit': 'Pesanan dalam perjalanan kembali ke pengirim', + 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman', + 'rejected': 'Pesanan ditolak, silakan hubungi Biteship', + 'courier_not_found': 'Pesanan dibatalkan karena tidak ada kurir tersedia', + 'returned': 'Pesanan berhasil dikembalikan', + 'disposed': 'Pesanan sudah dimusnahkan', + 'cancelled': 'Pesanan dibatalkan oleh sistem atau pengguna', + } - if status == "delivered" and dt and not picking.driver_arrival_date: - updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) + return description_map.get(status, f"Status '{status}' diterima dari Biteship") - # Buat log unik dengan waktu lokal Asia/Jakarta - if dt and desc: - try: - dt_local = parser.parse(dt_str).replace(tzinfo=None) - except Exception as e: - _logger.warning(f"[Biteship Sync] Gagal parse dt_str untuk log: {e}") - dt_local = dt # fallback + def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None): + self.ensure_one() + updated_fields = {} - desc_clean = ' '.join(desc.strip().split()) - log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" - if not picking._has_existing_log(log_line): - picking.message_post(body=log_line) - seen_logs.add(log_line) + try: + dt = self._convert_to_utc_datetime(timestamp) + except Exception as e: + _logger.warning(f"[Webhook] Gagal konversi waktu: {e}") + dt = datetime.utcnow() + + # Penanganan status pengiriman + if status == "picked" and not self.driver_departure_date: + updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) + if status == "delivered" and not self.driver_arrival_date: + updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) + + shipping_status = self._map_status_biteship(status) + if shipping_status and self.shipping_status != shipping_status: + updated_fields["shipping_status"] = shipping_status + + # Penanganan extra data dari webhook + if extra_data: + # Informasi kurir + if extra_data.get("courier_driver_name"): + updated_fields["biteship_driver_name"] = extra_data["courier_driver_name"] + if extra_data.get("courier_driver_phone"): + updated_fields["biteship_driver_phone"] = extra_data["courier_driver_phone"] + if extra_data.get("courier_driver_plate_number"): + updated_fields["biteship_driver_plate_number"] = extra_data["courier_driver_plate_number"] + if extra_data.get("courier_link"): + updated_fields["biteship_courier_link"] = extra_data["courier_link"] + # Informasi harga + if extra_data.get("order_price"): + updated_fields["biteship_shipping_price"] = extra_data["order_price"] + # Status mentah dari Biteship + if extra_data.get("status"): + updated_fields["biteship_shipping_status"] = extra_data["status"] + + # Tambahan untuk handle order.waybill_id + if extra_data.get("tracking_id"): + updated_fields["biteship_tracking_id"] = extra_data["tracking_id"] + updated_fields["delivery_tracking_no"] = extra_data["tracking_id"] + if extra_data.get("waybill_id"): + updated_fields["biteship_waybill_id"] = extra_data["waybill_id"] + + # Konversi waktu lokal untuk log + try: + dt_parsed = parser.parse(timestamp) + if dt_parsed.tzinfo is None: + dt_parsed = dt_parsed.replace(tzinfo=pytz.utc) + dt_local = dt_parsed.astimezone(pytz.timezone("Asia/Jakarta")) + except Exception: + dt_local = dt.astimezone(pytz.timezone("Asia/Jakarta")) + + # Format pesan log + desc_clean = ' '.join(description.strip().split()) + log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}:<br/>{desc_clean}" + + # Hindari log duplikat + if not self._has_existing_log(log_line): + biteship_user = self.env['res.users'].sudo().browse(15710) # ID live + # biteship_user = self.env['res.users'].sudo().browse(15710) # ID user (cek di db) + self.sudo().message_post( + body=log_line, + author_id=biteship_user.partner_id.id + ) - if updated_fields: - picking.write(updated_fields) + # Update field-field terkait + if updated_fields: + self.write(updated_fields) + _logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}") def _has_existing_log(self, log_line): self.ensure_one() @@ -1872,10 +1998,15 @@ class StockPicking(models.Model): "allocated": "pending", "picking_up": "pending", "picked": "shipment", - "cancelled": "cancelled", - "on_hold": "on_hold", "dropping_off": "shipment", - "delivered": "completed" + "delivered": "completed", + "return_in_transit": "returning", + "on_hold": "on_hold", + "rejected": "cancelled", + "courier_not_found": "cancelled", + "returned": "returned", + "disposed": "disposed", + "cancelled": "cancelled" } return status_mapping.get(status, "Hubungi Admin") @@ -1885,8 +2016,7 @@ class StockPicking(models.Model): days_end = self.sale_id.estimated_arrival_days or (self.sale_id.estimated_arrival_days + 3) start_date = self.sale_id.create_date + datetime.timedelta(days=days_start) end_date = self.sale_id.create_date + datetime.timedelta(days=days_end) - - + add_day_start = 0 add_day_end = 0 sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', self.sale_id.name)], limit=1) @@ -1895,11 +2025,11 @@ class StockPicking(models.Model): add_day_start = sale_order_delay.days_delayed add_day_end = sale_order_delay.days_delayed elif sale_order_delay.status == 'early': - add_day_start = -abs(sale_order_delay.days_delayed) - add_day_end = -abs(sale_order_delay.days_delayed) - - fastest_eta = start_date +datetime.timedelta(days=add_day_start + add_day_start) - + add_day_start = -abs(sale_order_delay.days_delayed) + add_day_end = -abs(sale_order_delay.days_delayed) + + fastest_eta = start_date + datetime.timedelta(days=add_day_start + add_day_start) + longest_eta = end_date + datetime.timedelta(days=add_day_end) format_time = '%d %b %Y' diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index c2b0895d..85781524 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -182,6 +182,8 @@ access_sale_order_delay,sale.order.delay,model_sale_order_delay,,1,1,1,1 access_production_purchase_match,access.production.purchase.match,model_production_purchase_match,,1,1,1,1 access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1 access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1 +access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,1,1,1,1 + access_tukar_guling_all_users,tukar.guling.all.users,model_tukar_guling,base.group_user,1,1,1,1 access_tukar_guling_line_all_users,tukar.guling.line.all.users,model_tukar_guling_line,base.group_user,1,1,1,1 access_tukar_guling_po_all_users,tukar.guling.po.all.users,model_tukar_guling_po,base.group_user,1,1,1,1 diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml index ad52a74a..2f52b3d9 100644 --- a/indoteknik_custom/views/account_move.xml +++ b/indoteknik_custom/views/account_move.xml @@ -70,6 +70,7 @@ <field name="so_shipping_covered_by"/> <field name="so_delivery_amt"/> <field name="flag_delivery_amt"/> + <field name="length_of_payment"/> </field> <field name="amount_untaxed" position="after"> <field name="other_subtotal" invisible="1"/> diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml new file mode 100644 index 00000000..cc9db914 --- /dev/null +++ b/indoteknik_custom/views/approval_payment_term.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <record id="approval_payment_term_tree" model="ir.ui.view"> + <field name="name">approval.payment.term.tree</field> + <field name="model">approval.payment.term</field> + <field name="arch" type="xml"> + <tree default_order="create_date desc"> + <field name="number"/> + <field name="partner_id"/> + <field name="parent_id"/> + <field name="property_payment_term_id"/> + <field name="create_date" optional="hide"/> + <field name="approve_date" optional="hide"/> + <field name="approve_sales_manager" optional="hide"/> + <field name="approve_finance" optional="hide"/> + <field name="approve_leader" optional="hide"/> + <field name="create_uid" optional="hide"/> + <field name="sale_order_ids" optional="hide" widget="many2many_tags"/> + <field name="total" optional="hide"/> + <field name="grand_total" optional="hide"/> + <field name="state" widget="badge" decoration-danger="state == 'rejected'" + decoration-success="state == 'approved'" + decoration-info="state in ['waiting_approval_sales_manager', 'waiting_approval_finance', 'waiting_approval_leader']"/> + <field name="change_log_688" optional="hide"/> + </tree> + </field> + </record> + + <record id="approval_payment_term_form" model="ir.ui.view"> + <field name="name">approval.payment.term.form</field> + <field name="model">approval.payment.term</field> + <field name="arch" type="xml"> + <form> + <header> + <button name="button_approve" + string="Approve" + type="object" + attrs="{'invisible': [('approve_leader', '=', True)]}" + /> + <button name="button_reject" + string="Reject" + type="object" + attrs="{'invisible': [('approve_leader', '=', True)]}" + /> + <field name="state" widget="statusbar" + statusbar_visible="waiting_approval,approved,rejected" + statusbar_colors='{"rejected":"red"}'/> + </header> + <sheet string="Approval Payment Term"> + <group> + <group> + <field name="number" readonly="1"/> + <field name="partner_id"/> + <field name="parent_id" readonly="1"/> + <field name="blocking_stage" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/> + <field name="warning_stage" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/> + <field name="property_payment_term_id" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/> + <field name="active_limit" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/> + </group> + <group> + <field name="reason"/> + <field name="reason_reject" attrs="{'invisible': [('state', '!=', 'rejected')]}"/> + <field name="approve_date" readonly="1"/> + <field name="approve_sales_manager" readonly="1"/> + <field name="approve_finance" readonly="1"/> + <field name="approve_leader" readonly="1"/> + <field name="sale_order_ids" widget="many2many_tags"/> + <field name="total"/> + <field name="grand_total"/> + </group> + </group> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> + </form> + </field> + </record> + + <record id="approval_payment_term_action" model="ir.actions.act_window"> + <field name="name">Approval Payment Term</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">approval.payment.term</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="menu_approval_payment_term" name="Approval Payment Term" + parent="account.menu_finance_receivables" + action="approval_payment_term_action" + sequence="100" + /> + +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/customer_commision.xml b/indoteknik_custom/views/customer_commision.xml index 1c17bf63..514e6284 100644 --- a/indoteknik_custom/views/customer_commision.xml +++ b/indoteknik_custom/views/customer_commision.xml @@ -103,6 +103,7 @@ <field name="notification" readonly="1"/> <!-- <field name="status" readonly="1"/>--> <field name="payment_status" readonly="1"/> + <field name="biaya_lain_lain"/> <field name="total_dpp"/> </group> </group> diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml index 8ae532cc..4da9a8bc 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -150,6 +150,16 @@ <field name="number_increment">1</field> <field name="active">True</field> </record> + + <record id="sequence_approval_payment_term" model="ir.sequence"> + <field name="name">Approval Payment Term</field> + <field name="code">approval.payment.term</field> + <field name="prefix">APT/%(year)s/</field> + <field name="padding">5</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + <field name="active">True</field> + </record> <record id="sequence_commision_fee" model="ir.sequence"> <field name="name">Customer Commision Fee</field> diff --git a/indoteknik_custom/views/mail_template_invoice_reminder.xml b/indoteknik_custom/views/mail_template_invoice_reminder.xml new file mode 100644 index 00000000..21055eb0 --- /dev/null +++ b/indoteknik_custom/views/mail_template_invoice_reminder.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="0"> + <record id="mail_template_invoice_due_reminder" model="mail.template"> + <field name="name">Invoice Reminder: Due Date Notification</field> + <field name="model_id" ref="account.model_account_move"/> + <field name="subject">Reminder Invoice Due - ${object.name}</field> + <field name="email_from">finance@indoteknik.co.id</field> + <field name="email_to">andrifebriyadiputra@gmail.com</field> + <field name="body_html" type="html"> + <div> + <p><b>Dear ${object.name},</b></p> + + <p>Berikut adalah daftar invoice Anda yang mendekati atau telah jatuh tempo:</p> + + <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; width: 100%; font-size: 12px"> + <thead> + <tr style="background-color: #f2f2f2;" align="left"> + <th>Invoice Number</th> + <th>Tanggal Invoice</th> + <th>Jatuh Tempo</th> + <th>Sisa Hari</th> + <th>Total</th> + <th>Referensi</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + + <p>Mohon bantuan dan kerjasamanya agar tetap bisa bekerjasama dengan baik</p> + <p>Terima Kasih.</p> + <br/> + <br/> + <p><b>Best Regards, + <br/> + <br/> + Widya R.<br/> + Dept. Finance<br/> + PT. INDOTEKNIK DOTCOM GEMILANG<br/> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" alt="Indoteknik" style="max-width: 18%; height: auto;"></img><br/> + <a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> | + <a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a> + </b></p> + + </div> + </field> + <field name="auto_delete" eval="True"/> + </record> + </data> +</odoo> diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml index ac4d0364..a030a75c 100644 --- a/indoteknik_custom/views/res_partner.xml +++ b/indoteknik_custom/views/res_partner.xml @@ -108,13 +108,6 @@ <xpath expr="//field[@name='property_supplier_payment_term_id']" position="attributes"> <attribute name="readonly">1</attribute> </xpath> - <xpath expr="//notebook/page[@name='accounting']" position="inside"> - <group string="Aging Info"> - <field name="avg_aging" readonly="1"/> - <field name="payment_difficulty" attrs="{'readonly': [('parent_id', '!=', False)]}" /> - <field name="payment_history_url" readonly="1" /> - </group> - </xpath> <notebook> <page string="Pengajuan Tempo"> <!-- Informasi Usaha Section --> @@ -188,11 +181,6 @@ <field name="dokumen_pengiriman_input"/> <field name="dokumen_invoice"/> </group> - <!-- <group string="Aging Info"> - <field name="avg_aging" readonly="1"/> - <field name="payment_difficulty" attrs="{'readonly': [('parent_id', '!=', False)]}" /> - <field name="payment_history_url" readonly="1" /> - </group> --> </group> <!-- Supplier Lines Section --> @@ -225,6 +213,13 @@ <field name="dokumen_ktp_dirut" /> </group> </page> + <page string="Aging Info"> + <group string="Aging Info"> + <field name="avg_aging" readonly="1"/> + <field name="payment_difficulty" attrs="{'readonly': [('parent_id', '!=', False)]}" /> + <field name="payment_history_url" readonly="1" /> + </group> + </page> </notebook> </field> </record> diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index f4159b1b..f9200dfa 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -64,12 +64,12 @@ string="Biteship" type="object" /> - <button name="action_sync_biteship_tracking" + <!-- <button name="action_sync_biteship_tracking" type="object" string="Lacak dari Biteship" class="btn-primary" attrs="{'invisible': [('biteship_id', '=', False)]}" - /> + /> --> <button name="track_envio_shipment" string="Tracking Envio" type="object" @@ -192,19 +192,33 @@ <field name="note_logistic"/> <field name="note_info"/> <field name="responsible" /> - <field name="carrier_id"/> + <field name="carrier_id" attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}" /> <field name="biteship_id" invisible="1"/> <field name="out_code" attrs="{'invisible': [['out_code', '=', False]]}"/> <field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/> <field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/> <field name="driver_departure_date" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> <field name="driver_arrival_date"/> - <field name="delivery_tracking_no"/> + <field name="delivery_tracking_no" attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/> <field name="driver_id"/> <field name='sj_return_date'/> <field name="sj_documentation" widget="image" /> <field name="paket_documentation" widget="image" /> </group> + <!-- Biteship Group --> + <group attrs="{'invisible': [('select_shipping_option_so', '!=', 'biteship')]}"> + <field name="delivery_tracking_no" /> + <field name="shipping_method_so_id"/> + <field name="shipping_option_so_id"/> + <field name="biteship_shipping_price" readonly="1"/> + <field name="currency_id" invisible="1"/> + <field name="biteship_shipping_status" readonly="1"/> + <field name="biteship_driver_name" readonly="1"/> + <field name="biteship_driver_phone" readonly="1"/> + <field name="biteship_driver_plate_number" readonly="1"/> + <button name="action_open_biteship_tracking" string="Visit Biteship Tracking" type="object"/> + </group> + <group attrs="{'invisible': [('carrier_id', '!=', 151)]}"> <field name="envio_id" invisible="1"/> <field name="envio_code"/> |
