from re import search
from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
from datetime import datetime, timedelta
import logging, random, string, requests, math, json, re, qrcode, base64
from io import BytesIO
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 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_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")
approval_status = fields.Selection([
('pengajuan1', 'Approval Manager'),
('pengajuan2', 'Approval Pimpinan'),
('approved', 'Approved'),
], string='Approval Status', readonly=True, copy=False, index=True, tracking=3)
carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3)
have_visit_service = fields.Boolean(string='Have Visit Service', compute='_have_visit_service', help='To compute is customer get visit service')
delivery_amt = fields.Float(string='Delivery Amt', copy=False)
shipping_cost_covered = fields.Selection([
('indoteknik', 'Indoteknik'),
('customer', 'Customer')
], 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, 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')
have_outstanding_po = fields.Boolean('Have Outstanding PO', compute='_have_outstanding_po')
purchase_ids = fields.Many2many('purchase.order', string='Purchases', compute='_get_purchases')
real_shipping_id = fields.Many2one(
'res.partner', string='Real Delivery Address', readonly=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Dipakai untuk alamat tempel", tracking=True)
real_invoice_id = fields.Many2one(
'res.partner', string='Delivery Invoice Address', required=True,
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'),
('menunggu', 'Menunggu Diproses'),
], copy=False)
partner_purchase_order_name = fields.Char(string='Nama PO Customer', copy=False, help="Nama purchase order customer, diisi oleh customer melalui website.", tracking=3)
partner_purchase_order_description = fields.Text(string='Keterangan PO Customer', copy=False, help="Keterangan purchase order customer, diisi oleh customer melalui website.", tracking=3)
partner_purchase_order_file = fields.Binary(string='File PO Customer', copy=False, help="File purchase order customer, diisi oleh customer melalui website.")
payment_status = fields.Selection([
('pending', 'Pending'),
('capture', 'Capture'),
('settlement', 'Settlement'),
('deny', 'Deny'),
('cancel', 'Cancel'),
('expire', 'Expire'),
('failure', 'Failure'),
('refund', 'Refund'),
('chargeback', 'Chargeback'),
('partial_refund', 'Partial Refund'),
('partial_chargeback', 'Partial Chargeback'),
('authorize', 'Authorize'),
], tracking=True, string='Payment Status', help='Payment Gateway Status / Midtrans / Web, https://docs.midtrans.com/en/after-payment/status-cycle')
date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', help="Tanggal Kirim di cetakan SJ yang terakhir, tidak berpengaruh ke Accounting")
payment_type = fields.Char(string='Payment Type', help='Jenis pembayaran dengan Midtrans')
gross_amount = fields.Float(string='Gross Amount', help='Jumlah pembayaran yang dilakukan dengan Midtrans')
notification = fields.Char(string='Notification', help='Dapat membantu error dari approval')
delivery_service_type = fields.Char(string='Delivery Service Type', help='data dari rajaongkir')
grand_total = fields.Monetary(string='Grand Total', help='Amount total + amount delivery', compute='_compute_grand_total')
payment_link_midtrans = fields.Char(string='Payment Link', help='Url payment yg digenerate oleh midtrans, harap diserahkan ke customer agar dapat dilakukan pembayaran secara mandiri')
payment_qr_code = fields.Binary("Payment QR Code")
due_id = fields.Many2one('due.extension', string="Due Extension", readonly=True, tracking=True)
vendor_approval_id = fields.Many2many('vendor.approval', string="Vendor Approval", readonly=True, tracking=True, copy=False)
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)
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)
amount_voucher_disc = fields.Float(string='Voucher Discount')
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 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_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([
('company', 'Company'),
('cust_manager', 'Customer Manager'),
('cust_director', 'Customer Director'),
('cust_procurement', 'Customer Procurement')
], string='Web Approval', copy=False)
compute_fullfillment = fields.Boolean(string='Compute Fullfillment', compute="_compute_fullfillment")
vendor_approval = fields.Boolean(string='Vendor Approval')
note_ekspedisi = fields.Char(string="Note Ekspedisi")
date_kirim_ril = fields.Datetime(string='Tanggal Kirim SJ', compute='_compute_date_kirim', copy=False)
date_status_done = fields.Datetime(string='Date Done DO', compute='_compute_date_kirim', copy=False)
date_driver_arrival = fields.Datetime(string='Arrival Date', compute='_compute_date_kirim', copy=False)
date_driver_departure = fields.Datetime(string='Departure Date', compute='_compute_date_kirim', copy=False)
note_website = fields.Char(string="Note Website")
use_button = fields.Boolean(string='Using Calculate Selling Price', copy=False)
unreserve_id = fields.Many2one('stock.picking', 'Unreserve Picking')
voucher_shipping_id = fields.Many2one(comodel_name='voucher', string='Voucher Shipping', copy=False)
margin_after_delivery_purchase = fields.Float(string='Margin After Delivery Purchase', compute='_compute_margin_after_delivery_purchase')
percent_margin_after_delivery_purchase = fields.Float(string='% Margin After Delivery Purchase', compute='_compute_margin_after_delivery_purchase')
purchase_delivery_amt = fields.Float(string='Purchase Delivery Amount', compute='_compute_purchase_delivery_amount')
type_promotion = fields.Char(string='Type Promotion', compute='_compute_type_promotion')
partner_invoice_id = fields.Many2one(
'res.partner', string='Invoice Address',
readonly=True, required=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
tracking=True, # Menambahkan tracking=True
)
partner_shipping_id = fields.Many2one(
'res.partner', string='Delivery Address', readonly=True, required=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
payment_term_id = fields.Many2one(
'account.payment.term', string='Payment Terms', check_company=True, # Unrequired company
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
total_weight = fields.Float(string='Total Weight', compute='_compute_total_weight')
pareto_status = fields.Selection([
('PR', 'Pareto Repeating'),
('PPR', 'Potensi Pareto Repeating'),
('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)]")
@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):
if not self.is_continue_transaction:
if self.payment_status == 'settlement':
self.is_continue_transaction = True
else:
self.is_continue_transaction = False
def _compute_total_weight(self):
total_weight = 0
missing_weight_products = []
for line in self.order_line:
if line.weight > 0:
total_weight += line.weight * line.product_uom_qty
self.total_weight = total_weight
def action_indoteknik_estimate_shipping(self):
if not self.real_shipping_id.kota_id.is_jabodetabek:
raise UserError('Estimasi ongkir hanya bisa dilakukan di kota Jabodetabek')
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 = '
'.join(missing_weight_products)
self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}")
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"Estimasi pengiriman Indoteknik berhasil:
"
f"Layanan: {shipping_option.name}
"
f"ETD: {shipping_option.etd}
"
f"Biaya: Rp {shipping_option.price:,}
"
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 = []
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 = '
'.join(missing_weight_products)
self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}")
if total_weight == 0:
raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.")
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,
"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}
Detail Lain:
"
f"{'
'.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}
Detail Lain:
{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment")
else:
raise UserError("Gagal mendapatkan estimasi ongkir.")
def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id):
url = 'https://pro.rajaongkir.com/api/cost'
headers = {
'key': '9b1310f644056d84d60b0af6bb21611a',
}
courier = self.carrier_id.name.lower()
data = {
'origin': 2127,
'originType': 'subdistrict',
'destination': int(destination_subsdistrict_id),
'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
def _normalize_city_name(self, city_name):
city_name = city_name.lower()
if city_name.startswith('kabupaten'):
city_name = city_name.replace('kabupaten', '').strip()
elif city_name.startswith('kota'):
city_name = city_name.replace('kota', '').strip()
city_name = " ".join(city_name.split())
return city_name
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 _compute_type_promotion(self):
for rec in self:
promotion_types = []
for promotion in rec.order_promotion_ids:
for line_program in promotion.program_line_id:
if line_program.promotion_type:
promotion_types.append(dict(line_program._fields['promotion_type'].selection).get(line_program.promotion_type))
rec.type_promotion = ', '.join(sorted(set(promotion_types)))
def _compute_purchase_delivery_amount(self):
for order in self:
match = self.env['purchase.order.sales.match']
result2 = match.search([
('sale_id.id', '=', order.id)
])
delivery_amt = 0
for res in result2:
delivery_amt = res.delivery_amt
order.purchase_delivery_amt = delivery_amt
def _compute_margin_after_delivery_purchase(self):
for order in self:
order.margin_after_delivery_purchase = order.total_margin - order.purchase_delivery_amt
if order.amount_untaxed == 0:
order.percent_margin_after_delivery_purchase = 0
continue
if order.shipping_cost_covered == 'indoteknik':
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-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']), ('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
rec.date_driver_departure = picking.driver_departure_date
def open_form_multi_create_uang_muka(self):
action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_sale_order_multi_uangmuka')
action['context'] = {
'so_ids': [x.id for x in self]
}
return action
def _compute_fullfillment(self):
for rec in self:
# rec.fullfillment_line.unlink()
#
# for line in rec.order_line:
# line._compute_reserved_from()
rec.compute_fullfillment = True
@api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start')
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)
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
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 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):
"""
Prepare the dict of values to create the new invoice for a sales order. This method may be
overridden to implement custom invoice generation (making sure to call super() to establish
a clean extension chain).
"""
self.ensure_one()
journal = self.env['account.move'].with_context(default_move_type='out_invoice')._get_default_journal()
if not journal:
raise UserError(_('Please define an accounting sales journal for the company %s (%s).') % (self.company_id.name, self.company_id.id))
parent_id = self.partner_id.parent_id
parent_id = parent_id if parent_id else self.partner_id
invoice_vals = {
'ref': self.client_order_ref or '',
'move_type': 'out_invoice',
'narration': self.note,
'currency_id': self.pricelist_id.currency_id.id,
'campaign_id': self.campaign_id.id,
'medium_id': self.medium_id.id,
'source_id': self.source_id.id,
'user_id': self.user_id.id,
'sale_id': self.id,
'invoice_user_id': self.user_id.id,
'team_id': self.team_id.id,
'partner_id': parent_id.id,
'partner_shipping_id': parent_id.id,
'real_invoice_id': self.real_invoice_id.id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(self.partner_invoice_id.id)).id,
'partner_bank_id': self.company_id.partner_id.bank_ids[:1].id,
'journal_id': journal.id, # company comes from the journal
'invoice_origin': self.name,
'invoice_payment_term_id': self.payment_term_id.id,
'payment_reference': self.reference,
'transaction_ids': [(6, 0, self.transaction_ids.ids)],
'invoice_line_ids': [],
'company_id': self.company_id.id,
}
return invoice_vals
@api.constrains('email')
def _validate_email(self):
rule_regex = self.env['ir.config_parameter'].sudo().get_param('sale.order.validate_email') or ''
pattern = rf'^{rule_regex}$'
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')
for term in self.payment_term_id.line_ids:
if term.days > 0:
raise UserError('Hanya dapat digunakan pada Cash Before Delivery')
for line in self.order_line:
line.qty_to_invoice = line.product_uom_qty
# def _get_pickings(self):
# state = ['assigned']
# for order in self:
# pickings = self.env['stock.picking'].search([
# ('sale_id.id', '=', order.id),
# ('state', 'in', state)
# ])
# order.picking_ids = pickings
@api.model
def action_multi_update_state(self):
for sale in self:
for picking_ids in sale.picking_ids:
if not picking_ids.state == 'cancel':
raise UserError('DO harus cancel terlebih dahulu')
sale.update({
'state': 'cancel',
})
if sale.state == 'cancel':
sale.update({
'approval_status': False,
})
def open_form_multi_update_status(self):
action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_sale_orders_multi_update')
action['context'] = {
'sale_ids': [x.id for x in self]
}
return action
def open_form_multi_update_state(self):
action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_quotation_so_multi_update')
action['context'] = {
'quotation_ids': [x.id for x in self]
}
return action
def action_multi_update_invoice_status(self):
for sale in self:
sale.update({
'invoice_status': 'invoiced',
})
def _compute_purchase_total(self):
for order in self:
total = 0
for line in order.order_line:
total += line.vendor_subtotal
order.purchase_total = total
def check_data_real_delivery_address(self):
real_delivery_address = self.real_shipping_id
if not real_delivery_address.state_id:
raise UserError('State Real Delivery Address harus diisi')
if not real_delivery_address.zip:
raise UserError('Zip code Real Delivery Address harus diisi')
if not real_delivery_address.mobile:
raise UserError('Mobile Real Delivery Address harus diisi')
if not real_delivery_address.phone:
raise UserError('Phone Real Delivery Address harus diisi')
if not real_delivery_address.kecamatan_id:
raise UserError('Kecamatan Real Delivery Address harus diisi')
# if not real_delivery_address.kelurahan_id:
# raise UserError('Kelurahan Real Delivery Address harus diisi')
def generate_payment_link_midtrans_sales_order(self):
# midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox
# midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox
midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production
midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production
so_number = self.name
so_number = so_number.replace('/', '-')
so_grandtotal = math.floor(self.grand_total)
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': midtrans_auth,
}
check_url = f'https://api.midtrans.com/v2/{so_number}/status'
check_response = requests.get(check_url, headers=headers)
if check_response.status_code == 200:
status_response = check_response.json()
if status_response.get('transaction_status') == 'expire' or status_response.get('transaction_status') == 'cancel':
so_number = so_number + '-cpl'
json_data = {
'transaction_details': {
'order_id': so_number,
'gross_amount': so_grandtotal,
},
'credit_card': {
'secure': True,
},
}
response = requests.post(midtrans_url, headers=headers, json=json_data).json()
lookup_json = json.dumps(response, indent=4, sort_keys=True)
redirect_url = json.loads(lookup_json)['redirect_url']
self.payment_link_midtrans = str(redirect_url)
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()
self.payment_qr_code = qr_code_img
@api.model
def _generate_so_access_token(self, limit=50):
orders = self.search([('access_token', '=', False)], limit=limit)
for order in orders:
token_source = string.ascii_letters + string.digits
order.access_token = ''.join(random.choice(token_source) for i in range(20))
def calculate_line_no(self):
line_no = 0
for line in self.order_line:
if line.product_id.type == 'product':
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 calculate_so_status(self):
so_state = ['sale']
sales = self.search([
('state', 'in', so_state),
('so_status', '!=', 'terproses'),
])
for sale in sales:
picking_states = ['draft', 'assigned', 'confirmed', 'waiting']
have_outstanding_pick = any(x.state in picking_states for x in sale.picking_ids)
sum_qty_so = sum(so_line.product_uom_qty for so_line in sale.order_line)
sum_qty_ship = sum(so_line.qty_delivered for so_line in sale.order_line)
if sum_qty_so > sum_qty_ship > 0:
sale.so_status = 'sebagian'
elif not have_outstanding_pick:
sale.so_status = 'terproses'
else:
sale.so_status = 'menunggu'
for picking in sale.picking_ids:
sum_qty_pick = sum(move_line.product_uom_qty for move_line in picking.move_ids_without_package)
sum_qty_reserved = sum(move_line.product_uom_qty for move_line in picking.move_line_ids_without_package)
if picking.state == 'done':
continue
elif sum_qty_pick == sum_qty_reserved and not picking.date_reserved:# baru ke reserved
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
picking.date_reserved = current_time
elif sum_qty_pick == sum_qty_reserved:# sudah ada data reserved
picking.date_reserved = picking.date_reserved
else:
picking.date_reserved = ''
_logger.info('Calculate SO Status %s' % sale.id)
# def _search_picking_ids(self, operator, value):
# if operator == 'in' and value:
# self.env.cr.execute("""
# SELECT array_agg(so.sale_id)
# FROM stock_picking so
# WHERE
# so.sale_id is not null and so.id = ANY(%s)
# """, (list(value),))
# so_ids = self.env.cr.fetchone()[0] or []
# return [('id', 'in', so_ids)]
# elif operator == '=' and not value:
# order_ids = self._search([
# ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund'))
# ])
# 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.onchange('partner_id')
def onchange_partner_contact(self):
parent_id = self.partner_id.parent_id
parent_id = parent_id if parent_id else self.partner_id
self.npwp = parent_id.npwp
self.sppkp = parent_id.sppkp
self.customer_type = parent_id.customer_type
self.email = parent_id.email
self.pareto_status = parent_id.pareto_status
self.user_id = parent_id.user_id
@api.onchange('partner_id')
def onchange_partner_id(self):
# INHERIT
result = super(SaleOrder, self).onchange_partner_id()
parent_id = self.partner_id.parent_id
parent_id = parent_id if parent_id else self.partner_id
self.partner_invoice_id = parent_id
return result
def _get_purchases(self):
po_state = ['done', 'draft', 'purchase']
for order in self:
purchases = self.env['purchase.order'].search([
('sale_order_id', '=', order.id),
('state', 'in', po_state)
])
order.purchase_ids = purchases
def _have_outstanding_invoice(self):
invoice_state = ['posted', 'draft']
for order in self:
order.have_outstanding_invoice = any(inv.state in invoice_state for inv in order.invoice_ids)
def _have_outstanding_picking(self):
picking_state = ['done', 'confirmed', 'draft']
for order in self:
order.have_outstanding_picking = any(pick.state in picking_state for pick in order.picking_ids)
def _have_outstanding_po(self):
po_state = ['done', 'draft', 'purchase']
for order in self:
order.have_outstanding_po = any(po.state in po_state for po in order.purchase_ids)
def _have_visit_service(self):
minimum_amount = 20000000
for order in self:
order.have_visit_service = self.amount_total > minimum_amount
def _get_helper_ids(self):
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 check_due(self):
"""To show the due amount and warning stage"""
for order in self:
partner = order.partner_id.parent_id or order.partner_id
if partner and partner.active_limit and partner.enable_credit_limit:
order.has_due = partner.due_amount > 0
if order.outstanding_amount >= partner.warning_stage and partner.warning_stage != 0:
order.is_warning = True
else:
order.has_due = False
order.is_warning = False
def _validate_order(self):
if self.payment_term_id.id == 31 and self.total_percent_margin < 25:
raise UserError("Jika ingin menggunakan Tempo 90 Hari maka margin harus di atas 25%")
if self.warehouse_id.id != 8 and self.warehouse_id.id != 10: #GD Bandengan
raise UserError('Gudang harus Bandengan')
if self.state not in ['draft', 'sent']:
raise UserError("Status harus draft atau sent")
self._validate_npwp()
def _validate_npwp(self):
num_digits = sum(c.isdigit() for c in self.npwp)
if num_digits < 10:
raise UserError("NPWP harus memiliki minimal 10 digit")
# pattern = r'^\d{10,}$'
# return re.match(pattern, self.npwp) is not None
def sale_order_check_approve(self):
self.check_due()
self._validate_order()
for order in self:
order.order_line.validate_line()
term_days = 0
for term_line in order.payment_term_id.line_ids:
term_days += term_line.days
partner = order.partner_id.parent_id or order.partner_id
if not partner.property_payment_term_id:
raise UserError("Payment Term pada Master Data Customer harus diisi")
if not partner.active_limit and term_days > 0:
raise UserError("Credit Limit pada Master Data Customer harus diisi")
if order.payment_term_id != partner.property_payment_term_id:
raise UserError("Payment Term berbeda pada Master Data Customer")
if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp:
raise UserError("NPWP berbeda pada Master Data Customer")
if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.sppkp != partner.sppkp:
raise UserError("SPPKP berbeda pada Master Data Customer")
if not order.client_order_ref and order.create_date > datetime(2024, 6, 27):
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.order_line.validate_line()
order.check_data_real_delivery_address()
order._validate_order()
order.sale_order_check_approve()
main_parent = order.partner_id.get_main_parent()
SYSTEM_UID = 25
FROM_WEBSITE = order.create_uid.id == SYSTEM_UID
if FROM_WEBSITE and main_parent.use_so_approval and order.web_approval not in ['cust_procurement','cust_director']:
raise UserError("This order not yet approved by customer procurement or director")
if not order.client_order_ref and order.create_date > datetime(2024, 6, 27):
raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO")
if not order.commitment_date and order.create_date > datetime(2024, 9, 12):
raise UserError("Expected Delivery Date kosong, wajib diisi")
if not order.real_shipping_id:
UserError('Real Delivery Address harus di isi')
if order.validate_partner_invoice_due():
return self._create_notification_action('Notification','Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
term_days = 0
for term_line in order.payment_term_id.line_ids:
term_days += term_line.days
partner = order.partner_id.parent_id or order.partner_id
if not partner.property_payment_term_id:
raise UserError("Payment Term pada Master Data Customer harus diisi")
if not partner.active_limit and term_days > 0:
raise UserError("Credit Limit pada Master Data Customer harus diisi")
if order.payment_term_id != partner.property_payment_term_id:
raise UserError("Payment Term berbeda pada Master Data Customer")
if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp:
raise UserError("NPWP berbeda pada Master Data Customer")
if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.sppkp != partner.sppkp:
raise UserError("SPPKP berbeda pada Master Data Customer")
if not order.client_order_ref and order.create_date > datetime(2024, 6, 27):
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")
if order.validate_partner_invoice_due():
return self._create_notification_action('Notification', 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
if order._requires_approval_margin_leader():
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')
raise UserError("Bisa langsung Confirm")
def send_notif_to_salesperson(self, cancel=False):
if not cancel:
grouping_so = self.search([
('partner_id.parent_id.id', '=', self.partner_id.parent_id.id),
('partner_id.site_id.id', '=', self.partner_id.site_id.id),
])
else:
grouping_so = self.search([
('partner_id.parent_id.id', '=', self.partner_id.parent_id.id),
('partner_id.site_id.id', '=', self.partner_id.site_id.id),
('id', '!=', self.id),
])
# Kelompokkan data berdasarkan salesperson
salesperson_data = {}
for rec in grouping_so:
if rec.user_id.id not in salesperson_data:
salesperson_data[rec.user_id.id] = {'name': rec.user_id.name, 'orders': [], 'total_amount': 0, 'sum_total_amount': 0, 'business_partner': '', 'site': ''} # Menetapkan nilai awal untuk 'site'
if rec.picking_ids:
if not any(picking.state in ['assigned', 'confirmed', 'waiting'] for picking in rec.picking_ids):
continue
if all(picking.state == 'done' for picking in rec.picking_ids):
continue
if all(picking.state == 'cancel' for picking in rec.picking_ids):
continue
if not rec.partner_id.main_parent_id.use_so_approval:
continue
order_total_amount = rec.amount_total # Mengakses langsung rec.amount_total
salesperson_data[rec.user_id.id]['orders'].append({
'order_name': rec.name,
'parent_name': rec.partner_id.name,
'site_name': rec.partner_id.site_id.name,
'total_amount': rec.amount_total,
})
salesperson_data[rec.user_id.id]['sum_total_amount'] += order_total_amount
salesperson_data[rec.user_id.id]['business_partner'] = grouping_so[0].partner_id.main_parent_id.name
salesperson_data[rec.user_id.id]['site'] = grouping_so[0].partner_id.site_id.name # Menambahkan nilai hanya jika ada
# Kirim email untuk setiap salesperson
for salesperson_id, data in salesperson_data.items():
if data['orders']:
# Buat isi tabel untuk email
table_content = ''
for order_data in data['orders']:
table_content += f"""