summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/sale_order.py
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-07-28 15:09:55 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-07-28 15:09:55 +0700
commitd15ce4e186e2b77f01e8dfd03886298cc733d4c1 (patch)
tree1b32a4c29c4fcea85070fcecb5b77a7d55d30029 /indoteknik_custom/models/sale_order.py
parentdeba962d7368a5c4e30441b5a640102608e3dde6 (diff)
parent36a53535dbdc5777266fd9276b4c557259dab6be (diff)
<hafid> merging odoo-backup
Diffstat (limited to 'indoteknik_custom/models/sale_order.py')
-rwxr-xr-xindoteknik_custom/models/sale_order.py1568
1 files changed, 1384 insertions, 184 deletions
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index f89dfb10..7be0e8ff 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -2,11 +2,13 @@ from re import search
from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone, time
import logging, random, string, requests, math, json, re, qrcode, base64
import pytz
from io import BytesIO
from collections import defaultdict
+import pytz
+from lxml import etree
_logger = logging.getLogger(__name__)
@@ -65,6 +67,7 @@ class ShippingOption(models.Model):
price = fields.Float(string="Price", required=True)
provider = fields.Char(string="Provider")
etd = fields.Char(string="Estimated Delivery Time")
+ courier_service_code = fields.Char(string="Courier Service Code")
sale_order_id = fields.Many2one('sale.order', string="Sale Order", ondelete="cascade")
@@ -72,6 +75,7 @@ class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def unlink(self):
+
lines_to_reject = []
for line in self:
if line.order_id:
@@ -121,6 +125,7 @@ class SaleOrderLine(models.Model):
class SaleOrder(models.Model):
_inherit = "sale.order"
+ ccm_id = fields.Many2one('tukar.guling', string='Doc. CCM', readonly=True, compute='_has_ccm', copy=False)
ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True,
tracking=3)
@@ -148,8 +153,8 @@ class SaleOrder(models.Model):
help="Total Margin in Sales Order Header")
total_percent_margin = fields.Float('Total Percent Margin', compute='_compute_total_percent_margin',
help="Total % Margin in Sales Order Header")
- total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header",
- compute='_compute_total_margin_excl_third_party')
+ total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header")
+
approval_status = fields.Selection([
('pengajuan1', 'Approval Manager'),
('pengajuan2', 'Approval Pimpinan'),
@@ -324,8 +329,13 @@ class SaleOrder(models.Model):
string="Attachment Bukti Cancel", readonly=False,
)
nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3)
- shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option",
- domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]")
+
+ shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]")
+ select_shipping_option = fields.Selection([
+ ('biteship', 'Biteship'),
+ ('custom', 'Custom'),
+ ], string='Shipping Option', help="Select shipping option for delivery", tracking=True, default='custom')
+
hold_outgoing = fields.Boolean('Hold Outgoing SO', tracking=3)
state_ask_cancel = fields.Selection([
('hold', 'Hold'),
@@ -340,16 +350,364 @@ class SaleOrder(models.Model):
date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold'
)
- def _compute_total_margin_excl_third_party(self):
+ et_products = fields.Datetime(string='ET Products', compute='_compute_et_products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.")
+
+ eta_date_reserved = fields.Datetime(
+ string="Date Reserved",
+ compute="_compute_eta_date_reserved",
+ help="Tanggal pertama kali barang berhasil di-reservasi pada DO (BU/PICK/) yang berstatus Siap Dikirim."
+ )
+ refund_ids = fields.Many2many('refund.sale.order', compute='_compute_refund_ids', string='Refunds')
+ has_refund = fields.Boolean(string='Has Refund', compute='_compute_has_refund')
+ refund_count = fields.Integer(string='Refund Count', compute='_compute_refund_count')
+ advance_payment_move_id = fields.Many2one(
+ 'account.move',
+ compute='_compute_advance_payment_move',
+ string='Advance Payment Move',
+ )
+ advance_payment_move_ids = fields.Many2many(
+ 'account.move',
+ compute='_compute_advance_payment_moves',
+ string='All Advance Payment Moves',
+ )
+
+ advance_payment_move_count = fields.Integer(
+ string='Jumlah Jurnal Uang Muka',
+ compute='_compute_advance_payment_moves',
+ store=False
+ )
+
+ def _has_ccm(self):
+ if self.id:
+ self.ccm_id = self.env['tukar.guling'].search([('origin', 'ilike', self.name)], limit=1)
+
+ @api.depends('order_line.product_id', 'date_order')
+ def _compute_et_products(self):
+ jakarta = pytz.timezone("Asia/Jakarta")
for order in self:
- if order.amount_untaxed == 0:
- order.total_margin_excl_third_party = 0
+ if not order.order_line or not order.date_order:
+ order.et_products = False
continue
- # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2)
- order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2)
- # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2)
+ # Ambil tanggal order sebagai basis
+ base_date = order.date_order
+ if base_date.tzinfo is None:
+ base_date = jakarta.localize(base_date)
+ else:
+ base_date = base_date.astimezone(jakarta)
+
+ # Ambil nilai SLA vendor dalam hari
+ sla_data = order.calculate_sla_by_vendor(order.order_line)
+ sla_days = sla_data.get('slatime', 1)
+
+ # Hitung ETA produk (tanpa logistik)
+ eta_datetime = base_date + timedelta(days=sla_days)
+ # Simpan ke field sebagai UTC-naive datetime (standar Odoo)
+ order.et_products = eta_datetime.astimezone(pytz.utc).replace(tzinfo=None)
+
+ @api.depends('picking_ids.state', 'picking_ids.date_done')
+ def _compute_eta_date_reserved(self):
+ for order in self:
+ pickings = order.picking_ids.filtered(
+ lambda p: p.state in ('assigned', 'done') and p.date_reserved and 'BU/PICK/' in (p.name or '')
+ )
+ done_dates = [d for d in pickings.mapped('date_done') if d]
+ order.eta_date_reserved = min(done_dates) if done_dates else False
+ # order.eta_date_reserved = min(pickings.mapped('date_done')) if pickings else False
+
+ @api.onchange('shipping_cost_covered')
+ def _onchange_shipping_cost_covered(self):
+ if self.shipping_cost_covered == 'indoteknik' and self.select_shipping_option == 'biteship':
+ self.shipping_cost_covered = 'customer'
+ return {
+ 'warning': {
+ 'title': "Biteship Tidak Diizinkan",
+ 'message': (
+ "Biaya pengiriman ditanggung Indoteknik, sehingga tidak diizinkan menggunakan metode Biteship. "
+ "Pilihan penanggung biaya akan dikembalikan sebelumnya"
+ )
+ }
+ }
+
+ def get_biteship_carrier_ids(self):
+ courier_codes = tuple(self._get_biteship_courier_codes() or [])
+ if not courier_codes:
+ return []
+
+ self.env.cr.execute("""
+ SELECT delivery_carrier_id
+ FROM rajaongkir_kurir
+ WHERE name IN %s AND delivery_carrier_id IS NOT NULL
+ """, (courier_codes,))
+ result = self.env.cr.fetchall()
+ carrier_ids = [row[0] for row in result if row[0]]
+ return carrier_ids
+
+ # @api.model
+ # def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
+ # res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
+
+ # if view_type == 'form':
+ # doc = etree.XML(res['arch'])
+
+ # # Ambil semua delivery_carrier_id dari mapping rajaongkir_kurir
+ # biteship_ids = self.env['rajaongkir.kurir'].search([]).mapped('delivery_carrier_id.id')
+ # biteship_ids = list(set(filter(None, biteship_ids))) # pastikan unik dan bukan None
+
+ # all_ids = self.env['delivery.carrier'].search([]).ids
+ # custom_ids = list(set(all_ids) - set(biteship_ids))
+
+ # # Format sebagai string Python list
+ # biteship_ids_str = ','.join(str(i) for i in biteship_ids) or '-1'
+ # custom_ids_str = ','.join(str(i) for i in custom_ids) or '-1'
+
+ # # Terapkan domain ke field carrier_id
+ # for node in doc.xpath("//field[@name='carrier_id']"):
+ # # Domain tergantung select_shipping_option
+ # node.set(
+ # 'domain',
+ # "[('id', 'in', [%s]) if select_shipping_option == 'biteship' else ('id', 'in', [%s])]" %
+ # (biteship_ids_str, custom_ids_str)
+ # )
+
+ # # Simpan kembali hasil XML ke arsitektur form
+ # res['arch'] = etree.tostring(doc, encoding='unicode')
+
+ # return res
+
+ # @api.onchange('shipping_option_id')
+ # def _onchange_shipping_option_id(self):
+ # if self.shipping_option_id:
+ # self.delivery_amt = self.shipping_option_id.price
+ # self.delivery_service_type = self.shipping_option_id.courier_service_code
+
+ def _get_biteship_courier_codes(self):
+ return [
+ 'gojek','grab','deliveree','lalamove','jne','tiki','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','pos','anteraja','sap','paxel','borzo'
+ ]
+
+ @api.onchange('carrier_id')
+ def _onchange_carrier_id(self):
+ if not self._origin or not self._origin.id:
+ return
+
+ sale_order_id = self._origin.id
+ self.shipping_option_id = False
+
+ if not self.carrier_id:
+ return {'domain': {'shipping_option_id': [('id', '=', -1)]}}
+
+ # Ambil provider dari mapping
+ self.env.cr.execute("""
+ SELECT name FROM rajaongkir_kurir
+ WHERE delivery_carrier_id = %s LIMIT 1
+ """, (self.carrier_id.id,))
+ row = self.env.cr.fetchone()
+ provider = row[0].lower() if row and row[0] else (
+ self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False
+ )
+
+ _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}")
+
+ # ───────────────────────────────────────────────────────────────
+ # Validasi koordinat untuk kurir instan
+ # ───────────────────────────────────────────────────────────────
+ instan_kurir = ['gojek', 'grab', 'lalamove', 'borzo', 'rara', 'deliveree']
+ if provider in instan_kurir:
+ lat = self.real_shipping_id.latitude
+ lng = self.real_shipping_id.longtitude
+ def is_invalid(val):
+ try:
+ return not val or float(val) == 0.0
+ except (ValueError, TypeError):
+ return True
+
+ if is_invalid(lat) or is_invalid(lng):
+ self.carrier_id = self._origin.carrier_id
+ self.shipping_option_id = self._origin.shipping_option_id or False
+ return {
+ 'warning': {
+ 'title': "Alamat Belum Pin Point",
+ 'message': (
+ "Kurir instan seperti Gojek, Grab, Lalamove, Borzo, Rara, dan Deliveree "
+ "membutuhkan alamat pengiriman yang sudah Pin Point.\n\n"
+ "Silakan tentukan lokasi dengan tepat pada Pin Point Location yang tersedia di kontak."
+ )
+ },
+ 'domain': {'shipping_option_id': [('id', '=', -1)]}
+ }
+
+ # ───────────────────────────────────────────────────────────────
+ # Baru cek apakah shipping option sudah ada
+ # ───────────────────────────────────────────────────────────────
+ total_so_options = self.env['shipping.option'].search_count([
+ ('sale_order_id', '=', sale_order_id)
+ ])
+
+ if total_so_options == 0:
+ return {'domain': {'shipping_option_id': [('id', '=', -1)]}}
+
+ # Validasi: apakah shipping option ada untuk provider ini?
+ matched = self.env['shipping.option'].search_count([
+ ('sale_order_id', '=', sale_order_id),
+ ('provider', 'ilike', provider),
+ ])
+ if self.select_shipping_option == 'biteship' and matched == 0:
+ self.carrier_id = self._origin.carrier_id
+ self.shipping_option_id = self._origin.shipping_option_id or False
+ return {
+ 'warning': {
+ 'title': "Shipping Option Tidak Ditemukan",
+ 'message': (
+ "Layanan kurir ini tidak tersedia pada pengiriman ini. "
+ "Pilihan dikembalikan ke sebelumnya."
+ )
+ },
+ 'domain': {'shipping_option_id': [('id', '=', -1)]}
+ }
+
+ # Kalau semua valid, kembalikan domain normal
+ domain = [
+ '|',
+ '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'),
+ '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%')
+ ]
+ return {'domain': {'shipping_option_id': domain}}
+
+ @api.onchange('shipping_option_id')
+ def _onchange_shipping_option_id(self):
+ if not self.shipping_option_id:
+ return
+
+ if not self.carrier_id:
+ # Jika belum pilih carrier, tetap update harga dan service type
+ self.delivery_amt = self.shipping_option_id.price
+ self.delivery_service_type = self.shipping_option_id.courier_service_code
+ return
+
+ # Ambil provider dari carrier
+ self.env.cr.execute("""
+ SELECT name FROM rajaongkir_kurir
+ WHERE delivery_carrier_id = %s LIMIT 1
+ """, (self.carrier_id.id,))
+ row = self.env.cr.fetchone()
+ provider = row[0].lower() if row and row[0] else self.carrier_id.name.lower().split()[0]
+
+ selected_provider = (self.shipping_option_id.provider or '').lower()
+
+ if provider not in selected_provider:
+ warning_msg = {
+ 'title': "Opsi Tidak Valid",
+ 'message': f"Opsi pengiriman '{self.shipping_option_id.name}' tidak cocok dengan metode '{self.carrier_id.name}'. Dikembalikan ke sebelumnya."
+ }
+
+ # Kembalikan ke nilai lama (jika record sudah disimpan)
+ self.shipping_option_id = self._origin.shipping_option_id if self._origin else False
+ return {'warning': warning_msg}
+
+ # Jika valid
+ self.delivery_amt = self.shipping_option_id.price
+ self.delivery_service_type = self.shipping_option_id.courier_service_code
+
+ def _update_delivery_service_type_from_shipping_option(self, vals):
+ shipping_option_id = vals.get('shipping_option_id') or self.shipping_option_id.id
+ if shipping_option_id:
+ shipping_option = self.env['shipping.option'].browse(shipping_option_id)
+ if shipping_option.exists():
+ courier_service = shipping_option.courier_service_code
+ vals['delivery_service_type'] = courier_service
+ _logger.info("Set delivery_service_type: %s from shipping_option_id: %s", courier_service, shipping_option_id)
+ else:
+ _logger.warning("shipping_option_id %s not found or invalid.", shipping_option_id)
+ else:
+ _logger.info("shipping_option_id not found in vals or record.")
+
+ # @api.model
+ # def fields_get(self, allfields=None, attributes=None):
+ # res = super().fields_get(allfields=allfields, attributes=attributes)
+
+ # # Aktifkan hanya kalau sedang buka form Sales Order (safety check)
+ # if self.env.context.get('params', {}).get('model') == 'sale.order' and \
+ # self.env.context.get('params', {}).get('id'):
+
+ # sale_id = self.env.context['params']['id']
+
+ # # Ambil carrier_id dari SO yang sedang dibuka
+ # self.env.cr.execute("SELECT carrier_id FROM sale_order WHERE id = %s", (sale_id,))
+ # row = self.env.cr.fetchone()
+ # carrier_id = row[0] if row else None
+
+ # provider = None
+ # if carrier_id:
+ # self.env.cr.execute("""
+ # SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1
+ # """, (carrier_id,))
+ # row = self.env.cr.fetchone()
+ # if row and row[0]:
+ # provider = row[0].lower()
+ # else:
+ # self.env.cr.execute("SELECT name FROM delivery_carrier WHERE id = %s", (carrier_id,))
+ # row = self.env.cr.fetchone()
+ # provider = row[0].lower().split()[0] if row and row[0] else ''
+
+ # if provider:
+ # domain = [
+ # '|',
+ # '&', ('sale_order_id', '=', sale_id), ('provider', 'ilike', f'%{provider}%'),
+ # '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%')
+ # ]
+
+ # if 'shipping_option_id' in res:
+ # res['shipping_option_id']['domain'] = domain
+ # _logger.info(f"fields_get - Injected domain for shipping_option_id: {domain}")
+ # return res
+
+
+ @api.onchange('select_shipping_option')
+ def _onchange_select_shipping_option(self):
+ self.shipping_option_id = False
+ self.delivery_service_type = False
+ self.carrier_id = False
+ self.delivery_amt = 0
+
+ biteship_carrier_ids = []
+ self.env.cr.execute("""
+ SELECT delivery_carrier_id
+ FROM rajaongkir_kurir
+ WHERE name IN %s
+ """, (tuple(self._get_biteship_courier_codes()),))
+ biteship_carrier_ids = [row[0] for row in self.env.cr.fetchall() if row[0]]
+
+ if self.select_shipping_option == 'biteship':
+ if self.shipping_cost_covered == 'indoteknik':
+ self.select_shipping_option = self._origin.select_shipping_option if self._origin else 'custom'
+ return {
+ 'warning': {
+ 'title': "Biteship Tidak Diizinkan",
+ 'message': (
+ "Biaya pengiriman ditanggung Indoteknik. Tidak diizinkan memilih metode Biteship. "
+ "Opsi pengiriman dikembalikan ke sebelumnya."
+ )
+ }
+ }
+
+ domain = [('id', 'in', biteship_carrier_ids)] if biteship_carrier_ids else [('id', '=', -1)]
+ else:
+ domain = [] # tampilkan semua
+
+ return {'domain': {'carrier_id': domain}}
+
+ # def _compute_total_margin_excl_third_party(self):
+ # for order in self:
+ # if order.amount_untaxed == 0:
+ # order.total_margin_excl_third_party = 0
+ # continue
+ #
+ # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2)
+ # order.total_margin_excl_third_party = round((order.total_before_margin / (order.amount_untaxed)) * 100, 2)
+ # # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2)
+ #
def ask_retur_cancel_purchasing(self):
for rec in self:
if self.env.user.has_group('indoteknik_custom.group_role_purchasing'):
@@ -403,24 +761,18 @@ class SaleOrder(models.Model):
if len(tax_sets) > 1:
raise ValidationError("Semua produk dalam Sales Order harus memiliki kombinasi pajak yang sama.")
- # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain')
+ # @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain', 'ongkir_ke_xpdc')
# def _check_total_margin_excl_third_party(self):
# for rec in self:
# if rec.fee_third_party == 0 and rec.total_margin_excl_third_party != rec.total_percent_margin:
# # Gunakan direct SQL atau flag context untuk menghindari rekursi
# self.env.cr.execute("""
- # UPDATE sale_order
- # SET total_margin_excl_third_party = %s
+ # UPDATE sale_order
+ # SET total_margin_excl_third_party = %s
# WHERE id = %s
# """, (rec.total_percent_margin, rec.id))
# self.invalidate_cache()
- @api.constrains('shipping_option_id')
- def _check_shipping_option(self):
- for rec in self:
- if rec.shipping_option_id:
- rec.delivery_amt = rec.shipping_option_id.price
-
def _compute_shipping_method_picking(self):
for order in self:
if order.picking_ids:
@@ -493,17 +845,93 @@ class SaleOrder(models.Model):
)
def action_estimate_shipping(self):
- if self.carrier_id.id in [1, 151]:
- self.action_indoteknik_estimate_shipping()
- return
+ # if self.carrier_id.id in [1, 151]:
+ # self.action_indoteknik_estimate_shipping()
+ # return
+
+ if self.select_shipping_option == 'biteship':
+ return self.action_estimate_shipping_biteship()
+ elif self.carrier_id.id in [1, 151]: # ID untuk Indoteknik Delivery
+ return self.action_indoteknik_estimate_shipping()
+ else:
+ total_weight = 0
+ missing_weight_products = []
+
+ for line in self.order_line:
+ if line.weight > 0:
+ total_weight += line.weight * line.product_uom_qty
+ line.product_id.weight = line.weight
+ else:
+ missing_weight_products.append(line.product_id.name)
+
+ if missing_weight_products:
+ product_names = '<br/>'.join(missing_weight_products)
+ self.message_post(body=f"Produk berikut tidak memiliki berat:<br/>{product_names}")
+
+ if total_weight == 0:
+ raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.")
+
+ kecamatan_name = self.real_shipping_id.kecamatan_id.name
+ kota_name = self.real_shipping_id.kota_id.name
+ kelurahan_name = self.real_shipping_id.kelurahan_id.name
+
+ destination_subsdistrict_id = self._get_subdistrict_id_from_komerce(kecamatan_name, kota_name, kelurahan_name)
+
+ # destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id
+ if not destination_subsdistrict_id:
+ raise UserError("Gagal mendapatkan ID kota tujuan.")
+ result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id)
+ if not result or not result.get('data'):
+ raise UserError(_("Kurir %s tidak tersedia untuk tujuan ini. Silakan pilih kurir lain.") % self.carrier_id.name)
+
+ if result:
+ shipping_options = []
+
+ for cost in result.get('data', []):
+ service = cost.get('service')
+ description = cost.get('description')
+ etd = cost.get('etd', '')
+ value = cost.get('cost', 0)
+ provider = cost.get('code')
+
+ shipping_options.append((service, description, etd, value, provider))
+
+ self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink()
+
+ _logger.info(f"Shipping options: {shipping_options}")
+
+ for service, description, etd, value, provider in shipping_options:
+ self.env["shipping.option"].create({
+ "name": service,
+ "price": value,
+ "provider": provider,
+ "etd": etd,
+ "sale_order_id": self.id,
+ })
+
+ self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id
+
+ _logger.info(f"Shipping option SO ID: {self.shipping_option_id}")
+
+ self.message_post(
+ body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Detail Lain:<br/>"
+ f"{'<br/>'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]}, Cost: Rp {s[3]}' for s in shipping_options])}",
+ message_type="comment"
+ )
+ else:
+ raise UserError("Gagal mendapatkan estimasi ongkir.")
+
+ def _validate_for_shipping_estimate(self):
+ # Cek berat produk
total_weight = 0
missing_weight_products = []
for line in self.order_line:
- if line.weight > 0:
- total_weight += line.weight * line.product_uom_qty
- line.product_id.weight = line.weight
+ product_weight = line.product_id.weight or 0
+ if product_weight > 0:
+ total_weight += product_weight * line.product_uom_qty
+ line.weight = product_weight
else:
missing_weight_products.append(line.product_id.name)
@@ -512,71 +940,314 @@ class SaleOrder(models.Model):
self.message_post(body=f"Produk berikut tidak memiliki berat:<br/>{product_names}")
if total_weight == 0:
- raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.")
+ raise UserError("Tidak dapat mengestimasi ongkir tanpa karena berat produk = 0 kg.")
+
+ # Validasi alamat pengiriman
+ if not self.real_shipping_id:
+ raise UserError("Alamat pengiriman (Real Delivery Address) harus diisi.")
+
+ if not self.real_shipping_id.kota_id:
+ raise UserError("Kota pada alamat pengiriman harus diisi.")
+
+ if not self.real_shipping_id.zip:
+ raise UserError("Kode pos pada alamat pengiriman harus diisi.")
+
+ if not self.real_shipping_id.state_id:
+ raise UserError("Provinsi pada alamat pengiriman harus diisi.")
+
+ return total_weight
+
+ def action_estimate_shipping_biteship(self):
+ total_weight = self._validate_for_shipping_estimate()
+
+ weight_gram = int(total_weight * 1000)
+ if weight_gram < 100:
+ weight_gram = 100
+
+ value = int(self.amount_untaxed or sum(line.price_subtotal for line in self.order_line))
+
+ items = [{
+ "name": "Paket Pesanan",
+ "description": f"Sale Order {self.name}",
+ "value": value,
+ "weight": weight_gram,
+ "quantity": 1,
+ }]
+
+ shipping_address = self.real_shipping_id
+ _logger.info(f"Shipping Address: {shipping_address}")
+
+ origin_data = {
+ "origin_latitude": -6.3031123,
+ "origin_longitude": 106.7794934,
+ }
+
+ destination_data = {}
+ use_coordinate = False
+
+ if hasattr(shipping_address, 'latitude') and hasattr(shipping_address, 'longtitude'):
+ if shipping_address.latitude and shipping_address.longtitude:
+ try:
+ lat = float(shipping_address.latitude)
+ lng = float(shipping_address.longtitude)
+ destination_data = {
+ "destination_latitude": lat,
+ "destination_longitude": lng
+ }
+ use_coordinate = True
+ _logger.info(f"Using coordinates: lat={lat}, lng={lng}")
+ except (ValueError, TypeError):
+ _logger.warning(f"Invalid coordinates, falling back to postal code")
+ use_coordinate = False
+
+ if not use_coordinate:
+ if shipping_address.zip:
+ origin_data = {"origin_postal_code": 14440}
+ destination_data = {
+ "destination_postal_code": shipping_address.zip
+ }
+ _logger.info(f"Using postal code: {shipping_address.zip}")
+ else:
+ raise UserError("Tidak dapat mengestimasikan ongkir: Kode pos tujuan tidak tersedia.")
+
+ couriers = ','.join(self._get_biteship_courier_codes())
+
+ api_mode = "koordinat" if use_coordinate else "kode_pos"
+ _logger.info(f"Calling Biteship API with mode: {api_mode}")
+
+ result = self._call_biteship_api(origin_data, destination_data, items, couriers)
+
+ if not result:
+ raise UserError("Gagal mendapatkan estimasi ongkir dari Biteship.")
+
+ self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink()
+
+ shipping_options = []
+ courier_options = {}
+ shipping_services = result.get('pricing', [])
+
+ _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman")
+
+ for service in shipping_services:
+ courier_code = service.get('courier_code', '').lower()
+ courier_name = service.get('courier_name', '')
+ service_name = service.get('courier_service_name', '')
+ raw_price = service.get('price', 0)
+ markup_price = int(raw_price * 1.1)
+ price = round(markup_price / 1000) * 1000
+
+ _logger.info(f"Layanan: {courier_name} - {service_name}, Harga: {price}")
+
+ if not price:
+ _logger.warning(f"Melewati layanan dengan harga 0: {courier_name} - {service_name}")
+ continue
+
+ duration = service.get('duration', '')
+ shipment_range = service.get('shipment_duration_range', '')
+ shipment_unit = service.get('shipment_duration_unit', 'days')
+
+ if duration:
+ etd = duration
+ elif shipment_range:
+ etd = f"{shipment_range} {shipment_unit}"
+ else:
+ etd = "1-3 days"
- destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id
- if not destination_subsdistrict_id:
- raise UserError("Gagal mendapatkan ID kota tujuan.")
-
- result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id)
- if result:
- shipping_options = []
- for courier in result['rajaongkir']['results']:
- for cost_detail in courier['costs']:
- service = cost_detail['service']
- description = cost_detail['description']
- etd = cost_detail['cost'][0]['etd']
- value = cost_detail['cost'][0]['value']
- shipping_options.append((service, description, etd, value, courier['code']))
-
- self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink()
-
- _logger.info(f"Shipping options: {shipping_options}")
-
- for service, description, etd, value, provider in shipping_options:
- self.env["shipping.option"].create({
- "name": service,
- "price": value,
- "provider": provider,
+ try:
+ shipping_option = self.env["shipping.option"].create({
+ "name": f"{courier_name} - {service_name}",
+ "price": price,
+ "provider": courier_code,
"etd": etd,
+ "courier_service_code": service.get('courier_service_code'),
"sale_order_id": self.id,
})
- self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id
+ shipping_options.append(shipping_option)
- _logger.info(f"Shipping option SO ID: {self.shipping_option_id}")
+ courier_upper = courier_code.upper()
+ if courier_upper not in courier_options:
+ courier_options[courier_upper] = []
+ courier_options[courier_upper].append({
+ "name": service_name,
+ "etd": etd,
+ "price": price
+ })
- self.message_post(
- body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Detail Lain:<br/>"
- f"{'<br/>'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}",
- message_type="comment"
- )
+ _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}")
+ except Exception as e:
+ _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}")
+
+ if not shipping_options:
+ raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data.get('destination_postal_code', '')}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.")
+
+ # Temukan shipping option yang cocok berdasarkan carrier_id
+ selected_option = None
- # self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Detail Lain:<br/>{'<br/>'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment")
+ if self.carrier_id:
+ rajaongkir_kurir = self.env['rajaongkir.kurir'].search([
+ ('delivery_carrier_id', '=', self.carrier_id.id)
+ ], limit=1)
+ if rajaongkir_kurir:
+ courier_code = rajaongkir_kurir.name.lower()
+ carrier_name = self.carrier_id.name.lower()
+
+ possible_codes = list({
+ courier_code,
+ carrier_name,
+ carrier_name.split()[0] if ' ' in carrier_name else carrier_name
+ })
+
+ _logger.info(f"[MATCHING] Mencari shipping option untuk kurir: {possible_codes}")
+
+ for option in shipping_options:
+ option_provider = (option.provider or '').lower()
+ option_name = (option.name or '').lower()
+
+ if any(code in option_provider or code in option_name for code in possible_codes):
+ selected_option = option
+ _logger.info(f"[MATCHED] Shipping option cocok: {option.name}")
+ break
+
+ if not selected_option and shipping_options:
+ selected_option = shipping_options[0]
+ _logger.info(f"[DEFAULT] Tidak ada yang cocok, pakai opsi pertama: {selected_option.name}")
+
+ # ❗ Ganti carrier_id hanya jika BELUM terisi sama sekali (contoh: user dari backend)
+ if not self.carrier_id:
+ provider = selected_option.provider.lower()
+ self.env.cr.execute("""
+ SELECT delivery_carrier_id FROM rajaongkir_kurir
+ WHERE LOWER(name) = %s AND delivery_carrier_id IS NOT NULL
+ LIMIT 1
+ """, (provider,))
+ row = self.env.cr.fetchone()
+ matched_carrier_id = row[0] if row else False
+ if matched_carrier_id:
+ self.carrier_id = matched_carrier_id
+ _logger.info(f"[AUTO-SET] Carrier diisi otomatis ke ID {matched_carrier_id} (provider: {provider})")
+ else:
+ _logger.warning(f"[WARNING] Provider {provider} tidak ditemukan di rajaongkir_kurir")
+
+ # Set shipping option dan nilai ongkir
+ if selected_option:
+ self.shipping_option_id = selected_option.id
+ self.delivery_amt = selected_option.price
+ self.delivery_service_type = selected_option.courier_service_code
+ message_lines = [f"<b>Estimasi Ongkos Kirim Biteship:</b><br/>"]
+
+ for courier, options in courier_options.items():
+ message_lines.append(f"<b>{courier}:</b><br/>")
+ for opt in options:
+ message_lines.append(f"Service: {opt['name']}, ETD: {opt['etd']}, Cost: Rp {int(opt['price']):,}<br/>")
+ if courier != list(courier_options.keys())[-1]:
+ message_lines.append("<br/>")
+
+ origin_address = "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN"
+ destination_address = ', '.join(filter(None, [
+ shipping_address.street,
+ shipping_address.kelurahan_id.name if shipping_address.kelurahan_id else None,
+ shipping_address.kecamatan_id.name if shipping_address.kecamatan_id else None,
+ shipping_address.kota_id.name if shipping_address.kota_id else None,
+ shipping_address.state_id.name if shipping_address.state_id else None
+ ]))
+ if use_coordinate:
+ origin_suffix = f"(Koordinat: {origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})"
+ destination_suffix = f"(Koordinat: {destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})"
else:
- raise UserError("Gagal mendapatkan estimasi ongkir.")
+ origin_suffix = f"(Kode Pos: {origin_data.get('origin_postal_code')})"
+ destination_suffix = f"(Kode Pos: {destination_data.get('destination_postal_code')})"
+
+ message_lines.append("<br/><br/><br><b>Info Lokasi:</b><br/>")
+ message_lines.append(f"<b>Asal</b>: {origin_address} {origin_suffix}<br/>")
+ message_lines.append(f"<b>Tujuan</b>: {destination_address} {destination_suffix}<br/>")
+
+ message_body = "".join(message_lines)
+
+ self.message_post(
+ body=message_body,
+ message_type="comment"
+ )
+
+ # Simpan informasi untuk note ekspedisi
+ # selected_option = shipping_options[0] # Opsi pertama dipilih sebagai default
+ # self.note_ekspedisi = f"Pengiriman: {selected_option.name} - Rp {selected_option.price:,.0f} ({selected_option.etd}) [via {api_mode}]"
+
+
+ def _call_biteship_api(self, origin_data, destination_data, items, couriers=None):
+
+ url = "https://api.biteship.com/v1/rates/couriers"
+ api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
+ # api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
+ # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live')
+ # api_key = self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test')
+ headers = {
+ 'Authorization': api_key,
+ 'Content-Type': 'application/json'
+ }
+
+ if not couriers:
+ couriers = ','.join(self._get_biteship_courier_codes())
+
+ # Persiapkan payload dengan menggabungkan origin, destination, dan items
+ payload = {
+ **origin_data,
+ **destination_data,
+ "couriers": couriers,
+ "items": items
+ }
+
+ api_mode = "koordinat" if "destination_latitude" in destination_data else "kode_pos"
+
+ try:
+ _logger.info(f"Calling Biteship API with mode: {api_mode}")
+ _logger.info(f"Payload: {payload}")
+
+ response = requests.post(url, headers=headers, json=payload, timeout=30)
+
+ _logger.info(f"Biteship API Status Code: {response.status_code}")
+ if response.status_code != 200:
+ _logger.error(f"Biteship API Error Response: {response.text}")
+
+ if response.status_code == 200:
+ result = response.json()
+ result['api_mode'] = api_mode # Tambahkan info mode API
+ return result
+ else:
+ error_msg = response.text
+ _logger.error(f"Error calling Biteship API: {response.status_code} - {error_msg}")
+ return False
+ except Exception as e:
+ _logger.error(f"Exception calling Biteship API: {str(e)}")
+ return False
+
def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id):
- url = 'https://pro.rajaongkir.com/api/cost'
+ url = 'https://rajaongkir.komerce.id/api/v1/calculate/domestic-cost'
headers = {
'key': '9b1310f644056d84d60b0af6bb21611a',
}
courier = self.carrier_id.name.lower()
data = {
- 'origin': 2127,
- 'originType': 'subdistrict',
+ 'origin': 17656,
+ # 'originType': 'subdistrict',
'destination': int(destination_subsdistrict_id),
- 'destinationType': 'subdistrict',
+ # 'destinationType': 'subdistrict',
'weight': int(total_weight * 1000),
'courier': courier,
}
- response = requests.post(url, headers=headers, data=data)
- if response.status_code == 200:
- return response.json()
- return None
+ try:
+ _logger.info(f"Calling RajaOngkir API with data: {data}")
+ response = requests.post(url, headers=headers, data=data)
+ _logger.info(f"RajaOngkir response: {response.status_code} - {response.text}")
+
+ if response.status_code == 200:
+ return response.json()
+ except Exception as e:
+ _logger.error(f"Exception while calling RajaOngkir: {str(e)}")
def _normalize_city_name(self, city_name):
city_name = city_name.lower()
@@ -590,37 +1261,82 @@ class SaleOrder(models.Model):
return city_name
- def _get_city_id_by_name(self, city_name):
- url = 'https://pro.rajaongkir.com/api/city'
+ # def _get_city_id_by_name(self, city_name):
+ # url = 'https://pro.rajaongkir.com/api/city'
+ # headers = {
+ # 'key': '9b1310f644056d84d60b0af6bb21611a',
+ # }
+
+ # normalized_city_name = self._normalize_city_name(city_name)
+
+ # response = requests.get(url, headers=headers)
+ # if response.status_code == 200:
+ # city_data = response.json()
+ # for city in city_data['rajaongkir']['results']:
+ # if city['city_name'].lower() == normalized_city_name:
+ # return city['city_id']
+ # return None
+
+ # def _get_subdistrict_id_by_name(self, city_id, subdistrict_name):
+ # url = f'https://pro.rajaongkir.com/api/subdistrict?city={city_id}'
+ # headers = {
+ # 'key': '9b1310f644056d84d60b0af6bb21611a',
+ # }
+
+ # response = requests.get(url, headers=headers)
+ # if response.status_code == 200:
+ # subdistrict_data = response.json()
+ # for subdistrict in subdistrict_data['rajaongkir']['results']:
+ # subsdistrict_1 = subdistrict['subdistrict_name'].lower()
+ # subsdistrict_2 = subdistrict_name.lower()
+
+ # if subsdistrict_1 == subsdistrict_2:
+ # return subdistrict['subdistrict_id']
+ # return None
+
+ def _get_subdistrict_id_from_komerce(self, kecamatan_name, kota_name, kelurahan_name=None):
+ url = 'https://rajaongkir.komerce.id/api/v1/destination/domestic-destination'
headers = {
'key': '9b1310f644056d84d60b0af6bb21611a',
}
- normalized_city_name = self._normalize_city_name(city_name)
-
- response = requests.get(url, headers=headers)
- if response.status_code == 200:
- city_data = response.json()
- for city in city_data['rajaongkir']['results']:
- if city['city_name'].lower() == normalized_city_name:
- return city['city_id']
- return None
+ if kelurahan_name:
+ search = f"{kelurahan_name} {kecamatan_name} {kota_name}"
+ else:
+ search = f"{kecamatan_name} {kota_name}"
- def _get_subdistrict_id_by_name(self, city_id, subdistrict_name):
- url = f'https://pro.rajaongkir.com/api/subdistrict?city={city_id}'
- headers = {
- 'key': '9b1310f644056d84d60b0af6bb21611a',
+ params = {
+ 'search': search,
+ 'limit': 5
}
- response = requests.get(url, headers=headers)
- if response.status_code == 200:
- subdistrict_data = response.json()
- for subdistrict in subdistrict_data['rajaongkir']['results']:
- subsdistrict_1 = subdistrict['subdistrict_name'].lower()
- subsdistrict_2 = subdistrict_name.lower()
+ try:
+ response = requests.get(url, headers=headers, params=params, timeout=10)
+ if response.status_code == 200:
+ data = response.json().get('data', [])
+ _logger.info(f"[Komerce] Fetched {len(data)} subdistricts for search '{search}'")
+ _logger.info(f"[Komerce] Response: {data}")
+
+ normalized_kota = self._normalize_city_name(kota_name)
+
+ for item in data:
+ match_kelurahan = (
+ not kelurahan_name or
+ item.get('subdistrict_name', '').lower() == kelurahan_name.lower()
+ )
+ if (
+ match_kelurahan and
+ item.get('district_name', '').lower() == kecamatan_name.lower() and
+ item.get('city_name', '').lower() == normalized_kota
+ ):
+ return item.get('id')
+
+ _logger.warning(f"[Komerce] No match for '{kecamatan_name}' in city '{kota_name}' with kelurahan '{kelurahan_name}'")
+ else:
+ _logger.error(f"[Komerce] HTTP Error {response.status_code}: {response.text}")
+ except Exception as e:
+ _logger.error(f"[Komerce] Exception: {e}")
- if subsdistrict_1 == subsdistrict_2:
- return subdistrict['subdistrict_id']
return None
def _compute_type_promotion(self):
@@ -684,38 +1400,102 @@ class SaleOrder(models.Model):
rec.compute_fullfillment = True
- @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start')
+ @api.depends('expected_ready_to_ship', 'shipping_option_id.etd', 'state')
def _compute_eta_date(self):
- current_date = datetime.now().date()
for rec in self:
- if rec.date_order and rec.state not in [
- 'cancel'] and rec.estimated_arrival_days and rec.estimated_arrival_days_start:
- rec.eta_date = current_date + timedelta(days=rec.estimated_arrival_days)
- rec.eta_date_start = current_date + timedelta(days=rec.estimated_arrival_days_start)
+ if rec.expected_ready_to_ship and rec.shipping_option_id and rec.shipping_option_id.etd and rec.state not in ['cancel']:
+ etd_text = rec.shipping_option_id.etd.strip().lower()
+ match = re.match(r"(\d+)\s*-\s*(\d+)\s*(days?|hours?)", etd_text)
+ single_match = re.match(r"(\d+)\s*(days?|hours?)", etd_text)
+
+ if match:
+ start_val = int(match.group(1))
+ end_val = int(match.group(2))
+ unit = match.group(3)
+
+ if 'hour' in unit:
+ rec.eta_date_start = rec.expected_ready_to_ship + timedelta(hours=start_val)
+ rec.eta_date = rec.expected_ready_to_ship + timedelta(hours=end_val)
+ else:
+ rec.eta_date_start = rec.expected_ready_to_ship + timedelta(days=start_val)
+ rec.eta_date = rec.expected_ready_to_ship + timedelta(days=end_val)
+
+ elif single_match:
+ val = int(single_match.group(1))
+ unit = single_match.group(2)
+
+ if 'hour' in unit:
+ rec.eta_date_start = rec.expected_ready_to_ship + timedelta(hours=val)
+ rec.eta_date = rec.expected_ready_to_ship + timedelta(hours=val)
+ else:
+ rec.eta_date_start = rec.expected_ready_to_ship + timedelta(days=val)
+ rec.eta_date = rec.expected_ready_to_ship + timedelta(days=val)
+
+ else:
+ rec.eta_date_start = False
+ rec.eta_date = False
else:
- rec.eta_date = False
rec.eta_date_start = False
-
+ rec.eta_date = False
+
+
def get_days_until_next_business_day(self, start_date=None, *args, **kwargs):
- today = start_date or datetime.today().date()
- offset = 0 # Counter jumlah hari yang ditambahkan
- holiday = self.env['hr.public.holiday']
+ jakarta = pytz.timezone("Asia/Jakarta")
+ now = datetime.now(jakarta)
- while True:
- today += timedelta(days=1)
- offset += 1
+ if start_date is None:
+ start_date = now
- if today.weekday() >= 5:
- continue
+ if start_date.tzinfo is None:
+ start_date = jakarta.localize(start_date)
- is_holiday = holiday.search([("start_date", "=", today)])
- if is_holiday:
- continue
+ holiday = self.env['hr.public.holiday']
+ batas_waktu = datetime.strptime("15:00", "%H:%M").time()
+ current_day = start_date.date()
+ offset = 0
+ is3pm = False
+
+ # Step 1: Lewat jam 15 → Tambah 1 hari
+ if start_date.time() > batas_waktu:
+ is3pm = True
+ offset += 1
- break
+ # Step 2: Hitung hari libur selama offset itu
+ i = 0
+ total_days = 0
+ while i < offset:
+ current_day += timedelta(days=1)
+ total_days += 1
+ is_weekend = current_day.weekday() >= 5
+ is_holiday = holiday.search([("start_date", "=", current_day)])
+ if not is_weekend and not is_holiday:
+ i += 1 # hanya hitung hari kerja
+
+ # Step 3: Tambah 1 hari masa persiapan gudang
+ i = 0
+ while i < 1:
+ current_day += timedelta(days=1)
+ total_days += 1
+ is_weekend = current_day.weekday() >= 5
+ is_holiday = holiday.search([("start_date", "=", current_day)])
+ if not is_weekend and not is_holiday:
+ i += 1
+
+ # Step 4: Kalau current_day ternyata weekend/libur, cari hari kerja berikutnya
+ while True:
+ is_weekend = current_day.weekday() >= 5
+ is_holiday = holiday.search([("start_date", "=", current_day)])
+ if is_weekend or is_holiday:
+ current_day += timedelta(days=1)
+ total_days += 1
+ else:
+ break
+
+ offset = (current_day - start_date.date()).days
+ return offset, is3pm
- return offset
+
def calculate_sla_by_vendor(self, products):
product_ids = products.mapped('product_id.id') # Kumpulkan semua ID produk
include_instant = True # Default True, tetapi bisa menjadi False
@@ -724,7 +1504,7 @@ class SaleOrder(models.Model):
all_fast_products = all(
product.product_id.qty_free_bandengan >= product.product_uom_qty for product in products)
if all_fast_products:
- return {'slatime': 1, 'include_instant': include_instant}
+ return {'slatime': 0, 'include_instant': include_instant}
# Cari semua vendor pemenang untuk produk yang diberikan
vendors = self.env['purchase.pricelist'].search([
@@ -758,48 +1538,109 @@ class SaleOrder(models.Model):
if not rec.date_order:
rec.expected_ready_to_ship = False
return
-
- current_date = datetime.now().date()
-
+
+ jakarta = pytz.timezone("Asia/Jakarta")
+ current_date = datetime.now(jakarta)
+
max_slatime = 1 # Default SLA jika tidak ada
slatime = self.calculate_sla_by_vendor(rec.order_line)
max_slatime = max(max_slatime, slatime['slatime'])
-
- sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1
+
+ offset , is3pm = self.get_days_until_next_business_day(current_date)
+ sum_days = max_slatime + offset
+ sum_days -= 1
if not rec.estimated_arrival_days:
rec.estimated_arrival_days = sum_days
eta_date = current_date + timedelta(days=sum_days)
+ if is3pm:
+ eta_date = datetime.combine(eta_date, time(10, 0)) # jam 10:00
+ eta_date = jakarta.localize(eta_date).astimezone(timezone.utc) # ubah ke UTC
+
+
+ eta_date = eta_date.astimezone(timezone.utc).replace(tzinfo=None)
rec.commitment_date = eta_date
rec.expected_ready_to_ship = eta_date
@api.depends("order_line.product_id", "date_order")
def _compute_etrts_date(self): # Function to calculate Estimated Ready To Ship Date
self._calculate_etrts_date()
+
+
+ # def _validate_expected_ready_ship_date(self):
+ # for rec in self:
+ # if not rec.order_line:
+ # _logger.info("⏩ Lewati validasi ERTS karena belum ada produk.")
+ # return # Lewati validasi jika belum ada produk
+
+ # now = fields.Datetime.now()
+ # expected_date = rec.expected_ready_to_ship and rec.expected_ready_to_ship.date() or None
+ # if not expected_date:
+ # return # Tidak validasi jika tidak ada input sama sekali
+
+ # sla = rec.calculate_sla_by_vendor()
+ # offset_day, lewat_jam_3 = rec.get_days_until_next_business_day()
+ # eta_minimum = now + timedelta(days=sla + offset_day)
+
+ # if expected_date < eta_minimum.date():
+ # rec.expected_ready_to_ship = eta_minimum
+ # raise ValidationError(
+ # "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}."
+ # .format(eta_minimum.strftime('%d-%m-%Y'), eta_minimum.strftime('%d-%m-%Y'))
+ # )
def _validate_expected_ready_ship_date(self):
+ """
+ Pastikan expected_ready_to_ship tidak lebih awal dari SLA minimum.
+ Dipanggil setiap onchange / simpan SO.
+ """
for rec in self:
- if rec.expected_ready_to_ship and rec.commitment_date:
- current_date = datetime.now().date()
- # Hanya membandingkan tanggal saja, tanpa jam
- expected_date = rec.expected_ready_to_ship.date()
-
- max_slatime = 1 # Default SLA jika tidak ada
- slatime = self.calculate_sla_by_vendor(rec.order_line)
- max_slatime = max(max_slatime, slatime['slatime'])
- sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1
- eta_minimum = current_date + timedelta(days=sum_days)
-
- if expected_date < eta_minimum:
- rec.expected_ready_to_ship = eta_minimum
- raise ValidationError(
- "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}."
- .format(eta_minimum.strftime('%d-%m-%Y'), eta_minimum.strftime('%d-%m-%Y'))
- )
- else:
- rec.commitment_date = rec.expected_ready_to_ship
+ # ─────────────────────────────────────────────────────
+ # 1. Hanya validasi kalau field sudah terisi
+ # (quotation baru / belum ada tanggal → abaikan)
+ # ─────────────────────────────────────────────────────
+ if not rec.expected_ready_to_ship:
+ continue
+
+ current_date = datetime.now()
+
+ # ─────────────────────────────────────────────────────
+ # 2. Hitung SLA berdasarkan product lines (jika ada)
+ # ─────────────────────────────────────────────────────
+ products = rec.order_line
+ if products:
+ sla_data = rec.calculate_sla_by_vendor(products)
+ max_sla_time = sla_data.get('slatime', 1)
+ else:
+ # belum ada item → gunakan default 1 hari
+ max_sla_time = 1
+
+ # offset hari libur / weekend
+ offset, is3pm = rec.get_days_until_next_business_day(current_date)
+ min_days = max_sla_time + offset - 1
+ eta_minimum = current_date + timedelta(days=min_days)
+
+ # ─────────────────────────────────────────────────────
+ # 3. Validasi - raise error bila terlalu cepat
+ # ─────────────────────────────────────────────────────
+ if rec.expected_ready_to_ship.date() < eta_minimum.date():
+ # set otomatis ke tanggal minimum supaya user tidak perlu
+ # menekan Save dua kali
+ rec.expected_ready_to_ship = eta_minimum
+
+ raise ValidationError(
+ _("Tanggal 'Expected Ready to Ship' tidak boleh "
+ "lebih kecil dari %(tgl)s. Mohon pilih minimal %(tgl)s.")
+ % {'tgl': eta_minimum.strftime('%d-%m-%Y')}
+ )
+ else:
+ # sinkronkan ke field commitment_date
+ rec.commitment_date = rec.expected_ready_to_ship
- @api.onchange('expected_ready_to_ship') # Hangle Onchange form Expected Ready to Ship
+
+
+
+ @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship
def _onchange_expected_ready_ship_date(self):
self._validate_expected_ready_ship_date()
@@ -834,6 +1675,7 @@ class SaleOrder(models.Model):
'campaign_id': self.campaign_id.id,
'medium_id': self.medium_id.id,
'source_id': self.source_id.id,
+ 'down_payment': 229625 in [line.product_id.id for line in self.order_line],
'user_id': self.user_id.id,
'sale_id': self.id,
'invoice_user_id': self.user_id.id,
@@ -866,7 +1708,6 @@ class SaleOrder(models.Model):
def _validate_delivery_amt(self):
is_indoteknik = self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik'
is_active_id = not self.env.context.get('active_id', [])
-
if is_indoteknik and is_active_id:
if self.delivery_amt == 0:
if self.carrier_id.id == 1:
@@ -1035,11 +1876,11 @@ class SaleOrder(models.Model):
line_no += 1
line.line_no = line_no
- def write(self, vals):
- if 'carrier_id' in vals:
- for picking in self.picking_ids:
- if picking.state == 'assigned':
- picking.carrier_id = self.carrier_id
+ # def write(self, vals):
+ # if 'carrier_id' in vals:
+ # for picking in self.picking_ids:
+ # if picking.state == 'assigned':
+ # picking.carrier_id = self.carrier_id
def calculate_so_status(self):
so_state = ['sale']
@@ -1157,12 +1998,12 @@ class SaleOrder(models.Model):
helper_ids_str = self.env['ir.config_parameter'].sudo().get_param('sale.order.user_helper_ids')
return helper_ids_str.split(', ')
- def write(self, values):
- helper_ids = self._get_helper_ids()
- if str(self.env.user.id) in helper_ids:
- values['helper_by_id'] = self.env.user.id
-
- return super(SaleOrder, self).write(values)
+ # def write(self, values):
+ # helper_ids = self._get_helper_ids()
+ # if str(self.env.user.id) in helper_ids:
+ # values['helper_by_id'] = self.env.user.id
+ #
+ # return super(SaleOrder, self).write(values)
def check_due(self):
"""To show the due amount and warning stage"""
@@ -1236,9 +2077,9 @@ class SaleOrder(models.Model):
confirmed_bom = search_bom.filtered(lambda x: x.state == 'confirmed' or x.state == 'done')
if not confirmed_bom:
raise UserError(
- "Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi MD.")
+ "Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi Purchasing.")
else:
- raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi MD")
+ raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi Purchasing")
def check_duplicate_product(self):
for order in self:
@@ -1259,6 +2100,7 @@ class SaleOrder(models.Model):
self._validate_order()
for order in self:
+ order._validate_delivery_amt()
order._validate_uniform_taxes()
order.order_line.validate_line()
order.check_data_real_delivery_address()
@@ -1501,6 +2343,7 @@ class SaleOrder(models.Model):
def action_confirm(self):
for order in self:
+ order._validate_delivery_amt()
order._validate_uniform_taxes()
order.check_duplicate_product()
order.check_product_bom()
@@ -1691,20 +2534,95 @@ class SaleOrder(models.Model):
total_before_margin = sum(line.item_before_margin for line in order.order_line if line.product_id)
order.total_before_margin = total_before_margin
+ # Perhitungan Lama
+ # def _compute_total_percent_margin(self):
+ # for order in self:
+ # if order.amount_untaxed == 0:
+ # order.total_percent_margin = 0
+ # continue
+ # if order.shipping_cost_covered == 'indoteknik':
+ # delivery_amt = order.delivery_amt
+ # else:
+ # delivery_amt = 0
+ #
+ # net_margin = order.total_margin - order.biaya_lain_lain
+ #
+ # order.total_percent_margin = round(
+ # (net_margin / (order.amount_untaxed - order.fee_third_party)) * 100, 2)
+
+ # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2)
+ # order.total_percent_margin = round(
+ # (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2)
+ # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2)
+
def _compute_total_percent_margin(self):
for order in self:
if order.amount_untaxed == 0:
order.total_percent_margin = 0
continue
+
if order.shipping_cost_covered == 'indoteknik':
delivery_amt = order.delivery_amt
else:
delivery_amt = 0
- # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2)
- order.total_percent_margin = round(
- (order.total_margin / (order.amount_untaxed - order.fee_third_party - order.biaya_lain_lain)) * 100, 2)
- # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2)
+ net_margin = order.total_margin - order.fee_third_party - order.biaya_lain_lain
+
+
+ if order.amount_untaxed > 0:
+ order.total_percent_margin = round((net_margin / order.amount_untaxed) * 100, 2)
+ else:
+ order.total_percent_margin = 0
+
+ # @api.onchange('biaya_lain_lain')
+ # def _onchange_biaya_lain_lain(self):
+ # """Ketika biaya_lain_lain berubah, simpan nilai margin sebelumnya"""
+ # if hasattr(self, '_origin') and self._origin.id:
+ # # Hitung margin sebelum biaya_lain_lain ditambahkan
+ # if self.amount_untaxed > 0:
+ # original_net_margin = self.total_margin # tanpa biaya_lain_lain
+ # self.total_margin_excl_third_party = round(
+ # (original_net_margin / (self.amount_untaxed - self.fee_third_party)) * 100, 2)
+
+ def _prepare_before_margin_values(self, vals):
+ margin_sebelumnya = {}
+
+ margin_affecting_fields = [
+ 'biaya_lain_lain', 'fee_third_party', 'delivery_amt',
+ 'ongkir_ke_xpdc', 'shipping_cost_covered', 'order_line'
+ ]
+
+ if not any(field in vals for field in margin_affecting_fields):
+ return {}
+
+ for order in self:
+ if order.amount_untaxed <= 0:
+ continue
+
+ current_before = order.total_margin_excl_third_party or 0
+
+ # CASE 1: Before margin masih kosong → ambil dari item_percent_margin
+ if current_before == 0:
+ line_margin = 0
+ for line in order.order_line:
+ if line.item_percent_margin is not None:
+ line_margin = line.item_percent_margin
+ break
+ margin_sebelumnya[order.id] = line_margin
+ _logger.info(f"[BEFORE] SO {order.name}: Before margin kosong, ambil dari order line: {line_margin}%")
+ else:
+ # CASE 2: Ada perubahan field yang mempengaruhi margin
+ for field in margin_affecting_fields:
+ if field in vals:
+ old_val = getattr(order, field, 0) or 0
+ new_val = vals[field] or 0
+ if old_val != new_val:
+ margin_sebelumnya[order.id] = order.total_percent_margin
+ _logger.info(
+ f"[BEFORE] SO {order.name}: {field} berubah dari {old_val} ke {new_val}, simpan {order.total_percent_margin}%")
+ break
+
+ return margin_sebelumnya
@api.onchange('sales_tax_id')
def onchange_sales_tax_id(self):
@@ -1956,13 +2874,82 @@ class SaleOrder(models.Model):
order_line.discount = discount
order_line.order_id.use_button = True
+ def _auto_set_shipping_from_website(self):
+ if not self.env.context.get('from_website_checkout'):
+ return
+
+ for order in self:
+ # Validasi source website
+ if not order.source_id or order.source_id.id != 59:
+ continue
+
+ # Skip jika Self Pick Up
+ if int(order.carrier_id.id or 0) == 32:
+ _logger.info(f"[Checkout] Skip estimasi: Self Pickup untuk SO {order.name}")
+ order.select_shipping_option = 'custom'
+ continue
+
+ # Simpan pilihan user sebelum estimasi
+ user_carrier_id = order.carrier_id.id if order.carrier_id else None
+ user_service = order.delivery_service_type
+ user_amount = order.delivery_amt
+
+ # Jalankan estimasi untuk refresh data
+ order.select_shipping_option = 'biteship'
+ order.action_estimate_shipping()
+
+ # Restore pilihan user setelah estimasi
+ if user_carrier_id and user_service:
+ # Dapatkan provider
+ self.env.cr.execute("SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s LIMIT 1", (user_carrier_id,))
+ result = self.env.cr.fetchone()
+ provider = result[0].lower() if result else order.env['delivery.carrier'].browse(user_carrier_id).name.lower().split()[0]
+
+ # Cari opsi yang cocok (prioritas: service code > nama > harga > fallback)
+ domain_options = [
+ [('courier_service_code', '=', user_service), ('provider', 'ilike', provider)], # exact service
+ [('name', 'ilike', user_service), ('provider', 'ilike', provider)], # nama service
+ [('price', '=', user_amount), ('provider', 'ilike', provider)] if user_amount > 0 else None, # harga sama
+ [('provider', 'ilike', provider)] # fallback
+ ]
+
+ matched_option = None
+ for domain in domain_options:
+ if domain:
+ matched_option = self.env['shipping.option'].search([('sale_order_id', '=', order.id)] + domain, limit=1)
+ if matched_option:
+ break
+
+ # Set opsi yang cocok atau buat manual
+ if matched_option:
+ order.shipping_option_id = matched_option.id
+ order.delivery_amt = matched_option.price
+ order.delivery_service_type = matched_option.courier_service_code
+
+ # Notif jika harga berubah
+ if user_amount > 0 and abs(matched_option.price - user_amount) > 1000:
+ order.message_post(body=f"Harga shipping berubah dari Rp {user_amount:,} ke Rp {matched_option.price:,}")
+
+ elif user_amount > 0:
+ # Buat opsi manual jika tidak ada yang cocok
+ manual_option = self.env['shipping.option'].create({
+ 'name': f"{provider.upper()} - {user_service}",
+ 'price': user_amount,
+ 'provider': provider,
+ 'courier_service_code': user_service,
+ 'sale_order_id': order.id,
+ })
+ order.shipping_option_id = manual_option.id
+
@api.model
def create(self, vals):
# Ensure partner details are updated when a sale order is created
order = super(SaleOrder, self).create(vals)
+ # _logger.info(f"[CREATE CONTEXT] {self.env.context}")
+ # order._auto_set_shipping_from_website()
order._compute_etrts_date()
order._validate_expected_ready_ship_date()
- order._validate_delivery_amt()
+ # order._validate_delivery_amt()
# order._check_total_margin_excl_third_party()
# order._update_partner_details()
return order
@@ -1995,31 +2982,42 @@ class SaleOrder(models.Model):
'customer_type': partner.customer_type,
})
- 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)
- # self._check_total_margin_excl_third_party()
- if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']):
- self._validate_delivery_amt()
- if any(field in vals for field in ["order_line", "client_order_ref"]):
- self._calculate_etrts_date()
- return res
+ # 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)
+ # # self._check_total_margin_excl_third_party()
+ # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']):
+ # self._validate_delivery_amt()
+ # if any(field in vals for field in ["order_line", "client_order_ref"]):
+ # self._calculate_etrts_date()
+ # return res
# @api.depends('commitment_date')
def _compute_ready_to_ship_status_detail(self):
+ def is_empty(val):
+ """Helper untuk cek data kosong yang umum di Odoo."""
+ return val is None or val == "" or val == [] or val == {}
+
for order in self:
+ order.ready_to_ship_status_detail = 'On Track' # Default value
+
+ # Skip if no commitment date
+ if is_empty(order.commitment_date):
+ continue
+
eta = order.commitment_date
match_lines = self.env['purchase.order.sales.match'].search([
('sale_id', '=', order.id)
])
+
if match_lines:
for match in match_lines:
po = match.purchase_order_id
@@ -2028,17 +3026,219 @@ class SaleOrder(models.Model):
('order_id', '=', po.id),
('product_id', '=', product.id)
], limit=1)
+
+ if is_empty(po_line):
+ continue
+
stock_move = self.env['stock.move'].search([
('purchase_line_id', '=', po_line.id)
], limit=1)
+
+ if is_empty(stock_move) or is_empty(stock_move.picking_id):
+ continue
+
picking_in = stock_move.picking_id
- result_date = picking_in.date_done if picking_in else None
- if result_date:
- status = "Early" if result_date < eta else "Delay"
- result_date_str = result_date.strftime('%m/%d/%Y')
- eta_str = eta.strftime('%m/%d/%Y')
- order.ready_to_ship_status_detail = f"Expected: {eta_str} | Realtime: {result_date_str} | {status}"
- else:
- order.ready_to_ship_status_detail = "On Track"
- else:
- order.ready_to_ship_status_detail = 'On Track' \ No newline at end of file
+ result_date = picking_in.date_done
+
+ if is_empty(result_date):
+ continue
+
+ try:
+ if result_date < eta:
+ order.ready_to_ship_status_detail = f"Early (Actual: {result_date.strftime('%m/%d/%Y')})"
+ else:
+ order.ready_to_ship_status_detail = f"Delay (Actual: {result_date.strftime('%m/%d/%Y')})"
+ except Exception as e:
+ _logger.error(f"Error computing ready to ship status: {str(e)}")
+ continue
+
+ def write(self, vals):
+
+ margin_sebelumnya = self._prepare_before_margin_values(vals)
+
+ for order in self:
+ if order.state in ['sale', 'cancel']:
+ if 'order_line' in vals:
+ for command in vals.get('order_line', []):
+ if command[0] == 0:
+ raise UserError(
+ "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.")
+
+ order._update_delivery_service_type_from_shipping_option(vals)
+
+ if 'carrier_id' in vals:
+ for order in self:
+ for picking in order.picking_ids:
+ 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:
+ vals['helper_by_id'] = self.env.user.id
+ except:
+ pass
+
+ res = super(SaleOrder, self).write(vals)
+
+ # Update before margin setelah write
+ if margin_sebelumnya:
+ for order_id, margin_value in margin_sebelumnya.items():
+ _logger.info(f"[UPDATE] SO ID {order_id}: Set before margin ke {margin_value}%")
+ self.env.cr.execute("""
+ UPDATE sale_order
+ SET total_margin_excl_third_party = %s
+ WHERE id = %s
+ """, (margin_value, order_id))
+
+ self.env.cr.commit()
+ self.invalidate_cache(['total_margin_excl_third_party'])
+
+ # Validasi setelah write
+ if any(field in vals for field in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']):
+ self._validate_delivery_amt()
+
+ if any(field in vals for field in ["order_line", "client_order_ref"]):
+ self._calculate_etrts_date()
+
+ return res
+
+ def button_refund(self):
+ self.ensure_one()
+
+ invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
+
+ return {
+ 'name': 'Refund Sale Order',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'refund.sale.order',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': {
+ 'default_sale_order_ids': [(6, 0, [self.id])],
+ 'default_invoice_ids': [(6, 0, invoice_ids.ids)],
+ 'default_uang_masuk': sum(invoice_ids.mapped('amount_total')) + (self.delivery_amt or 0.0) + 1000,
+ 'default_ongkir': self.delivery_amt or 0.0,
+ 'default_bank': '', # bisa isi default bank kalau mau
+ 'default_account_name': '',
+ 'default_account_no': '',
+ 'default_refund_type': '',
+ },
+ }
+
+ def open_form_multi_create_refund(self):
+ if not self:
+ raise UserError("Tidak ada Sale Order yang dipilih.")
+
+ partner_set = set(self.mapped('partner_id.id'))
+ if len(partner_set) > 1:
+ raise UserError("Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.")
+
+ invoice_status_set = set(self.mapped('invoice_status'))
+ if len(invoice_status_set) > 1:
+ raise UserError("Tidak dapat membuat refund untuk SO dengan status invoice berbeda. Harus memiliki status invoice yang sama.")
+
+ already_refunded = self.filtered(lambda so: so.has_refund)
+ if already_refunded:
+ so_names = ', '.join(already_refunded.mapped('name'))
+ raise UserError(f"❌ Tidak bisa refund ulang. {so_names} sudah melakukan refund.")
+
+ invoice_ids = self.mapped('invoice_ids').filtered(lambda inv: inv.state != 'cancel')
+ delivery_total = sum(self.mapped('delivery_amt'))
+ total_invoice = sum(invoice_ids.mapped('amount_total'))
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Create Refund',
+ 'res_model': 'refund.sale.order',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': {
+ 'default_sale_order_ids': [(6, 0, self.ids)],
+ 'default_invoice_ids': [(6, 0, invoice_ids.ids)],
+ 'default_uang_masuk': total_invoice + delivery_total + 1000,
+ 'default_ongkir': delivery_total,
+ 'default_bank': '',
+ 'default_account_name': '',
+ 'default_account_no': '',
+ 'default_refund_type': '',
+ }
+ }
+
+ @api.depends('refund_ids')
+ def _compute_has_refund(self):
+ for so in self:
+ so.has_refund = bool(so.refund_ids)
+
+ def action_view_related_refunds(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Refunds',
+ 'res_model': 'refund.sale.order',
+ 'view_mode': 'tree,form',
+ 'domain': [('sale_order_ids', 'in', [self.id])],
+ 'context': {'default_sale_order_ids': [self.id]},
+ }
+
+ def _compute_refund_ids(self):
+ for order in self:
+ refunds = self.env['refund.sale.order'].search([
+ ('sale_order_ids', 'in', [order.id])
+ ])
+ order.refund_ids = refunds
+
+ def _compute_refund_count(self):
+ for order in self:
+ order.refund_count = self.env['refund.sale.order'].search_count([
+ ('sale_order_ids', 'in', order.id)
+ ])
+
+ @api.depends('invoice_ids')
+ def _compute_advance_payment_move(self):
+ for order in self:
+ move = self.env['account.move'].search([
+ ('sale_id', '=', order.id),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ], limit=1, order="id desc")
+ order.advance_payment_move_id = move
+
+ @api.depends('invoice_ids')
+ def _compute_advance_payment_moves(self):
+ for order in self:
+ moves = self.env['account.move'].search([
+ ('sale_id', '=', order.id),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ])
+ order.advance_payment_move_ids = moves
+
+ @api.depends('invoice_ids')
+ def _compute_advance_payment_moves(self):
+ for order in self:
+ moves = self.env['account.move'].search([
+ ('sale_id', '=', order.id),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ])
+ order.advance_payment_move_ids = moves
+ order.advance_payment_move_count = len(moves)
+
+ def action_open_advance_payment_moves(self):
+ self.ensure_one()
+ moves = self.advance_payment_move_ids
+ if not moves:
+ return
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Journals Sales Order',
+ 'res_model': 'account.move',
+ 'view_mode': 'tree,form',
+ 'domain': [('id', 'in', moves.ids)],
+ 'target': 'current',
+ } \ No newline at end of file