summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/sale_order.py
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-05-14 09:23:14 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-05-14 09:23:14 +0700
commitf120c760c6a837681ebed26d9eea33a8961cd1aa (patch)
tree728ecdd5ffd1530d97d58a9f18b850186b39aa67 /indoteknik_custom/models/sale_order.py
parenta571531bd8626f9bee25e89c62bbd9268ed30597 (diff)
parent2469ee37cfe854f0419a8c3fbabed5bc32bcaa6e (diff)
Merge branch 'odoo-backup' into CR/form-merchant
# Conflicts: # indoteknik_custom/models/__init__.py # indoteknik_custom/security/ir.model.access.csv
Diffstat (limited to 'indoteknik_custom/models/sale_order.py')
-rwxr-xr-xindoteknik_custom/models/sale_order.py639
1 files changed, 577 insertions, 62 deletions
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 48195b77..0d4fc6c3 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -1,3 +1,5 @@
+from re import search
+
from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
from datetime import datetime, timedelta
@@ -7,16 +9,133 @@ from collections import defaultdict
_logger = logging.getLogger(__name__)
+class CancelReasonOrder(models.TransientModel):
+ _name = 'cancel.reason.order'
+ _description = 'Wizard for Cancel Reason order'
+
+ request_id = fields.Many2one('sale.order', string='Request')
+ reason_cancel = fields.Selection([
+ ('harga_terlalu_mahal', 'Harga barang terlalu mahal'),
+ ('harga_web_tidak_valid', 'Harga web tidak valid'),
+ ('stok_kosong', 'Stock kosong'),
+ ('tidak_mau_indent', 'Customer tidak mau indent'),
+ ('batal_rencana_pembelian', 'Customer membatalkan rencana pembelian'),
+ ('vendor_tidak_support_demo', 'Vendor tidak support demo/trial product'),
+ ('product_knowledge_kurang', 'Product knowledge kurang baik'),
+ ('barang_tidak_sesuai', 'Barang tidak sesuai/tepat'),
+ ('tidak_sepakat_pembayaran', 'Tidak menemukan kesepakatan untuk pembayaran'),
+ ('dokumen_tidak_support', 'Indoteknik tidak bisa support document yang dibutuhkan (Ex: TKDN, COO, SNI)'),
+ ('ganti_quotation', 'Ganti Quotation'),
+ ('testing_internal', 'Testing Internal'),
+ ('revisi_data', 'Revisi Data'),
+ ], string='Reason for Cancel', required=True, copy=False, index=True, tracking=3)
+ attachment_bukti = fields.Many2many(
+ 'ir.attachment',
+ string="Attachment Bukti", readonly=False,
+ tracking=3, required=True
+ )
+ nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3)
+
+ def confirm_reject(self):
+ order = self.request_id
+ if order:
+ order.write({'reason_cancel': self.reason_cancel})
+ if not self.attachment_bukti:
+ raise UserError('Attachment bukti wajib disertakan')
+ order.write({'attachment_bukti': self.attachment_bukti})
+ order.message_post(body='Attachment Bukti Cancel',
+ attachment_ids=[self.attachment_bukti.id])
+ if self.reason_cancel == 'ganti_quotation':
+ if self.nomor_so_pengganti:
+ order.write({'nomor_so_pengganti': self.nomor_so_pengganti})
+ else:
+ raise UserError('Nomor SO pengganti wajib disertakan')
+ order.confirm_cancel_order()
+
+ return {'type': 'ir.actions.act_window_close'}
+
+class ShippingOption(models.Model):
+ _name = "shipping.option"
+ _description = "Shipping Option"
+
+ name = fields.Char(string="Option Name", required=True)
+ price = fields.Float(string="Price", required=True)
+ provider = fields.Char(string="Provider")
+ etd = fields.Char(string="Estimated Delivery Time")
+ sale_order_id = fields.Many2one('sale.order', string="Sale Order", ondelete="cascade")
+
+class SaleOrderLine(models.Model):
+ _inherit = 'sale.order.line'
+
+ def unlink(self):
+ lines_to_reject = []
+ for line in self:
+ if line.order_id:
+ now = fields.Datetime.now()
+
+ initial_reason="Product Rejected"
+
+ # Buat lognote untuk product yang di delete
+ log_note = (f"<li>Product '{line.product_id.name}' rejected. </li>"
+ f"<li>Quantity: {line.product_uom_qty}, </li>"
+ f"<li>Date: {now.strftime('%d-%m-%Y')}, </li>"
+ f"<li>Time: {now.strftime('%H:%M:%S')} </li>"
+ f"<li>Reason reject: {initial_reason} </li>")
+
+ lines_to_reject.append({
+ 'sale_order_id': line.order_id.id,
+ 'product_id': line.product_id.id,
+ 'qty_reject': line.product_uom_qty,
+ 'reason_reject': initial_reason, # pesan reason reject
+ 'message_body': log_note,
+ 'order_id': line.order_id,
+ })
+
+ # Call the original unlink method
+ result = super(SaleOrderLine, self).unlink()
+
+ # After deletion, create reject lines and post messages
+ SalesOrderReject = self.env['sales.order.reject']
+ for reject_data in lines_to_reject:
+ # Buat line baru di reject line
+ SalesOrderReject.create({
+ 'sale_order_id': reject_data['sale_order_id'],
+ 'product_id': reject_data['product_id'],
+ 'qty_reject': reject_data['qty_reject'],
+ 'reason_reject': reject_data['reason_reject'],
+ })
+
+ # Post to chatter with a more prominent message
+ reject_data['order_id'].message_post(
+ body=reject_data['message_body'],
+ author_id=self.env.user.partner_id.id, # menampilkan pesan di lognote sebagai current user
+ )
+
+ return result
class SaleOrder(models.Model):
_inherit = "sale.order"
+ ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True, tracking=3)
+
+ metode_kirim_ke_xpdc = fields.Selection([
+ ('indoteknik_deliv', 'Indoteknik Delivery'),
+ ('lalamove', 'Lalamove'),
+ ('grab', 'Grab'),
+ ('gojek', 'Gojek'),
+ ('deliveree', 'Deliveree'),
+ ('other', 'Other'),
+ ], string='Metode Kirim Ke Ekspedisi', copy=False, index=True, tracking=3)
+
+ koli_lines = fields.One2many('sales.order.koli', 'sale_order_id', string='Sales Order Koli', auto_join=True)
fulfillment_line_v2 = fields.One2many('sales.order.fulfillment.v2', 'sale_order_id', string='Fullfillment2')
fullfillment_line = fields.One2many('sales.order.fullfillment', 'sales_order_id', string='Fullfillment')
reject_line = fields.One2many('sales.order.reject', 'sale_order_id', string='Reject Lines')
order_sales_match_line = fields.One2many('sales.order.purchase.match', 'sales_order_id', string='Purchase Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
total_margin = fields.Float('Total Margin', compute='_compute_total_margin', help="Total Margin in Sales Order Header")
+ total_before_margin = fields.Float('Total Before Margin', compute='_compute_total_before_margin', 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')
approval_status = fields.Selection([
('pengajuan1', 'Approval Manager'),
('pengajuan2', 'Approval Pimpinan'),
@@ -28,11 +147,11 @@ class SaleOrder(models.Model):
shipping_cost_covered = fields.Selection([
('indoteknik', 'Indoteknik'),
('customer', 'Customer')
- ], string='Shipping Covered by', help='Siapa yang menanggung biaya ekspedisi?', copy=False)
+ ], string='Shipping Covered by', help='Siapa yang menanggung biaya ekspedisi?', copy=False, tracking=3)
shipping_paid_by = fields.Selection([
('indoteknik', 'Indoteknik'),
('customer', 'Customer')
- ], string='Shipping Paid by', help='Siapa yang talangin dulu Biaya ekspedisi-nya?', copy=False)
+ ], string='Shipping Paid by', help='Siapa yang talangin dulu Biaya ekspedisi-nya?', copy=False, tracking=3)
sales_tax_id = fields.Many2one('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)])
have_outstanding_invoice = fields.Boolean('Have Outstanding Invoice', compute='_have_outstanding_invoice')
have_outstanding_picking = fields.Boolean('Have Outstanding Picking', compute='_have_outstanding_picking')
@@ -48,6 +167,7 @@ class SaleOrder(models.Model):
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Dipakai untuk alamat tempel", tracking=True)
fee_third_party = fields.Float('Fee Pihak Ketiga')
+ biaya_lain_lain = fields.Float('Biaya Lain Lain')
so_status = fields.Selection([
('terproses', 'Terproses'),
('sebagian', 'Sebagian Diproses'),
@@ -83,9 +203,9 @@ class SaleOrder(models.Model):
customer_type = fields.Selection([
('pkp', 'PKP'),
('nonpkp', 'Non PKP')
- ], required=True)
- sppkp = fields.Char(string="SPPKP", required=True, tracking=True)
- npwp = fields.Char(string="NPWP", required=True, tracking=True)
+ ], required=True, compute='_compute_partner_field')
+ sppkp = fields.Char(string="SPPKP", required=True, tracking=True, compute='_compute_partner_field')
+ npwp = fields.Char(string="NPWP", required=True, tracking=True, compute='_compute_partner_field')
purchase_total = fields.Monetary(string='Purchase Total', compute='_compute_purchase_total')
voucher_id = fields.Many2one(comodel_name='voucher', string='Voucher', copy=False)
applied_voucher_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False)
@@ -93,11 +213,13 @@ class SaleOrder(models.Model):
applied_voucher_shipping_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False)
amount_voucher_shipping_disc = fields.Float(string='Voucher Discount')
source_id = fields.Many2one('utm.source', 'Source', domain="[('id', 'in', [32, 59, 60, 61])]", required=True)
- estimated_arrival_days = fields.Integer('Estimated Arrival Days', default=0)
+ estimated_arrival_days = fields.Integer('Estimated Arrival To', default=0)
+ estimated_arrival_days_start = fields.Integer('Estimated Arrival From', default=0)
email = fields.Char(string='Email')
picking_iu_id = fields.Many2one('stock.picking', 'Picking IU')
helper_by_id = fields.Many2one('res.users', 'Helper By')
- eta_date = fields.Datetime(string='ETA Date', copy=False, compute='_compute_eta_date')
+ eta_date_start = fields.Datetime(string='ETA Date start', copy=False, compute='_compute_eta_date')
+ eta_date = fields.Datetime(string='ETA Date end', copy=False, compute='_compute_eta_date')
flash_sale = fields.Boolean(string='Flash Sale', help='Data dari web')
is_continue_transaction = fields.Boolean(string='Button Transaction', help='Data dari web')
web_approval = fields.Selection([
@@ -144,6 +266,121 @@ class SaleOrder(models.Model):
('PNR', 'Pareto Non Repeating'),
('NP', 'Non Pareto')
])
+ # estimated_ready_ship_date = fields.Datetime(
+ # string='ET Ready to Ship compute',
+ # compute='_compute_etrts_date'
+ # )
+ expected_ready_to_ship = fields.Datetime(
+ string='ET Ready to Ship',
+ copy=False
+ )
+ shipping_method_picking = fields.Char(string='Shipping Method Picking', compute='_compute_shipping_method_picking')
+
+ reason_cancel = fields.Selection([
+ ('harga_terlalu_mahal', 'Harga barang terlalu mahal'),
+ ('harga_web_tidak_valid', 'Harga web tidak valid'),
+ ('stok_kosong', 'Stock kosong'),
+ ('tidak_mau_indent', 'Customer tidak mau indent'),
+ ('batal_rencana_pembelian', 'Customer membatalkan rencana pembelian'),
+ ('vendor_tidak_support_demo', 'Vendor tidak support demo/trial product'),
+ ('product_knowledge_kurang', 'Product knowledge kurang baik'),
+ ('barang_tidak_sesuai', 'Barang tidak sesuai/tepat'),
+ ('tidak_sepakat_pembayaran', 'Tidak menemukan kesepakatan untuk pembayaran'),
+ ('dokumen_tidak_support', 'Indoteknik tidak bisa support document yang dibutuhkan (Ex: TKDN, COO, SNI)'),
+ ('ganti_quotation', 'Ganti Quotation'),
+ ('testing_internal', 'Testing Internal'),
+ ('revisi_data', 'Revisi Data'),
+ ], string='Reason for Cancel', copy=False, index=True, tracking=3)
+ attachment_bukti = fields.Many2one(
+ 'ir.attachment',
+ 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)]")
+ hold_outgoing = fields.Boolean('Hold Outgoing SO', tracking=3)
+ state_ask_cancel = fields.Selection([
+ ('hold', 'Hold'),
+ ('approve', 'Approve')
+ ], tracking=True, string='State Cancel', copy=False)
+
+ 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'):
+ rec.state_ask_cancel = 'approve'
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Persetujuan Diberikan',
+ 'message': 'Proses cancel sudah disetujui',
+ 'type': 'success',
+ 'sticky': True
+ }
+ }
+ else:
+ rec.state_ask_cancel = 'hold'
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Menunggu Persetujuan',
+ 'message': 'Tim Purchasing akan memproses permintaan Anda',
+ 'type': 'warning',
+ 'sticky': False
+ }
+ }
+
+ def hold_unhold_qty_outgoing_so(self):
+ if self.hold_outgoing == True:
+ self.hold_outgoing = False
+ else:
+ self.hold_outgoing = True
+
+ def _validate_uniform_taxes(self):
+ for order in self:
+ tax_sets = set()
+ for line in order.order_line:
+ tax_ids = tuple(sorted(line.tax_id.ids))
+ if tax_ids:
+ tax_sets.add(tax_ids)
+ 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')
+ # 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
+ # 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:
+ carrier_names = order.picking_ids.mapped('carrier_id.name')
+ order.shipping_method_picking = ', '.join(filter(None, carrier_names))
+ else:
+ order.shipping_method_picking = False
@api.onchange('payment_status')
def _is_continue_transaction(self):
@@ -183,15 +420,36 @@ class SaleOrder(models.Model):
if total_weight == 0:
raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.")
-
+
if total_weight < 10:
total_weight = 10
+
self.delivery_amt = total_weight * 3000
-
+
+ shipping_option = self.env["shipping.option"].create({
+ "name": "Indoteknik Delivery",
+ "price": self.delivery_amt,
+ "provider": "Indoteknik",
+ "etd": "1-2 Hari",
+ "sale_order_id": self.id,
+ })
+ self.shipping_option_id = shipping_option.id
+ self.message_post(
+ body=(
+ f"<b>Estimasi pengiriman Indoteknik berhasil:</b><br/>"
+ f"Layanan: {shipping_option.name}<br/>"
+ f"ETD: {shipping_option.etd}<br/>"
+ f"Biaya: Rp {shipping_option.price:,}<br/>"
+ f"Provider: {shipping_option.provider}"
+ ),
+ message_type="comment",
+ )
+
def action_estimate_shipping(self):
if self.carrier_id.id in [1, 151]:
self.action_indoteknik_estimate_shipping()
return
+
total_weight = 0
missing_weight_products = []
@@ -209,35 +467,50 @@ class SaleOrder(models.Model):
if total_weight == 0:
raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.")
- # Mendapatkan city_id berdasarkan nama kota
- origin_city_name = self.warehouse_id.partner_id.kota_id.name
destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id
-
if not destination_subsdistrict_id:
- raise UserError("Gagal mendapatkan ID kota asal atau tujuan.")
+ raise UserError("Gagal mendapatkan ID kota tujuan.")
result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id)
if result:
- estimated_cost = result['rajaongkir']['results'][0]['costs'][0]['cost'][0]['value']
- self.delivery_amt = estimated_cost
-
- shipping_info = []
+ 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_info.append(f"Service: {service}, Description: {description}, ETD: {etd} hari, Cost: Rp {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,
+ "etd": etd,
+ "sale_order_id": self.id,
+ })
+
- log_message = "<br/>".join(shipping_info)
+ 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]} hari, Cost: Rp {s[3]}' for s in shipping_options])}",
+ message_type="comment"
+ )
+
+ # 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")
- description_ongkir = result['rajaongkir']['results'][0]['costs'][0]['description']
- etd_ongkir = result['rajaongkir']['results'][0]['costs'][0]['cost'][0]['etd']
- service_ongkir = result['rajaongkir']['results'][0]['costs'][0]['service']
- self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}<br/>Service: {service_ongkir}<br/>Description: {description_ongkir}<br/>ETD: {etd_ongkir}<br/>Detail Lain:<br/>{log_message}")
else:
raise UserError("Gagal mendapatkan estimasi ongkir.")
+
def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id):
url = 'https://pro.rajaongkir.com/api/cost'
@@ -337,11 +610,11 @@ class SaleOrder(models.Model):
delivery_amt = order.delivery_amt
else:
delivery_amt = 0
- order.percent_margin_after_delivery_purchase = round((order.margin_after_delivery_purchase / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2)
+ order.percent_margin_after_delivery_purchase = round((order.margin_after_delivery_purchase / (order.amount_untaxed-delivery_amt-order.fee_third_party-order.biaya_lain_lain)) * 100, 2)
def _compute_date_kirim(self):
for rec in self:
- picking = self.env['stock.picking'].search([('sale_id', '=', rec.id), ('state', 'not in', ['cancel'])], order='date_doc_kirim desc', limit=1)
+ picking = self.env['stock.picking'].search([('sale_id', '=', rec.id), ('state', 'not in', ['cancel']), ('name', 'not ilike', 'BU/PICK/%')], order='date_doc_kirim desc', limit=1)
rec.date_kirim_ril = picking.date_doc_kirim
rec.date_status_done = picking.date_done
rec.date_driver_arrival = picking.driver_arrival_date
@@ -363,19 +636,131 @@ class SaleOrder(models.Model):
rec.compute_fullfillment = True
+ @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start')
def _compute_eta_date(self):
- max_leadtime = 0
+ 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)
+ else:
+ rec.eta_date = False
+ rec.eta_date_start = 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']
+
+ while True :
+ today += timedelta(days=1)
+ offset += 1
+
+ if today.weekday() >= 5:
+ continue
- for line in self.order_line:
- leadtime = line.vendor_id.leadtime
- max_leadtime = max(max_leadtime, leadtime)
+ is_holiday = holiday.search([("start_date", "=", today)])
+ if is_holiday:
+ continue
+
+ break
+
+ 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
+
+ # Cek apakah SEMUA produk memiliki qty_free_bandengan >= qty_needed
+ 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}
+
+ # Cari semua vendor pemenang untuk produk yang diberikan
+ vendors = self.env['purchase.pricelist'].search([
+ ('product_id', 'in', product_ids),
+ ('is_winner', '=', True)
+ ])
+ max_slatime = 1
+
+ for vendor in vendors:
+ vendor_sla = self.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1)
+ slatime = 15
+ if vendor_sla:
+ if vendor_sla.unit == 'hari':
+ vendor_duration = vendor_sla.duration * 24 * 60
+ include_instant = False
+ else :
+ vendor_duration = vendor_sla.duration * 60
+ include_instant = True
+
+ estimation_sla = (1 * 24 * 60) + vendor_duration
+ estimation_sla_days = estimation_sla / (24 * 60)
+ slatime = math.ceil(estimation_sla_days)
+
+ max_slatime = max(max_slatime, slatime)
+
+ return {'slatime': max_slatime, 'include_instant': include_instant}
+
+ def _calculate_etrts_date(self):
for rec in self:
- if rec.date_order and rec.state not in ['cancel', 'draft']:
- eta_date = datetime.now() + timedelta(days=max_leadtime)
- rec.eta_date = eta_date
- else:
- rec.eta_date = False
+ if not rec.date_order:
+ rec.expected_ready_to_ship = False
+ return
+
+ current_date = datetime.now().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
+ if not rec.estimated_arrival_days:
+ rec.estimated_arrival_days = sum_days
+
+ eta_date = current_date + timedelta(days=sum_days)
+ 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 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
+
+
+ @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()
+
+ def _set_etrts_date(self):
+ for order in self:
+ if order.state in ('done', 'cancel', 'sale'):
+ raise UserError(_("You cannot change the Estimated Ready To Ship Date on a done, sale or cancelled order."))
+ # order.move_lines.write({'estimated_ready_ship_date': order.estimated_ready_ship_date})
def _prepare_invoice(self):
"""
@@ -426,6 +811,33 @@ class SaleOrder(models.Model):
if self.email and not re.match(pattern, self.email):
raise UserError('Email yang anda input kurang valid')
+ # @api.constrains('delivery_amt', 'carrier_id', 'shipping_cost_covered')
+ 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:
+ raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum diisi.')
+ else:
+ raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum diisi.')
+
+ if self.delivery_amt < 100:
+ if self.carrier_id.id == 1:
+ raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum memenuhi tarif minimum.')
+ else:
+ raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum memenuhi tarif minimum.')
+
+
+ # if self.delivery_amt < 5000:
+ # if (self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik') and not self.env.context.get('active_id', []):
+ # if self.carrier_id.id == 1:
+ # raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum memenuhi jumlah minimum.')
+ # else:
+ # raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum memenuhi jumlah minimum.')
+
+
def override_allow_create_invoice(self):
if not self.env.user.is_accounting:
raise UserError('Hanya Finance Accounting yang dapat klik tombol ini')
@@ -540,22 +952,22 @@ class SaleOrder(models.Model):
redirect_url = json.loads(lookup_json)['redirect_url']
self.payment_link_midtrans = str(redirect_url)
- # Generate QR code
- qr = qrcode.QRCode(
- version=1,
- error_correction=qrcode.constants.ERROR_CORRECT_L,
- box_size=10,
- border=4,
- )
- qr.add_data(redirect_url)
- qr.make(fit=True)
- img = qr.make_image(fill_color="black", back_color="white")
+ if 'redirect_url' in response:
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ box_size=10,
+ border=4,
+ )
+ qr.add_data(redirect_url)
+ qr.make(fit=True)
+ img = qr.make_image(fill_color="black", back_color="white")
- buffer = BytesIO()
- img.save(buffer, format="PNG")
- qr_code_img = base64.b64encode(buffer.getvalue()).decode()
+ buffer = BytesIO()
+ img.save(buffer, format="PNG")
+ qr_code_img = base64.b64encode(buffer.getvalue()).decode()
- self.payment_qr_code = qr_code_img
+ self.payment_qr_code = qr_code_img
@api.model
def _generate_so_access_token(self, limit=50):
@@ -570,17 +982,14 @@ class SaleOrder(models.Model):
if line.product_id.type == 'product':
line_no += 1
line.line_no = line_no
+
def write(self, vals):
- res = super(SaleOrder, self).write(vals)
-
if 'carrier_id' in vals:
for picking in self.picking_ids:
if picking.state == 'assigned':
picking.carrier_id = self.carrier_id
- return res
-
def calculate_so_status(self):
so_state = ['sale']
sales = self.search([
@@ -634,6 +1043,13 @@ class SaleOrder(models.Model):
# return [('id', 'not in', order_ids)]
# return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)]
+ @api.depends('partner_id')
+ def _compute_partner_field(self):
+ for order in self:
+ partner = order.partner_id.parent_id or order.partner_id
+ order.npwp = partner.npwp
+ order.sppkp = partner.sppkp
+ order.customer_type = partner.customer_type
@api.onchange('partner_id')
def onchange_partner_contact(self):
@@ -735,6 +1151,7 @@ class SaleOrder(models.Model):
self._validate_order()
for order in self:
+ order._validate_uniform_taxes()
order.order_line.validate_line()
term_days = 0
@@ -756,15 +1173,38 @@ class SaleOrder(models.Model):
raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO")
if not order.user_id.active:
raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact")
-
+
+ def check_product_bom(self):
+ for order in self:
+ for line in order.order_line:
+ if 'bom-it' in line.name.lower() or 'bom' in line.product_id.default_code.lower() if line.product_id.default_code else False:
+ search_bom = self.env['mrp.production'].search([('product_id', '=', line.product_id.id)],order='name desc')
+ if search_bom:
+ confirmed_bom = search_bom.filtered(lambda x: x.state == 'confirmed')
+ if not confirmed_bom:
+ raise UserError("Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi MD.")
+ else:
+ raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi MD")
+
+ def check_duplicate_product(self):
+ for order in self:
+ for line in order.order_line:
+ search_product = self.env['sale.order.line'].search([('product_id', '=', line.product_id.id), ('order_id', '=', order.id)])
+ if len(search_product) > 1:
+ raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name))
+
def sale_order_approve(self):
+ self.check_duplicate_product()
+ self.check_product_bom()
self.check_credit_limit()
+ self.check_limit_so_to_invoice()
if self.validate_different_vendor() and not self.vendor_approval:
return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor')
self.check_due()
self._validate_order()
for order in self:
+ order._validate_uniform_taxes()
order.order_line.validate_line()
order.check_data_real_delivery_address()
order._validate_order()
@@ -816,7 +1256,9 @@ class SaleOrder(models.Model):
order.approval_status = 'pengajuan2'
return self._create_approval_notification('Pimpinan')
elif order._requires_approval_margin_manager():
+ self.check_product_bom()
self.check_credit_limit()
+ self.check_limit_so_to_invoice()
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
@@ -923,6 +1365,29 @@ class SaleOrder(models.Model):
raise UserError(_("%s is in Blocking Stage, Remaining credit limit is %s, from %s and outstanding %s")
% (rec.partner_id.name, remaining_credit_limit, block_stage, outstanding_amount))
+ def check_limit_so_to_invoice(self):
+ for rec in self:
+ # Ambil jumlah outstanding_amount dan rec.amount_total sebagai current_total
+ outstanding_amount = rec.outstanding_amount
+ current_total = rec.amount_total + outstanding_amount
+
+ # Ambil blocking stage dari partner
+ block_stage = rec.partner_id.parent_id.blocking_stage if rec.partner_id.parent_id else rec.partner_id.blocking_stage or 0
+ is_cbd = rec.partner_id.parent_id.property_payment_term_id.id == 26 if rec.partner_id.parent_id else rec.partner_id.property_payment_term_id.id == 26 or False
+
+ # Ambil jumlah nilai dari SO yang invoice_status masih 'to invoice'
+ so_to_invoice = 0
+ for sale in rec.partner_id.sale_order_ids:
+ if sale.invoice_status == 'to invoice':
+ so_to_invoice = so_to_invoice + sale.amount_total
+ # Hitung remaining credit limit
+ remaining_credit_limit = block_stage - current_total - so_to_invoice
+
+ # Validasi limit
+ if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd:
+ raise UserError(_("The credit limit for %s will exceed the Blocking Stage if the Sale Order is confirmed. The remaining credit limit is %s, from %s and the outstanding amount is %s.")
+ % (rec.partner_id.name, block_stage - current_total, block_stage, outstanding_amount))
+
def validate_different_vendor(self):
if self.vendor_approval_id.filtered(lambda v: v.state == 'draft'):
draft_names = ", ".join(self.vendor_approval_id.filtered(lambda v: v.state == 'draft').mapped('number'))
@@ -971,6 +1436,11 @@ class SaleOrder(models.Model):
def action_confirm(self):
for order in self:
+ order._validate_uniform_taxes()
+ order.check_duplicate_product()
+ order.check_product_bom()
+ order.check_credit_limit()
+ order.check_limit_so_to_invoice()
if self.validate_different_vendor() and not self.vendor_approval:
return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor')
@@ -1009,6 +1479,7 @@ class SaleOrder(models.Model):
order._set_sppkp_npwp_contact()
order.calculate_line_no()
order.send_notif_to_salesperson()
+ # order._compute_etrts_date()
# order.order_line.get_reserved_from()
res = super(SaleOrder, self).action_confirm()
@@ -1026,15 +1497,22 @@ class SaleOrder(models.Model):
def action_cancel(self):
# TODO stephan prevent cancel if have invoice, do, and po
+ if self.state_ask_cancel != 'approve' and self.state not in ['draft', 'sent']:
+ raise UserError("Anda harus approval purchasing terlebih dahulu")
main_parent = self.partner_id.get_main_parent()
if self._name != 'sale.order':
return super(SaleOrder, self).action_cancel()
if self.have_outstanding_invoice:
raise UserError("Invoice harus di Cancel dahulu")
+
+ disallow_states = ['draft', 'waiting', 'confirmed', 'assigned']
+ for picking in self.picking_ids:
+ if picking.state in disallow_states:
+ raise UserError("DO yang draft, waiting, confirmed, atau assigned harus di-cancel oleh Logistik")
for line in self.order_line:
if line.qty_delivered > 0:
- raise UserError("DO harus di-cancel terlebih dahulu.")
+ raise UserError("DO yang done harus di-Return oleh Logistik")
if not self.web_approval:
self.web_approval = 'company'
@@ -1045,8 +1523,24 @@ class SaleOrder(models.Model):
self.due_id = False
if main_parent.use_so_approval:
self.send_notif_to_salesperson(cancel=True)
+ for order in self:
+ if order.amount_total > 30000000:
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': _('Cancel Reason'),
+ 'res_model': 'cancel.reason.order',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {'default_request_id': self.id},
+ }
return super(SaleOrder, self).action_cancel()
-
+
+ def confirm_cancel_order(self):
+ """Fungsi ini akan dipanggil oleh wizard setelah alasan pembatalan dipilih"""
+ if self.state != 'cancel':
+ self.state = 'cancel'
+ return super(SaleOrder, self).action_cancel()
+
def validate_partner_invoice_due(self):
parent_id = self.partner_id.parent_id.id
parent_id = parent_id if parent_id else self.partner_id.id
@@ -1078,10 +1572,11 @@ class SaleOrder(models.Model):
return False
def _requires_approval_margin_leader(self):
- return self.total_percent_margin <= 13 and not self.env.user.is_leader
+ return self.total_percent_margin <= 15 and not self.env.user.is_leader
def _requires_approval_margin_manager(self):
- return self.total_percent_margin <= 20 and not self.env.user.is_leader and not self.env.user.is_sales_manager
+ return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.is_leader
+ # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager
def _create_approval_notification(self, approval_role):
title = 'Warning'
@@ -1118,8 +1613,16 @@ class SaleOrder(models.Model):
def _compute_total_margin(self):
for order in self:
total_margin = sum(line.item_margin for line in order.order_line if line.product_id)
+ if order.ongkir_ke_xpdc:
+ total_margin -= order.ongkir_ke_xpdc
+
order.total_margin = total_margin
+ def _compute_total_before_margin(self):
+ for order in self:
+ 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
+
def _compute_total_percent_margin(self):
for order in self:
if order.amount_untaxed == 0:
@@ -1129,7 +1632,9 @@ class SaleOrder(models.Model):
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-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)
@api.onchange('sales_tax_id')
@@ -1383,18 +1888,22 @@ class SaleOrder(models.Model):
def create(self, vals):
# Ensure partner details are updated when a sale order is created
order = super(SaleOrder, self).create(vals)
+ order._compute_etrts_date()
+ order._validate_expected_ready_ship_date()
+ order._validate_delivery_amt()
+ # order._check_total_margin_excl_third_party()
# order._update_partner_details()
return order
- def write(self, vals):
+ # def write(self, vals):
# Call the super method to handle the write operation
- res = super(SaleOrder, self).write(vals)
-
+ # res = super(SaleOrder, self).write(vals)
+ # self._compute_etrts_date()
# Check if the update is coming from a save operation
# if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']):
# self._update_partner_details()
- return res
+ # return res
def _update_partner_details(self):
for order in self:
@@ -1423,5 +1932,11 @@ class SaleOrder(models.Model):
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 \ No newline at end of file