summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-05-27 10:19:09 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-05-27 10:19:09 +0700
commitf0f414383b3bd34e6fce12e68e171014c08d2a55 (patch)
treef9eef4c1331f6507fadc680bdd801656ff9f8ea7
parent431229f2a6f1203fbdfe470229e55da8ebd3ea01 (diff)
parentd3f530b94569059106164172485aaa9665e80709 (diff)
Merge branch 'odoo-backup' into CR/repeat-order
-rw-r--r--indoteknik_api/controllers/api_v1/banner.py30
-rw-r--r--indoteknik_api/controllers/api_v1/flash_sale.py2
-rw-r--r--indoteknik_api/controllers/api_v1/product.py96
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py26
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py9
-rw-r--r--indoteknik_api/controllers/api_v1/user.py1
-rw-r--r--indoteknik_api/models/product_pricelist.py12
-rwxr-xr-xindoteknik_custom/__manifest__.py2
-rw-r--r--indoteknik_custom/controllers/website.py7
-rwxr-xr-xindoteknik_custom/models/__init__.py3
-rw-r--r--indoteknik_custom/models/account_move.py15
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py2
-rw-r--r--indoteknik_custom/models/approval_date_doc.py10
-rw-r--r--indoteknik_custom/models/approval_invoice_date.py47
-rw-r--r--indoteknik_custom/models/automatic_purchase.py99
-rw-r--r--indoteknik_custom/models/barcoding_product.py55
-rw-r--r--indoteknik_custom/models/commision.py259
-rw-r--r--indoteknik_custom/models/coretax_fatur.py88
-rw-r--r--indoteknik_custom/models/delivery_order.py7
-rw-r--r--indoteknik_custom/models/dunning_run.py5
-rw-r--r--indoteknik_custom/models/invoice_reklas.py6
-rw-r--r--indoteknik_custom/models/logbook_sj.py5
-rw-r--r--indoteknik_custom/models/manufacturing.py7
-rw-r--r--indoteknik_custom/models/mrp_production.py482
-rw-r--r--indoteknik_custom/models/product_pricelist.py7
-rwxr-xr-xindoteknik_custom/models/product_template.py649
-rwxr-xr-xindoteknik_custom/models/purchase_order.py280
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py3
-rw-r--r--indoteknik_custom/models/purchase_order_sales_match.py1
-rwxr-xr-xindoteknik_custom/models/purchase_pricelist.py91
-rw-r--r--indoteknik_custom/models/purchasing_job.py12
-rw-r--r--indoteknik_custom/models/purchasing_job_multi_update.py2
-rw-r--r--indoteknik_custom/models/purchasing_job_state.py3
-rw-r--r--indoteknik_custom/models/requisition.py14
-rw-r--r--indoteknik_custom/models/res_partner.py37
-rwxr-xr-xindoteknik_custom/models/sale_order.py837
-rw-r--r--indoteknik_custom/models/sale_order_line.py67
-rw-r--r--indoteknik_custom/models/sales_order_koli.py26
-rw-r--r--indoteknik_custom/models/sales_order_reject.py27
-rw-r--r--indoteknik_custom/models/shipment_group.py50
-rw-r--r--indoteknik_custom/models/solr/product_product.py5
-rw-r--r--indoteknik_custom/models/solr/product_template.py9
-rw-r--r--indoteknik_custom/models/solr/x_banner_banner.py3
-rw-r--r--indoteknik_custom/models/stock_backorder_confirmation.py33
-rw-r--r--indoteknik_custom/models/stock_immediate_transfer.py8
-rw-r--r--indoteknik_custom/models/stock_move.py31
-rw-r--r--indoteknik_custom/models/stock_picking.py1187
-rw-r--r--indoteknik_custom/models/stock_picking_return.py13
-rw-r--r--indoteknik_custom/models/user_pengajuan_tempo_request.py28
-rw-r--r--indoteknik_custom/models/vendor_approval.py6
-rw-r--r--indoteknik_custom/models/voucher.py270
-rw-r--r--indoteknik_custom/models/website_user_cart.py121
-rwxr-xr-xindoteknik_custom/models/x_banner_banner.py3
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv13
-rw-r--r--indoteknik_custom/views/account_move.xml1
-rw-r--r--indoteknik_custom/views/approval_invoice_date.xml92
-rw-r--r--indoteknik_custom/views/barcoding_product.xml8
-rw-r--r--indoteknik_custom/views/customer_commision.xml247
-rw-r--r--indoteknik_custom/views/ir_sequence.xml12
-rw-r--r--indoteknik_custom/views/mrp_production.xml46
-rw-r--r--indoteknik_custom/views/product_pricelist.xml1
-rw-r--r--indoteknik_custom/views/product_product.xml1
-rwxr-xr-xindoteknik_custom/views/product_template.xml17
-rw-r--r--indoteknik_custom/views/project_views.xml13
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml5
-rwxr-xr-xindoteknik_custom/views/purchase_pricelist.xml7
-rw-r--r--indoteknik_custom/views/purchasing_job.xml1
-rwxr-xr-xindoteknik_custom/views/sale_order.xml516
-rw-r--r--indoteknik_custom/views/shipment_group.xml16
-rw-r--r--indoteknik_custom/views/stock_picking.xml120
-rw-r--r--indoteknik_custom/views/user_pengajuan_tempo_request.xml2
-rw-r--r--indoteknik_custom/views/vendor_payment_term.xml4
-rwxr-xr-xindoteknik_custom/views/voucher.xml74
-rwxr-xr-xindoteknik_custom/views/x_banner_banner.xml1
74 files changed, 5156 insertions, 1139 deletions
diff --git a/indoteknik_api/controllers/api_v1/banner.py b/indoteknik_api/controllers/api_v1/banner.py
index 308d2765..64a6167b 100644
--- a/indoteknik_api/controllers/api_v1/banner.py
+++ b/indoteknik_api/controllers/api_v1/banner.py
@@ -15,7 +15,8 @@ class Banner(controller.Controller):
limit = int(kw.get('limit', 0))
offset = int(kw.get('offset', 0))
order = kw.get('order', 'write_date DESC')
-
+ keyword = kw.get('keyword')
+
query = [('x_status_banner', '=', 'tayang')]
if type:
query += [('x_banner_category.x_studio_field_KKVl4', '=', type)]
@@ -25,9 +26,27 @@ class Banner(controller.Controller):
if manufacture_id:
query += [('x_relasi_manufacture', '=', int(manufacture_id))]
-
- banners = request.env['x_banner.banner'].search(query, limit=limit, offset=offset, order=order)
-
+
+ banner_kumpulan = []
+ banner_ids = set() # Set untuk menyimpan ID banner agar tidak duplikat
+
+ if keyword:
+ keyword_list = [word.strip() for word in keyword.split() if word.strip()] # Pisahkan berdasarkan spasi
+
+ for word in keyword_list:
+ keyword_query = query + [('x_keyword_banner', 'ilike', word)] # Buat query baru dengan keyword
+ banners = request.env['x_banner.banner'].search(keyword_query, limit=limit, offset=offset, order=order)
+
+ for banner in banners:
+ if banner.id not in banner_ids: # Pastikan tidak ada duplikasi
+ banner_kumpulan.append(banner)
+ banner_ids.add(banner.id)
+
+ if not keyword:
+ banners = request.env['x_banner.banner'].search(query, limit=limit, offset=offset, order=order)
+ else:
+ banners = banner_kumpulan if len(banner_kumpulan) > 0 else request.env['x_banner.banner'].search(query, limit=limit, offset=offset, order=order)
+
week_number = self.get_week_number_of_current_month()
end_datas = []
@@ -41,7 +60,8 @@ class Banner(controller.Controller):
'group_by_week': banner.group_by_week,
'image': request.env['ir.attachment'].api_image('x_banner.banner', 'x_banner_image', banner.id),
'headline_banner': banner.x_headline_banner,
- 'description_banner': banner.x_description_banner
+ 'description_banner': banner.x_description_banner,
+ 'keyword_banner': banner.x_keyword_banner
}
if banner.group_by_week and int(banner.group_by_week) < week_number and type == 'index-a-1':
diff --git a/indoteknik_api/controllers/api_v1/flash_sale.py b/indoteknik_api/controllers/api_v1/flash_sale.py
index 6c4ad8c0..1038500c 100644
--- a/indoteknik_api/controllers/api_v1/flash_sale.py
+++ b/indoteknik_api/controllers/api_v1/flash_sale.py
@@ -14,7 +14,7 @@ class FlashSale(controller.Controller):
def _get_flash_sale_header(self, **kw):
try:
# base_url = request.env['ir.config_parameter'].get_param('web.base.url')
- active_flash_sale = request.env['product.pricelist'].get_is_show_program_flash_sale()
+ active_flash_sale = request.env['product.pricelist'].get_is_show_program_flash_sale(is_show_program=kw.get('is_show_program'))
data = []
for pricelist in active_flash_sale:
query = [
diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py
index 557215ea..a88c3368 100644
--- a/indoteknik_api/controllers/api_v1/product.py
+++ b/indoteknik_api/controllers/api_v1/product.py
@@ -34,60 +34,66 @@ class Product(controller.Controller):
categories.reverse()
return self.response(categories, headers=[('Cache-Control', 'max-age=3600, public')])
- @http.route(prefix + 'product/variants/sla', auth='public', methods=['GET', 'OPTIONS'])
+ @http.route(prefix + 'product/variants/sla', auth='public', methods=['POST', 'OPTIONS'] , csrf=False)
@controller.Controller.must_authorized()
def get_product_template_sla_by_id(self, **kwargs):
- body_params = kwargs.get('ids')
- if not body_params:
- return self.response('Failed', code=400, description='id is required')
+ raw_data = kwargs.get('products', '[]')
+ product_data = json.loads(raw_data)
- ids = [int(id.strip()) for id in body_params.split(',') if id.strip().isdigit()]
-
- sla_duration = 0
- sla_unit = 'Hari'
- include_instant = True
- products = request.env['product.product'].search([('id', 'in', ids)])
- if len(products) < 1:
- return self.response(
- 'Failed',
- code=400,
- description='Produk Tidak Di Temukan.'
- )
+ product_ids = [int(item["id"]) for item in product_data]
+ products = request.env['purchase.pricelist'].search([
+ ('product_id', 'in', product_ids),
+ ('is_winner', '=', True)
+ ])
- product_slas = request.env['product.sla'].search([('product_variant_id', 'in', ids)])
- if len(product_slas) < 1:
- return self.response(
- 'Failed',
- code=400,
- description='SLA Tidak Di Temukan.'
- )
-
- # Mapping SLA untuk mempermudah lookup
- sla_map = {sla.product_variant_id.id: sla for sla in product_slas}
-
- for product in products:
- product_sla = sla_map.get(product.id)
- if product_sla:
- sla_duration = max(sla_duration, int(product_sla.sla))
- sla_unit = product_sla.sla_vendor_id.unit
- if product.qty_free_bandengan < 1 :
- if product_sla.sla_vendor_id.unit != 'jam':
- include_instant = False
- break
-
start_date = datetime.today().date()
additional_days = request.env['sale.order'].get_days_until_next_business_day(start_date)
+ include_instant = True
+
+ if(len(products) != len(product_ids)):
+ products_data_params = {product["id"] : product for product in product_data }
+
+ all_fast_products = all(
+ product.product_id.qty_free_bandengan >= products_data_params.get(product.product_id.id, {}).get("quantity", 0)
+ for product in products
+ )
- # Jika semua loop selesai tanpa include_instant menjadi False
+ if all_fast_products:
+ return self.response({
+ 'include_instant': include_instant,
+ 'sla_duration': 1,
+ 'sla_additional_days': additional_days,
+ 'sla_total' : int(1) + int(additional_days),
+ 'sla_unit': 'Hari'
+ })
+
+ max_slatime = 1
+
+ for vendor in products:
+ vendor_sla = request.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 self.response({
- 'include_instant': include_instant,
- 'sla_duration': sla_duration,
- 'sla_additional_days': additional_days,
- 'sla_total' : int(sla_duration) + int(additional_days),
- 'sla_unit': 'Hari' if additional_days > 0 else sla_unit
- }
- )
+ 'include_instant': include_instant,
+ 'sla_duration': max_slatime,
+ 'sla_additional_days': additional_days,
+ 'sla_total' : int(max_slatime) + int(additional_days),
+ 'sla_unit': 'Hari'
+ })
@http.route(prefix + 'product_variant/<id>/stock', auth='public', methods=['GET', 'OPTIONS'])
@controller.Controller.must_authorized()
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index 3219fc07..39fa0e13 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -54,6 +54,20 @@ class SaleOrder(controller.Controller):
# sales = request.env['sale.order'].search_read([('name', '=', sale_number)], fields=['id', 'name', 'amount_total', 'state'])
sales = request.env['sale.order'].search(query, limit=1)
data = []
+ INDONESIAN_MONTHS = {
+ 1: 'Januari',
+ 2: 'Februari',
+ 3: 'Maret',
+ 4: 'April',
+ 5: 'Mei',
+ 6: 'Juni',
+ 7: 'Juli',
+ 8: 'Agustus',
+ 9: 'September',
+ 10: 'Oktober',
+ 11: 'November',
+ 12: 'Desember',
+ }
for sale in sales:
product_name = ''
product_not_in_id = 0
@@ -69,6 +83,7 @@ class SaleOrder(controller.Controller):
'amount_untaxed': sale.amount_untaxed,
'amount_tax': sale.amount_tax,
'amount_total': sale.amount_total,
+ 'expected_ready_to_ship': f"{sale.expected_ready_to_ship.day} {INDONESIAN_MONTHS[sale.expected_ready_to_ship.month]} {sale.expected_ready_to_ship.year}",
'product_name': product_name,
'product_not_in_id': product_not_in_id,
'details': [request.env['sale.order.line'].api_single_response(x, context='with_detail') for x in sale.order_line]
@@ -186,6 +201,15 @@ class SaleOrder(controller.Controller):
sale_order = request.env['sale.order'].search(domain)
if sale_order:
data = request.env['sale.order'].api_v1_single_response(sale_order, context='with_detail')
+ if sale_order.expected_ready_to_ship:
+ bulan_id = [
+ "Januari", "Februari", "Maret", "April", "Mei", "Juni",
+ "Juli", "Agustus", "September", "Oktober", "November", "Desember"
+ ]
+ tanggal = sale_order.expected_ready_to_ship.day
+ bulan = bulan_id[sale_order.expected_ready_to_ship.month - 1]
+ tahun = sale_order.expected_ready_to_ship.year
+ data['expected_ready_to_ship'] = f"{tanggal} {bulan} {tahun}"
return self.response(data)
@@ -510,6 +534,8 @@ class SaleOrder(controller.Controller):
'program_line_id': cart['id'],
'quantity': cart['quantity']
})
+
+ sale_order._compute_etrts_date()
request.env['sale.order.promotion'].create(promotions)
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 55e07152..31706b99 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -116,10 +116,10 @@ class StockPicking(controller.Controller):
return self.response(picking.get_tracking_detail())
- @http.route(prefix + 'stock-picking/<picking_code>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
+ @http.route(prefix + 'stock-picking/<scanid>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
def write_partner_stock_picking_documentation(self, **kw):
- picking_code = int(kw.get('picking_code', 0))
+ scanid = int(kw.get('scanid', 0))
sj_document = kw.get('sj_document', False)
paket_document = kw.get('paket_document', False)
@@ -128,7 +128,10 @@ class StockPicking(controller.Controller):
'driver_arrival_date': datetime.utcnow(),
}
- picking_data = request.env['stock.picking'].search([('picking_code', '=', picking_code)], limit=1)
+ picking_data = request.env['stock.picking'].search([('id', '=', scanid)], limit=1)
+
+ if not picking_data:
+ picking_data = request.env['stock.picking'].search([('picking_code', '=', scanid)], limit=1)
if not picking_data:
return self.response(code=404, description='picking not found')
diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py
index 8523d90b..b5b7e055 100644
--- a/indoteknik_api/controllers/api_v1/user.py
+++ b/indoteknik_api/controllers/api_v1/user.py
@@ -131,6 +131,7 @@ class User(controller.Controller):
nama_wajib_pajak = kw.get('nama_wajib_pajak', False)
is_pkp = kw.get('is_pkp')
is_terdaftar = kw.get('is_terdaftar', False)
+ is_terdaftar = False if is_terdaftar == 'false' else is_terdaftar
type_acc = kw.get('type_acc', 'individu') or 'individu'
if not name or not email or not password:
diff --git a/indoteknik_api/models/product_pricelist.py b/indoteknik_api/models/product_pricelist.py
index 6e88517c..e0debf38 100644
--- a/indoteknik_api/models/product_pricelist.py
+++ b/indoteknik_api/models/product_pricelist.py
@@ -95,18 +95,24 @@ class ProductPricelist(models.Model):
], limit=1, order='start_date asc')
return pricelist
- def get_is_show_program_flash_sale(self):
+ def get_is_show_program_flash_sale(self, is_show_program):
"""
Check whether have active flash sale in range of date
@return: returns pricelist: object
"""
+
+ if is_show_program == 'true':
+ is_show_program = True
+ else:
+ is_show_program = False
+
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
pricelist = self.search([
('is_flash_sale', '=', True),
- ('is_show_program', '=', True),
+ ('is_show_program', '=', is_show_program),
('start_date', '<=', current_time),
('end_date', '>=', current_time)
- ], order='start_date asc')
+ ], order='number asc')
return pricelist
def is_flash_sale_product(self, product_id: int):
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index a7096346..9fe3dcdb 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -143,6 +143,7 @@
'views/sale_order_multi_uangmuka_penjualan.xml',
'views/shipment_group.xml',
'views/approval_date_doc.xml',
+ 'views/approval_invoice_date.xml',
'views/partner_payment_term.xml',
'views/vendor_payment_term.xml',
'views/approval_unreserve.xml',
@@ -154,6 +155,7 @@
'views/user_pengajuan_tempo.xml',
'views/stock_backorder_confirmation_views.xml',
'views/barcoding_product.xml',
+ 'views/project_views.xml',
'report/report.xml',
'report/report_banner_banner.xml',
'report/report_banner_banner2.xml',
diff --git a/indoteknik_custom/controllers/website.py b/indoteknik_custom/controllers/website.py
index 2e3df519..120dddad 100644
--- a/indoteknik_custom/controllers/website.py
+++ b/indoteknik_custom/controllers/website.py
@@ -1,7 +1,12 @@
-from odoo.http import request, Controller
+from odoo.http import request, Controller, route
from odoo import http, _
class Website(Controller):
+
+ @route(['/shop', '/shop/cart'], auth='public', website=True)
+ def shop(self, **kw):
+ return request.redirect('https://indoteknik.com/shop/promo', code=302)
+
@http.route('/content', auth='public')
def content(self, **kw):
url = kw.get('url', '')
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 37a49332..08fa9803 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -145,5 +145,8 @@ from . import coretax_fatur
from . import public_holiday
from . import ir_actions_report
from . import barcoding_product
+from . import sales_order_koli
+from . import stock_backorder_confirmation
from . import account_payment_register
from . import stock_inventory
+from . import approval_invoice_date
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 45fdb8df..30de67be 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -66,6 +66,19 @@ class AccountMove(models.Model):
other_taxes = fields.Float(string="Other Taxes", compute='compute_other_taxes')
is_hr = fields.Boolean(string="Is HR?", default=False)
purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order')
+ length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment')
+
+ def compute_length_of_payment(self):
+ for rec in self:
+ payment_term = rec.invoice_payment_term_id.line_ids[0].days
+ terima_faktur = rec.date_terima_tukar_faktur
+ payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1)
+
+ if payment and terima_faktur:
+ date_diff = terima_faktur - payment.date
+ rec.length_of_payment = date_diff.days + payment_term
+ else:
+ rec.length_of_payment = 0
def _update_line_name_from_ref(self):
"""Update all account.move.line name fields with ref from account.move"""
@@ -81,7 +94,7 @@ class AccountMove(models.Model):
def compute_other_subtotal(self):
for rec in self:
- rec.other_subtotal = rec.amount_untaxed * (11 / 12)
+ rec.other_subtotal = round(rec.amount_untaxed * (11 / 12))
@api.model
def generate_attachment(self, record):
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index c48c2372..4a3f40e2 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -96,7 +96,7 @@ class DueExtension(models.Model):
sales = self.env['sale.order'].browse(self.order_id.id)
- sales.action_confirm()
+ sales.with_context({'due_approve': True}).action_confirm()
self.order_id.due_id = self.id
self.approve_by = self.env.user.id
self.date_approve = datetime.utcnow()
diff --git a/indoteknik_custom/models/approval_date_doc.py b/indoteknik_custom/models/approval_date_doc.py
index 751bae82..638b44d7 100644
--- a/indoteknik_custom/models/approval_date_doc.py
+++ b/indoteknik_custom/models/approval_date_doc.py
@@ -39,12 +39,16 @@ class ApprovalDateDoc(models.Model):
if not self.env.user.is_accounting:
raise UserError("Hanya Accounting Yang Bisa Approve")
self.check_invoice_so_picking
- self.picking_id.driver_departure_date = self.driver_departure_date
- self.picking_id.date_doc_kirim = self.driver_departure_date
+ # Tambahkan context saat mengupdate date_doc_kirim
+ self.picking_id.with_context(from_button_approve=True).write({
+ 'driver_departure_date': self.driver_departure_date,
+ 'date_doc_kirim': self.driver_departure_date,
+ 'update_date_doc_kirim_add': True
+ })
self.state = 'done'
self.approve_date = datetime.utcnow()
self.approve_by = self.env.user.id
-
+
def button_cancel(self):
self.state = 'cancel'
diff --git a/indoteknik_custom/models/approval_invoice_date.py b/indoteknik_custom/models/approval_invoice_date.py
new file mode 100644
index 00000000..e1bc4c9b
--- /dev/null
+++ b/indoteknik_custom/models/approval_invoice_date.py
@@ -0,0 +1,47 @@
+from odoo import models, api, fields
+from odoo.exceptions import AccessError, UserError, ValidationError
+from datetime import timedelta, date, datetime
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class ApprovalInvoiceDate(models.Model):
+ _name = "approval.invoice.date"
+ _description = "Approval Invoice Date"
+ _rec_name = 'number'
+
+ picking_id = fields.Many2one('stock.picking', string='Picking')
+ number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True)
+ date_invoice = fields.Datetime(
+ string='Invoice Date',
+ copy=False
+ )
+ date_doc_do = fields.Datetime(
+ string='Tanggal Kirim di SJ',
+ copy=False
+ )
+ state = fields.Selection([('draft', 'Draft'), ('done', 'Done'), ('cancel', 'Cancel')], string='State', default='draft', tracking=True)
+ approve_date = fields.Datetime(string='Approve Date', copy=False)
+ approve_by = fields.Many2one('res.users', string='Approve By', copy=False)
+ sale_id = fields.Many2one('sale.order', string='Sale Order')
+ partner_id = fields.Many2one('res.partner', string='Partner')
+ move_id = fields.Many2one('account.move', string='Invoice')
+ note = fields.Char(string='Note')
+
+ def button_approve(self):
+ if not self.env.user.is_accounting:
+ raise UserError("Hanya Accounting Yang Bisa Approve")
+ self.move_id.invoice_date = self.date_doc_do.date()
+ self.picking_id.date_doc_kirim = self.date_doc_do
+ self.state = 'done'
+ self.approve_date = datetime.utcnow()
+ self.approve_by = self.env.user.id
+
+ def button_cancel(self):
+ self.state = 'cancel'
+
+ @api.model
+ def create(self, vals):
+ vals['number'] = self.env['ir.sequence'].next_by_code('approval.invoice.date') or '0'
+ result = super(ApprovalInvoiceDate, self).create(vals)
+ return result
diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py
index fbdf8dae..c9edf07c 100644
--- a/indoteknik_custom/models/automatic_purchase.py
+++ b/indoteknik_custom/models/automatic_purchase.py
@@ -1,4 +1,4 @@
-from odoo import models, api, fields
+from odoo import models, api, fields, tools
from odoo.exceptions import UserError
from datetime import datetime
import logging, math
@@ -67,6 +67,15 @@ class AutomaticPurchase(models.Model):
if count > 0:
raise UserError('Ada sekitar %s SO Yang sudah create PO, berikut SO nya: %s' % (count, ', '.join(names)))
+
+ def unlink_note_pj(self):
+ product = self.purchase_lines.mapped('product_id')
+ pj_state = self.env['purchasing.job.state'].search([
+ ('purchasing_job_id', 'in', product.ids)
+ ])
+
+ for line in pj_state:
+ line.unlink()
def create_po_from_automatic_purchase(self):
if not self.purchase_lines:
@@ -75,6 +84,7 @@ class AutomaticPurchase(models.Model):
raise UserError('Sudah pernah di create PO')
current_time = datetime.now()
+ self.unlink_note_pj()
vendor_ids = self.env['automatic.purchase.line'].read_group(
[('automatic_purchase_id', '=', self.id), ('partner_id', '!=', False)],
fields=['partner_id'],
@@ -284,7 +294,7 @@ class AutomaticPurchase(models.Model):
def create_purchase_order_sales_match(self, purchase_order):
matches_so_product_ids = [line.product_id.id for line in purchase_order.order_line]
- matches_so = self.env['automatic.purchase.sales.match'].search([
+ matches_so = self.env['v.sale.notin.matchpo'].search([
('automatic_purchase_id', '=', self.id),
('sale_line_id.product_id', 'in', matches_so_product_ids),
])
@@ -292,6 +302,8 @@ class AutomaticPurchase(models.Model):
sale_ids_set = set()
sale_ids_name = set()
for sale_order in matches_so:
+ # @stephan skip so line yang sudah pernah ada di purchase order sales match sebelumnya
+
salesperson_name = sale_order.sale_id.user_id.name
sale_id_with_salesperson = f"{sale_order.sale_id.name} - {salesperson_name}"
@@ -474,7 +486,7 @@ class AutomaticPurchase(models.Model):
# _logger.info('test %s' % point.product_id.name)
if point.product_id.qty_available_bandengan > point.product_min_qty:
continue
- qty_purchase = point.product_max_qty - point.product_id.qty_available_bandengan
+ qty_purchase = point.product_max_qty - point.product_id.qty_incoming_bandengan - point.product_id.qty_onhand_bandengan
po_line = self.env['purchase.order.line'].search([('product_id', '=', point.product_id.id), ('order_id.state', '=', 'done')], order='id desc', limit=1)
if self.vendor_id:
@@ -577,18 +589,18 @@ class AutomaticPurchaseLine(models.Model):
def _get_valid_purchase_price(self, purchase_price):
price = 0
- taxes = ''
+ taxes = 24
human_last_update = purchase_price.human_last_update or datetime.min
system_last_update = purchase_price.system_last_update or datetime.min
- if purchase_price.taxes_product_id.type_tax_use == 'purchase':
- price = purchase_price.product_price
- taxes = purchase_price.taxes_product_id.id
+ #if purchase_price.taxes_product_id.type_tax_use == 'purchase':
+ price = purchase_price.product_price
+ taxes = purchase_price.taxes_product_id.id or 24
if system_last_update > human_last_update:
- if purchase_price.taxes_system_id.type_tax_use == 'purchase':
- price = purchase_price.system_price
- taxes = purchase_price.taxes_system_id.id
+ #if purchase_price.taxes_system_id.type_tax_use == 'purchase':
+ price = purchase_price.system_price
+ taxes = purchase_price.taxes_system_id.id or 24
return price, taxes
@@ -655,3 +667,70 @@ class SyncPurchasingJob(models.Model):
outgoing = fields.Float(string="Outgoing")
action = fields.Char(string="Status")
date = fields.Datetime(string="Date Sync")
+
+
+class SaleNotInMatchPO(models.Model):
+ # created by @stephan for speed up performance while create po from automatic purchase
+ _name = 'v.sale.notin.matchpo'
+ _auto = False
+ _rec_name = 'id'
+
+ id = fields.Integer()
+ automatic_purchase_id = fields.Many2one('automatic.purchase', string='APO')
+ automatic_purchase_line_id = fields.Many2one('automatic.purchase.line', string='APO Line')
+ sale_id = fields.Many2one('sale.order', string='Sale')
+ sale_line_id = fields.Many2one('sale.order.line', string='Sale Line')
+ picking_id = fields.Many2one('stock.picking', string='Picking')
+ move_id = fields.Many2one('stock.move', string='Move')
+ partner_id = fields.Many2one('res.partner', string='Partner')
+ partner_invoice_id = fields.Many2one('res.partner', string='Partner Invoice')
+ salesperson_id = fields.Many2one('res.user', string='Salesperson')
+ product_id = fields.Many2one('product.product', string='Product')
+ qty_so = fields.Float(string='Qty SO')
+ qty_po = fields.Float(string='Qty PO')
+ create_uid = fields.Many2one('res.user', string='Created By')
+ create_date = fields.Datetime(string='Create Date')
+ write_uid = fields.Many2one('res.user', string='Updated By')
+ write_date = fields.Many2one(string='Updated')
+ purchase_price = fields.Many2one(string='Purchase Price')
+ purchase_tax_id = fields.Many2one('account.tax', string='Purchase Tax')
+ note_procurement = fields.Many2one(string='Note Procurement')
+
+ # 1. yang bug
+ # def init(self):
+ # tools.drop_view_if_exists(self.env.cr, self._table)
+ # self.env.cr.execute("""
+ # CREATE OR REPLACE VIEW %s AS(
+ # select apsm.id, apsm.automatic_purchase_id, apsm.automatic_purchase_line_id, apsm.sale_id, apsm.sale_line_id,
+ # apsm.picking_id, apsm.move_id, apsm.partner_id,
+ # apsm.partner_invoice_id, apsm.salesperson_id, apsm.product_id, apsm.qty_so, apsm.qty_po, apsm.create_uid,
+ # apsm.create_date, apsm.write_uid, apsm.write_date, apsm.purchase_price,
+ # apsm.purchase_tax_id, apsm.note_procurement
+ # from automatic_purchase_sales_match apsm
+ # where apsm.sale_line_id not in (
+ # select distinct coalesce(posm.sale_line_id,0)
+ # from purchase_order_sales_match posm
+ # join purchase_order po on po.id = posm.purchase_order_id
+ # where po.state not in ('cancel')
+ # )
+ # )
+ # """ % self._table)
+
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""
+ CREATE OR REPLACE VIEW %s AS(
+ select apsm.id, apsm.automatic_purchase_id, apsm.automatic_purchase_line_id, apsm.sale_id, apsm.sale_line_id,
+ apsm.picking_id, apsm.move_id, apsm.partner_id,
+ apsm.partner_invoice_id, apsm.salesperson_id, apsm.product_id, apsm.qty_so, apsm.qty_po, apsm.create_uid,
+ apsm.create_date, apsm.write_uid, apsm.write_date, apsm.purchase_price,
+ apsm.purchase_tax_id, apsm.note_procurement
+ from automatic_purchase_sales_match apsm
+ where apsm.sale_line_id not in (
+ select distinct coalesce(posm.sale_line_id,0)
+ from purchase_order_sales_match posm
+ join purchase_order po on po.id = posm.purchase_order_id
+ where po.state not in ('cancel')
+ )
+ )
+ """ % self._table) \ No newline at end of file
diff --git a/indoteknik_custom/models/barcoding_product.py b/indoteknik_custom/models/barcoding_product.py
index e1b8f41f..335b481a 100644
--- a/indoteknik_custom/models/barcoding_product.py
+++ b/indoteknik_custom/models/barcoding_product.py
@@ -12,27 +12,57 @@ class BarcodingProduct(models.Model):
barcoding_product_line = fields.One2many('barcoding.product.line', 'barcoding_product_id', string='Barcoding Product Lines', auto_join=True)
product_id = fields.Many2one('product.product', string="Product", tracking=3)
quantity = fields.Float(string="Quantity", tracking=3)
- type = fields.Selection([('print', 'Print Barcode'), ('barcoding', 'Add Barcode To Product')], string='Type', default='print')
+ type = fields.Selection([('print', 'Print Barcode'), ('barcoding', 'Add Barcode To Product'), ('barcoding_box', 'Add Barcode Box To Product'), ('multiparts', 'Multiparts Product')], string='Type', default='print')
barcode = fields.Char(string="Barcode")
+ qty_pcs_box = fields.Char(string="Quantity Pcs Box")
+
+ def check_duplicate_barcode(self):
+ if self.type in ['barcoding_box', 'barcoding']:
+ barcode_product = self.env['product.product'].search([('barcode', '=', self.barcode)])
+
+ if barcode_product:
+ raise UserError('Barcode sudah digunakan {}'.format(barcode_product.display_name))
+
+ barcode_box = self.env['product.product'].search([('barcode_box', '=', self.barcode)])
+
+ if barcode_box:
+ raise UserError('Barcode box sudah digunakan {}'.format(barcode_box.display_name))
@api.constrains('barcode')
def _send_barcode_to_product(self):
for record in self:
- if record.barcode and not record.product_id.barcode:
+ record.check_duplicate_barcode()
+ if record.type == 'barcoding_box':
+ record.product_id.barcode_box = record.barcode
+ record.product_id.qty_pcs_box = record.qty_pcs_box
+ else:
record.product_id.barcode = record.barcode
-
+
@api.onchange('product_id', 'quantity')
def _onchange_product_or_quantity(self):
- """Update barcoding_product_line based on product_id and quantity"""
if self.product_id and self.quantity > 0:
- # Clear existing lines
self.barcoding_product_line = [(5, 0, 0)]
- # Add a new line with the current product and quantity
- self.barcoding_product_line = [(0, 0, {
- 'product_id': self.product_id.id,
- 'barcoding_product_id': self.id,
- }) for _ in range(int(self.quantity))]
+ lines = []
+ for i in range(int(self.quantity)):
+ lines.append((0, 0, {
+ 'product_id': self.product_id.id,
+ 'barcoding_product_id': self.id,
+ 'sequence_with_total': f"{i+1}/{int(self.quantity)}"
+ }))
+ self.barcoding_product_line = lines
+
+ def write(self, vals):
+ res = super().write(vals)
+ if 'quantity' in vals and self.type == 'multiparts':
+ self._update_sequence_with_total()
+ return res
+
+ def _update_sequence_with_total(self):
+ for rec in self:
+ total = int(rec.quantity)
+ for index, line in enumerate(rec.barcoding_product_line, start=1):
+ line.sequence_with_total = f"{index}/{total}"
class BarcodingProductLine(models.Model):
@@ -42,4 +72,7 @@ class BarcodingProductLine(models.Model):
barcoding_product_id = fields.Many2one('barcoding.product', string='Barcoding Product Ref', required=True, ondelete='cascade', index=True, copy=False)
product_id = fields.Many2one('product.product', string="Product")
- qr_code_variant = fields.Binary("QR Code Variant", related='product_id.qr_code_variant') \ No newline at end of file
+ qr_code_variant = fields.Binary("QR Code Variant", related='product_id.qr_code_variant')
+ sequence_with_total = fields.Char(
+ string="Sequence"
+ ) \ No newline at end of file
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index 0d31e954..eeaa8efc 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -1,7 +1,10 @@
-from odoo import models, api, fields
+from odoo import models, api, fields, _
from odoo.exceptions import UserError
from datetime import datetime
+# import datetime
import logging
+from terbilang import Terbilang
+import pytz
_logger = logging.getLogger(__name__)
@@ -12,8 +15,10 @@ class CustomerRebate(models.Model):
_inherit = ['mail.thread']
partner_id = fields.Many2one('res.partner', string='Customer', required=True)
- date_from = fields.Date(string='Date From', required=True, help="Pastikan tanggal 1 januari, jika tidak, code akan break")
- date_to = fields.Date(string='Date To', required=True, help="Pastikan tanggal 31 desember, jika tidak, code akan break")
+ date_from = fields.Date(string='Date From', required=True,
+ help="Pastikan tanggal 1 januari, jika tidak, code akan break")
+ date_to = fields.Date(string='Date To', required=True,
+ help="Pastikan tanggal 31 desember, jika tidak, code akan break")
description = fields.Char(string='Description')
target_1st = fields.Float(string='Target/Quarter 1st')
target_2nd = fields.Float(string='Target/Quarter 2nd')
@@ -35,7 +40,7 @@ class CustomerRebate(models.Model):
line.dpp_q2 = line._get_current_dpp_q2(line)
line.dpp_q3 = line._get_current_dpp_q3(line)
line.dpp_q4 = line._get_current_dpp_q4(line)
-
+
def _compute_achievement(self):
for line in self:
line.status_q1 = line._check_achievement(line.target_1st, line.target_2nd, line.dpp_q1)
@@ -52,18 +57,18 @@ class CustomerRebate(models.Model):
else:
status = 'not achieve'
return status
-
+
def _get_current_dpp_q1(self, line):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', line.date_from),
- ('move_id.invoice_date', '<=', '2023-03-31'),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', line.date_from),
+ ('move_id.invoice_date', '<=', '2023-03-31'),
+ ('product_id.x_manufacture', 'in', brand),
]
invoice_lines = self.env['account.move.line'].search(where, order='id')
for invoice_line in invoice_lines:
@@ -74,13 +79,13 @@ class CustomerRebate(models.Model):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', '2023-04-01'),
- ('move_id.invoice_date', '<=', '2023-06-30'),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', '2023-04-01'),
+ ('move_id.invoice_date', '<=', '2023-06-30'),
+ ('product_id.x_manufacture', 'in', brand),
]
invoices = self.env['account.move.line'].search(where, order='id')
for invoice in invoices:
@@ -91,13 +96,13 @@ class CustomerRebate(models.Model):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', '2023-07-01'),
- ('move_id.invoice_date', '<=', '2023-09-30'),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', '2023-07-01'),
+ ('move_id.invoice_date', '<=', '2023-09-30'),
+ ('product_id.x_manufacture', 'in', brand),
]
invoices = self.env['account.move.line'].search(where, order='id')
for invoice in invoices:
@@ -108,13 +113,13 @@ class CustomerRebate(models.Model):
sum_dpp = 0
brand = [10, 89, 122]
where = [
- ('move_id.move_type', '=', 'out_invoice'),
- ('move_id.state', '=', 'posted'),
- ('move_id.is_customer_commision', '=', False),
- ('move_id.partner_id.id', '=', line.partner_id.id),
- ('move_id.invoice_date', '>=', '2023-10-01'),
- ('move_id.invoice_date', '<=', line.date_to),
- ('product_id.x_manufacture', 'in', brand),
+ ('move_id.move_type', '=', 'out_invoice'),
+ ('move_id.state', '=', 'posted'),
+ ('move_id.is_customer_commision', '=', False),
+ ('move_id.partner_id.id', '=', line.partner_id.id),
+ ('move_id.invoice_date', '>=', '2023-10-01'),
+ ('move_id.invoice_date', '<=', line.date_to),
+ ('product_id.x_manufacture', 'in', brand),
]
invoices = self.env['account.move.line'].search(where, order='id')
for invoice in invoices:
@@ -122,6 +127,22 @@ class CustomerRebate(models.Model):
return sum_dpp
+class RejectReasonCommision(models.TransientModel):
+ _name = 'reject.reason.commision'
+ _description = 'Wizard for Reject Reason Customer Commision'
+
+ request_id = fields.Many2one('customer.commision', string='Request')
+ reason_reject = fields.Text(string='Reason for Rejection', required=True, tracking=True)
+
+ def confirm_reject(self):
+ commision = self.request_id
+ if commision:
+ commision.last_status = commision.status
+ commision.write({'reason_reject': self.reason_reject})
+ commision.status = 'reject'
+ return {'type': 'ir.actions.act_window_close'}
+
+
class CustomerCommision(models.Model):
_name = 'customer.commision'
_order = 'id desc'
@@ -134,45 +155,133 @@ class CustomerCommision(models.Model):
partner_ids = fields.Many2many('res.partner', String='Customer', required=True)
description = fields.Char(string='Description')
notification = fields.Char(string='Notification')
- commision_lines = fields.One2many('customer.commision.line', 'customer_commision_id', string='Lines', auto_join=True)
+ commision_lines = fields.One2many('customer.commision.line', 'customer_commision_id', string='Lines',
+ auto_join=True)
status = fields.Selection([
- ('pengajuan1', 'Menunggu Approval Marketing'),
- ('pengajuan2', 'Menunggu Approval Pimpinan'),
- ('approved', 'Approved')
- ], string='Status', copy=False, readonly=True, tracking=3)
+ ('draft', 'Draft'),
+ ('pengajuan1', 'Menunggu Approval Manager Sales'),
+ ('pengajuan2', 'Menunggu Approval Marketing'),
+ ('pengajuan3', 'Menunggu Approval Pimpinan'),
+ ('pengajuan4', 'Menunggu Approval Accounting'),
+ ('approved', 'Approved'),
+ ('reject', 'Rejected'),
+ ], string='Status', copy=False, readonly=True, tracking=3, index=True, track_visibility='onchange', default='draft')
+ last_status = fields.Selection([
+ ('draft', 'Draft'),
+ ('pengajuan1', 'Menunggu Approval Manager Sales'),
+ ('pengajuan2', 'Menunggu Approval Marketing'),
+ ('pengajuan3', 'Menunggu Approval Pimpinan'),
+ ('pengajuan4', 'Menunggu Approval Accounting'),
+ ('approved', 'Approved'),
+ ('reject', 'Rejected'),
+ ], string='Status')
commision_percent = fields.Float(string='Commision %', tracking=3)
commision_amt = fields.Float(string='Commision Amount', tracking=3)
+ commision_amt_text = fields.Char(string='Commision Amount Text', compute='compute_delivery_amt_text')
total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp')
commision_type = fields.Selection([
('fee', 'Fee'),
('cashback', 'Cashback'),
('rebate', 'Rebate'),
], string='Commision Type', required=True)
- bank_name = fields.Char(string='Bank', tracking=3)
- account_name = fields.Char(string='Account Name', tracking=3)
- bank_account = fields.Char(string='Account No', tracking=3)
+ bank_name = fields.Char(string='Bank', tracking=3, required=True)
+ account_name = fields.Char(string='Account Name', tracking=3, required=True)
+ bank_account = fields.Char(string='Account No', tracking=3, required=True)
note_transfer = fields.Char(string='Keterangan')
brand_ids = fields.Many2many('x_manufactures', string='Brands')
payment_status = fields.Selection([
('pending', 'Pending'),
('payment', 'Payment'),
], string='Payment Status', copy=False, readonly=True, tracking=3, default='pending')
+ note_finnance = fields.Text('Notes Finnance')
+ reason_reject = fields.Char(string='Reason Reaject', tracking=True, track_visibility='onchange')
+ approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always')
+
grouped_so_number = fields.Char(string='Group SO Number', compute='_compute_grouped_numbers')
grouped_invoice_number = fields.Char(string='Group Invoice Number', compute='_compute_grouped_numbers')
+ sales_id = fields.Many2one('res.users', string="Sales", tracking=True, default=lambda self: self.env.user,
+ domain=lambda self: [
+ ('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman').id)])
+
+ date_approved_sales = fields.Datetime(string="Date Approved Sales", tracking=True)
+ date_approved_marketing = fields.Datetime(string="Date Approved Marketing", tracking=True)
+ date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan", tracking=True)
+ date_approved_accounting = fields.Datetime(string="Date Approved Accounting", tracking=True)
+
+ position_sales = fields.Char(string="Position Sales", tracking=True)
+ position_marketing = fields.Char(string="Position Marketing", tracking=True)
+ position_pimpinan = fields.Char(string="Position Pimpinan", tracking=True)
+ position_accounting = fields.Char(string="Position Accounting", tracking=True)
+
+ # get partner ids so it can be grouped by
+ @api.model
+ def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+ if 'partner_ids' in groupby:
+ # Get all records matching the domain
+ records = self.search(domain)
+
+ # Create groups for each partner
+ groups = {}
+ for record in records:
+ for partner in record.partner_ids:
+ if partner.id not in groups:
+ groups[partner.id] = {
+ 'partner_ids': partner,
+ 'records': self.env['customer.commision']
+ }
+ groups[partner.id]['records'] |= record
+
+ # Format the result
+ result = []
+ for partner_id, group_data in groups.items():
+ partner = group_data['partner_ids']
+ record_ids = group_data['records'].ids
+
+ # Create the domain
+ group_domain = [('id', 'in', record_ids)]
+ if domain:
+ group_domain = ['&'] + domain + group_domain
+
+ result.append({
+ 'partner_ids': (partner.id, partner.display_name),
+ 'partner_ids_count': len(record_ids),
+ '__domain': group_domain,
+ '__count': len(record_ids),
+ })
+
+ return result
+
+ return super(CustomerCommision, self).read_group(domain, fields, groupby, offset, limit, orderby, lazy)
+
+ def compute_delivery_amt_text(self):
+ tb = Terbilang()
+
+ for record in self:
+ res = ''
+
+ try:
+ if record.commision_amt > 0:
+ tb.parse(int(record.commision_amt))
+ res = tb.getresult().title()
+ record.commision_amt_text = res + ' Rupiah'
+ except:
+ record.commision_amt_text = res
+
def _compute_grouped_numbers(self):
for rec in self:
so_numbers = set()
invoice_numbers = set()
-
+
for line in rec.commision_lines:
if line.invoice_id:
if line.invoice_id.sale_id:
so_numbers.add(line.invoice_id.sale_id.name)
invoice_numbers.add(line.invoice_id.name)
-
+
rec.grouped_so_number = ', '.join(sorted(so_numbers))
rec.grouped_invoice_number = ', '.join(sorted(invoice_numbers))
+
# add status for type of commision, fee, rebate / cashback
# include child or not?
@@ -197,22 +306,25 @@ class CustomerCommision(models.Model):
self.commision_percent = achieve_2nd
else:
self.commision_percent = 0
-
+
self._onchange_commision_amt()
@api.constrains('commision_percent')
def _onchange_commision_percent(self):
if not self.env.context.get('_onchange_commision_percent', True):
return
-
+
if self.commision_amt == 0:
self.commision_amt = self.commision_percent * self.total_dpp // 100
-
+
@api.constrains('commision_amt')
def _onchange_commision_amt(self):
+ """
+ Constrain to update commision percent from commision amount
+ """
if not self.env.context.get('_onchange_commision_amt', True):
return
-
+
if self.total_dpp > 0 and self.commision_percent == 0:
self.commision_percent = (self.commision_amt / self.total_dpp) * 100
@@ -234,19 +346,54 @@ class CustomerCommision(models.Model):
result = super(CustomerCommision, self).create(vals)
return result
- def action_confirm_customer_commision(self):#add 2 step approval
- if not self.status:
+ def action_confirm_customer_commision(self):
+ jakarta_tz = pytz.timezone('Asia/Jakarta')
+ now = datetime.now(jakarta_tz)
+
+ now_naive = now.replace(tzinfo=None)
+
+ if not self.status or self.status == 'draft':
self.status = 'pengajuan1'
- elif self.status == 'pengajuan1' and self.env.user.id == 19:
+ elif self.status == 'pengajuan1' and self.env.user.is_sales_manager:
self.status = 'pengajuan2'
- elif self.status == 'pengajuan2' and self.env.user.is_leader:
+ self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
+ self.date_approved_sales = now_naive
+ self.position_sales = 'Sales Manager'
+ elif self.status == 'pengajuan2' and self.env.user.id == 19:
+ self.status = 'pengajuan3'
+ self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
+ self.date_approved_marketing = now_naive
+ self.position_marketing = 'Marketing Manager'
+ elif self.status == 'pengajuan3' and self.env.user.is_leader:
+ self.status = 'pengajuan4'
+ self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
+ self.date_approved_pimpinan = now_naive
+ self.position_pimpinan = 'Pimpinan'
+ elif self.status == 'pengajuan4' and self.env.user.id == 1272:
for line in self.commision_lines:
line.invoice_id.is_customer_commision = True
self.status = 'approved'
+ self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
+ self.date_approved_accounting = now_naive
+ self.position_accounting = 'Accounting'
else:
raise UserError('Harus di approved oleh yang bersangkutan')
return
+ def action_reject(self): # add 2 step approval
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': _('Reject Reason'),
+ 'res_model': 'reject.reason.commision',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {'default_request_id': self.id},
+ }
+
+ def button_draft(self):
+ for commision in self:
+ commision.status = commision.last_status if commision.last_status else 'draft'
+
def action_confirm_customer_payment(self):
if self.status != 'approved':
raise UserError('Commision harus di approve terlebih dahulu sebelum di konfirmasi pembayarannya')
@@ -264,7 +411,7 @@ class CustomerCommision(models.Model):
def generate_customer_commision(self):
if self.commision_lines:
raise UserError('Line sudah ada, tidak bisa di generate ulang')
-
+
if self.commision_type == 'fee':
self._generate_customer_commision_fee()
else:
@@ -339,11 +486,13 @@ class CustomerCommision(models.Model):
}])
return
+
class CustomerCommisionLine(models.Model):
_name = 'customer.commision.line'
_order = 'id'
- customer_commision_id = fields.Many2one('customer.commision', string='Ref', required=True, ondelete='cascade', copy=False)
+ customer_commision_id = fields.Many2one('customer.commision', string='Ref', required=True, ondelete='cascade',
+ copy=False)
invoice_id = fields.Many2one('account.move', string='Invoice')
partner_id = fields.Many2one('res.partner', string='Customer')
state = fields.Char(string='InvStatus')
@@ -351,7 +500,11 @@ class CustomerCommisionLine(models.Model):
tax = fields.Float(string='TaxAmt')
total = fields.Float(string='Total')
total_percent_margin = fields.Float('Total Margin', related='invoice_id.sale_id.total_percent_margin')
+ total_margin_excl_third_party = fields.Float('Before Margin',
+ related='invoice_id.sale_id.total_margin_excl_third_party')
product_id = fields.Many2one('product.product', string='Product')
+ sale_order_id = fields.Many2one('sale.order', string='Sale Order', related='invoice_id.sale_id')
+
class AccountMove(models.Model):
_inherit = 'account.move'
diff --git a/indoteknik_custom/models/coretax_fatur.py b/indoteknik_custom/models/coretax_fatur.py
index b4bffbd2..92ff1a72 100644
--- a/indoteknik_custom/models/coretax_fatur.py
+++ b/indoteknik_custom/models/coretax_fatur.py
@@ -4,19 +4,23 @@ from xml.dom import minidom
import base64
import re
+
class CoretaxFaktur(models.Model):
_name = 'coretax.faktur'
_description = 'Export Faktur ke XML'
-
- export_file = fields.Binary(string="Export File", )
- export_filename = fields.Char(string="Export File", )
-
+
+ export_file = fields.Binary(string="Export File", )
+ export_filename = fields.Char(string="Export File", )
+
+ DISCOUNT_ACCOUNT_ID = 463
+
def validate_and_format_number(slef, input_number):
# Hapus semua karakter non-digit
cleaned_number = re.sub(r'\D', '', input_number)
-
+
total_sum = sum(int(char) for char in cleaned_number)
- if total_sum == 0 :
+
+ if total_sum == 0:
return '0000000000000000'
# Hitung jumlah digit
@@ -39,22 +43,16 @@ class CoretaxFaktur(models.Model):
# Tambahkan elemen ListOfTaxInvoice
list_of_tax_invoice = ET.SubElement(root, 'ListOfTaxInvoice')
- # Dapatkan data faktur
- # inv_obj = self.env['account.move']
- # invoices = inv_obj.search([('is_efaktur_exported','=',True),
- # ('state','=','posted'),
- # ('efaktur_id','!=', False),
- # ('move_type','=','out_invoice')], limit = 5)
-
for invoice in invoices:
tax_invoice = ET.SubElement(list_of_tax_invoice, 'TaxInvoice')
buyerTIN = self.validate_and_format_number(invoice.partner_id.npwp)
nitku = invoice.partner_id.nitku
formula = nitku if nitku else buyerTIN.ljust(len(buyerTIN) + 6, '0')
- buyerIDTKU = formula if sum(int(char) for char in buyerTIN) > 0 else '000000'
+ buyerIDTKU = formula if sum(int(char) for char in buyerTIN) > 0 else '000000'
# Tambahkan elemen faktur
- ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime('%Y-%m-%d') if invoice.invoice_date else ''
+ ET.SubElement(tax_invoice, 'TaxInvoiceDate').text = invoice.invoice_date.strftime(
+ '%Y-%m-%d') if invoice.invoice_date else ''
ET.SubElement(tax_invoice, 'TaxInvoiceOpt').text = 'Normal'
ET.SubElement(tax_invoice, 'TrxCode').text = '04'
ET.SubElement(tax_invoice, 'AddInfo')
@@ -64,30 +62,72 @@ class CoretaxFaktur(models.Model):
ET.SubElement(tax_invoice, 'FacilityStamp')
ET.SubElement(tax_invoice, 'SellerIDTKU').text = '0742260227086000000000'
ET.SubElement(tax_invoice, 'BuyerTin').text = buyerTIN
- ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum(int(char) for char in buyerTIN) > 0 else 'Other ID'
+ ET.SubElement(tax_invoice, 'BuyerDocument').text = 'TIN' if sum(
+ int(char) for char in buyerTIN) > 0 else 'Other ID'
ET.SubElement(tax_invoice, 'BuyerCountry').text = 'IDN'
- ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum(int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id)
+ ET.SubElement(tax_invoice, 'BuyerDocumentNumber').text = '-' if sum(
+ int(char) for char in buyerTIN) > 0 else str(invoice.partner_id.id)
ET.SubElement(tax_invoice, 'BuyerName').text = invoice.partner_id.nama_wajib_pajak or ''
ET.SubElement(tax_invoice, 'BuyerAdress').text = invoice.partner_id.alamat_lengkap_text or ''
ET.SubElement(tax_invoice, 'BuyerEmail').text = invoice.partner_id.email or ''
ET.SubElement(tax_invoice, 'BuyerIDTKU').text = buyerIDTKU
+ # Filter product
+ product_lines = invoice.invoice_line_ids.filtered(
+ lambda l: not l.display_type and hasattr(l, 'account_id') and
+ l.account_id and l.product_id and
+ l.account_id.id != self.DISCOUNT_ACCOUNT_ID and
+ l.quantity != -1
+ )
+
+ # Filter discount
+ discount_lines = invoice.invoice_line_ids.filtered(
+ lambda l: not l.display_type and (
+ (hasattr(l, 'account_id') and l.account_id and
+ l.account_id.id == self.DISCOUNT_ACCOUNT_ID) or
+ (l.quantity == -1)
+ )
+ )
+
+ # Calculate total product amount (before discount)
+ total_product_amount = sum(line.price_subtotal for line in product_lines)
+ if total_product_amount == 0:
+ total_product_amount = 1 # Avoid division by zero
+
+ # Calculate total discount amount
+ total_discount_amount = abs(sum(line.price_subtotal for line in discount_lines))
+
# Tambahkan elemen ListOfGoodService
list_of_good_service = ET.SubElement(tax_invoice, 'ListOfGoodService')
- for line in invoice.invoice_line_ids:
- otherTaxBase = round(line.price_subtotal * (11/12)) if line.price_subtotal else 0
+
+ for line in product_lines:
+ # Calculate prorated discount
+ line_proportion = line.price_subtotal / total_product_amount
+ line_discount = total_discount_amount * line_proportion
+
+ # unit_price = line.price_unit
+ subtotal = line.price_subtotal
+ quantity = line.quantity
+ total_discount = round(line_discount, 2)
+
+ # Calculate other tax values
+ otherTaxBase = round(subtotal * (11 / 12), 2) if subtotal else 0
+ vat_amount = round(otherTaxBase * 0.12, 2)
+
+ # Create the line in XML
good_service = ET.SubElement(list_of_good_service, 'GoodService')
ET.SubElement(good_service, 'Opt').text = 'A'
ET.SubElement(good_service, 'Code').text = '000000'
ET.SubElement(good_service, 'Name').text = line.name
ET.SubElement(good_service, 'Unit').text = 'UM.0018'
- ET.SubElement(good_service, 'Price').text = str(round(line.price_subtotal/line.quantity, 2)) if line.price_subtotal else '0'
- ET.SubElement(good_service, 'Qty').text = str(line.quantity)
- ET.SubElement(good_service, 'TotalDiscount').text = '0'
- ET.SubElement(good_service, 'TaxBase').text = str(round(line.price_subtotal)) if line.price_subtotal else '0'
+ ET.SubElement(good_service, 'Price').text = str(round(subtotal / quantity, 2)) if subtotal else '0'
+ ET.SubElement(good_service, 'Qty').text = str(quantity)
+ ET.SubElement(good_service, 'TotalDiscount').text = str(total_discount)
+ ET.SubElement(good_service, 'TaxBase').text = str(
+ round(subtotal)) if subtotal else '0'
ET.SubElement(good_service, 'OtherTaxBase').text = str(otherTaxBase)
ET.SubElement(good_service, 'VATRate').text = '12'
- ET.SubElement(good_service, 'VAT').text = str(round(otherTaxBase * 0.12, 2))
+ ET.SubElement(good_service, 'VAT').text = str(vat_amount)
ET.SubElement(good_service, 'STLGRate').text = '0'
ET.SubElement(good_service, 'STLG').text = '0'
diff --git a/indoteknik_custom/models/delivery_order.py b/indoteknik_custom/models/delivery_order.py
index 3473197b..2dd0c802 100644
--- a/indoteknik_custom/models/delivery_order.py
+++ b/indoteknik_custom/models/delivery_order.py
@@ -25,7 +25,8 @@ class DeliveryOrder(models.TransientModel):
picking = False
if delivery_order_line[2]['name']:
picking = self.env['stock.picking'].search([('picking_code', '=', delivery_order_line[2]['name'])], limit=1)
-
+ if not picking:
+ picking = self.env['stock.picking'].search([('out_code', '=', delivery_order_line[2]['name'])], limit=1)
if picking:
line_tracking_no = delivery_order_line[2]['tracking_no']
@@ -86,6 +87,10 @@ class DeliveryOrderLine(models.TransientModel):
if len(self.name) == 13:
self.name = self.name[:-1]
picking = self.env['stock.picking'].search([('picking_code', '=', self.name)], limit=1)
+
+ if not picking:
+ picking = self.env['stock.picking'].search([('out_code', '=', self.name)], limit=1)
+
if picking:
if picking.driver_id:
self.driver_id = picking.driver_id
diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py
index c167aab7..bb53fc0c 100644
--- a/indoteknik_custom/models/dunning_run.py
+++ b/indoteknik_custom/models/dunning_run.py
@@ -19,7 +19,7 @@ class DunningRun(models.Model):
partner_id = fields.Many2one(
'res.partner', string='Customer',
required=True, change_default=True, index=True, tracking=1)
- dunning_line = fields.One2many('dunning.run.line', 'dunning_id', string='Dunning Lines', auto_join=True)
+ dunning_line = fields.One2many('dunning.run.line', 'dunning_id', string='Dunning Lines', auto_join=True, order='invoice_id desc')
# dunning_level = fields.Integer(string='Dunning Level', default=30, help='30 hari sebelum jatuh tempo invoice')
date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur')
resi_tukar_faktur = fields.Char(string='Resi Faktur')
@@ -122,7 +122,8 @@ class DunningRun(models.Model):
class DunningRunLine(models.Model):
_name = 'dunning.run.line'
_description = 'Dunning Run Line'
- _order = 'dunning_id, id'
+ # _order = 'dunning_id, id'
+ _order = 'invoice_id desc, id'
dunning_id = fields.Many2one('dunning.run', string='Dunning Ref', required=True, ondelete='cascade', index=True, copy=False)
partner_id = fields.Many2one('res.partner', string='Customer')
diff --git a/indoteknik_custom/models/invoice_reklas.py b/indoteknik_custom/models/invoice_reklas.py
index f5bb5a25..d10d4c31 100644
--- a/indoteknik_custom/models/invoice_reklas.py
+++ b/indoteknik_custom/models/invoice_reklas.py
@@ -18,6 +18,12 @@ class InvoiceReklas(models.TransientModel):
('pembelian', 'Pembelian'),
], string='Reklas Tipe')
+ @api.onchange('reklas_type')
+ def _onchange_reklas_type(self):
+ if self.reklas_type == 'penjualan':
+ invoices = self.env['account.move'].browse(self._context.get('active_ids', []))
+ self.pay_amt = invoices.amount_total
+
def create_reklas(self):
if not self.reklas_type:
raise UserError('Reklas Tipe harus diisi')
diff --git a/indoteknik_custom/models/logbook_sj.py b/indoteknik_custom/models/logbook_sj.py
index 9f349882..75b2622f 100644
--- a/indoteknik_custom/models/logbook_sj.py
+++ b/indoteknik_custom/models/logbook_sj.py
@@ -26,6 +26,8 @@ class LogbookSJ(models.TransientModel):
report_logbook = self.env['report.logbook.sj'].create([parameters_header])
for line in logbook_line:
picking = self.env['stock.picking'].search([('picking_code', '=', line.name)], limit=1)
+ if not picking:
+ picking = self.env['stock.picking'].search([('out_code', '=', line.name)], limit=1)
stock = picking
parent_id = stock.partner_id.parent_id.id
parent_id = parent_id if parent_id else stock.partner_id.id
@@ -80,6 +82,9 @@ class LogbookSJLine(models.TransientModel):
if len(self.name) == 13:
self.name = self.name[:-1]
picking = self.env['stock.picking'].search([('picking_code', '=', self.name)], limit=1)
+
+ if not picking:
+ picking = self.env['stock.picking'].search([('out_code', '=', self.name)], limit=1)
if picking:
if picking.driver_id:
self.driver_id = picking.driver_id
diff --git a/indoteknik_custom/models/manufacturing.py b/indoteknik_custom/models/manufacturing.py
index 24a8b8c3..715d8513 100644
--- a/indoteknik_custom/models/manufacturing.py
+++ b/indoteknik_custom/models/manufacturing.py
@@ -26,6 +26,13 @@ class Manufacturing(models.Model):
# Check product category
if self.product_id.categ_id.name != 'Finish Good':
raise UserError('Tidak bisa di complete karna product category bukan Unit / Finish Good')
+
+ if self.sale_order and self.sale_order.state != 'sale':
+ raise UserError(
+ ('Tidak bisa Mark as Done.\nSales Order "%s" (Nomor: %s) belum dikonfirmasi.')
+ % (self.sale_order.partner_id.name, self.sale_order.name)
+ )
+
for line in self.move_raw_ids:
# if line.quantity_done > 0 and line.quantity_done != self.product_uom_qty:
# raise UserError('Qty Consume per Line tidak sama dengan Qty to Produce')
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index 54d90256..14821f27 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -1,10 +1,488 @@
-from odoo import fields, models, api, _
+from odoo import models, fields, api, tools, _
+from datetime import datetime, timedelta
+import math
+import logging
from odoo.exceptions import AccessError, UserError, ValidationError
class MrpProduction(models.Model):
_inherit = 'mrp.production'
+ check_bom_product_lines = fields.One2many('check.bom.product', 'production_id', string='Check Product', auto_join=True, copy=False)
desc = fields.Text(string='Description')
+ sale_order = fields.Many2one('sale.order', string='Sale Order', copy=False)
+ production_purchase_match = fields.One2many('production.purchase.match', 'production_id', string='Purchase Matches', auto_join=True)
+ is_po = fields.Boolean(string='Is PO')
+ state_reserve = fields.Selection([
+ ('waiting', 'Waiting For Fullfilment'),
+ ('ready', 'Ready to Ship'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled'),
+ ], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.")
+ date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
+
+
+ @api.constrains('check_bom_product_lines')
+ def constrains_check_bom_product_lines(self):
+ for rec in self:
+ if len(rec.check_bom_product_lines) > 0:
+ rec.qty_producing = rec.product_qty
+
+ def button_mark_done(self):
+ """Override button_mark_done untuk mengirim pesan ke Sale Order jika state berubah menjadi 'confirmed'."""
+ if self._name != 'mrp.production':
+ return super(MrpProduction, self).button_mark_done()
+
+ result = super(MrpProduction, self).button_mark_done()
+
+ for record in self:
+ if len(record.check_bom_product_lines) < 1:
+ raise UserError("Check Product Tidak Boleh Kosong")
+ if not record.sale_order:
+ raise UserError("Sale Order Tidak Boleh Kosong")
+ if record.sale_order and record.state == 'confirmed':
+ message = _("Manufacturing order telah dibuat dengan nomor %s") % (record.name)
+ record.sale_order.message_post(body=message)
+
+ return result
+
+ def action_confirm(self):
+ """Override action_confirm untuk mengirim pesan ke Sale Order jika state berubah menjadi 'confirmed'."""
+ if self._name != 'mrp.production':
+ return super(MrpProduction, self).action_confirm()
+
+ result = super(MrpProduction, self).action_confirm()
+
+ for record in self:
+ # if len(record.check_bom_product_lines) < 1:
+ # raise UserError("Check Product Tidak Boleh Kosong")
+ if record.sale_order and record.state == 'confirmed':
+ message = _("Manufacturing order telah dibuat dengan nomor %s") % (record.name)
+ record.sale_order.message_post(body=message)
+
+ return result
+
+
+ def create_po_from_manufacturing(self):
+ if not self.state == 'confirmed':
+ raise UserError('Harus Di Approve oleh Merchandiser')
+
+ if self.is_po == True:
+ raise UserError('Sudah pernah di buat PO')
+
+ if not self.move_raw_ids:
+ raise UserError('Tidak ada Lines, belum bisa create PO')
+ # if self.is_po:
+ # raise UserError('Sudah pernah di create PO')
+
+ vendor_ids = self.env['stock.move'].read_group([
+ ('raw_material_production_id', '=', self.id),
+ ('vendor_id', '!=', False)
+ ], fields=['vendor_id'], groupby=['vendor_id'])
+
+ po_ids = []
+ for vendor in vendor_ids:
+ result_po = self.create_po_by_vendor(vendor['vendor_id'][0])
+ po_ids += result_po
+ return {
+ 'name': _('Purchase Order'),
+ 'view_mode': 'tree,form',
+ 'res_model': 'purchase.order',
+ 'target': 'current',
+ 'type': 'ir.actions.act_window',
+ 'domain': [('id', 'in', po_ids)],
+ }
- \ No newline at end of file
+
+ def create_po_by_vendor(self, vendor_id):
+ current_time = datetime.now()
+
+ PRODUCT_PER_PO = 20
+
+ stock_move = self.env['stock.move']
+
+ param_header = {
+ 'partner_id': vendor_id,
+ # 'partner_ref': self.sale_order_id.name,
+ 'currency_id': 12,
+ 'user_id': self.env.user.id,
+ 'company_id': 1, # indoteknik dotcom gemilang
+ 'picking_type_id': 28, # indoteknik bandengan receipts
+ 'date_order': current_time,
+ 'product_bom_id': self.product_id.id,
+ # 'sale_order_id': self.sale_order_id.id,
+ 'note_description': 'from Manufacturing Order'
+ }
+
+ domain = [
+ ('raw_material_production_id', '=', self.id),
+ ('vendor_id', '=', vendor_id),
+ ('state', 'in', ['waiting','confirmed','partially_available'])
+ ]
+
+ products_len = stock_move.search_count(domain)
+ page = math.ceil(products_len / PRODUCT_PER_PO)
+ po_ids = []
+ # i start from zero (0)
+ for i in range(page):
+ new_po = self.env['purchase.order'].create([param_header])
+ new_po.name = new_po.name + "/MO/" + str(i + 1)
+ po_ids.append(new_po.id)
+ lines = stock_move.search(
+ domain,
+ offset=i * PRODUCT_PER_PO,
+ limit=PRODUCT_PER_PO
+ )
+ tax = [22]
+
+ for line in lines:
+ product = line.product_id
+ price, taxes, vendor = self._get_purchase_price(product)
+
+ param_line = {
+ 'order_id' : new_po.id,
+ 'product_id': product.id,
+ 'product_qty': line.product_uom_qty if line.state in ['confirmed', 'waiting'] else line.product_uom_qty - line.forecast_availability,
+ 'product_uom_qty': line.product_uom_qty if line.state in ['confirmed', 'waiting'] else line.product_uom_qty - line.forecast_availability,
+ 'name': product.display_name,
+ 'price_unit': price if price else 0.0,
+ 'taxes_id': [taxes] if taxes else [],
+ }
+ new_po_line = self.env['purchase.order.line'].create([param_line])
+
+ self.env['production.purchase.match'].create([{
+ 'production_id': self.id,
+ 'order_id': new_po.id
+ }])
+
+ self.is_po = True
+
+ return po_ids
+
+ def _get_purchase_price(self, product_id):
+ override_vendor = product_id.x_manufacture.override_vendor_id
+ query = [('product_id', '=', product_id.id),
+ ('vendor_id', '=', override_vendor.id)]
+ purchase_price = self.env['purchase.pricelist'].search(query, limit=1)
+ if purchase_price:
+ return self._get_valid_purchase_price(purchase_price)
+ else:
+ purchase_price = self.env['purchase.pricelist'].search(
+ [('product_id', '=', product_id.id),
+ ('is_winner', '=', True)],
+ limit=1)
+
+ return self._get_valid_purchase_price(purchase_price)
+
+ def _get_valid_purchase_price(self, purchase_price):
+ current_time = datetime.now()
+ delta_time = current_time - timedelta(days=365)
+ # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
+
+ price = 0
+ taxes = 24
+ vendor_id = ''
+ human_last_update = purchase_price.human_last_update or datetime.min
+ system_last_update = purchase_price.system_last_update or datetime.min
+
+ #if purchase_price.taxes_product_id.type_tax_use == 'purchase':
+ price = purchase_price.product_price
+ taxes = purchase_price.taxes_product_id.id or 24
+ vendor_id = purchase_price.vendor_id.id
+ if delta_time > human_last_update:
+ price = 0
+ taxes = ''
+ vendor_id = ''
+
+ if system_last_update > human_last_update:
+ #if purchase_price.taxes_system_id.type_tax_use == 'purchase':
+ price = purchase_price.system_price
+ taxes = purchase_price.taxes_system_id.id or 24
+ vendor_id = purchase_price.vendor_id.id
+ if delta_time > system_last_update:
+ price = 0
+ taxes = 24
+ vendor_id = ''
+
+ return price, taxes, vendor_id
+
+class CheckBomProduct(models.Model):
+ _name = 'check.bom.product'
+ _description = 'Check Product'
+ _order = 'production_id, id'
+
+ production_id = fields.Many2one(
+ 'mrp.production',
+ string='Bom Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ product_id = fields.Many2one('product.product', string='Product')
+ quantity = fields.Float(string='Quantity')
+ status = fields.Char(string='Status', compute='_compute_status')
+ code_product = fields.Char(string='Code Product')
+
+ @api.constrains('production_id')
+ def _check_missing_components(self):
+ for mo in self:
+ required = mo.production_id.move_raw_ids.mapped('product_id')
+ entered = mo.production_id.check_bom_product_lines.mapped('product_id')
+ missing = required - entered
+
+ # Jika HTML tidak bekerja sama sekali, gunakan format text biasa yang rapi
+ if missing:
+ product_list = "\n- " + "\n- ".join(p.display_name for p in missing)
+ raise UserError(
+ "⚠️ Komponen Wajib Diisi\n\n"
+ "Produk berikut harus ditambahkan:\n"
+ f"{product_list}\n\n"
+ "Silakan lengkapi terlebih dahulu."
+ )
+
+ @api.constrains('production_id', 'product_id')
+ def _check_product_bom_validation(self):
+ for record in self:
+ if record.production_id.sale_order.state not in ['sale', 'done']:
+ raise UserError((
+ "SO harus diconfirm terlebih dahulu."
+ ))
+ if not record.production_id or not record.product_id:
+ continue
+
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+
+ if not moves:
+ raise UserError((
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
+
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ # Find existing lines for the same product, excluding the current line
+ existing_lines = record.production_id.check_bom_product_lines.filtered(
+ lambda line: line.product_id == record.product_id
+ )
+
+ if existing_lines:
+ total_quantity = sum(existing_lines.mapped('quantity'))
+
+ if total_quantity < total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' kurang dari quantity demand."
+ ) % (record.product_id.display_name))
+ else:
+ # Check if the quantity exceeds the allowed total
+ if record.quantity < total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' kurang dari quantity demand."
+ ) % (record.product_id.display_name))
+
+ # Set the quantity to the entered value
+ record.quantity = record.quantity
+
+ @api.onchange('code_product')
+ def _onchange_code_product(self):
+ if not self.code_product:
+ return
+
+ # Cari product berdasarkan default_code, barcode, atau barcode_box
+ product = self.env['product.product'].search([
+ '|',
+ ('default_code', '=', self.code_product),
+ '|',
+ ('barcode', '=', self.code_product),
+ ('barcode_box', '=', self.code_product)
+ ], limit=1)
+
+ if not product:
+ raise UserError("Product tidak ditemukan")
+
+ # Jika scan barcode_box, set quantity sesuai qty_pcs_box
+ if product.barcode_box == self.code_product:
+ self.product_id = product.id
+ self.quantity = product.qty_pcs_box
+ self.code_product = product.default_code or product.barcode
+ # return {
+ # 'warning': {
+ # 'title': 'Info',8994175025871
+
+ # 'message': f'Product box terdeteksi. Quantity di-set ke {product.qty_pcs_box}'
+ # }
+ # }
+ else:
+ # Jika scan biasa
+ self.product_id = product.id
+ self.code_product = product.default_code or product.barcode
+ self.quantity = 1
+
+ def unlink(self):
+ # Get all affected pickings before deletion
+ productions = self.mapped('production_id')
+
+ # Store product_ids that will be deleted
+ deleted_product_ids = self.mapped('product_id')
+
+ # Perform the deletion
+ result = super(CheckBomProduct, self).unlink()
+
+ # After deletion, update moves for affected pickings
+ for production in productions:
+ # For products that were completely removed (no remaining check.bom.product lines)
+ remaining_product_ids = production.check_bom_product_lines.mapped('product_id')
+ removed_product_ids = deleted_product_ids - remaining_product_ids
+
+ # Set quantity_done to 0 for moves of completely removed products
+ moves_to_reset = production.move_raw_ids.filtered(
+ lambda move: move.product_id in removed_product_ids
+ )
+ for move in moves_to_reset:
+ move.quantity_done = 0.0
+
+ production.qty_producing = 0
+
+ # Also sync remaining products in case their totals changed
+ self._sync_check_product_to_moves(production)
+
+ return result
+
+ @api.depends('quantity')
+ def _compute_status(self):
+ for record in self:
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ if record.quantity < total_qty_in_moves:
+ record.status = 'Pending'
+ else:
+ record.status = 'Done'
+
+
+ def create(self, vals):
+ # Create the record
+ record = super(CheckBomProduct, self).create(vals)
+ # Ensure uniqueness after creation
+ if not self.env.context.get('skip_consolidate'):
+ record.with_context(skip_consolidate=True)._consolidate_duplicate_lines()
+ return record
+
+ def write(self, vals):
+ # Write changes to the record
+ result = super(CheckBomProduct, self).write(vals)
+ # Ensure uniqueness after writing
+ if not self.env.context.get('skip_consolidate'):
+ self.with_context(skip_consolidate=True)._consolidate_duplicate_lines()
+ return result
+
+ def _sync_check_product_to_moves(self, production):
+ """
+ Sinkronisasi quantity_done di move_raw_ids
+ dengan total quantity dari check.bom.product berdasarkan product_id.
+ """
+ for product_id in production.check_bom_product_lines.mapped('product_id'):
+ # Totalkan quantity dari semua baris check.bom.product untuk product_id ini
+ total_quantity = sum(
+ line.quantity for line in production.check_bom_product_lines.filtered(lambda line: line.product_id == product_id)
+ )
+ # Update quantity_done di move yang relevan
+ moves = production.move_raw_ids.filtered(lambda move: move.product_id == product_id)
+ for move in moves:
+ move.quantity_done = total_quantity
+
+ def _consolidate_duplicate_lines(self):
+ """
+ Consolidate duplicate lines with the same product_id under the same production_id
+ and sync the total quantity to related moves.
+ """
+ for production in self.mapped('production_id'):
+ lines_to_remove = self.env['check.bom.product'] # Recordset untuk menyimpan baris yang akan dihapus
+ product_lines = production.check_bom_product_lines.filtered(lambda line: line.product_id)
+
+ # Group lines by product_id
+ product_groups = {}
+ for line in product_lines:
+ product_groups.setdefault(line.product_id.id, []).append(line)
+
+ for product_id, lines in product_groups.items():
+ if len(lines) > 1:
+ # Consolidate duplicate lines
+ first_line = lines[0]
+ total_quantity = sum(line.quantity for line in lines)
+
+ # Update the first line's quantity
+ first_line.with_context(skip_consolidate=True).write({'quantity': total_quantity})
+
+ # Add the remaining lines to the lines_to_remove recordset
+ lines_to_remove |= self.env['check.bom.product'].browse([line.id for line in lines[1:]])
+
+ # Perform unlink after consolidation
+ if lines_to_remove:
+ lines_to_remove.unlink()
+
+ # Sync total quantities to moves
+ self._sync_check_product_to_moves(production)
+
+ @api.onchange('product_id', 'quantity')
+ def check_product_validity(self):
+ for record in self:
+ if not record.production_id or not record.product_id:
+ continue
+
+ # Filter moves related to the selected product
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+
+ if not moves:
+ raise UserError((
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
+
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ # Find existing lines for the same product, excluding the current line
+ existing_lines = record.production_id.check_bom_product_lines.filtered(
+ lambda line: line.product_id == record.product_id
+ )
+
+ if existing_lines:
+ # Get the first existing line
+ first_line = existing_lines[0]
+
+ # Calculate the total quantity after addition
+ total_quantity = sum(existing_lines.mapped('quantity'))
+
+ if total_quantity > total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
+ else:
+ # Check if the quantity exceeds the allowed total
+ if record.quantity == total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
+
+ # Set the quantity to the entered value
+ record.quantity = record.quantity
+
+
+class ProductionPurchaseMatch(models.Model):
+ _name = 'production.purchase.match'
+ _order = 'production_id, id'
+
+ production_id = fields.Many2one('mrp.production', string='Ref', required=True, ondelete='cascade', index=True, copy=False)
+ order_id = fields.Many2one('purchase.order', string='Purchase Order')
+ vendor = fields.Char(string='Vendor', compute='_compute_info_po')
+ total = fields.Float(string='Total', compute='_compute_info_po')
+
+ def _compute_info_po(self):
+ for match in self:
+ match.vendor = match.order_id.partner_id.name
+ match.total = match.order_id.amount_total
+
diff --git a/indoteknik_custom/models/product_pricelist.py b/indoteknik_custom/models/product_pricelist.py
index c299ff2f..94a9b239 100644
--- a/indoteknik_custom/models/product_pricelist.py
+++ b/indoteknik_custom/models/product_pricelist.py
@@ -3,8 +3,8 @@ from datetime import datetime, timedelta
class ProductPricelist(models.Model):
- _inherit = 'product.pricelist'
-
+ _inherit = 'product.pricelist'
+
is_flash_sale = fields.Boolean(string='Flash Sale', default=False)
is_show_program = fields.Boolean(string='Show Program', default=False)
banner = fields.Binary(string='Banner')
@@ -17,6 +17,7 @@ class ProductPricelist(models.Model):
], string='Flash Sale Option')
banner_top = fields.Binary(string='Banner Top')
flashsale_tag = fields.Char(string='Flash Sale Tag')
+ number = fields.Integer(string='Sequence')
def _check_end_date_and_update_solr(self):
today = datetime.utcnow().date()
@@ -55,7 +56,7 @@ class ProductPricelist(models.Model):
return tier_name
class ProductPricelistItem(models.Model):
- _inherit = 'product.pricelist.item'
+ _inherit = 'product.pricelist.item'
manufacture_id = fields.Many2one('x_manufactures', string='Manufacture')
computed_price = fields.Float(string='Computed Price') \ No newline at end of file
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index 600dd90e..3bb54f44 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -15,6 +15,14 @@ _logger = logging.getLogger(__name__)
class ProductTemplate(models.Model):
_inherit = "product.template"
+
+ image_carousel_lines = fields.One2many(
+ comodel_name="image.carousel",
+ inverse_name="product_id",
+ string="Image Carousel",
+ auto_join=True,
+ copy=False
+ )
x_studio_field_tGhJR = fields.Many2many('x_product_tags', string="Product Tags")
x_manufacture = fields.Many2one(
comodel_name="x_manufactures",
@@ -246,7 +254,7 @@ class ProductTemplate(models.Model):
# product.default_code = 'ITV.'+str(product.id)
# _logger.info('Updated Variant %s' % product.name)
- @api.onchange('name','default_code','x_manufacture','product_rating','website_description','image_1920','weight','public_categ_ids')
+ @api.onchange('name','default_code','x_manufacture','product_rating','website_description','image_1920','weight','public_categ_ids','image_carousel_lines')
def update_solr_flag(self):
for tmpl in self:
if tmpl.solr_flag == 1:
@@ -380,12 +388,505 @@ class ProductTemplate(models.Model):
self.env['token.storage'].create([values])
return values
- def write(self, vals):
- # for rec in self:
- # if rec.id == 224484:
- # raise UserError('Tidak dapat mengubah produk sementara')
+ # ==============================
+ def get_vendor_name(self, rec):
+ """Get formatted name for vendor/supplier"""
+ return rec.name.name if rec.name else f"ID {rec.id}"
+
+ def get_attribute_line_name(self, rec):
+ """Get formatted name for attribute line"""
+ if rec.attribute_id and rec.value_ids:
+ values = ", ".join(rec.value_ids.mapped('name'))
+ return f"{rec.attribute_id.name}: {values}"
+ return f"ID {rec.id}"
+
+ def _get_vendor_field_label(self, field_name):
+ """Get human-readable label for vendor fields"""
+ field_labels = {
+ 'name': 'Vendor',
+ 'currency_id': 'Currency',
+ 'product_uom': 'Unit of Measure',
+ 'price': 'Price',
+ 'delay': 'Delivery Lead Time',
+ 'product_id': 'Product Variant',
+ 'product_name': 'Vendor Product Name',
+ 'product_code': 'Vendor Product Code',
+ 'date_start': 'Start Date',
+ 'date_end': 'End Date',
+ 'min_qty': 'Quantity'
+ }
+ return field_labels.get(field_name, field_name.replace('_', ' ').title())
+
+ # ==============================
+
+ def _collect_old_values(self, vals):
+ """Collect old values before write"""
+ return {
+ record.id: {
+ field_name: record[field_name]
+ for field_name in vals.keys()
+ if field_name in record._fields
+ }
+ for record in self
+ }
+
+ def _prepare_attribute_line_info(self):
+ """Prepare attribute line info for logging and update comparison"""
+ line_info = {}
+ for line in self.attribute_line_ids:
+ line_info[line.id] = {
+ 'name': self.get_attribute_line_name(line),
+ 'attribute_id': line.attribute_id.id if line.attribute_id else None,
+ 'attribute_name': line.attribute_id.name if line.attribute_id else None,
+ 'value_ids': [(v.id, v.name) for v in line.value_ids],
+ 'value_names': ", ".join(line.value_ids.mapped('name'))
+ }
+ return line_info
+
+ def _prepare_vendor_info(self):
+ """Prepare vendor info for logging before they are deleted"""
+ vendor_info = {}
+ for seller in self.seller_ids:
+ vendor_info[seller.id] = {
+ 'name': self.get_vendor_name(seller),
+ 'price': seller.price,
+ 'min_qty': seller.min_qty,
+ 'delay': seller.delay,
+ 'product_name': seller.product_name,
+ 'product_code': seller.product_code,
+ 'currency_id': seller.currency_id.id if seller.currency_id else None,
+ 'product_uom': seller.product_uom.id if seller.product_uom else None,
+ 'product_id': seller.product_id.id if seller.product_id else None,
+ 'date_start': seller.date_start,
+ 'date_end': seller.date_end,
+ }
+ return vendor_info
+
+ # ==========================
+
+ def _get_context_with_all_info(self, vals):
+ """Get context with all necessary info (attributes and vendors)"""
+ context = dict(self.env.context)
+
+ # Check for attribute line changes
+ if 'attribute_line_ids' in vals:
+ attribute_line_info = {}
+ for product in self:
+ product_line_info = product._prepare_attribute_line_info()
+ attribute_line_info.update(product_line_info)
+ context['attribute_line_info'] = attribute_line_info
+
+ # Check for vendor changes - store both for deletion and for comparing old values
+ if 'seller_ids' in vals:
+ vendor_info = {}
+ vendor_old_values = {}
+ for product in self:
+ # For deletion logging
+ product_vendor_info = product._prepare_vendor_info()
+ vendor_info.update(product_vendor_info)
+
+ # For update comparison
+ product_vendor_old = product._prepare_vendor_info()
+ vendor_old_values.update(product_vendor_old)
+
+ context['vendor_info'] = vendor_info
+ context['vendor_old_values'] = vendor_old_values
+
+ return context
+
+ # ========================
+
+ def _log_image_changes(self, field_name, old_val, new_val):
+ """Log image field changes"""
+ label_map = {
+ 'image_1920': 'Main Image',
+ 'image_carousel_lines': 'Carousel Images',
+ 'product_template_image_ids': 'Extra Product Media',
+ }
+
+ label = label_map.get(field_name, field_name)
+
+ if old_val == 'None' and new_val != 'None':
+ return f"<li><b>{label}</b>: image added</li>"
+ elif old_val != 'None' and new_val == 'None':
+ return f"<li><b>{label}</b>: image removed</li>"
+ elif old_val != new_val:
+ return f"<li><b>{label}</b>: image updated</li>"
+ return None
+
+ def _log_attribute_line_changes(self, commands):
+ """Log changes to attribute lines with complete information"""
+ # Get stored info from context
+ stored_info = self.env.context.get('attribute_line_info', {})
- return super(ProductTemplate, self).write(vals)
+ for cmd in commands:
+ if cmd[0] == 0: # Add
+ new = self.env['product.template.attribute.line'].new(cmd[2])
+ attribute_name = new.attribute_id.name if new.attribute_id else 'Attribute'
+ values = ", ".join(new.value_ids.mapped('name')) if new.value_ids else ''
+
+ message = f"<b>Product Attribute</b>:<br/>{attribute_name} added<br/>"
+ if values:
+ message += f"Values: '{values}'"
+ self.message_post(body=message)
+
+ elif cmd[0] == 1: # Update
+ rec_id = cmd[1]
+ vals = cmd[2]
+
+ # Get old values from context
+ old_data = stored_info.get(rec_id, {})
+ if not old_data:
+ # Fallback: get current record
+ rec = self.env['product.template.attribute.line'].browse(rec_id)
+ if not rec.exists():
+ continue
+ old_data = {
+ 'name': self.get_attribute_line_name(rec),
+ 'attribute_id': rec.attribute_id.id if rec.attribute_id else None,
+ 'attribute_name': rec.attribute_id.name if rec.attribute_id else None,
+ 'value_ids': [(v.id, v.name) for v in rec.value_ids],
+ 'value_names': ", ".join(rec.value_ids.mapped('name'))
+ }
+
+ changes = []
+ attribute_name = old_data.get('attribute_name', 'Attribute')
+
+ # Check for attribute change
+ if 'attribute_id' in vals:
+ old_attr = old_data.get('attribute_name', '-')
+ new_attr = self.env['product.attribute'].browse(vals['attribute_id']).name
+ if old_attr != new_attr:
+ attribute_name = new_attr # Update attribute name for display
+ changes.append(f"Attribute changed from '{old_attr}' to '{new_attr}'")
+
+ # Check for value changes
+ if 'value_ids' in vals:
+ old_vals = old_data.get('value_names', '')
+
+ # Parse the command for value_ids
+ new_value_ids = []
+ for value_cmd in vals['value_ids']:
+ if isinstance(value_cmd, (list, tuple)):
+ if value_cmd[0] == 6: # Replace all
+ new_value_ids = value_cmd[2]
+ elif value_cmd[0] == 4: # Add
+ new_value_ids.append(value_cmd[1])
+ elif value_cmd[0] == 3: # Remove
+ # This is more complex, would need current state
+ pass
+
+ # Get new value names
+ if new_value_ids:
+ new_values = self.env['product.attribute.value'].browse(new_value_ids)
+ new_vals = ", ".join(new_values.mapped('name'))
+ else:
+ new_vals = ""
+
+ if old_vals != new_vals:
+ changes.append(f"Values: '{old_vals}' → '{new_vals}'")
+
+ if changes:
+ # Format with attribute name
+ message = f"<b>Product Attribute</b>:<br/>{attribute_name} updated<br/>"
+ message += "<br/>".join(changes)
+ self.message_post(body=message)
+
+ elif cmd[0] in (2, 3): # Remove
+ # Use info from stored data
+ line_data = stored_info.get(cmd[1])
+ if line_data:
+ attribute_name = line_data.get('attribute_name', 'Attribute')
+ values = line_data.get('value_names', '')
+ else:
+ rec = self.env['product.template.attribute.line'].browse(cmd[1])
+ if rec.exists():
+ attribute_name = rec.attribute_id.name if rec.attribute_id else 'Attribute'
+ values = ", ".join(rec.value_ids.mapped('name')) if rec.value_ids else ''
+ else:
+ attribute_name = 'Attribute'
+ values = f"ID {cmd[1]}"
+
+ message = f"<b>Product Attribute</b>:<br/>{attribute_name} removed<br/>"
+ if values:
+ message += f"Values: '{values}'"
+ self.message_post(body=message)
+
+ elif cmd[0] == 5: # Clear all
+ self.message_post(body=f"<b>Product Attribute</b>:<br/>All attributes removed")
+
+ def _log_vendor_pricelist_changes(self, commands):
+ """Log changes to vendor pricelist with complete information"""
+ # Get stored info from context
+ stored_info = self.env.context.get('vendor_info', {})
+ old_values_info = self.env.context.get('vendor_old_values', {})
+
+ for cmd in commands:
+ if cmd[0] == 0: # Add
+ vals = cmd[2]
+
+ # Create temporary record to get proper display values
+ temp_values = vals.copy()
+ temp_values['product_tmpl_id'] = self.id
+ new = self.env['product.supplierinfo'].new(temp_values)
+
+ name = self.get_vendor_name(new)
+ details = []
+
+ if 'price' in vals and vals['price'] is not None:
+ details.append(f"<li>Price: {vals['price']}</li>")
+ if 'min_qty' in vals and vals['min_qty'] is not None:
+ details.append(f"<li>Quantity: {vals['min_qty']}</li>")
+ if 'delay' in vals and vals['delay'] is not None:
+ details.append(f"<li>Delivery Lead Time: {vals['delay']}</li>")
+ if 'product_name' in vals and vals['product_name']:
+ details.append(f"<li>Vendor Product Name: {vals['product_name']}</li>")
+ if 'product_code' in vals and vals['product_code']:
+ details.append(f"<li>Vendor Product Code: {vals['product_code']}</li>")
+ if 'currency_id' in vals and vals['currency_id']:
+ currency = self.env['res.currency'].browse(vals['currency_id'])
+ details.append(f"<li>Currency: {currency.name}</li>")
+ if 'product_uom' in vals and vals['product_uom']:
+ uom = self.env['uom.uom'].browse(vals['product_uom'])
+ details.append(f"<li>Unit of Measure: {uom.name}</li>")
+
+ if details:
+ detail_str = f" with:<ul>{''.join(details)}</ul>"
+ else:
+ detail_str = ""
+
+ self.message_post(body=f"<b>Vendor Pricelist</b>: added '{name}'{detail_str}")
+
+ elif cmd[0] == 1: # Update
+ rec_id = cmd[1]
+ vals = cmd[2]
+
+ # Get old values from context
+ old_data = old_values_info.get(rec_id, {})
+ if not old_data:
+ # Fallback: get current record
+ rec = self.env['product.supplierinfo'].browse(rec_id)
+ if not rec.exists():
+ continue
+ old_data = {
+ 'name': self.get_vendor_name(rec),
+ 'price': rec.price,
+ 'min_qty': rec.min_qty,
+ 'delay': rec.delay,
+ 'product_name': rec.product_name,
+ 'product_code': rec.product_code,
+ 'currency_id': rec.currency_id.id if rec.currency_id else None,
+ 'product_uom': rec.product_uom.id if rec.product_uom else None,
+ 'product_id': rec.product_id.id if rec.product_id else None,
+ 'date_start': rec.date_start,
+ 'date_end': rec.date_end,
+ }
+
+ name = old_data.get('name', f'ID {rec_id}')
+ changes = []
+
+ # Check each field in vals for changes
+ for field, new_value in vals.items():
+ if field == 'name':
+ # Special handling for vendor name change
+ if new_value != old_data.get('name'):
+ old_name = old_data.get('name', 'None')
+ new_name = self.env['res.partner'].browse(new_value).name if new_value else 'None'
+ changes.append(f"<li>Vendor: {old_name} → {new_name}</li>")
+ continue
+
+ old_value = old_data.get(field)
+
+ # Format values based on field type
+ if field == 'currency_id':
+ if old_value != new_value:
+ old_str = self.env['res.currency'].browse(old_value).name if old_value else 'None'
+ new_str = self.env['res.currency'].browse(new_value).name if new_value else 'None'
+ else:
+ continue
+ elif field == 'product_uom':
+ if old_value != new_value:
+ old_str = self.env['uom.uom'].browse(old_value).name if old_value else 'None'
+ new_str = self.env['uom.uom'].browse(new_value).name if new_value else 'None'
+ else:
+ continue
+ elif field == 'product_id':
+ if old_value != new_value:
+ old_str = self.env['product.product'].browse(old_value).display_name if old_value else 'None'
+ new_str = self.env['product.product'].browse(new_value).display_name if new_value else 'None'
+ else:
+ continue
+ elif field in ['date_start', 'date_end']:
+ if str(old_value) != str(new_value):
+ old_str = old_value.strftime('%Y-%m-%d') if old_value else 'None'
+ new_str = new_value if new_value else 'None'
+ else:
+ continue
+ else:
+ # For numeric and other fields
+ if field in ['price', 'min_qty', 'delay']:
+ # Compare numeric values properly
+ old_num = float(old_value) if old_value is not None else 0.0
+ new_num = float(new_value) if new_value is not None else 0.0
+
+ if field == 'delay': # Integer field
+ old_num = int(old_num)
+ new_num = int(new_num)
+
+ if old_num == new_num:
+ continue
+
+ old_str = str(old_value) if old_value is not None else 'None'
+ new_str = str(new_value) if new_value is not None else 'None'
+ else:
+ # String and other types
+ if str(old_value) == str(new_value):
+ continue
+ old_str = str(old_value) if old_value is not None else 'None'
+ new_str = str(new_value) if new_value is not None else 'None'
+
+ label = self._get_vendor_field_label(field)
+ changes.append(f"<li>{label}: {old_str} → {new_str}</li>")
+
+ if changes:
+ changes_str = f"<ul>{''.join(changes)}</ul>"
+ self.message_post(body=f"<b>Vendor Pricelist</b>: updated '{name}':{changes_str}")
+
+ elif cmd[0] in (2, 3): # Remove
+ vendor_data = stored_info.get(cmd[1])
+ if vendor_data:
+ name = vendor_data['name']
+ details = []
+
+ if vendor_data.get('price'):
+ details.append(f"<li>Price: {vendor_data['price']}</li>")
+ if vendor_data.get('min_qty'):
+ details.append(f"<li>Quantity: {vendor_data['min_qty']}</li>")
+ if vendor_data.get('product_name'):
+ details.append(f"<li>Product Name: {vendor_data['product_name']}</li>")
+ if vendor_data.get('delay'):
+ details.append(f"<li>Delivery Lead Time: {vendor_data['delay']}</li>")
+
+ if details:
+ detail_str = f"<ul>{''.join(details)}</ul>"
+ else:
+ detail_str = ""
+ else:
+ rec = self.env['product.supplierinfo'].browse(cmd[1])
+ if rec.exists():
+ name = self.get_vendor_name(rec)
+ details = []
+ if rec.price:
+ details.append(f"<li>Price: {rec.price}</li>")
+ if rec.min_qty:
+ details.append(f"<li>Quantity: {rec.min_qty}</li>")
+ if rec.product_name:
+ details.append(f"<li>Product Name: {rec.product_name}</li>")
+
+ if details:
+ detail_str = f"<ul>{''.join(details)}</ul>"
+ else:
+ detail_str = ""
+ else:
+ name = f"ID {cmd[1]}"
+ detail_str = ""
+
+ self.message_post(body=f"<b>Vendor Pricelist</b>: removed '{name}'{detail_str}")
+
+ elif cmd[0] == 5: # Clear all
+ self.message_post(body=f"<b>Vendor Pricelist</b>: all removed")
+
+ def _log_field_changes_product(self, vals, old_values):
+ """Revised - Log general field changes for product template without posting to variants"""
+ exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
+ image_fields = ['image_1920', 'image_carousel_lines', 'product_template_image_ids']
+
+ for record in self:
+ changes = []
+
+ for field_name in vals:
+ if field_name not in record._fields or field_name in exclude_fields:
+ continue
+
+ field = record._fields[field_name]
+
+ # Handle image fields specially
+ if field_name in image_fields:
+ old_val = 'None' if not old_values.get(record.id, {}).get(field_name) else 'Yes'
+ new_val = 'None' if not record[field_name] else 'Yes'
+ image_msg = record._log_image_changes(field_name, old_val, new_val)
+ if image_msg:
+ changes.append(image_msg)
+ continue
+
+ # Handle vendor fields
+ if field_name == 'seller_ids':
+ commands = vals[field_name]
+ if isinstance(commands, list):
+ record._log_vendor_pricelist_changes(commands)
+ continue
+
+ # Handle attribute lines
+ if field_name == 'attribute_line_ids':
+ commands = vals[field_name]
+ if isinstance(commands, list):
+ record._log_attribute_line_changes(commands)
+ continue
+
+ # Handle other fields
+ def stringify(val):
+ if val in [None, False]:
+ return 'None'
+ if isinstance(field, fields.Selection):
+ selection = field.selection(record) if callable(field.selection) else field.selection
+ return dict(selection).get(val, str(val))
+ if isinstance(field, fields.Boolean):
+ return 'Yes' if val else 'No'
+ if isinstance(field, fields.Many2one):
+ if isinstance(val, int):
+ rec = record.env[field.comodel_name].browse(val)
+ return rec.display_name if rec.exists() else str(val)
+ elif isinstance(val, models.BaseModel):
+ return val.display_name
+ return str(val)
+ if isinstance(field, fields.Many2many):
+ records = val if isinstance(val, models.BaseModel) else record[field.name]
+ if not records:
+ return 'None'
+ for attr in ['name', 'x_name', 'display_name']:
+ if hasattr(records[0], attr):
+ return ", ".join(records.mapped(attr))
+ return ", ".join(str(r.id) for r in records)
+ return str(val)
+
+ old_val_str = stringify(old_values.get(record.id, {}).get(field_name))
+ new_val_str = stringify(record[field_name])
+
+ if old_val_str != new_val_str:
+ field_label = field.string or field_name
+ changes.append(f"<li><b>{field_label}</b>: '{old_val_str}' → '{new_val_str}'</li>")
+
+ if changes:
+ # PERBAIKAN: Hanya post ke template, HAPUS bagian log ke variants
+ record.message_post(body=f"<b>Updated:</b><ul>{''.join(changes)}</ul>")
+
+ # simpan data lama dan log perubahan field
+ def write(self, vals):
+ context = self._get_context_with_all_info(vals)
+ if context != self.env.context:
+ self = self.with_context(**context)
+ old_values = self._collect_old_values(vals)
+ result = super().write(vals)
+ # Log changes
+ self._log_field_changes_product(vals, old_values)
+ return result
+
+ # def write(self, vals):
+ # # for rec in self:
+ # # if rec.id == 224484:
+ # # raise UserError('Tidak dapat mengubah produk sementara')
+ # self._log_field_changes(vals)
+ # return super(ProductTemplate, self).write(vals)
class ProductProduct(models.Model):
_inherit = "product.product"
@@ -408,7 +909,8 @@ class ProductProduct(models.Model):
qty_onhand_bandengan = fields.Float(string='Onhand BU', compute='_get_qty_onhand_bandengan')
clean_website_description = fields.Char(string='Clean Website Description', compute='_get_clean_website_description')
qty_incoming_bandengan = fields.Float(string='Incoming BU', compute='_get_qty_incoming_bandengan')
- qty_outgoing_bandengan = fields.Float(string='Outgoing BU', compute='_get_qty_outgoing_bandengan')
+ qty_outgoing_bandengan = fields.Float(string='Outgoing BU', compute='_get_qty_outgoing_bandengan', help='only outgoing from sales order bandengan')
+ qty_outgoing_mo_bandengan = fields.Float(string='Outgoing MO BU', compute='_get_qty_outgoing_mo_bandengan', help='only outgoing from manufacturing order bandengan')
qty_available_bandengan = fields.Float(string='Available BU', compute='_get_qty_available_bandengan')
qty_free_bandengan = fields.Float(string='Free BU', compute='_get_qty_free_bandengan')
qty_upcoming = fields.Float(string='Qty Upcoming', compute='_get_qty_upcoming')
@@ -421,6 +923,8 @@ class ProductProduct(models.Model):
plafon_qty = fields.Float(string='Max Plafon', compute='_get_plafon_qty_product')
merchandise_ok = fields.Boolean(string='Product Promotion')
qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
+ qty_pcs_box = fields.Float("Pcs Box")
+ barcode_box = fields.Char("Barcode Box")
def generate_product_sla(self):
product_variant_ids = self.env.context.get('active_ids', [])
@@ -608,12 +1112,24 @@ class ProductProduct(models.Model):
domain=[
('product_id', '=', product.id),
('location_id', 'in', [57, 83]),
+ ('mo_id', '=', False),
+ ('hold_outgoing', '=', False)
],
fields=['qty_need'],
groupby=[]
)[0].get('qty_need', 0.0)
product.qty_outgoing_bandengan = qty
+ def _get_qty_outgoing_mo_bandengan(self):
+ for product in self:
+ records = self.env['v.move.outstanding'].search([
+ ('product_id.id', '=', product.id),
+ ('location_id.id', 'in', [57, 83]),
+ ('mo_id.id', '>', 0)
+ ])
+ qty = sum(records.mapped('qty_need') or [0.0])
+ product.qty_outgoing_mo_bandengan = qty
+
def _get_qty_onhand_bandengan(self):
for product in self:
qty_onhand = self.env['stock.quant'].search([
@@ -625,7 +1141,7 @@ class ProductProduct(models.Model):
def _get_qty_available_bandengan(self):
for product in self:
- qty_available = product.qty_incoming_bandengan + product.qty_onhand_bandengan - product.qty_outgoing_bandengan
+ qty_available = product.qty_incoming_bandengan + product.qty_onhand_bandengan - product.qty_outgoing_bandengan - product.qty_outgoing_mo_bandengan
product.qty_available_bandengan = qty_available or 0
def _get_qty_free_bandengan(self):
@@ -702,6 +1218,107 @@ class ProductProduct(models.Model):
], limit=1)
return pricelist
+ # simpan data lama
+ def _collect_old_values(self, vals):
+ return {
+ record.id: {
+ field_name: record[field_name]
+ for field_name in vals.keys()
+ if field_name in record._fields
+ }
+ for record in self
+ }
+
+ # log perubahan field
+ def _log_field_changes_product_variants(self, vals, old_values):
+ """Revised - Log field changes for variants without posting to template"""
+ exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
+
+ # Custom labels for image fields
+ custom_labels = {
+ 'image_1920': 'Main Image',
+ 'image_carousel_lines': 'Carousel Images',
+ 'product_template_image_ids': 'Extra Product Media',
+ }
+
+ for record in self:
+ changes = []
+ for field_name in vals:
+ if field_name not in record._fields or field_name in exclude_fields:
+ continue
+
+ field = record._fields[field_name]
+ field_label = custom_labels.get(field_name, field.string or field_name)
+ old_value = old_values.get(record.id, {}).get(field_name)
+ new_value = record[field_name]
+
+ def stringify(val, field, record):
+ if val in [None, False]:
+ return 'None'
+ if isinstance(field, fields.Selection):
+ selection = field.selection
+ if callable(selection):
+ selection = selection(record)
+ return dict(selection).get(val, str(val))
+ if isinstance(field, fields.Boolean):
+ return 'Yes' if val else 'No'
+ if isinstance(field, fields.Many2one):
+ if isinstance(val, int):
+ rec = record.env[field.comodel_name].browse(val)
+ return rec.display_name if rec.exists() else str(val)
+ elif isinstance(val, models.BaseModel):
+ return val.display_name
+ return str(val)
+ if isinstance(field, fields.Many2many):
+ records = val if isinstance(val, models.BaseModel) else record[field.name]
+ if not records:
+ return 'None'
+ for attr in ['name', 'x_name', 'display_name']:
+ if hasattr(records[0], attr):
+ return ", ".join(records.mapped(attr))
+ return ", ".join(str(r.id) for r in records)
+ if isinstance(field, fields.One2many):
+ records = val if isinstance(val, models.BaseModel) else record[field.name]
+ if not records:
+ return 'None'
+ return f"{field.comodel_name}({', '.join(str(r.id) for r in records)})"
+ return str(val)
+
+ old_val_str = stringify(old_value, field, record)
+ new_val_str = stringify(new_value, field, record)
+
+ if old_val_str != new_val_str:
+ if field_name in custom_labels: # handle image field
+ if old_val_str == 'None' and new_val_str != 'None':
+ changes.append(f"<li><b>{field_label}</b>: image added</li>")
+ elif old_val_str != 'None' and new_val_str == 'None':
+ changes.append(f"<li><b>{field_label}</b>: image removed</li>")
+ else:
+ changes.append(f"<li><b>{field_label}</b>: image updated</li>")
+ else:
+ changes.append(f"<li><b>{field_label}</b>: '{old_val_str}' → '{new_val_str}'</li>")
+
+ if changes:
+ # PERBAIKAN: Hanya post message ke variant, HAPUS bagian template_changes
+ variant_message = "<b>Updated:</b><ul>%s</ul>" % "".join(changes)
+ record.message_post(body=variant_message)
+
+ # simpan data lama dan log perubahan field
+ def write(self, vals):
+ tracked_fields = [
+ 'default_code', 'name', 'weight', 'x_manufacture',
+ 'public_categ_ids', 'search_rank', 'search_rank_weekly',
+ 'image_1920', 'unpublished', 'image_carousel_lines'
+ ]
+
+ if any(field in vals for field in tracked_fields):
+ old_values = self._collect_old_values(vals)
+ result = super().write(vals)
+ self._log_field_changes_product_variants(vals, old_values)
+ return result
+ else:
+ return super().write(vals)
+
class OutstandingMove(models.Model):
_name = 'v.move.outstanding'
@@ -714,6 +1331,8 @@ class OutstandingMove(models.Model):
qty_need = fields.Float(string='Qty Need', help='Qty yang akan outgoing / incoming')
location_id = fields.Many2one('stock.location', string='Location', help='Lokasi asal')
location_dest_id = fields.Many2one('stock.location', string='Location To', help='Lokasi tujuan')
+ mo_id = fields.Many2one('mrp.production', string='Manufacturing Order')
+ hold_outgoing = fields.Boolean(string='Hold Outgoing')
def init(self):
# where clause 'state in' follow the origin of outgoing and incoming odoo
@@ -722,8 +1341,12 @@ class OutstandingMove(models.Model):
CREATE OR REPLACE VIEW %s AS
select sm.id, sm.reference, sm.product_id,
sm.product_uom_qty as qty_need,
- sm.location_id, sm.location_dest_id
+ sm.location_id, sm.location_dest_id,
+ sm.raw_material_production_id as mo_id,
+ so.hold_outgoing
from stock_move sm
+ left join procurement_group pg on pg.id = sm.group_id
+ left join sale_order so on so.id = pg.sale_id
where 1=1
and sm.state in(
'waiting',
@@ -732,3 +1355,11 @@ class OutstandingMove(models.Model):
'partially_available'
)
""" % self._table)
+
+class ImageCarousel(models.Model):
+ _name = 'image.carousel'
+ _description = 'Image Carousel'
+ _order = 'product_id, id'
+
+ product_id = fields.Many2one('product.template', string='Product', required=True, ondelete='cascade', index=True, copy=False)
+ image = fields.Binary(string='Image')
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index d90c4a8a..cbfd4acd 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -74,6 +74,7 @@ class PurchaseOrder(models.Model):
date_done_picking = fields.Datetime(string='Date Done Picking', compute='get_date_done')
bills_dp_id = fields.Many2one('account.move', string='Bills DP')
bills_pelunasan_id = fields.Many2one('account.move', string='Bills Pelunasan')
+ product_bom_id = fields.Many2one('product.product', string='Product Bom')
grand_total = fields.Monetary(string='Grand Total', help='Amount total + amount delivery', compute='_compute_grand_total')
total_margin_match = fields.Float(string='Total Margin Match', compute='_compute_total_margin_match')
approve_by = fields.Many2one('res.users', string='Approve By')
@@ -88,6 +89,112 @@ class PurchaseOrder(models.Model):
store_name = fields.Char(string='Nama Toko')
purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count')
+ # cek payment term
+ def _check_payment_term(self):
+ _logger.info("Check Payment Term Terpanggil")
+
+ cbd_term = self.env['account.payment.term'].search([
+ ('name', 'ilike', 'Cash Before Delivery')
+ ], limit=1)
+
+ for order in self:
+ if not order.partner_id or not order.partner_id.minimum_amount:
+ continue
+
+ if not order.order_line or order.amount_total == 0:
+ continue
+
+ if order.amount_total < order.partner_id.minimum_amount:
+ if cbd_term and order.payment_term_id != cbd_term:
+ order.payment_term_id = cbd_term.id
+ self.env.user.notify_info(
+ message="Total belanja PO belum mencapai minimum yang ditentukan vendor. "
+ "Payment Term telah otomatis diubah menjadi Cash Before Delivery (C.B.D).",
+ title="Payment Term Diperbarui"
+ )
+ else:
+ vendor_term = order.partner_id.property_supplier_payment_term_id
+ if vendor_term and order.payment_term_id != vendor_term:
+ order.payment_term_id = vendor_term.id
+ self.env.user.notify_info(
+ message=f"Total belanja PO telah memenuhi jumlah minimum vendor. "
+ f"Payment Term otomatis dikembalikan ke pengaturan vendor awal: *{vendor_term.name}*.",
+ title="Payment Term Diperbarui"
+ )
+
+ def _check_tax_rule(self):
+ _logger.info("Check Tax Rule Terpanggil")
+
+ # Pajak 11%
+ tax_11 = self.env['account.tax'].search([
+ ('type_tax_use', '=', 'purchase'),
+ ('name', 'ilike', '11%')
+ ], limit=1)
+
+ # Pajak "No Tax"
+ no_tax = self.env['account.tax'].search([
+ ('type_tax_use', '=', 'purchase'),
+ ('name', 'ilike', 'no tax')
+ ], limit=1)
+
+ if not tax_11:
+ raise UserError("Pajak 11% tidak ditemukan. Mohon pastikan pajak 11% tersedia.")
+
+ if not no_tax:
+ raise UserError("Pajak 'No Tax' tidak ditemukan. Harap buat tax dengan nama 'No Tax' dan tipe 'Purchase'.")
+
+ for order in self:
+ partner = order.partner_id
+ minimum_tax = partner.minimum_amount_tax
+
+ _logger.info("Partner ID: %s, Minimum Tax: %s, Untaxed Total: %s", partner.id, minimum_tax, order.amount_untaxed)
+
+ if not minimum_tax or not order.order_line:
+ continue
+
+ if order.amount_total < minimum_tax:
+ _logger.info(">>> Total di bawah minimum → apply No Tax")
+ for line in order.order_line:
+ line.taxes_id = [(6, 0, [no_tax.id])]
+
+ if self.env.context.get('notify_tax'):
+ self.env.user.notify_info(
+ message="Total belanja PO belum mencapai minimum pajak vendor. "
+ "Pajak diganti menjadi 'No Tax'.",
+ title="Pajak Diperbarui",
+ )
+ else:
+ _logger.info(">>> Total memenuhi minimum → apply Pajak 11%")
+ for line in order.order_line:
+ line.taxes_id = [(6, 0, [tax_11.id])]
+
+ if self.env.context.get('notify_tax'):
+ self.env.user.notify_info(
+ message="Total belanja sebelum pajak telah memenuhi minimum. "
+ "Pajak 11%% diterapkan",
+ title="Pajak Diperbarui",
+ )
+
+ # set default no_tax pada order line
+ # @api.onchange('order_line')
+ # def _onchange_order_line_tax_default(self):
+ # _logger.info("Onchange Order Line Tax Default Terpanggil")
+
+ # no_tax = self.env['account.tax'].search([
+ # ('type_tax_use', '=', 'purchase'),
+ # ('name', 'ilike', 'no tax')
+ # ], limit=1)
+
+ # if not no_tax:
+ # _logger.info("No Tax tidak ditemukan")
+ # return
+
+ # for order in self:
+ # for line in order.order_line:
+ # if not line.taxes_id:
+ # line.taxes_id = [(6, 0, [no_tax.id])]
+ # _logger.info("Auto-set No tax ke baris product: %s", line.product_id.name)
+
@api.onchange('total_cost_service')
def _onchange_total_cost_service(self):
for order in self:
@@ -455,7 +562,9 @@ class PurchaseOrder(models.Model):
i = 0
for line in self.order_line:
i += 1
- current_time = datetime.utcnow()
+
+ utc_time = fields.Datetime.now()
+ current_time = utc_time.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
# print(i, len(self.order_line))
price_unit = line.price_unit
@@ -473,10 +582,11 @@ class PurchaseOrder(models.Model):
purchase_pricelist = self.env['purchase.pricelist'].search([
('product_id', '=', line.product_id.id),
('vendor_id', '=', line.order_id.partner_id.id)
- ])
- purchase_pricelist = purchase_pricelist.with_context(update_by='system')
+ ])
+
if not purchase_pricelist:
- purchase_pricelist.create([{
+ # Buat pricelist baru dengan context
+ new_pricelist = self.env['purchase.pricelist'].with_context(update_by='system').create([{
'vendor_id': line.order_id.partner_id.id,
'product_id': line.product_id.id,
'product_price': 0,
@@ -484,12 +594,51 @@ class PurchaseOrder(models.Model):
'system_price': price_unit,
'system_last_update': current_time
}])
+
+ # Buat lognote untuk pricelist baru
+ message = f"""
+ <b>New Purchase Pricelist Created from PO</b><br/>
+ <b>PO:</b> <a href="#id={line.order_id.id}&model=purchase.order&view_type=form">{line.order_id.name}</a><br/>
+ <b>System Price:</b> {price_unit:,.2f}<br/>
+ <b>System Tax:</b> {taxes.name if taxes else 'No Tax'}<br/>
+ <b>System Update:</b> {current_time}<br/>
+ """
+ new_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
else:
+ # Simpan nilai lama untuk logging
+ old_values = {
+ 'system_price': purchase_pricelist.system_price,
+ 'taxes_system_id': purchase_pricelist.taxes_system_id,
+ }
+
+ # Update dengan context
+ purchase_pricelist = purchase_pricelist.with_context(update_by='system')
purchase_pricelist.write({
'system_last_update': current_time,
'taxes_system_id': taxes.id,
'system_price': price_unit
})
+
+ # Buat lognote jika ada perubahan
+ changes = []
+ if old_values['system_price'] != price_unit:
+ changes.append(f"<li><b>System Price</b>: {old_values['system_price']:,.2f} → {price_unit:,.2f}</li>")
+ if old_values['taxes_system_id'] != taxes:
+ old_tax_name = old_values['taxes_system_id'].name if old_values['taxes_system_id'] else 'No Tax'
+ new_tax_name = taxes.name if taxes else 'No Tax'
+ changes.append(f"<li><b>System Tax</b>: {old_tax_name} → {new_tax_name}</li>")
+
+ if changes:
+ message = f"""
+ <b>System Fields Updated from PO</b><br/>
+ <b>PO:</b> <a href="#id={line.order_id.id}&model=purchase.order&view_type=form">{line.order_id.name}</a><br/>
+ <b>Changes:</b>
+ <ul>
+ {"".join(changes)}
+ <li><b>System Update</b>: {current_time}</li>
+ </ul>
+ """
+ purchase_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
def _compute_date_planned(self):
for order in self:
@@ -671,17 +820,30 @@ class PurchaseOrder(models.Model):
raise UserError("Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")")
def button_confirm(self):
+ # self._check_payment_term() # check payment term
res = super(PurchaseOrder, self).button_confirm()
current_time = datetime.now()
self.check_ppn_mix()
self.check_different_vendor_so_po()
# self.check_data_vendor()
- if self.amount_untaxed >= 50000000 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
- raise UserError("Hanya Merchandiser yang bisa approve")
+ if self.amount_untaxed >= 50000000 and not self.env.user.id == 21:
+ raise UserError("Hanya Rafly Hanggara yang bisa approve")
if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader:
- raise UserError("Beda Margin dengan Sales, harus approval Merchandise")
+ self.env.user.notify_danger(
+ title='WARNING!!!',
+ message='Beda Margin dengan Sale Order',
+ sticky=True
+ )
+
+ if len(self.order_sales_match_line) == 0 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader:
+ self.env.user.notify_danger(
+ title='WARNING!!!',
+ message='Tidak ada matches SO',
+ sticky=True
+ )
+
if not self.from_apo:
if not self.matches_so and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader:
raise UserError("Tidak ada link dengan SO, harus approval Merchandise")
@@ -726,9 +888,25 @@ class PurchaseOrder(models.Model):
self.unlink_purchasing_job_state()
self._check_qty_plafon_product()
+ if self.product_bom_id:
+ self._remove_product_bom()
return res
+ def _remove_product_bom(self):
+ pj = self.env['v.purchasing.job'].search([
+ ('product_id', '=', self.product_bom_id.id)
+ ])
+
+ if pj:
+ pj_state = self.env['purchasing.job.state'].search([
+ ('purchasing_job_id', '=', pj.id)
+ ])
+
+ if pj_state:
+ pj_state.note = 'Product BOM Sudah Di PO'
+ pj_state.date_po = datetime.utcnow()
+
def check_ppn_mix(self):
reference_taxes = self.order_line[0].taxes_id
@@ -1062,6 +1240,94 @@ class PurchaseOrder(models.Model):
return super(PurchaseOrder, self).button_unlock()
+ @api.model #override custom create & write for check payment term
+ def create(self, vals):
+ order = super().create(vals)
+ # order.with_context(skip_check_payment=True)._check_payment_term()
+ # order.with_context(notify_tax=True)._check_tax_rule()
+ return order
+
+ def write(self, vals):
+ res = super().write(vals)
+ if not self.env.context.get('skip_check_payment'):
+ self.with_context(skip_check_payment=True)._check_payment_term()
+ self.with_context(notify_tax=True)._check_tax_rule()
+ # Tambahkan pemanggilan method untuk handle pricelist system update
+ self._handle_pricelist_system_update(vals)
+ return res
+
+ def _handle_pricelist_system_update(self, vals):
+ if 'order_line' in vals or any(key in vals for key in ['state', 'approval_status']):
+ for order in self:
+ # Hanya proses jika PO sudah approved
+ if order.state in ['purchase', 'done'] and order.approval_status == 'approved':
+ self._process_pricelist_update(order)
+
+ def _process_pricelist_update(self, order):
+ for line in order.order_line:
+ pricelist = self._get_related_pricelist(line.product_id, order.partner_id)
+
+ if pricelist:
+ # Simpan nilai lama
+ old_values = self._get_pricelist_old_values(pricelist)
+
+ # Update dan cek perubahan
+ self._update_and_log_pricelist(pricelist, line, old_values)
+
+ def _get_related_pricelist(self, product_id, vendor_id):
+ return self.env['purchase.pricelist'].search([
+ ('product_id', '=', product_id.id),
+ ('vendor_id', '=', vendor_id.id)
+ ], limit=1)
+
+ def _get_pricelist_old_values(self, pricelist):
+ return {
+ 'system_price': pricelist.system_price,
+ 'taxes_system_id': pricelist.taxes_system_id,
+ 'system_last_update': pricelist.system_last_update
+ }
+
+ def _update_and_log_pricelist(self, pricelist, po_line, old_values):
+ changes = []
+ current_time = fields.Datetime.now()
+
+ # Cek perubahan System Price
+ if pricelist.system_price != po_line.price_unit:
+ if old_values['system_price'] != po_line.price_unit:
+ changes.append(f"<li><b>System Price</b>: {old_values['system_price']:,.2f} → {po_line.price_unit:,.2f}</li>")
+
+ # Cek perubahan System Tax
+ if pricelist.taxes_system_id != po_line.taxes_id:
+ old_tax = old_values['taxes_system_id']
+ old_tax_name = old_tax.name if old_tax else 'No Tax'
+ new_tax_name = po_line.taxes_id.name if po_line.taxes_id else 'No Tax'
+ if old_tax != po_line.taxes_id:
+ changes.append(f"<li><b>System Tax</b>: {old_tax_name} → {new_tax_name}</li>")
+
+ # Update fields jika ada perubahan
+ if changes:
+ pricelist.with_context(update_by='system').write({
+ 'system_price': po_line.price_unit,
+ 'taxes_system_id': po_line.taxes_id.id if po_line.taxes_id else False,
+ 'system_last_update': current_time
+ })
+
+ # Buat lognote
+ self._create_pricelist_lognote(pricelist, po_line, changes, current_time)
+
+ def _create_pricelist_lognote(self, pricelist, po_line, changes, timestamp):
+ message = f"""
+ <b>System Fields Updated from PO</b><br/>
+ <b>PO:</b> <a href="#id={po_line.order_id.id}&model=purchase.order&view_type=form">{po_line.order_id.name}</a><br/>
+ <b>Changes:</b>
+ <ul>
+ {"".join(changes)}
+ <li><b>System Update</b>: {timestamp}</li>
+ </ul>
+ <b>Updated By:</b> {self.env.user.name}
+ """
+
+ pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
class PurchaseOrderUnlockWizard(models.TransientModel):
_name = 'purchase.order.unlock.wizard'
diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py
index 033469b8..315795d5 100755
--- a/indoteknik_custom/models/purchase_order_line.py
+++ b/indoteknik_custom/models/purchase_order_line.py
@@ -385,3 +385,6 @@ class PurchaseOrderLine(models.Model):
line.delivery_amt_line = delivery_amt * contribution
else:
line.delivery_amt_line = 0
+
+
+
diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py
index ed013dd5..0bd0092b 100644
--- a/indoteknik_custom/models/purchase_order_sales_match.py
+++ b/indoteknik_custom/models/purchase_order_sales_match.py
@@ -27,6 +27,7 @@ class PurchaseOrderSalesMatch(models.Model):
purchase_price_so = fields.Float(string='Purchase Price Sale Order', related='sale_line_id.purchase_price')
purchase_price_po = fields.Float('Purchase Price PO', compute='_compute_purchase_price_po')
purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Line', compute='_compute_purchase_line_id')
+ hold_outgoing_so = fields.Boolean(string='Hold Outgoing SO', related='sale_id.hold_outgoing')
def _compute_purchase_line_id(self):
for line in self:
diff --git a/indoteknik_custom/models/purchase_pricelist.py b/indoteknik_custom/models/purchase_pricelist.py
index e5b35d7f..b3a473b6 100755
--- a/indoteknik_custom/models/purchase_pricelist.py
+++ b/indoteknik_custom/models/purchase_pricelist.py
@@ -6,6 +6,7 @@ from pytz import timezone
class PurchasePricelist(models.Model):
_name = 'purchase.pricelist'
_rec_name = 'product_id'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Name', compute="_compute_name")
product_id = fields.Many2one('product.product', string="Product", required=True)
@@ -83,6 +84,15 @@ class PurchasePricelist(models.Model):
massage="Ada duplikat product dan vendor, berikut data yang anda duplikat : \n" + str(existing_purchase.product_id.name) + " - " + str(existing_purchase.vendor_id.name) + " - " + str(existing_purchase.product_price)
if existing_purchase:
raise UserError(massage)
+
+ def sync_pricelist_item_promo(self, product):
+ pricelist_product = self.env['product.pricelist.item'].search([('product_id', '=', product.id), ('pricelist_id', '=', 17022)])
+ for pricelist in pricelist_product:
+ if pricelist.fixed_price == 0:
+ flashsale = self.env['product.pricelist.item'].search([('product_id', '=', product.id), ('pricelist_id.is_flash_sale', '=', True)])
+ if flashsale:
+ flashsale.fixed_price = 0
+ return
def action_calculate_pricelist(self):
MAX_PRICELIST = 10
@@ -94,6 +104,8 @@ class PurchasePricelist(models.Model):
records = self.env['purchase.pricelist'].browse(active_ids)
price_group = self.env['price.group'].collect_price_group()
for rec in records:
+ if rec.include_price == 0:
+ rec.sync_pricelist_item_promo(rec.product_id)
product_group = rec.product_id.product_tmpl_id.x_manufacture.pricing_group or None
price_incl = rec.include_price
@@ -120,4 +132,81 @@ class PurchasePricelist(models.Model):
rec.sync_pricelist_tier()
rec.product_id.product_tmpl_id._create_solr_queue('_sync_price_to_solr')
- \ No newline at end of file
+
+ def _collect_old_values(self, vals):
+ return {
+ record.id: {
+ field_name: record[field_name]
+ for field_name in vals.keys()
+ if field_name in record._fields
+ }
+ for record in self
+ }
+
+ def _log_field_changes(self, vals, old_values):
+ exclude_fields = ['solr_flag', 'desc_update_solr', 'last_update_solr', 'is_edited']
+
+ for record in self:
+ changes = []
+ for field_name in vals:
+ if field_name not in record._fields or field_name in exclude_fields:
+ continue
+
+ field = record._fields[field_name]
+ field_label = field.string or field_name
+ old_value = old_values.get(record.id, {}).get(field_name)
+ new_value = record[field_name]
+
+ def stringify(val, field, record):
+ if val in [None, False]:
+ return 'None'
+ # Handle Selection
+ if isinstance(field, fields.Selection):
+ selection = field.selection
+ if callable(selection):
+ selection = selection(record)
+ return dict(selection).get(val, str(val))
+ # Handle Boolean
+ if isinstance(field, fields.Boolean):
+ return 'Yes' if val else 'No'
+ # Handle Many2one
+ if isinstance(field, fields.Many2one):
+ if isinstance(val, int):
+ rec = record.env[field.comodel_name].browse(val)
+ return rec.display_name if rec.exists() else str(val)
+ elif isinstance(val, models.BaseModel):
+ return val.display_name
+ return str(val)
+ # Handle Many2many
+ if isinstance(field, fields.Many2many):
+ records = val if isinstance(val, models.BaseModel) else record[field.name]
+ if not records:
+ return 'None'
+ for attr in ['name', 'x_name', 'display_name']:
+ if hasattr(records[0], attr):
+ return ", ".join(records.mapped(attr))
+ return ", ".join(str(r.id) for r in records)
+ # Handle One2many
+ if isinstance(field, fields.One2many):
+ records = val if isinstance(val, models.BaseModel) else record[field.name]
+ if not records:
+ return 'None'
+ return f"{field.comodel_name}({', '.join(str(r.id) for r in records)})"
+ # Default case (Char, Float, Integer, etc)
+ return str(val)
+
+ old_val_str = stringify(old_value, field, record)
+ new_val_str = stringify(new_value, field, record)
+
+ if old_val_str != new_val_str:
+ changes.append(f"<li><b>{field_label}</b>: '{old_val_str}' → '{new_val_str}'</li>")
+
+ if changes:
+ message = "<b>Updated:</b><ul>%s</ul>" % "".join(changes)
+ record.message_post(body=message)
+
+ def write(self, vals):
+ old_values = self._collect_old_values(vals)
+ result = super(PurchasePricelist, self).write(vals)
+ self._log_field_changes(vals, old_values)
+ return result \ No newline at end of file
diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py
index 902bc34b..ea2f46cb 100644
--- a/indoteknik_custom/models/purchasing_job.py
+++ b/indoteknik_custom/models/purchasing_job.py
@@ -25,6 +25,15 @@ class PurchasingJob(models.Model):
], string='APO?')
purchase_representative_id = fields.Many2one('res.users', string="Purchase Representative", readonly=True)
note = fields.Char(string="Note Detail")
+ date_po = fields.Datetime(string='Date PO', copy=False)
+
+ def unlink(self):
+ # Example: Delete related records from the underlying model
+ underlying_records = self.env['purchasing.job'].search([
+ ('product_id', 'in', self.mapped('product_id').ids)
+ ])
+ underlying_records.unlink()
+ return super(PurchasingJob, self).unlink()
def redirect_to_pjs(self):
states = self.env['purchasing.job.state'].search([
@@ -56,8 +65,9 @@ class PurchasingJob(models.Model):
pmp.action,
max(pjs.status_apo::text) AS status_apo,
max(pjs.note::text) AS note,
+ max(pjs.date_po::text) AS date_po,
CASE
- WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco') THEN 27
+ WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco', 'RYU (Sparepart)') THEN 27
WHEN sub.vendor_id = 9688 THEN 397
WHEN sub.vendor_id = 35475 THEN 397
WHEN sub.vendor_id = 29712 THEN 397
diff --git a/indoteknik_custom/models/purchasing_job_multi_update.py b/indoteknik_custom/models/purchasing_job_multi_update.py
index deba960a..80a43e45 100644
--- a/indoteknik_custom/models/purchasing_job_multi_update.py
+++ b/indoteknik_custom/models/purchasing_job_multi_update.py
@@ -18,7 +18,7 @@ class PurchasingJobMultiUpdate(models.TransientModel):
('purchasing_job_id', '=', product.id)
])
- purchasing_job_state.unlink()
+ # purchasing_job_state.unlink()
purchasing_job_state.create({
'purchasing_job_id': product.id,
diff --git a/indoteknik_custom/models/purchasing_job_state.py b/indoteknik_custom/models/purchasing_job_state.py
index 1838a496..d014edfe 100644
--- a/indoteknik_custom/models/purchasing_job_state.py
+++ b/indoteknik_custom/models/purchasing_job_state.py
@@ -14,4 +14,5 @@ class PurchasingJobState(models.Model):
('not_apo', 'Belum APO'),
('apo', 'APO')
], string='APO?', copy=False)
- note = fields.Char(string="Note Detail")
+ note = fields.Char(string="Note Detail", copy=False)
+ date_po = fields.Datetime(string='Date PO', copy=False)
diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py
index 1d350929..74236850 100644
--- a/indoteknik_custom/models/requisition.py
+++ b/indoteknik_custom/models/requisition.py
@@ -299,18 +299,18 @@ class RequisitionLine(models.Model):
def _get_valid_purchase_price(self, purchase_price):
price = 0
- taxes = ''
+ taxes = 24
human_last_update = purchase_price.human_last_update or datetime.min
system_last_update = purchase_price.system_last_update or datetime.min
- if purchase_price.taxes_product_id.type_tax_use == 'purchase':
- price = purchase_price.product_price
- taxes = purchase_price.taxes_product_id.id
+ #if purchase_price.taxes_product_id.type_tax_use == 'purchase':
+ price = purchase_price.product_price
+ taxes = purchase_price.taxes_product_id.id or 24
if system_last_update > human_last_update:
- if purchase_price.taxes_system_id.type_tax_use == 'purchase':
- price = purchase_price.system_price
- taxes = purchase_price.taxes_system_id.id
+ #if purchase_price.taxes_system_id.type_tax_use == 'purchase':
+ price = purchase_price.system_price
+ taxes = purchase_price.taxes_system_id.id or 24
return price, taxes
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index fd3a0514..191a44c9 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -2,6 +2,7 @@ from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
from datetime import datetime
from odoo.http import request
+import re
class GroupPartner(models.Model):
_name = 'group.partner'
@@ -39,6 +40,14 @@ class ResPartner(models.Model):
estimasi_tempo = fields.Char(string='Estimasi Pembelian Pertahun')
tempo_duration = fields.Many2one('account.payment.term', string='Durasi Tempo')
tempo_limit = fields.Char(string='Limit Tempo')
+ minimum_amount = fields.Float(
+ string="Minimum Order",
+ help="Jika total belanja kurang dari ini, maka payment term akan otomatis menjadi CBD."
+ )
+ minimum_amount_tax = fields.Float(
+ string="Minimum Amount Tax",
+ help="Jika total belanja kurang dari ini, maka tax akan otomatis menjadi 0%."
+ )
category_produk_ids = fields.Many2many('product.public.category', string='Kategori Produk yang Digunakan', domain=lambda self: self._get_default_category_domain())
@api.model
@@ -200,6 +209,32 @@ class ResPartner(models.Model):
if existing_partner:
raise ValidationError(f"Nama '{record.name}' sudah digunakan oleh partner lain!")
+ @api.constrains('npwp')
+ def _check_npwp(self):
+ for record in self:
+ npwp = record.npwp.strip() if record.npwp else ''
+ # Abaikan validasi jika NPWP kosong atau diisi "0"
+ if not npwp or npwp == '0' or npwp == '00.000.000.0-000.000':
+ continue
+
+ # Validasi untuk NPWP 15 digit (format: 99.999.999.9-999.999)
+ if len(npwp) == 20:
+ # Regex untuk 15 digit dengan format titik dan tanda hubung
+ pattern_15_digit = r'^\d{2}\.\d{3}\.\d{3}\.\d{1}-\d{3}\.\d{3}$'
+ if not re.match(pattern_15_digit, npwp):
+ raise ValidationError("Format NPWP 15 digit yang dimasukkan salah. Pastikan format yang benar adalah: 99.999.999.9-999.999")
+
+ # Validasi untuk NPWP 16 digit (hanya angka tanpa titik atau tanda hubung)
+ elif len(npwp) == 16:
+ pattern_16_digit = r'^\d{16}$'
+ if not re.match(pattern_16_digit, npwp):
+ raise ValidationError("Format NPWP 16 digit yang dimasukkan salah. Format yang benar adalah 16 digit angka tanpa titik atau tanda hubung.")
+
+ # Validasi panjang NPWP jika lebih atau kurang dari 15 atau 16 digit
+ else:
+ raise ValidationError("Digit NPWP yang dimasukkan tidak sesuai. Pastikan NPWP memiliki 15 digit dengan format tertentu (99.999.999.9-999.999) atau 16 digit tanpa tanda hubung.")
+
+
def write(self, vals):
# Fungsi rekursif untuk meng-update semua child, termasuk child dari child
def update_children_recursively(partner, vals_for_child):
@@ -465,6 +500,8 @@ class ResPartner(models.Model):
def _onchange_customer_type(self):
if self.customer_type == 'nonpkp':
self.npwp = '00.000.000.0-000.000'
+ elif self.customer_type == 'pkp':
+ self.npwp = '00.000.000.0-000.000'
def get_check_payment_term(self):
self.ensure_one()
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 14a8e688..f89dfb10 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -1,12 +1,16 @@
+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
+import pytz
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'
@@ -42,7 +46,7 @@ class CancelReasonOrder(models.TransientModel):
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])
+ 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})
@@ -51,7 +55,8 @@ class CancelReasonOrder(models.TransientModel):
order.confirm_cancel_order()
return {'type': 'ir.actions.act_window_close'}
-
+
+
class ShippingOption(models.Model):
_name = "shipping.option"
_description = "Shipping Option"
@@ -62,22 +67,97 @@ class ShippingOption(models.Model):
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)
+
+ notes = fields.Text(string="Notes", 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")
+ 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'),
('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')
+ 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'),
@@ -87,7 +167,8 @@ class SaleOrder(models.Model):
('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)])
+ 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')
@@ -102,14 +183,20 @@ 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'),
('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.")
+ 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'),
@@ -123,23 +210,28 @@ class SaleOrder(models.Model):
('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")
+ ], 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')
+ 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)
+ 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)
+ ], 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)
@@ -173,8 +265,10 @@ class SaleOrder(models.Model):
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')
+ 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(
@@ -188,11 +282,11 @@ class SaleOrder(models.Model):
'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'),
@@ -200,17 +294,16 @@ 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'
- )
+ # 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,
- store=True
+ 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'),
@@ -231,12 +324,102 @@ 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)]")
+ 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)
+ ready_to_ship_status_detail = fields.Char(
+ string='Status Shipping Detail',
+ compute='_compute_ready_to_ship_status_detail'
+ )
+ date_hold = fields.Datetime(string='Date Hold', tracking=True, readonly=True, help='Waktu ketika SO di Hold'
+ )
+ 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):
+ 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
+ self.date_unhold = fields.Datetime.now()
+ else:
+ pick = self.env['stock.picking'].search([
+ ('sale_id', '=', self.id),
+ ('state', 'not in', ['cancel', 'done']),
+ ('name', 'ilike', 'BU/PICK/%')
+ ])
+ for picking in pick:
+ picking.do_unreserve()
+ self.hold_outgoing = True
+ self.date_hold = fields.Datetime.now()
+
+
+ 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:
- rec.delivery_amt = rec.shipping_option_id.price
+ if rec.shipping_option_id:
+ rec.delivery_amt = rec.shipping_option_id.price
def _compute_shipping_method_picking(self):
for order in self:
@@ -267,14 +450,14 @@ class SaleOrder(models.Model):
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
+ line.product_id.weight = line.weight
else:
missing_weight_products.append(line.product_id.name)
@@ -284,10 +467,10 @@ 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({
@@ -298,7 +481,17 @@ class SaleOrder(models.Model):
"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()
@@ -310,7 +503,7 @@ class SaleOrder(models.Model):
for line in self.order_line:
if line.weight > 0:
total_weight += line.weight * line.product_uom_qty
- line.product_id.weight = line.weight
+ line.product_id.weight = line.weight
else:
missing_weight_products.append(line.product_id.name)
@@ -335,9 +528,11 @@ class SaleOrder(models.Model):
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,
@@ -346,10 +541,19 @@ class SaleOrder(models.Model):
"etd": etd,
"sale_order_id": self.id,
})
-
+
self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id
- 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])}")
+ _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")
+
else:
raise UserError("Gagal mendapatkan estimasi ongkir.")
@@ -373,7 +577,7 @@ class SaleOrder(models.Model):
if response.status_code == 200:
return response.json()
return None
-
+
def _normalize_city_name(self, city_name):
city_name = city_name.lower()
@@ -393,7 +597,7 @@ class SaleOrder(models.Model):
}
normalized_city_name = self._normalize_city_name(city_name)
-
+
response = requests.get(url, headers=headers)
if response.status_code == 200:
city_data = response.json()
@@ -407,7 +611,7 @@ class SaleOrder(models.Model):
headers = {
'key': '9b1310f644056d84d60b0af6bb21611a',
}
-
+
response = requests.get(url, headers=headers)
if response.status_code == 200:
subdistrict_data = response.json()
@@ -419,15 +623,15 @@ class SaleOrder(models.Model):
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))
-
+ 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):
@@ -451,11 +655,14 @@ 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
@@ -467,74 +674,65 @@ class SaleOrder(models.Model):
'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.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):
- 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 = rec.date_order + timedelta(days=rec.estimated_arrival_days)
- rec.eta_date_start = rec.date_order + timedelta(days=rec.estimated_arrival_days_start)
+ 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):
+
+ 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 :
+ 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):
- # slatime = 15
- # for line in products:
- # product_sla = self.env['product.sla'].search([('product_variant_id', '=', line.product_id.id)], limit=1)
- # slatime = int(product_sla.sla) if product_sla and product_sla.sla and product_sla.sla != 'Indent' and "hari" in product_sla.sla.lower() else 15
-
- # return {
- # 'slatime' : slatime
- # }
-
+
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)
+ 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
+ max_slatime = 1
for vendor in vendors:
vendor_sla = self.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1)
@@ -543,59 +741,73 @@ class SaleOrder(models.Model):
if vendor_sla.unit == 'hari':
vendor_duration = vendor_sla.duration * 24 * 60
include_instant = False
- else :
+ 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}
- @api.depends("order_line.product_id")
- def _compute_etrts_date(self): #Function to calculate Estimated Ready To Ship Date
+ 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'])
- if rec.date_order:
- sum_days = max_slatime + self.get_days_until_next_business_day(rec.date_order) - 1
- if not rec.estimated_arrival_days:
- rec.estimated_arrival_days = sum_days
-
- eta_date = rec.date_order + timedelta(days=sum_days)
- rec.estimated_ready_ship_date = eta_date
- rec.commitment_date = eta_date
- # Jika expected_ready_to_ship kosong, set nilai default
- if not rec.expected_ready_to_ship:
- rec.expected_ready_to_ship = eta_date
-
-
-
- @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship
- def _onchange_expected_ready_ship_date(self):
+ 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.estimated_ready_ship_date:
+ 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()
- estimated_date = rec.estimated_ready_ship_date.date()
-
- if expected_date < estimated_date:
- rec.expected_ready_to_ship = rec.estimated_ready_ship_date
- rec.commitment_date = rec.estimated_ready_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(estimated_date.strftime('%d-%m-%Y'), estimated_date.strftime('%d-%m-%Y'))
+ .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."))
+ 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):
@@ -607,10 +819,12 @@ class SaleOrder(models.Model):
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))
+ 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
+ parent_id = parent_id if parent_id else self.partner_id
invoice_vals = {
'ref': self.client_order_ref or '',
@@ -627,7 +841,8 @@ class SaleOrder(models.Model):
'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,
+ '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,
@@ -643,10 +858,37 @@ class SaleOrder(models.Model):
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')
@@ -687,14 +929,14 @@ class SaleOrder(models.Model):
'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({
@@ -707,7 +949,7 @@ class SaleOrder(models.Model):
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
@@ -727,8 +969,8 @@ class SaleOrder(models.Model):
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
+ 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)
@@ -743,7 +985,8 @@ class SaleOrder(models.Model):
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':
+ if status_response.get('transaction_status') == 'expire' or status_response.get(
+ 'transaction_status') == 'cancel':
so_number = so_number + '-cpl'
json_data = {
@@ -791,17 +1034,12 @@ 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)
- # self._compute_etrts_date()
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']
@@ -809,7 +1047,7 @@ class SaleOrder(models.Model):
('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)
@@ -823,16 +1061,16 @@ class SaleOrder(models.Model):
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
+ 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
+ elif sum_qty_pick == sum_qty_reserved: # sudah ada data reserved
picking.date_reserved = picking.date_reserved
else:
picking.date_reserved = ''
@@ -856,11 +1094,18 @@ 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):
parent_id = self.partner_id.parent_id
- parent_id = parent_id if parent_id else self.partner_id
+ parent_id = parent_id if parent_id else self.partner_id
self.npwp = parent_id.npwp
self.sppkp = parent_id.sppkp
@@ -868,13 +1113,13 @@ class SaleOrder(models.Model):
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
+ parent_id = parent_id if parent_id else self.partner_id
self.partner_invoice_id = parent_id
return result
@@ -907,16 +1152,16 @@ class SaleOrder(models.Model):
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):
@@ -934,21 +1179,21 @@ class SaleOrder(models.Model):
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
+
+ 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
@@ -957,6 +1202,7 @@ class SaleOrder(models.Model):
self._validate_order()
for order in self:
+ order._validate_uniform_taxes()
order.order_line.validate_line()
term_days = 0
@@ -975,11 +1221,36 @@ class SaleOrder(models.Model):
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")
+ 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), ('sale_order', '=', order.id), ('state', '!=', 'cancel')],
+ order='name desc')
+ if search_bom:
+ 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.")
+ 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:
@@ -988,6 +1259,7 @@ class SaleOrder(models.Model):
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()
@@ -997,11 +1269,13 @@ class SaleOrder(models.Model):
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']:
+ 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")
+ 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")
@@ -1009,8 +1283,10 @@ class SaleOrder(models.Model):
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')
+ if not self.env.context.get('due_approve', []):
+ 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:
@@ -1028,24 +1304,27 @@ class SaleOrder(models.Model):
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")
+ 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.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:
@@ -1063,7 +1342,9 @@ class SaleOrder(models.Model):
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'
+ 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
@@ -1082,7 +1363,8 @@ class SaleOrder(models.Model):
})
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
+ 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():
@@ -1106,9 +1388,9 @@ class SaleOrder(models.Model):
template = self.env.ref('indoteknik_custom.mail_template_sale_order_notification_to_salesperson')
email_body = template.body_html.replace('${table_content}', table_content)
email_body = email_body.replace('${salesperson_name}', data['name'])
- email_body = email_body.replace('${sum_total_amount}', str(data['sum_total_amount']))
- email_body = email_body.replace('${site}', str(data['site']))
- email_body = email_body.replace('${business_partner}', str(data['business_partner']))
+ email_body = email_body.replace('${sum_total_amount}', str(data['sum_total_amount']))
+ email_body = email_body.replace('${site}', str(data['site']))
+ email_body = email_body.replace('${business_partner}', str(data['business_partner']))
# Kirim email
self.env['mail.mail'].create({
'subject': 'Notification: Sale Orders',
@@ -1144,8 +1426,9 @@ class SaleOrder(models.Model):
if (outstanding_amount + rec.amount_total) >= block_stage:
if block_stage != 0:
remaining_credit_limit = block_stage - outstanding_amount
- 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))
+ 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:
@@ -1167,7 +1450,8 @@ class SaleOrder(models.Model):
# 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.")
+ 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):
@@ -1177,11 +1461,11 @@ class SaleOrder(models.Model):
if self.vendor_approval_id and all(v.state != 'draft' for v in self.vendor_approval_id):
return False
-
+
different_vendor = self.order_line.filtered(
lambda l: l.vendor_id and l.vendor_md_id and l.vendor_id.id != l.vendor_md_id.id
)
-
+
if different_vendor:
vendor_approvals = []
for line in different_vendor:
@@ -1207,58 +1491,64 @@ class SaleOrder(models.Model):
if line.purchase_price_md else False
),
})
-
+
vendor_approvals.append(vendor_approval.id)
-
+
self.vendor_approval_id = [(4, vid) for vid in vendor_approvals]
return True
else:
return False
-
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')
-
+
order.check_data_real_delivery_address()
order.sale_order_check_approve()
order._validate_order()
order.order_line.validate_line()
-
+
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']:
+
+ 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")
+ 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')
-
+
+ if not self.env.context.get('due_approve', []):
+ 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():
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
-
+
order.approval_status = 'approved'
order._set_sppkp_npwp_contact()
order.calculate_line_no()
order.send_notif_to_salesperson()
- order._compute_etrts_date()
+ # order._compute_etrts_date()
# order.order_line.get_reserved_from()
res = super(SaleOrder, self).action_confirm()
@@ -1267,7 +1557,7 @@ class SaleOrder(models.Model):
for line in order.order_line:
if line.display_type == 'line_note':
note.append(line.name)
-
+
if order.picking_ids:
# Sort picking_ids by creation date to get the most recent one
latest_picking = order.picking_ids.sorted(key=lambda p: p.create_date, reverse=True)[0]
@@ -1276,10 +1566,12 @@ 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")
@@ -1290,7 +1582,7 @@ class SaleOrder(models.Model):
for line in self.order_line:
if line.qty_delivered > 0:
raise UserError("DO yang done harus di-Return oleh Logistik")
-
+
if not self.web_approval:
self.web_approval = 'company'
# elif self.have_outstanding_po:
@@ -1320,11 +1612,11 @@ class SaleOrder(models.Model):
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
+ parent_id = parent_id if parent_id else self.partner_id.id
if self.due_id and self.due_id.is_approve == False:
raise UserError('Document Over Due Yang Anda Buat Belum Di Approve')
-
+
query = [
('partner_id', '=', parent_id),
('state', '=', 'posted'),
@@ -1334,39 +1626,39 @@ class SaleOrder(models.Model):
invoices = self.env['account.move'].search(query, order='invoice_date')
if invoices:
- if not self.env.user.is_leader and not self.env.user.is_sales_manager:
- due_extension = self.env['due.extension'].create([{
- 'partner_id': parent_id,
- 'day_extension': '3',
- 'order_id': self.id,
- }])
- due_extension.generate_due_line()
- self.due_id = due_extension.id
- if len(self.due_id.due_line) > 0:
- return True
- else:
- due_extension.unlink()
- return False
-
+ due_extension = self.env['due.extension'].create([{
+ 'partner_id': parent_id,
+ 'day_extension': '3',
+ 'order_id': self.id,
+ }])
+ due_extension.generate_due_line()
+ self.due_id = due_extension.id
+ if len(self.due_id.due_line) > 0:
+ return True
+ else:
+ due_extension.unlink()
+ return False
+
def _requires_approval_margin_leader(self):
- return self.total_percent_margin < 15 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 >= 15 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.id == 375 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'
message = f'SO butuh approval {approval_role}'
return self._create_notification_action(title, message)
-
+
def _create_notification_action(self, title, message):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
- 'params': { 'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'} },
+ 'params': {'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'}},
}
-
- def _set_sppkp_npwp_contact(self):
+
+ def _set_sppkp_npwp_contact(self):
partner = self.partner_id.parent_id or self.partner_id
if not partner.sppkp:
@@ -1385,12 +1677,20 @@ class SaleOrder(models.Model):
# partner.npwp = self.npwp
# partner.sppkp = self.sppkp
# partner.email = self.email
-
+
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:
@@ -1400,7 +1700,10 @@ 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')
@@ -1423,20 +1726,20 @@ class SaleOrder(models.Model):
voucher = self.voucher_id
if voucher.limit > 0 and voucher.count_order >= voucher.limit:
raise UserError('Voucher tidak dapat digunakan karena sudah habis digunakan')
-
+
partner_voucher_orders = []
for order in voucher.order_ids:
if order.partner_id.id == self.partner_id.id:
partner_voucher_orders.append(order)
-
+
if voucher.limit_user > 0 and len(partner_voucher_orders) >= voucher.limit_user:
raise UserError('Voucher tidak dapat digunakan karena Customer ini sudah menghabiskan kuota voucher')
-
+
if self.pricelist_id.id in [x.id for x in voucher.excl_pricelist_ids]:
raise UserError('Voucher tidak dapat digunakan karena pricelist ini tidak berlaku pada voucher')
-
+
self.apply_voucher()
-
+
def action_apply_voucher_shipping(self):
for line in self.order_line:
if line.order_promotion_id:
@@ -1445,18 +1748,18 @@ class SaleOrder(models.Model):
voucher = self.voucher_shipping_id
if voucher.limit > 0 and voucher.count_order >= voucher.limit:
raise UserError('Voucher tidak dapat digunakan karena sudah habis digunakan')
-
+
partner_voucher_orders = []
for order in voucher.order_ids:
if order.partner_id.id == self.partner_id.id:
partner_voucher_orders.append(order)
-
+
if voucher.limit_user > 0 and len(partner_voucher_orders) >= voucher.limit_user:
raise UserError('Voucher tidak dapat digunakan karena Customer ini sudah menghabiskan kuota voucher')
-
+
if self.pricelist_id.id in [x.id for x in voucher.excl_pricelist_ids]:
raise UserError('Voucher tidak dapat digunakan karena pricelist ini tidak berlaku pada voucher')
-
+
self.apply_voucher_shipping()
def apply_voucher(self):
@@ -1473,7 +1776,7 @@ class SaleOrder(models.Model):
for line in self.order_line:
line.initial_discount = line.discount
-
+
voucher_type = voucher['type']
used_total = voucher['total'][voucher_type]
used_discount = voucher['discount'][voucher_type]
@@ -1489,11 +1792,11 @@ class SaleOrder(models.Model):
line_contribution = line.price_subtotal / used_total
line_voucher = used_discount * line_contribution
line_voucher_item = line_voucher / line.product_uom_qty
-
+
line_price_unit = line.price_unit / 1.11 if any(tax.id == 23 for tax in line.tax_id) else line.price_unit
line_discount_item = line_price_unit * line.discount / 100 + line_voucher_item
line_voucher_item = line_discount_item / line_price_unit * 100
-
+
line.amount_voucher_disc = line_voucher
line.discount = line_voucher_item
@@ -1504,27 +1807,27 @@ class SaleOrder(models.Model):
for order in self:
delivery_amt = order.delivery_amt
voucher = order.voucher_shipping_id
-
+
if voucher:
max_discount_amount = voucher.discount_amount
voucher_type = voucher.discount_type
-
+
if voucher_type == 'fixed_price':
discount = max_discount_amount
elif voucher_type == 'percentage':
discount = delivery_amt * (max_discount_amount / 100)
-
+
delivery_amt -= discount
-
+
delivery_amt = max(delivery_amt, 0)
-
+
order.delivery_amt = delivery_amt
-
+
order.amount_voucher_shipping_disc = discount
order.applied_voucher_shipping_id = order.voucher_id.id
def cancel_voucher(self):
- self.applied_voucher_id = False
+ self.applied_voucher_id = False
self.amount_voucher_disc = 0
for line in self.order_line:
line.amount_voucher_disc = 0
@@ -1533,17 +1836,18 @@ class SaleOrder(models.Model):
def cancel_voucher_shipping(self):
self.delivery_amt + self.amount_voucher_shipping_disc
- self.applied_voucher_shipping_id = False
+ self.applied_voucher_shipping_id = False
self.amount_voucher_shipping_disc = 0
def action_web_approve(self):
if self.env.uid != self.partner_id.user_id.id:
- raise UserError('You are not authorized to approve this order. Only %s can approve this order.' % self.partner_id.user_id.name)
-
+ raise UserError(
+ 'You are not authorized to approve this order. Only %s can approve this order.' % self.partner_id.user_id.name)
+
self.web_approval = 'company'
template = self.env.ref('indoteknik_custom.mail_template_sale_order_web_approve_notification')
template.send_mail(self.id, force_send=True)
-
+
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
@@ -1603,13 +1907,14 @@ class SaleOrder(models.Model):
if last_so and rec_purchase_price != last_so.purchase_price:
rec_taxes = self.env['account.tax'].search([('id', '=', rec_taxes_id)], limit=1)
if rec_taxes.price_include:
- selling_price = (rec_purchase_price / 1.11) / (1 - (last_so.item_percent_margin_without_deduction / 100))
+ selling_price = (rec_purchase_price / 1.11) / (
+ 1 - (last_so.item_percent_margin_without_deduction / 100))
else:
selling_price = rec_purchase_price / (1 - (last_so.item_percent_margin_without_deduction / 100))
tax_id = last_so.tax_id
for tax in tax_id:
if tax.price_include:
- selling_price = selling_price + (selling_price*11/100)
+ selling_price = selling_price + (selling_price * 11 / 100)
else:
selling_price = selling_price
discount = 0
@@ -1625,13 +1930,14 @@ class SaleOrder(models.Model):
elif last_so and rec_vendor_id == order_line.vendor_id.id and rec_purchase_price != last_so.purchase_price:
rec_taxes = self.env['account.tax'].search([('id', '=', rec_taxes_id)], limit=1)
if rec_taxes.price_include:
- selling_price = (rec_purchase_price / 1.11) / (1 - (last_so.item_percent_margin_without_deduction / 100))
+ selling_price = (rec_purchase_price / 1.11) / (
+ 1 - (last_so.item_percent_margin_without_deduction / 100))
else:
selling_price = rec_purchase_price / (1 - (last_so.item_percent_margin_without_deduction / 100))
tax_id = last_so.tax_id
for tax in tax_id:
if tax.price_include:
- selling_price = selling_price + (selling_price*11/100)
+ selling_price = selling_price + (selling_price * 11 / 100)
else:
selling_price = selling_price
discount = 0
@@ -1655,18 +1961,21 @@ class SaleOrder(models.Model):
# 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):
- # Call the super method to handle the write operation
- 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()
+ # def write(self, vals):
+ # Call the super method to handle the write operation
+ # 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:
@@ -1695,7 +2004,41 @@ 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)
- if 'order_line' in vals:
- self._compute_etrts_date()
- return res \ No newline at end of file
+ # 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):
+ for order in self:
+ 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
+ product = match.product_id
+ po_line = self.env['purchase.order.line'].search([
+ ('order_id', '=', po.id),
+ ('product_id', '=', product.id)
+ ], limit=1)
+ stock_move = self.env['stock.move'].search([
+ ('purchase_line_id', '=', po_line.id)
+ ], limit=1)
+ 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
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index aed95aab..9247d1c1 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header")
+ item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin', help="Total Margin in Sales Order Header")
item_percent_margin = fields.Float('%Margin', compute='compute_item_margin', help="Total % Margin in Sales Order Header")
initial_discount = fields.Float('Initial Discount')
vendor_id = fields.Many2one(
@@ -40,6 +41,19 @@ class SaleOrderLine(models.Model):
qty_free_bu = fields.Float(string='Free BU', compute='_get_qty_free_bandengan')
desc_updatable = fields.Boolean(string='desc boolean', default=True, compute='_get_desc_updatable')
+ def _get_outgoing_incoming_moves(self):
+ outgoing_moves = self.env['stock.move']
+ incoming_moves = self.env['stock.move']
+
+ for move in self.move_ids.filtered(lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id):
+ if move.location_dest_id.usage == "customer":
+ if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund):
+ outgoing_moves |= move
+ elif move.location_id.usage == "customer" and move.to_refund:
+ incoming_moves |= move
+
+ return outgoing_moves, incoming_moves
+
def _get_desc_updatable(self):
for line in self:
if line.product_id.id != 417724 and line.product_id.id:
@@ -146,6 +160,24 @@ class SaleOrderLine(models.Model):
if not line.margin_md:
line.margin_md = line.item_percent_margin
+ def compute_item_before_margin(self):
+ for line in self:
+ if not line.product_id or line.product_id.type == 'service' \
+ or line.price_unit <= 0 or line.product_uom_qty <= 0 \
+ or not line.vendor_id:
+ line.item_before_margin = 0
+ continue
+ # calculate margin without tax
+ sales_price = line.price_reduce_taxexcl * line.product_uom_qty
+
+ purchase_price = line.purchase_price
+ if line.purchase_tax_id.price_include:
+ purchase_price = line.purchase_price / 1.11
+
+ purchase_price = purchase_price * line.product_uom_qty
+ margin_per_item = sales_price - purchase_price
+ line.item_before_margin = margin_per_item
+
@api.onchange('vendor_id')
def onchange_vendor_id(self):
# TODO : need to change this logic @stephan
@@ -223,32 +255,33 @@ class SaleOrderLine(models.Model):
def _get_valid_purchase_price(self, purchase_price):
current_time = datetime.now()
delta_time = current_time - timedelta(days=365)
+ default_timestamp = datetime(1970, 1, 1, 0, 0, 0)
# delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
price = 0
- taxes = ''
+ taxes = 24
vendor_id = ''
human_last_update = purchase_price.human_last_update or datetime.min
system_last_update = purchase_price.system_last_update or datetime.min
-
- if purchase_price.taxes_product_id.type_tax_use == 'purchase':
- price = purchase_price.product_price
- taxes = purchase_price.taxes_product_id.id
+
+ # if purchase_price.taxes_product_id.type_tax_use == 'purchase':
+ price = purchase_price.product_price
+ taxes = purchase_price.taxes_product_id.id or 24
+ vendor_id = purchase_price.vendor_id.id
+ if delta_time > human_last_update:
+ price = 0
+ taxes = 24
+ vendor_id = ''
+
+ if system_last_update > human_last_update:
+ #if purchase_price.taxes_system_id.type_tax_use == 'purchase':
+ price = purchase_price.system_price
+ taxes = purchase_price.taxes_system_id.id or 24
vendor_id = purchase_price.vendor_id.id
- if delta_time > human_last_update:
+ if delta_time > system_last_update:
price = 0
- taxes = ''
+ taxes = 24
vendor_id = ''
-
- if system_last_update > human_last_update:
- if purchase_price.taxes_system_id.type_tax_use == 'purchase':
- price = purchase_price.system_price
- taxes = purchase_price.taxes_system_id.id
- vendor_id = purchase_price.vendor_id.id
- if delta_time > system_last_update:
- price = 0
- taxes = ''
- vendor_id = ''
return price, taxes, vendor_id
diff --git a/indoteknik_custom/models/sales_order_koli.py b/indoteknik_custom/models/sales_order_koli.py
new file mode 100644
index 00000000..c782a40e
--- /dev/null
+++ b/indoteknik_custom/models/sales_order_koli.py
@@ -0,0 +1,26 @@
+from odoo import fields, models, api, _
+from odoo.exceptions import AccessError, UserError, ValidationError
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class SalesOrderKoli(models.Model):
+ _name = 'sales.order.koli'
+ _description = 'Sales Order Koli'
+ _order = 'sale_order_id, id'
+ _rec_name = 'koli_id'
+
+ sale_order_id = fields.Many2one(
+ 'sale.order',
+ string='Sale Order Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ koli_id = fields.Many2one('check.koli', string='Koli')
+ picking_id = fields.Many2one('stock.picking', string='Picking')
+ state = fields.Selection([('not_delivered', 'Not Delivered'), ('delivered', 'Delivered')], string='Status', default='not_delivered')
+
diff --git a/indoteknik_custom/models/sales_order_reject.py b/indoteknik_custom/models/sales_order_reject.py
index 9983c64e..b180fad6 100644
--- a/indoteknik_custom/models/sales_order_reject.py
+++ b/indoteknik_custom/models/sales_order_reject.py
@@ -13,3 +13,30 @@ class SalesOrderReject(models.Model):
product_id = fields.Many2one('product.product', string='Product')
qty_reject = fields.Float(string='Qty')
reason_reject = fields.Char(string='Reason Reject')
+
+ def write(self, vals):
+ # Check if reason_reject is being updated
+ if 'reason_reject' in vals:
+ for record in self:
+ old_reason = record.reason_reject
+ new_reason = vals['reason_reject']
+
+ # Only post a message if the reason actually changed
+ if old_reason != new_reason:
+ now = fields.Datetime.now()
+
+ # Create the log note for the updated reason
+ log_note = (f"<li>Product '{record.product_id.name}' rejection reason updated:</li>"
+ f"<li>From: {old_reason}</li>"
+ f"<li>To: {new_reason}</li>"
+ f"<li>Updated on: {now.strftime('%d-%m-%Y')} at {now.strftime('%H:%M:%S')}</li>")
+
+ # Post ke lognote
+ if record.sale_order_id:
+ record.sale_order_id.message_post(
+ body=log_note,
+ author_id=self.env.user.partner_id.id,
+ )
+
+ # Call the original write method
+ return super(SalesOrderReject, self).write(vals) \ No newline at end of file
diff --git a/indoteknik_custom/models/shipment_group.py b/indoteknik_custom/models/shipment_group.py
index df3f1bb4..87d222a6 100644
--- a/indoteknik_custom/models/shipment_group.py
+++ b/indoteknik_custom/models/shipment_group.py
@@ -14,6 +14,21 @@ class ShipmentGroup(models.Model):
number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True)
shipment_line = fields.One2many('shipment.group.line', 'shipment_id', string='Shipment Group Lines', auto_join=True)
partner_id = fields.Many2one('res.partner', string='Customer')
+ carrier_id = fields.Many2one('delivery.carrier', string='Ekspedisi')
+ total_colly_line = fields.Float(string='Total Colly', compute='_compute_total_colly_line')
+
+ def sync_api_shipping(self):
+ for rec in self.shipment_line:
+ if rec.shipment_id.carrier_id == 173:
+ rec.picking_id.action_get_kgx_pod()
+
+ if rec.shipment_id.carrier_id == 151:
+ rec.picking_id.track_envio_shipment()
+
+ @api.depends('shipment_line.total_colly')
+ def _compute_total_colly_line(self):
+ for rec in self:
+ rec.total_colly_line = sum(rec.shipment_line.mapped('total_colly'))
@api.model
def create(self, vals):
@@ -35,6 +50,26 @@ class ShipmentGroupLine(models.Model):
('indoteknik', 'Indoteknik'),
('customer', 'Customer')
], string='Shipping Paid by', copy=False)
+ total_colly = fields.Float(string='Total Colly')
+ carrier_id = fields.Many2one('delivery.carrier', string='Ekspedisi')
+
+ @api.constrains('picking_id')
+ def _check_picking_id(self):
+ for rec in self:
+ if not rec.picking_id:
+ continue
+
+ duplicates = self.env['shipment.group.line'].search([
+ ('picking_id', '=', rec.picking_id.id),
+ ('id', '!=', rec.id)
+ ])
+
+ if duplicates:
+ shipment_numbers = duplicates.mapped('shipment_id.number')
+ raise UserError(
+ f"Picking {rec.picking_id.name} sudah discan dalam shipment group berikut: {', '.join(shipment_numbers)}! "
+ "Satu picking hanya boleh dimasukkan dalam satu shipment group."
+ )
@api.depends('picking_id.state')
def _compute_state(self):
@@ -60,14 +95,19 @@ class ShipmentGroupLine(models.Model):
if self.picking_id:
picking = self.env['stock.picking'].browse(self.picking_id.id)
- if self.shipment_id.partner_id and self.shipment_id.partner_id != picking.partner_id:
- raise UserError('Partner must be same as shipment group')
-
+ if self.shipment_id.carrier_id and self.shipment_id.carrier_id != picking.carrier_id:
+ raise UserError('carrier must be same as shipment group')
+
+ if picking.total_mapping_koli == 0:
+ raise UserError(f'Picking {picking.name} tidak memiliki mapping koli')
+
self.partner_id = picking.partner_id
self.shipping_paid_by = picking.sale_id.shipping_paid_by
+ self.carrier_id = picking.carrier_id.id
+ self.total_colly = picking.total_mapping_koli
- if not self.shipment_id.partner_id:
- self.shipment_id.partner_id = picking.partner_id
+ if not self.shipment_id.carrier_id:
+ self.shipment_id.carrier_id = picking.carrier_id
self.sale_id = picking.sale_id
diff --git a/indoteknik_custom/models/solr/product_product.py b/indoteknik_custom/models/solr/product_product.py
index 667511b2..d8bc3973 100644
--- a/indoteknik_custom/models/solr/product_product.py
+++ b/indoteknik_custom/models/solr/product_product.py
@@ -57,6 +57,8 @@ class ProductProduct(models.Model):
is_in_bu = True if variant.qty_free_bandengan > 0 else False
document = solr_model.get_doc('variants', variant.id)
+
+ carousel = [ir_attachment.api_image('image.carousel', 'image', carousel.product_id.id) for carousel in variant.product_tmpl_id.image_carousel_lines],
document.update({
'id': variant.id,
@@ -67,10 +69,11 @@ class ProductProduct(models.Model):
'product_id_i': variant.id,
'template_id_i': variant.product_tmpl_id.id,
'image_s': ir_attachment.api_image('product.template', 'image_512', variant.product_tmpl_id.id),
+ 'image_carousel_s': [ir_attachment.api_image('image.carousel', 'image', carousel.id) for carousel in variant.product_tmpl_id.image_carousel_lines],
'image_mobile_s': ir_attachment.api_image('product.template', 'image_256', variant.product_tmpl_id.id),
'stock_total_f': variant.qty_stock_vendor,
'weight_f': variant.weight,
- 'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0,
+ 'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0,
'manufacture_name_s': variant.product_tmpl_id.x_manufacture.x_name or '',
'manufacture_name': variant.product_tmpl_id.x_manufacture.x_name or '',
'image_promotion_1_s': ir_attachment.api_image('x_manufactures', 'image_promotion_1', variant.product_tmpl_id.x_manufacture.id),
diff --git a/indoteknik_custom/models/solr/product_template.py b/indoteknik_custom/models/solr/product_template.py
index 8afff6e3..c4aefe19 100644
--- a/indoteknik_custom/models/solr/product_template.py
+++ b/indoteknik_custom/models/solr/product_template.py
@@ -26,7 +26,7 @@ class ProductTemplate(models.Model):
'function_name': function_name
})
- @api.constrains('name', 'default_code', 'weight', 'x_manufacture', 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished')
+ @api.constrains('name', 'default_code', 'weight', 'x_manufacture', 'public_categ_ids', 'search_rank', 'search_rank_weekly', 'image_1920', 'unpublished','image_carousel_lines')
def _create_solr_queue_sync_product_template(self):
self._create_solr_queue('_sync_product_template_to_solr')
@@ -85,6 +85,12 @@ class ProductTemplate(models.Model):
cleaned_desc = BeautifulSoup(template.website_description or '', "html.parser").get_text()
website_description = template.website_description if cleaned_desc else ''
+ # carousel_images = ', '.join([self.env['ir.attachment'].api_image('image.carousel', 'image', carousel.id) for carousel in template.image_carousel_lines])
+ carousel_images = []
+ for carousel in template.image_carousel_lines:
+ image_url = self.env['ir.attachment'].api_image('image.carousel', 'image', carousel.id)
+ if image_url: # Hanya tambahkan jika URL valid
+ carousel_images.append(image_url)
document = solr_model.get_doc('product', template.id)
document.update({
"id": template.id,
@@ -94,6 +100,7 @@ class ProductTemplate(models.Model):
"product_rating_f": template.virtual_rating,
"product_id_i": template.id,
"image_s": self.env['ir.attachment'].api_image('product.template', 'image_512', template.id),
+ "image_carousel_ss": carousel_images if carousel_images else [],
'image_mobile_s': self.env['ir.attachment'].api_image('product.template', 'image_256', template.id),
"variant_total_i": template.product_variant_count,
"stock_total_f": template.qty_stock_vendor,
diff --git a/indoteknik_custom/models/solr/x_banner_banner.py b/indoteknik_custom/models/solr/x_banner_banner.py
index 8452644c..aa6e0c2a 100644
--- a/indoteknik_custom/models/solr/x_banner_banner.py
+++ b/indoteknik_custom/models/solr/x_banner_banner.py
@@ -23,7 +23,7 @@ class XBannerBanner(models.Model):
'function_name': function_name
})
- @api.constrains('x_name', 'x_url_banner', 'background_color', 'x_banner_image', 'x_banner_category', 'x_relasi_manufacture', 'x_sequence_banner', 'x_status_banner', 'sequence', 'group_by_week', 'headline_banner_s', 'description_banner_s')
+ @api.constrains('x_name', 'x_url_banner', 'background_color', 'x_banner_image', 'x_banner_category', 'x_relasi_manufacture', 'x_sequence_banner', 'x_status_banner', 'sequence', 'group_by_week', 'headline_banner_s', 'description_banner_s', 'x_keyword_banner')
def _create_solr_queue_sync_brands(self):
self._create_solr_queue('_sync_banners_to_solr')
@@ -51,6 +51,7 @@ class XBannerBanner(models.Model):
'group_by_week': banners.group_by_week or '',
'headline_banner_s': banners.x_headline_banner or '',
'description_banner_s': banners.x_description_banner or '',
+ 'keyword_banner_s': banners.x_keyword_banner or '',
})
self.solr().add([document])
banners.update_last_update_solr()
diff --git a/indoteknik_custom/models/stock_backorder_confirmation.py b/indoteknik_custom/models/stock_backorder_confirmation.py
new file mode 100644
index 00000000..d8a41f54
--- /dev/null
+++ b/indoteknik_custom/models/stock_backorder_confirmation.py
@@ -0,0 +1,33 @@
+from odoo import models, fields, api
+from odoo.tools.float_utils import float_compare
+
+class StockBackorderConfirmation(models.TransientModel):
+ _inherit = 'stock.backorder.confirmation'
+
+ def process(self):
+ pickings_to_do = self.env['stock.picking']
+ pickings_not_to_do = self.env['stock.picking']
+ for line in self.backorder_confirmation_line_ids:
+ line.picking_id.send_mail_bills()
+ # line.picking_id.send_koli_to_so()
+ if line.to_backorder is True:
+ pickings_to_do |= line.picking_id
+ else:
+ pickings_not_to_do |= line.picking_id
+
+ for pick_id in pickings_not_to_do:
+ moves_to_log = {}
+ for move in pick_id.move_lines:
+ if float_compare(move.product_uom_qty,
+ move.quantity_done,
+ precision_rounding=move.product_uom.rounding) > 0:
+ moves_to_log[move] = (move.quantity_done, move.product_uom_qty)
+ pick_id._log_less_quantities_than_expected(moves_to_log)
+
+ pickings_to_validate = self.env.context.get('button_validate_picking_ids')
+ if pickings_to_validate:
+ pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate).with_context(skip_backorder=True)
+ if pickings_not_to_do:
+ pickings_to_validate = pickings_to_validate.with_context(picking_ids_not_to_backorder=pickings_not_to_do.ids)
+ return pickings_to_validate.button_validate()
+ return True
diff --git a/indoteknik_custom/models/stock_immediate_transfer.py b/indoteknik_custom/models/stock_immediate_transfer.py
index 21210619..c2a293f9 100644
--- a/indoteknik_custom/models/stock_immediate_transfer.py
+++ b/indoteknik_custom/models/stock_immediate_transfer.py
@@ -5,17 +5,20 @@ class StockImmediateTransfer(models.TransientModel):
_inherit = 'stock.immediate.transfer'
def process(self):
- """Override process method to add send_mail_bills logic."""
pickings_to_do = self.env['stock.picking']
pickings_not_to_do = self.env['stock.picking']
for line in self.immediate_transfer_line_ids:
+ line.picking_id.send_mail_bills()
+ line.picking_id.send_koli_to_so()
if line.to_immediate is True:
pickings_to_do |= line.picking_id
else:
pickings_not_to_do |= line.picking_id
for picking in pickings_to_do:
+ # picking.send_mail_bills()
+ # picking.send_koli_to_so()
# If still in draft => confirm and assign
if picking.state == 'draft':
picking.action_confirm()
@@ -23,6 +26,7 @@ class StockImmediateTransfer(models.TransientModel):
picking.action_assign()
if picking.state != 'assigned':
raise UserError(_("Could not reserve all requested products. Please use the 'Mark as Todo' button to handle the reservation manually."))
+
for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']):
for move_line in move.move_line_ids:
move_line.qty_done = move_line.product_uom_qty
@@ -32,4 +36,6 @@ class StockImmediateTransfer(models.TransientModel):
pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate)
pickings_to_validate = pickings_to_validate - pickings_not_to_do
return pickings_to_validate.with_context(skip_immediate=True).button_validate()
+
return True
+
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index 6b631713..90ab30a4 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -13,6 +13,37 @@ class StockMove(models.Model):
)
qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
barcode = fields.Char(string='Barcode', related='product_id.barcode')
+ vendor_id = fields.Many2one('res.partner' ,string='Vendor')
+ hold_outgoingg = fields.Boolean('Hold Outgoing', default=False)
+
+ # @api.model_create_multi
+ # def create(self, vals_list):
+ # moves = super(StockMove, self).create(vals_list)
+
+ # for move in moves:
+ # if move.product_id and move.location_id.id == 58 and move.location_dest_id.id == 57 and move.picking_type_id.id == 75:
+ # po_line = self.env['purchase.order.line'].search([
+ # ('product_id', '=', move.product_id.id),
+ # ('order_id.name', '=', move.origin)
+ # ], limit=1)
+ # if po_line:
+ # move.write({'purchase_line_id': po_line.id})
+
+ # return moves
+
+ @api.constrains('product_id')
+ def constrains_product_to_fill_vendor(self):
+ for rec in self:
+ if rec.product_id and rec.bom_line_id:
+ if rec.product_id.x_manufacture.override_vendor_id:
+ rec.vendor_id = rec.product_id.x_manufacture.override_vendor_id.id
+ else:
+ purchase_pricelist = self.env['purchase.pricelist'].search(
+ [('product_id', '=', rec.product_id.id),
+ ('is_winner', '=', True)],
+ limit=1)
+ if purchase_pricelist:
+ rec.vendor_id = purchase_pricelist.vendor_id.id
def _compute_qr_code_variant(self):
for rec in self:
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index ab8109c7..0fcb7ca1 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -1,6 +1,8 @@
from odoo import fields, models, api, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.float_utils import float_is_zero
+from collections import defaultdict
+from datetime import timedelta, datetime
from datetime import timedelta, datetime as waktu
from itertools import groupby
import pytz, requests, json, requests
@@ -13,19 +15,26 @@ import requests
import time
import logging
import re
-from deep_translator import GoogleTranslator
+
_logger = logging.getLogger(__name__)
_biteship_url = "https://api.biteship.com/v1"
-_biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
-
-
+_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
+
+
+# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
+
class StockPicking(models.Model):
_inherit = 'stock.picking'
_order = 'final_seq ASC'
-
- check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True)
+ konfirm_koli_lines = fields.One2many('konfirm.koli', 'picking_id', string='Konfirm Koli', auto_join=True,
+ copy=False)
+ scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True, copy=False)
+ check_koli_lines = fields.One2many('check.koli', 'picking_id', string='Check Koli', auto_join=True, copy=False)
+
+ check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True,
+ copy=False)
barcode_product_lines = fields.One2many('barcode.product', 'picking_id', string='Barcode Product', auto_join=True)
is_internal_use = fields.Boolean('Internal Use', help='flag which is internal use or not')
account_id = fields.Many2one('account.account', string='Account')
@@ -71,36 +80,47 @@ class StockPicking(models.Model):
readonly=True,
copy=False
)
+ out_code = fields.Integer(
+ string="Out Code",
+ readonly=True,
+ related="id",
+ )
sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan", )
paket_documentation = fields.Binary(string="Dokumentasi Paket", )
- sj_return_date = fields.Datetime(string="SJ Return Date", )
+ sj_return_date = fields.Datetime(string="SJ Return Date", copy=False)
responsible = fields.Many2one('res.users', string='Responsible', tracking=True)
approval_status = fields.Selection([
('pengajuan1', 'Approval Accounting'),
('approved', 'Approved'),
- ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Internal Use")
+ ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3,
+ help="Approval Status untuk Internal Use")
approval_receipt_status = fields.Selection([
('pengajuan1', 'Approval Logistic'),
('approved', 'Approved'),
- ], string='Approval Receipt Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Receipt")
+ ], string='Approval Receipt Status', readonly=True, copy=False, index=True, tracking=3,
+ help="Approval Status untuk Receipt")
approval_return_status = fields.Selection([
('pengajuan1', 'Approval Finance'),
('approved', 'Approved'),
- ], string='Approval Return Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Return")
- date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', help="Tanggal Kirim di cetakan SJ, tidak berpengaruh ke Accounting", tracking=True)
+ ], string='Approval Return Status', readonly=True, copy=False, index=True, tracking=3,
+ help="Approval Status untuk Return")
+ date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ',
+ help="Tanggal Kirim di cetakan SJ, tidak berpengaruh ke Accounting", tracking=True,
+ copy=False)
note_logistic = fields.Selection([
- ('hold', 'Hold by Sales'),
+ ('wait_so_together', 'Tunggu SO Barengan'),
('not_paid', 'Customer belum bayar'),
- ('partial', 'Kirim Parsial'),
- ('indent', 'Indent'),
+ ('reserve_stock', 'Reserve Stock'),
+ ('waiting_schedule', 'Menunggu Jadwal Kirim'),
('self_pickup', 'Barang belum di pickup Customer'),
('expedition_closed', 'Eskpedisi belum buka')
], string='Note Logistic', help='jika field ini diisi maka tidak akan dihitung ke lead time')
waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill')
- purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', string="Purchase Representative")
+ purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id',
+ string="Purchase Representative")
carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method')
shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status")
date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
@@ -121,16 +141,74 @@ class StockPicking(models.Model):
('invoiced', 'Fully Invoiced'),
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
- ], string='Invoice Status', related="sale_id.invoice_status")
+ ], string='Invoice Status', related="sale_id.invoice_status")
note_return = fields.Text(string="Note Return", help="Catatan untuk kirim barang kembali")
-
+
state_reserve = fields.Selection([
('waiting', 'Waiting For Fullfilment'),
('ready', 'Ready to Ship'),
('done', 'Done'),
('cancel', 'Cancelled'),
], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.")
- notee = fields.Text(string="Note")
+ notee = fields.Text(string="Note SJ", help="Catatan untuk kirim barang")
+ note_info = fields.Text(string="Note Logistix (Text)", help="Catatan untuk pengiriman")
+ state_approve_md = fields.Selection([
+ ('waiting', 'Waiting For Approve by MD'),
+ ('pending', 'Pending (perlu koordinasi dengan MD)'),
+ ('done', 'Approve by MD'),
+ ], string='Approval MD Gudang Selisih', tracking=True, copy=False,
+ help="The current state of the MD Approval transfer barang from gudang selisih.")
+ # show_state_approve_md = fields.Boolean(compute="_compute_show_state_approve_md")
+
+ # def _compute_show_state_approve_md(self):
+ # for record in self:
+ # record.show_state_approve_md = record.location_id.id == 47 or record.location_id.complete_name == "Virtual Locations/Gudang Selisih"
+ quantity_koli = fields.Float(string="Quantity Koli", copy=False)
+ total_mapping_koli = fields.Float(string="Total Mapping Koli", compute='_compute_total_mapping_koli')
+ so_lama = fields.Boolean('SO LAMA', copy=False)
+ linked_manual_bu_out = fields.Many2one('stock.picking', string='BU Out', copy=False)
+
+ area_name = fields.Char(string="Area", compute="_compute_area_name")
+
+ @api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id')
+ def _compute_area_name(self):
+ for record in self:
+ district = record.real_shipping_id.kecamatan_id.name or ''
+ city = record.real_shipping_id.kota_id.name or ''
+ record.area_name = f"{district}, {city}".strip(', ')
+
+ # def write(self, vals):
+ # if 'linked_manual_bu_out' in vals:
+ # for record in self:
+ # if (record.picking_type_code == 'internal'
+ # and 'BU/PICK/' in record.name):
+ # # Jika menghapus referensi (nilai di-set False/None)
+ # if record.linked_manual_bu_out and not vals['linked_manual_bu_out']:
+ # record.linked_manual_bu_out.state_packing = 'not_packing'
+ # # Jika menambahkan referensi baru
+ # elif vals['linked_manual_bu_out']:
+ # new_picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out'])
+ # new_picking.state_packing = 'packing_done'
+ # return super().write(vals)
+
+ # @api.model
+ # def create(self, vals):
+ # record = super().create(vals)
+ # if (record.picking_type_code == 'internal'
+ # and 'BU/PICK/' in record.name
+ # and vals.get('linked_manual_bu_out')):
+ # picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out'])
+ # picking.state_packing = 'packing_done'
+ # return record
+
+ @api.depends('konfirm_koli_lines', 'konfirm_koli_lines.pick_id', 'konfirm_koli_lines.pick_id.quantity_koli')
+ def _compute_total_mapping_koli(self):
+ for record in self:
+ total = 0.0
+ for line in record.konfirm_koli_lines:
+ if line.pick_id and line.pick_id.quantity_koli:
+ total += line.pick_id.quantity_koli
+ record.total_mapping_koli = total
@api.model
def _compute_dokumen_tanda_terima(self):
@@ -142,8 +220,10 @@ class StockPicking(models.Model):
for picking in self:
picking.dokumen_pengiriman = picking.partner_id.dokumen_pengiriman_input
- dokumen_tanda_terima = fields.Char(string='Dokumen Tanda Terima yang Diberikan Pada Saat Pengiriman Barang', readonly=True, compute=_compute_dokumen_tanda_terima)
- dokumen_pengiriman = fields.Char(string='Dokumen yang Dibawa Saat Pengiriman Barang', readonly=True, compute=_compute_dokumen_pengiriman)
+ dokumen_tanda_terima = fields.Char(string='Dokumen Tanda Terima yang Diberikan Pada Saat Pengiriman Barang',
+ readonly=True, compute=_compute_dokumen_tanda_terima)
+ dokumen_pengiriman = fields.Char(string='Dokumen yang Dibawa Saat Pengiriman Barang', readonly=True,
+ compute=_compute_dokumen_pengiriman)
# Envio Tracking Section
envio_id = fields.Char(string="Envio ID", readonly=True)
@@ -175,6 +255,18 @@ class StockPicking(models.Model):
lalamove_image_url = fields.Char(string="Lalamove Image URL")
lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html")
+ # KGX Section
+ kgx_pod_photo_url = fields.Char('KGX Photo URL')
+ kgx_pod_photo = fields.Html('KGX Photo', compute='_compute_kgx_image_html')
+ kgx_pod_signature = fields.Char('KGX Signature URL')
+ kgx_pod_receive_time = fields.Datetime('KGX Ata Date')
+ kgx_pod_receiver = fields.Char('KGX Receiver')
+
+ total_koli = fields.Integer(compute='_compute_total_koli', string="Total Koli")
+ total_koli_display = fields.Char(compute='_compute_total_koli_display', string="Total Koli Display")
+ linked_out_picking_id = fields.Many2one('stock.picking', string="Linked BU/OUT", copy=False)
+ total_so_koli = fields.Integer(compute='_compute_total_so_koli', string="Total SO Koli")
+
# Biteship Section
biteship_id = fields.Char(string="Biteship Respon ID")
biteship_tracking_id = fields.Char(string="Biteship Trackcking ID")
@@ -183,13 +275,199 @@ class StockPicking(models.Model):
# countdown_hours = fields.Float(string='Countdown in Hours', compute='_callculate_sequance', default=False, store=False, compute_sudo=False)
# countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_callculate_sequance', store=False, compute_sudo=False)
final_seq = fields.Float(string='Remaining Time')
+ shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO',
+ related='sale_id.carrier_id')
+ state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')],
+ string='Packing Status')
+ approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date')
+ last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False)
+ update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD')
+
+ def _get_kgx_awb_number(self):
+ """Menggabungkan name dan origin untuk membuat AWB Number"""
+ self.ensure_one()
+ if not self.name or not self.origin:
+ return False
+ return f"{self.name} {self.origin}"
+ def _download_pod_photo(self, url):
+ """Mengunduh foto POD dari URL"""
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ return base64.b64encode(response.content)
+ except Exception as e:
+ raise UserError(f"Gagal mengunduh foto POD: {str(e)}")
+
+ def _parse_datetime(self, dt_str):
+ """Parse datetime string dari format KGX"""
+ try:
+ from datetime import datetime
+ # Hilangkan timezone jika ada masalah parsing
+ if '+' in dt_str:
+ dt_str = dt_str.split('+')[0]
+ return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S')
+ except ValueError:
+ return False
+ def action_get_kgx_pod(self):
+ self.ensure_one()
+
+ awb_number = self._get_kgx_awb_number()
+ if not awb_number:
+ raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin")
+
+ url = "https://kgx.co.id/get_detail_awb"
+ headers = {'Content-Type': 'application/json'}
+ payload = {"params" : {'awb_number': awb_number}}
+
+ try:
+ response = requests.post(url, headers=headers, data=json.dumps(payload))
+ response.raise_for_status()
+ data = response.json()
+
+ if data.get('result', {}).get('data', []):
+ pod_data = data['result']['data'][0].get('connote_pod', {})
+ photo_url = pod_data.get('photo')
+
+ self.kgx_pod_photo_url = photo_url
+ self.kgx_pod_signature = pod_data.get('signature')
+ self.kgx_pod_receiver = pod_data.get('receiver')
+ self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive'))
+ self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive'))
+
+ return data
+ else:
+ raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}")
+
+ except requests.exceptions.RequestException as e:
+ raise UserError(f"Gagal mengambil data POD: {str(e)}")
+
+ @api.constrains('sj_return_date')
+ def _check_sj_return_date(self):
+ for record in self:
+ if not record.driver_arrival_date:
+ if record.sj_return_date:
+ raise ValidationError(
+ _("Anda tidak dapat mengubah Tanggal Pengembalian setelah Tanggal Pengiriman!")
+ )
+
+ def _check_date_doc_kirim_modification(self):
+ for record in self:
+ if record.last_update_date_doc_kirim and not self.env.context.get('from_button_approve'):
+ kirim_date = fields.Datetime.from_string(record.last_update_date_doc_kirim)
+ now = fields.Datetime.now()
+
+ deadline = kirim_date + timedelta(days=1)
+ deadline = deadline.replace(hour=10, minute=0, second=0)
+
+ if now > deadline:
+ raise ValidationError(
+ _("Anda tidak dapat mengubah Tanggal Kirim setelah jam 10:00 pada hari berikutnya!")
+ )
+
+ @api.constrains('date_doc_kirim')
+ def _constrains_date_doc_kirim(self):
+ for rec in self:
+ rec.calculate_line_no()
+
+ if rec.picking_type_code == 'outgoing' and 'BU/OUT/' in rec.name and rec.partner_id.id != 96868:
+ invoice = self.env['account.move'].search(
+ [('sale_id', '=', rec.sale_id.id), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted')],
+ limit=1, order='create_date desc')
+
+ if invoice and not self.env.context.get('active_model') == 'stock.picking':
+ rec._check_date_doc_kirim_modification()
+ if rec.date_doc_kirim != invoice.invoice_date and not self.env.context.get('from_button_approve'):
+ get_approval_invoice_date = self.env['approval.invoice.date'].search(
+ [('picking_id', '=', rec.id), ('state', '=', 'draft')], limit=1)
+
+ if get_approval_invoice_date and get_approval_invoice_date.state == 'draft':
+ get_approval_invoice_date.date_doc_do = rec.date_doc_kirim
+ else:
+ approval_invoice_date = self.env['approval.invoice.date'].create({
+ 'picking_id': rec.id,
+ 'date_invoice': invoice.invoice_date,
+ 'date_doc_do': rec.date_doc_kirim,
+ 'sale_id': rec.sale_id.id,
+ 'move_id': invoice.id,
+ 'partner_id': rec.partner_id.id
+ })
+
+ rec.approval_invoice_date_id = approval_invoice_date.id
+
+ if approval_invoice_date:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {'title': 'Notification',
+ 'message': 'Invoice Date Tidak Sesuai, Document Approval Invoice Date Terbuat',
+ 'next': {'type': 'ir.actions.act_window_close'}},
+ }
+
+ rec.last_update_date_doc_kirim = datetime.datetime.utcnow()
+
+ @api.constrains('scan_koli_lines')
+ def _constrains_scan_koli_lines(self):
+ now = datetime.datetime.utcnow()
+ for picking in self:
+ if len(picking.scan_koli_lines) > 0:
+ if len(picking.scan_koli_lines) != picking.total_mapping_koli:
+ raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli")
+
+ picking.driver_departure_date = now
+
+ @api.depends('total_so_koli')
+ def _compute_total_so_koli(self):
+ for picking in self:
+ if picking.state == 'done':
+ picking.total_so_koli = self.env['sales.order.koli'].search_count(
+ [('picking_id.linked_out_picking_id', '=', picking.id), ('state', '=', 'delivered')])
+ else:
+ picking.total_so_koli = self.env['sales.order.koli'].search_count(
+ [('picking_id.linked_out_picking_id', '=', picking.id), ('state', '!=', 'delivered')])
+
+ @api.depends('total_koli')
+ def _compute_total_koli(self):
+ for picking in self:
+ picking.total_koli = self.env['scan.koli'].search_count([('picking_id', '=', picking.id)])
+
+ @api.depends('total_koli', 'total_so_koli')
+ def _compute_total_koli_display(self):
+ for picking in self:
+ picking.total_koli_display = f"{picking.total_koli} / {picking.total_so_koli}"
+
+ @api.constrains('quantity_koli')
+ def _constrains_quantity_koli(self):
+ for picking in self:
+ if not picking.linked_out_picking_id:
+ so_koli = self.env['sales.order.koli'].search([('picking_id', '=', picking.id)])
+
+ if so_koli:
+ so_koli.unlink()
+
+ for rec in picking.check_koli_lines:
+ self.env['sales.order.koli'].create({
+ 'sale_order_id': picking.sale_id.id,
+ 'picking_id': picking.id,
+ 'koli_id': rec.id,
+ })
+ else:
+ raise UserError(
+ 'Tidak Bisa Mengubah Quantity Koli Karena Koli Dari Picking Ini Sudah Dipakai Di BU/OUT!')
+
+ @api.onchange('quantity_koli')
+ def _onchange_quantity_koli(self):
+ self.check_koli_lines = [(5, 0, 0)]
+ self.check_koli_lines = [(0, 0, {
+ 'koli': f"{self.name}/{str(i + 1).zfill(3)}",
+ 'picking_id': self.id,
+ }) for i in range(int(self.quantity_koli))]
+
def schduled_update_sequance(self):
query = "SELECT update_sequance_stock_picking();"
self.env.cr.execute(query)
-
-
+
# @api.depends('estimated_ready_ship_date', 'state')
# def _callculate_sequance(self):
# for record in self:
@@ -198,9 +476,9 @@ class StockPicking(models.Model):
# rts = record.estimated_ready_ship_date - waktu.now()
# rts_days = rts.days
# rts_hours = divmod(rts.seconds, 3600)
-
+
# estimated_by_erts = rts.total_seconds() / 3600
-
+
# record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours"
# record.countdown_hours = estimated_by_erts
# else:
@@ -210,7 +488,6 @@ class StockPicking(models.Model):
# _logger.error(f"Error calculating sequance {record.id}: {str(e)}")
# print(str(e))
# return { 'error': str(e) }
-
# @api.depends('estimated_ready_ship_date', 'state')
# def _compute_countdown_hours(self):
@@ -244,13 +521,20 @@ class StockPicking(models.Model):
else:
record.lalamove_image_html = "No image available."
+ def _compute_kgx_image_html(self):
+ for record in self:
+ if record.kgx_pod_photo_url:
+ record.kgx_pod_photo = f'<img src="{record.kgx_pod_photo_url}" width="300" height="300"/>'
+ else:
+ record.kgx_pod_photo = "No image available."
+
def action_fetch_lalamove_order(self):
pickings = self.env['stock.picking'].search([
('picking_type_code', '=', 'outgoing'),
('state', '=', 'done'),
('carrier_id', '=', 9),
('lalamove_order_id', '!=', False)
- ])
+ ])
for picking in pickings:
try:
order_id = picking.lalamove_order_id
@@ -285,7 +569,7 @@ class StockPicking(models.Model):
for stop in stops:
pod = stop.get("POD", {})
if pod.get("status") == "DELIVERED":
- image_url = pod.get("image") # Sesuaikan jika key berbeda
+ image_url = pod.get("image") # Sesuaikan jika key berbeda
self.lalamove_image_url = image_url
address = stop.get("address")
@@ -306,7 +590,6 @@ class StockPicking(models.Model):
else:
raise UserError(f"Error {response.status_code}: {response.text}")
-
def _convert_to_wib(self, date_str):
"""
Mengonversi string waktu ISO 8601 ke format waktu Indonesia (WIB)
@@ -392,7 +675,7 @@ class StockPicking(models.Model):
picking.tracking_by = self.env.user.id
ata_at_str = data.get("ata_at")
envio_ata = self._convert_to_datetime(data.get("ata_at"))
-
+
picking.driver_arrival_date = envio_ata
if data.get("status") != 'delivered':
picking.driver_arrival_date = False
@@ -403,13 +686,13 @@ class StockPicking(models.Model):
raise UserError(f"Kesalahan tidak terduga: {str(e)}")
def action_send_to_biteship(self):
-
+
if self.biteship_tracking_id:
raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}")
-
+
# Mencari data sale.order.line berdasarkan sale_id
products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)])
-
+
# Fungsi untuk membangun items_data dari order lines
def build_items_data(lines):
return [{
@@ -431,18 +714,18 @@ class StockPicking(models.Model):
('order_id', '=', self.sale_id.id),
('product_id', '=', move_line.product_id.id)
], limit=1)
-
+
if order_line:
items_data_instant.append({
"name": order_line.product_id.name,
"description": order_line.name,
"value": order_line.price_unit,
- "quantity": move_line.qty_done, # Menggunakan qty_done dari move_line
+ "quantity": move_line.qty_done,
"weight": order_line.weight
})
payload = {
- "reference_id " : self.sale_id.name,
+ "reference_id ": self.sale_id.name,
"shipper_contact_name": self.carrier_id.pic_name or '',
"shipper_contact_phone": self.carrier_id.pic_phone or '',
"shipper_organization": self.carrier_id.name,
@@ -463,19 +746,20 @@ class StockPicking(models.Model):
}
# Cek jika pengiriman instant atau same_day
- if self.sale_id.delivery_service_type and ("instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type):
+ if self.sale_id.delivery_service_type and (
+ "instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type):
payload.update({
- "origin_coordinate" :{
+ "origin_coordinate": {
"latitude": -6.3031123,
- "longitude" : 106.7794934999
+ "longitude": 106.7794934999
},
- "destination_coordinate" : {
+ "destination_coordinate": {
"latitude": self.real_shipping_id.latitude,
"longitude": self.real_shipping_id.longtitude,
},
"items": items_data_instant
})
-
+
api_key = _biteship_api_key
headers = {
"Authorization": f"Bearer {api_key}",
@@ -483,7 +767,7 @@ class StockPicking(models.Model):
}
# Kirim request ke Biteship
- response = requests.post(_biteship_url+'/orders', headers=headers, json=payload)
+ response = requests.post(_biteship_url + '/orders', headers=headers, json=payload)
if response.status_code == 200:
data = response.json()
@@ -493,16 +777,26 @@ class StockPicking(models.Model):
self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "")
self.delivery_tracking_no = data.get("courier", {}).get("waybill_id", "")
- return data
+ waybill_id = data.get("courier", {}).get("waybill_id", "")
+
+ message = f"✅ Berhasil Order ke Biteship! Resi: {waybill_id}" if waybill_id else "⚠️ Order berhasil, tetapi tidak ada nomor resi."
+
+ return {
+ 'effect': {
+ 'fadeout': 'slow', # Efek menghilang perlahan
+ 'message': message, # Pesan sukses
+ 'type': 'rainbow_man', # Efek animasi lucu Odoo
+ }
+ }
else:
error_data = response.json()
error_message = error_data.get("error", "Unknown error")
error_code = error_data.get("code", "No code provided")
raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})")
-
+
@api.constrains('driver_departure_date')
- def constrains_driver_departure_date(self):
- if not self.date_doc_kirim:
+ def constrains_driver_departure_date(self):
+ if not self.date_doc_kirim:
self.date_doc_kirim = self.driver_departure_date
@api.constrains('arrival_time')
@@ -530,92 +824,64 @@ class StockPicking(models.Model):
if not self._context.get('darimana') == 'sale.order' and self.env.user.id not in users_in_group.mapped('id'):
self.sale_id.unreserve_id = self.id
return self._create_approval_notification('Logistic')
-
+
res = super(StockPicking, self).do_unreserve()
current_time = datetime.datetime.utcnow()
self.date_unreserve = current_time
- # self.check_state_reserve()
-
+
return res
-
- # def check_state_reserve(self):
- # do = self.search([
- # ('state', 'not in', ['cancel', 'draft', 'done']),
- # ('picking_type_code', '=', 'outgoing')
- # ])
-
- # for rec in do:
- # rec.state_reserve = 'ready'
- # rec.date_reserved = datetime.datetime.utcnow()
-
- # for line in rec.move_ids_without_package:
- # if line.product_uom_qty > line.reserved_availability:
- # rec.state_reserve = 'waiting'
- # rec.date_reserved = ''
- # break
def check_state_reserve(self):
pickings = self.search([
('state', 'not in', ['cancel', 'draft', 'done']),
- ('picking_type_code', '=', 'outgoing'),
- ('name', 'ilike', 'BU/OUT/'),
+ ('picking_type_code', '=', 'internal'),
+ ('name', 'ilike', 'BU/PICK/'),
])
- count = self.search_count([
- ('state', 'not in', ['cancel', 'draft', 'done']),
- ('picking_type_code', '=', 'outgoing')
- ])
-
for picking in pickings:
fullfillments = self.env['sales.order.fulfillment.v2'].search([
('sale_order_id', '=', picking.sale_id.id)
])
-
+
picking.state_reserve = 'ready'
picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow()
-
+
if any(rec.so_qty != rec.reserved_stock_qty for rec in fullfillments):
picking.state_reserve = 'waiting'
picking.date_reserved = ''
-
+
self.check_state_reserve_backorder()
def check_state_reserve_backorder(self):
pickings = self.search([
('backorder_id', '!=', False),
- ('name', 'ilike', 'BU/OUT/'),
- ('picking_type_code', '=', 'outgoing'),
+ ('name', 'ilike', 'BU/PICK/'),
+ ('picking_type_code', '=', 'internal'),
('state', 'not in', ['cancel', 'draft', 'done'])
])
- count = self.search_count([
- ('backorder_id', '!=', False),
- ('picking_type_code', '=', 'outgoing'),
- ('state', 'not in', ['cancel', 'draft', 'done'])
- ])
-
for picking in pickings:
fullfillments = self.env['sales.order.fulfillment.v2'].search([
('sale_order_id', '=', picking.sale_id.id)
])
-
+
picking.state_reserve = 'ready'
picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow()
-
+
if any(rec.so_qty != rec.reserved_stock_qty for rec in fullfillments):
picking.state_reserve = 'waiting'
picking.date_reserved = ''
-
+
def _create_approval_notification(self, approval_role):
title = 'Warning'
message = f'Butuh approval sales untuk unreserved'
return self._create_notification_action(title, message)
-
+
def _create_notification_action(self, title, message):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
- 'params': { 'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'} },
+ 'params': {'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'}},
}
def _compute_shipping_status(self):
@@ -625,7 +891,7 @@ class StockPicking(models.Model):
status = 'shipment'
elif rec.driver_departure_date and (rec.sj_return_date or rec.driver_arrival_date):
status = 'completed'
-
+
rec.shipping_status = status
def action_create_invoice_from_mr(self):
@@ -633,10 +899,10 @@ class StockPicking(models.Model):
"""
if not self.env.user.is_accounting:
raise UserError('Hanya Accounting yang bisa membuat Bill')
-
+
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- #custom here
+ # custom here
po = self.env['purchase.order'].search([
('name', '=', self.group_id.name)
])
@@ -653,24 +919,29 @@ class StockPicking(models.Model):
invoice_vals = order._prepare_invoice()
# Invoice line values (keep only necessary sections).
for line in self.move_ids_without_package:
- po_line = self.env['purchase.order.line'].search([('order_id', '=', po.id), ('product_id', '=', line.product_id.id)], limit=1)
+ po_line = self.env['purchase.order.line'].search(
+ [('order_id', '=', po.id), ('product_id', '=', line.product_id.id)], limit=1)
qty = line.product_uom_qty
if po_line.display_type == 'line_section':
pending_section = line
continue
if not float_is_zero(po_line.qty_to_invoice, precision_digits=precision):
if pending_section:
- invoice_vals['invoice_line_ids'].append((0, 0, pending_section._prepare_account_move_line_from_mr(po_line, qty)))
+ invoice_vals['invoice_line_ids'].append(
+ (0, 0, pending_section._prepare_account_move_line_from_mr(po_line, qty)))
pending_section = None
- invoice_vals['invoice_line_ids'].append((0, 0, line._prepare_account_move_line_from_mr(po_line, qty)))
+ invoice_vals['invoice_line_ids'].append(
+ (0, 0, line._prepare_account_move_line_from_mr(po_line, qty)))
invoice_vals_list.append(invoice_vals)
if not invoice_vals_list:
- raise UserError(_('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.'))
+ raise UserError(
+ _('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.'))
# 2) group by (company_id, partner_id, currency_id) for batch creation
new_invoice_vals_list = []
- for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: (x.get('company_id'), x.get('partner_id'), x.get('currency_id'))):
+ for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: (
+ x.get('company_id'), x.get('partner_id'), x.get('currency_id'))):
origins = set()
payment_refs = set()
refs = set()
@@ -700,7 +971,8 @@ class StockPicking(models.Model):
# 4) Some moves might actually be refunds: convert them if the total amount is negative
# We do this after the moves have been created since we need taxes, etc. to know if the total
# is actually negative or not
- moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note()
+ moves.filtered(
+ lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note()
return self.action_view_invoice_from_mr(moves)
@@ -741,9 +1013,12 @@ class StockPicking(models.Model):
def action_assign(self):
res = super(StockPicking, self).action_assign()
- current_time = datetime.datetime.utcnow()
- self.real_shipping_id = self.sale_id.real_shipping_id
- self.date_availability = current_time
+ for move in self:
+ # if not move.sale_id.hold_outgoing and move.location_id.id != 57 and move.location_dest_id.id != 60:
+ # TODO cant skip hold outgoing cause of not singleton method
+ current_time = datetime.datetime.utcnow()
+ move.real_shipping_id = move.sale_id.real_shipping_id
+ move.date_availability = current_time
# self.check_state_reserve()
return res
@@ -763,7 +1038,7 @@ class StockPicking(models.Model):
# for stock_move_line in stock_move_lines:
# if stock_move_line.picking_id.state not in list_state:
# continue
- # raise UserError('Sudah pernah dikirim kalender')
+ # raise UserError('Sudah pernah dikirim kalender')
for pick in self:
if not pick.is_internal_use:
@@ -784,23 +1059,27 @@ class StockPicking(models.Model):
if self.env.user.is_accounting:
pick.approval_return_status = 'approved'
continue
+ else:
+ pick.approval_return_status = 'pengajuan1'
action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard')
if self.picking_type_code == 'outgoing':
if self.env.user.id in [3988, 3401, 20] or (
- self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
+ self.env.user.has_group(
+ 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
):
action['context'] = {'picking_ids': [x.id for x in self]}
return action
- elif not self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
+ elif not self.env.user.has_group(
+ 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
raise UserError('Harus Purchasing yang Ask Return')
else:
raise UserError('Harus Sales Admin yang Ask Return')
elif self.picking_type_code == 'incoming':
if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or (
- self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
+ self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
):
action['context'] = {'picking_ids': [x.id for x in self]}
return action
@@ -810,7 +1089,7 @@ class StockPicking(models.Model):
raise UserError('Harus Purchasing yang Ask Return')
def calculate_line_no(self):
-
+
for picking in self:
name = picking.group_id.name
for move in picking.move_ids_without_package:
@@ -833,10 +1112,10 @@ class StockPicking(models.Model):
def _compute_summary_qty(self):
for picking in self:
sum_qty_detail = sum_qty_operation = count_line_detail = count_line_operation = 0
- for detail in picking.move_line_ids_without_package: # detailed operations
+ for detail in picking.move_line_ids_without_package: # detailed operations
sum_qty_detail += detail.qty_done
count_line_detail += 1
- for operation in picking.move_ids_without_package: # operations
+ for operation in picking.move_ids_without_package: # operations
sum_qty_operation += operation.product_uom_qty
count_line_operation += 1
picking.summary_qty_detail = sum_qty_detail
@@ -858,13 +1137,13 @@ class StockPicking(models.Model):
])
if (
- self.picking_type_id.id == 29
- and quant
- and line.location_id.id == bu_location_id
- and quant.inventory_quantity < line.product_uom_qty
+ self.picking_type_id.id == 29
+ and quant
+ and line.location_id.id == bu_location_id
+ and quant.inventory_quantity < line.product_uom_qty
):
raise UserError('Quantity reserved lebih besar dari quantity onhand di product')
-
+
def check_qty_done_stock(self):
for line in self.move_line_ids_without_package:
def check_qty_per_inventory(self, product, location):
@@ -877,12 +1156,74 @@ class StockPicking(models.Model):
return quant.quantity
return 0
-
+
qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id)
if line.qty_done > qty_onhand:
raise UserError('Quantity Done melebihi Quantity Onhand')
+ def button_state_approve_md(self):
+ group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
+ users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
+ active_model = self.env.context.get('active_model')
+ if self.env.user.id in users_in_group.mapped('id'):
+ self.state_approve_md = 'done'
+ else:
+ raise UserError('Hanya MD yang bisa Approve')
+
+ def button_state_pending_md(self):
+ group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
+ users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
+ active_model = self.env.context.get('active_model')
+ if self.env.user.id in users_in_group.mapped('id'):
+ self.state_approve_md = 'pending'
+ else:
+ raise UserError('Hanya MD yang bisa Approve')
+
def button_validate(self):
+ self.check_invoice_date()
+ threshold_datetime = waktu(2025, 4, 11, 6, 26)
+ group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
+ users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
+ active_model = self.env.context.get('active_model')
+ if self.location_id.id == 47 and self.env.user.id not in users_in_group.mapped(
+ 'id') and self.state_approve_md != 'done':
+ self.state_approve_md = 'waiting' if self.state_approve_md != 'pending' else 'pending'
+ self.env.cr.commit()
+ raise UserError("Transfer dari gudang selisih harus di approve MD, Hubungi MD agar bisa di Validate")
+ else:
+ if self.location_id.id == 47 and self.env.user.id in users_in_group.mapped('id'):
+ self.state_approve_md = 'done'
+
+ if (len(self.konfirm_koli_lines) == 0
+ and 'BU/OUT/' in self.name
+ and self.picking_type_code == 'outgoing'
+ and self.create_date > threshold_datetime
+ and not self.so_lama):
+ raise UserError(_("Tidak ada Mapping koli! Harap periksa kembali."))
+
+ if (len(self.scan_koli_lines) == 0
+ and 'BU/OUT/' in self.name
+ and self.picking_type_code == 'outgoing'
+ and self.create_date > threshold_datetime
+ and not self.so_lama):
+ raise UserError(_("Tidak ada scan koli! Harap periksa kembali."))
+
+ # if self.driver_departure_date == False and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing':
+ # raise UserError(_("Isi Driver Departure Date dulu sebelum validate"))
+
+ if len(self.check_koli_lines) == 0 and 'BU/PICK/' in self.name:
+ raise UserError(_("Tidak ada koli! Harap periksa kembali."))
+
+ if not self.linked_manual_bu_out and 'BU/PICK/' in self.name:
+ raise UserError(_("Isi BU Out terlebih dahulu!"))
+
+ if len(self.check_product_lines) == 0 and 'BU/PICK/' in self.name:
+ raise UserError(_("Tidak ada Check Product! Harap periksa kembali."))
+
+ if self.total_koli > self.total_so_koli:
+ raise UserError(_("Total Koli (%s) dan Total SO Koli (%s) tidak sama! Harap periksa kembali.")
+ % (self.total_koli, self.t1otal_so_koli))
+
if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking':
if self.origin and 'Return of' in self.origin:
raise UserError("Button ini hanya untuk Logistik")
@@ -904,10 +1245,10 @@ class StockPicking(models.Model):
if self.is_internal_use and not self.env.user.is_accounting:
raise UserError("Harus di Approve oleh Accounting")
-
+
if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver:
raise UserError("Harus di Approve oleh Logistik")
-
+
if self.location_dest_id.id == 47 and not self.env.user.is_purchasing_manager:
raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara")
@@ -930,14 +1271,130 @@ class StockPicking(models.Model):
self.validation_minus_onhand_quantity()
self.responsible = self.env.user.id
+ # self.send_koli_to_so()
+ if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name:
+ self.check_koli()
res = super(StockPicking, self).button_validate()
- self.calculate_line_no()
self.date_done = datetime.datetime.utcnow()
self.state_reserve = 'done'
self.final_seq = 0
+ self.set_picking_code_out()
+ self.send_koli_to_so()
+
+ if (self.state_reserve == 'done' and self.picking_type_code == 'internal' and 'BU/PICK/' in self.name
+ and self.linked_manual_bu_out):
+ if not self.linked_manual_bu_out.date_reserved:
+ current_datetime = datetime.datetime.utcnow()
+ self.linked_manual_bu_out.date_reserved = current_datetime
+ self.linked_manual_bu_out.message_post(
+ body=f"Date Reserved diisi secara otomatis dari validasi BU/PICK {self.name}"
+ )
+
+ if not self.env.context.get('skip_koli_check'):
+ for picking in self:
+ if picking.sale_id:
+ all_koli_ids = picking.sale_id.koli_lines.filtered(lambda k: k.state != 'delivered').ids
+ scanned_koli_ids = picking.scan_koli_lines.mapped('koli_id.id')
+
+ missing_koli_ids = set(all_koli_ids) - set(scanned_koli_ids)
+
+ if len(missing_koli_ids) > 0 and picking.picking_type_code == 'outgoing' and 'BU/OUT/' in picking.name:
+ missing_koli_names = picking.sale_id.koli_lines.filtered(
+ lambda k: k.id in missing_koli_ids and k.state != 'delivered').mapped('display_name')
+ missing_koli_list = "\n".join(f"- {name}" for name in missing_koli_names)
+
+ # Buat wizard modal warning
+ wizard = self.env['warning.modal.wizard'].create({
+ 'message': f"Berikut Koli yang belum discan:\n{missing_koli_list}",
+ 'picking_id': picking.id,
+ })
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'warning.modal.wizard',
+ 'view_mode': 'form',
+ 'res_id': wizard.id,
+ 'target': 'new',
+ }
self.send_mail_bills()
return res
+ def check_invoice_date(self):
+ for picking in self:
+ if picking.picking_type_code != 'outgoing' or 'BU/OUT/' not in picking.name or picking.partner_id.id == 96868:
+ continue
+
+ invoice = self.env['account.move'].search(
+ [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), ('move_type', '=', 'out_invoice')], limit=1)
+
+ if not invoice:
+ continue
+
+ if not picking.so_lama and invoice and (not picking.date_doc_kirim or not invoice.invoice_date):
+ raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!")
+
+ picking_date = fields.Date.to_date(picking.date_doc_kirim)
+ invoice_date = fields.Date.to_date(invoice.invoice_date)
+
+ if picking_date != invoice_date and picking.update_date_doc_kirim_add:
+ raise UserError("Tanggal Kirim (%s) tidak sesuai dengan Tanggal Invoice (%s)!" % (
+ picking_date.strftime('%d-%m-%Y'),
+ invoice_date.strftime('%d-%m-%Y')
+ ))
+
+ def set_picking_code_out(self):
+ for picking in self:
+ # Check if picking meets criteria
+ is_bu_pick = picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name
+ if not is_bu_pick:
+ continue
+
+ # Find matching outgoing transfers
+ bu_out_transfers = self.search([
+ ('name', 'like', 'BU/OUT/%'),
+ ('sale_id', '=', picking.sale_id.id),
+ ('picking_type_code', '=', 'outgoing'),
+ ('picking_code', '=', False),
+ ('state', 'not in', ['done', 'cancel'])
+ ])
+
+ # Assign sequence code to each matching transfer
+ for transfer in bu_out_transfers:
+ transfer.picking_code = self.env['ir.sequence'].next_by_code('stock.picking.code')
+
+ def check_koli(self):
+ for picking in self:
+ sale_id = picking.sale_id
+ for koli_lines in picking.scan_koli_lines:
+ if koli_lines.koli_id.sale_order_id != sale_id:
+ raise UserError('Koli tidak sesuai')
+
+ def send_koli_to_so(self):
+ for picking in self:
+ if picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name:
+ for koli_line in picking.check_koli_lines:
+ existing_koli = self.env['sales.order.koli'].search([
+ ('sale_order_id', '=', picking.sale_id.id),
+ ('picking_id', '=', picking.id),
+ ('koli_id', '=', koli_line.id)
+ ], limit=1)
+
+ if not existing_koli:
+ self.env['sales.order.koli'].create({
+ 'sale_order_id': picking.sale_id.id,
+ 'picking_id': picking.id,
+ 'koli_id': koli_line.id
+ })
+
+ if picking.picking_type_code == 'outgoing' and 'BU/OUT/' in picking.name:
+ if picking.state == 'done':
+ for koli_line in picking.scan_koli_lines:
+ existing_koli = self.env['sales.order.koli'].search([
+ ('sale_order_id', '=', picking.sale_id.id),
+ ('koli_id', '=', koli_line.koli_id.koli_id.id)
+ ], limit=1)
+
+ existing_koli.state = 'delivered'
def check_qty_done_stock(self):
for line in self.move_line_ids_without_package:
@@ -951,7 +1408,7 @@ class StockPicking(models.Model):
return quant.quantity
return 0
-
+
qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id)
if line.qty_done > qty_onhand:
raise UserError('Quantity Done melebihi Quantity Onhand')
@@ -1008,32 +1465,56 @@ class StockPicking(models.Model):
return True
def action_cancel(self):
- if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking':
+ if not self.env.user.is_logistic_approver and (
+ self.env.context.get('active_model') == 'stock.picking' or self.env.context.get(
+ 'active_model') == 'stock.picking.type'):
if self.origin and 'Return of' in self.origin:
raise UserError("Button ini hanya untuk Logistik")
+ if not self.env.user.has_group('indoteknik_custom.group_role_it') and not self.env.user.has_group(
+ 'indoteknik_custom.group_role_logistic') and self.picking_type_code == 'outgoing':
+ raise UserError("Button ini hanya untuk Logistik")
+
res = super(StockPicking, self).action_cancel()
return res
-
@api.model
def create(self, vals):
self._use_faktur(vals)
- if vals.get('picking_type_code') == 'incoming' and vals.get('location_dest_id') == 58:
- if 'name' in vals and vals['name'].startswith('BU/IN/'):
- vals['name'] = vals['name'].replace('BU/IN/', 'BU/INPUT/', 1)
-
- if vals.get('picking_type_code') == 'internal' and vals.get('location_id') == 58:
- if 'name' in vals and vals['name'].startswith('BU/INT'):
- new_name = vals['name'].replace('BU/INT', 'BU/IN', 1)
- # Periksa apakah nama sudah ada
- if self.env['stock.picking'].search_count([('name', '=', new_name), ('company_id', '=', vals.get('company_id'))]) > 0:
- new_name = f"{new_name}-DUP"
- vals['name'] = new_name
- return super(StockPicking, self).create(vals)
+ records = super(StockPicking, self).create(vals)
+
+ # Panggil sync_sale_line setelah record dibuat
+ # records.sync_sale_line(vals)
+ return records
+
+ def sync_sale_line(self, vals):
+ # Pastikan kita bekerja dengan record yang sudah ada
+ for picking in self:
+ if picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name:
+ for line in picking.move_ids_without_package:
+ if line.product_id and picking.sale_id:
+ sale_line = self.env['sale.order.line'].search([
+ ('product_id', '=', line.product_id.id),
+ ('order_id', '=', picking.sale_id.id)
+ ], limit=1) # Tambahkan limit=1 untuk efisiensi
+
+ if sale_line:
+ line.sale_line_id = sale_line.id
def write(self, vals):
+ if 'linked_manual_bu_out' in vals:
+ for record in self:
+ if (record.picking_type_code == 'internal'
+ and 'BU/PICK/' in record.name):
+ # Jika menghapus referensi (nilai di-set False/None)
+ if record.linked_manual_bu_out and not vals['linked_manual_bu_out']:
+ record.linked_manual_bu_out.state_packing = 'not_packing'
+ # Jika menambahkan referensi baru
+ elif vals['linked_manual_bu_out']:
+ new_picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out'])
+ new_picking.state_packing = 'packing_done'
self._use_faktur(vals)
+ self.sync_sale_line(vals)
for picking in self:
# Periksa apakah kondisi terpenuhi saat data diubah
if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and
@@ -1049,7 +1530,8 @@ class StockPicking(models.Model):
if name_to_modify.startswith('BU/INT'):
new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1)
# Periksa apakah nama sudah ada
- if self.env['stock.picking'].search_count([('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0:
+ if self.env['stock.picking'].search_count(
+ [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0:
new_name = f"{new_name}-DUP"
vals['name'] = new_name
return super(StockPicking, self).write(vals)
@@ -1093,7 +1575,7 @@ class StockPicking(models.Model):
def get_manifests(self):
if self.waybill_id and len(self.waybill_id.manifest_ids) > 0:
return [self.create_manifest_data(x.description, x.datetime) for x in self.waybill_id.manifest_ids]
-
+
status_mapping = {
'pickup': {
'arrival': 'Sudah diambil',
@@ -1118,7 +1600,7 @@ class StockPicking(models.Model):
if not status:
return manifest_datas
-
+
if arrival_date or self.sj_return_date:
manifest_datas.append(self.create_manifest_data(status['arrival'], arrival_date))
if departure_date:
@@ -1129,14 +1611,14 @@ class StockPicking(models.Model):
def get_tracking_detail(self):
self.ensure_one()
-
+
order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1)
response = {
'delivery_order': {
'name': self.name,
'carrier': self.carrier_id.name or '',
- 'service' : order.delivery_service_type or '',
+ 'service': order.delivery_service_type or '',
'receiver_name': '',
'receiver_city': ''
},
@@ -1148,75 +1630,76 @@ class StockPicking(models.Model):
'is_biteship': True if self.biteship_id else False,
'manifests': self.get_manifests()
}
-
- if self.biteship_id :
+
+ if self.biteship_id:
histori = self.get_manifest_biteship()
eta_start = order.date_order + timedelta(days=order.estimated_arrival_days_start)
eta_end = order.date_order + timedelta(days=order.estimated_arrival_days)
formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}"
response['eta'] = formatted_eta
- response['manifests'] = histori.get("manifests", [])
- response['delivered'] = histori.get("delivered", False) or self.sj_return_date != False or self.driver_arrival_date != False
+ response['manifests'] = histori.get("manifests", [])
+ response['delivered'] = histori.get("delivered",
+ False) or self.sj_return_date != False or self.driver_arrival_date != False
response['status'] = self._map_status_biteship(histori.get("delivered"))
-
+
return response
if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0:
response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False
return response
-
+
response['delivery_order']['receiver_name'] = self.waybill_id.receiver_name
response['delivery_order']['receiver_city'] = self.waybill_id.receiver_city
response['delivery_status'] = self.waybill_id._get_history('delivery_status')
response['delivered'] = self.waybill_id.delivered
return response
-
+
def get_manifest_biteship(self):
api_key = _biteship_api_key
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
-
-
+
manifests = []
-
+
try:
- # Kirim request ke Biteship
- response = requests.get(_biteship_url+'/trackings/'+self.biteship_tracking_id, headers=headers, json=manifests)
+ # Kirim request ke Biteship
+ response = requests.get(_biteship_url + '/trackings/' + self.biteship_tracking_id, headers=headers,
+ json=manifests)
result = response.json()
description = {
- 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up',
- 'allocated' : 'Kurir akan melakukan pick-up pesanan',
- 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up',
- 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("name", ""),
- 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman',
- 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli',
- 'delivered' : 'Pesanan telah sampai dan diterima oleh '+result.get("destination", {}).get("contact_name", "")
+ 'confirmed': 'Indoteknik telah melakukan permintaan pick-up',
+ 'allocated': 'Kurir akan melakukan pick-up pesanan',
+ 'picking_up': 'Kurir sedang dalam perjalanan menuju lokasi pick-up',
+ 'picked': 'Pesanan sudah di pick-up kurir ' + result.get("courier", {}).get("name", ""),
+ 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman',
+ 'dropping_off': 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli',
+ 'delivered': 'Pesanan telah sampai dan diterima oleh ' + result.get("destination", {}).get(
+ "contact_name", "")
}
- if(result.get('success') == True):
+ if (result.get('success') == True):
history = result.get("history", [])
status = result.get("status", "")
-
+
for entry in reversed(history):
manifests.append({
"status": re.sub(r'[^a-zA-Z0-9\s]', ' ', entry["status"]).lower().capitalize(),
"datetime": self._convert_to_local_time(entry["updated_at"]),
- # "description": GoogleTranslator(source='auto', target='id').translate(entry["note"]),
- "description": description[entry["status"]],
+ "description": description[entry["status"]],
})
-
+
return {
"manifests": manifests,
"delivered": status
}
return manifests
- except Exception as e :
+ except Exception as e:
_logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}")
- return { 'error': str(e) }
-
+ return {'error': str(e)}
+
def _convert_to_local_time(self, iso_date):
try:
dt_with_tz = waktu.fromisoformat(iso_date)
@@ -1228,7 +1711,7 @@ class StockPicking(models.Model):
return local_dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
return str(e)
-
+
def _map_status_biteship(self, status):
status_mapping = {
"confirmed": "pending",
@@ -1242,7 +1725,7 @@ class StockPicking(models.Model):
"delivered": "completed"
}
return status_mapping.get(status, "Hubungi Admin")
-
+
def generate_eta_delivery(self):
current_date = datetime.datetime.now()
prepare_days = 3
@@ -1256,7 +1739,7 @@ class StockPicking(models.Model):
fastest_eta = start_date + ead_datetime
if not self.driver_departure_date and fastest_eta < current_date:
fastest_eta = current_date + ead_datetime
-
+
longest_days = 3
longest_eta = fastest_eta + datetime.timedelta(days=longest_days)
@@ -1265,9 +1748,10 @@ class StockPicking(models.Model):
formatted_fastest_eta = fastest_eta.strftime(format_time_fastest)
formatted_longest_eta = longest_eta.strftime(format_time)
-
+
return f'{formatted_fastest_eta} - {formatted_longest_eta}'
-
+
+
class CheckProduct(models.Model):
_name = 'check.product'
_description = 'Check Product'
@@ -1281,9 +1765,73 @@ class CheckProduct(models.Model):
index=True,
copy=False,
)
- product_id = fields.Many2one('product.product', string='Product', required=True)
- quantity = fields.Float(string='Quantity', default=1.0, required=True)
+ product_id = fields.Many2one('product.product', string='Product')
+ quantity = fields.Float(string='Quantity')
status = fields.Char(string='Status', compute='_compute_status')
+ code_product = fields.Char(string='Code Product')
+
+ @api.onchange('code_product')
+ def _onchange_code_product(self):
+ if not self.code_product:
+ return
+
+ # Cari product berdasarkan default_code, barcode, atau barcode_box
+ product = self.env['product.product'].search([
+ '|',
+ ('default_code', '=', self.code_product),
+ '|',
+ ('barcode', '=', self.code_product),
+ ('barcode_box', '=', self.code_product)
+ ], limit=1)
+
+ if not product:
+ raise UserError("Product tidak ditemukan")
+
+ # Jika scan barcode_box, set quantity sesuai qty_pcs_box
+ if product.barcode_box == self.code_product:
+ self.product_id = product.id
+ self.quantity = product.qty_pcs_box
+ self.code_product = product.default_code or product.barcode
+ # return {
+ # 'warning': {
+ # 'title': 'Info',8994175025871
+
+ # 'message': f'Product box terdeteksi. Quantity di-set ke {product.qty_pcs_box}'
+ # }
+ # }
+ else:
+ # Jika scan biasa
+ self.product_id = product.id
+ self.code_product = product.default_code or product.barcode
+ self.quantity = 1
+
+ def unlink(self):
+ # Get all affected pickings before deletion
+ pickings = self.mapped('picking_id')
+
+ # Store product_ids that will be deleted
+ deleted_product_ids = self.mapped('product_id')
+
+ # Perform the deletion
+ result = super(CheckProduct, self).unlink()
+
+ # After deletion, update moves for affected pickings
+ for picking in pickings:
+ # For products that were completely removed (no remaining check.product lines)
+ remaining_product_ids = picking.check_product_lines.mapped('product_id')
+ removed_product_ids = deleted_product_ids - remaining_product_ids
+
+ # Set quantity_done to 0 for moves of completely removed products
+ moves_to_reset = picking.move_ids_without_package.filtered(
+ lambda move: move.product_id in removed_product_ids
+ )
+ for move in moves_to_reset:
+ move.quantity_done = 0.0
+
+ # Also sync remaining products in case their totals changed
+ self._sync_check_product_to_moves(picking)
+
+ return result
@api.depends('quantity')
def _compute_status(self):
@@ -1298,7 +1846,6 @@ class CheckProduct(models.Model):
else:
record.status = 'Done'
-
def create(self, vals):
# Create the record
record = super(CheckProduct, self).create(vals)
@@ -1323,7 +1870,8 @@ class CheckProduct(models.Model):
for product_id in picking.check_product_lines.mapped('product_id'):
# Totalkan quantity dari semua baris check.product untuk product_id ini
total_quantity = sum(
- line.quantity for line in picking.check_product_lines.filtered(lambda line: line.product_id == product_id)
+ line.quantity for line in
+ picking.check_product_lines.filtered(lambda line: line.product_id == product_id)
)
# Update quantity_done di move yang relevan
moves = picking.move_ids_without_package.filtered(lambda move: move.product_id == product_id)
@@ -1376,14 +1924,14 @@ class CheckProduct(models.Model):
if not moves:
raise UserError((
- "The product '%s' tidak ada di operations. "
- ) % record.product_id.display_name)
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
# Find existing lines for the same product, excluding the current line
existing_lines = record.picking_id.check_product_lines.filtered(
- lambda line: line.product_id == record.product_id and line.id != record.id
+ lambda line: line.product_id == record.product_id
)
if existing_lines:
@@ -1391,22 +1939,23 @@ class CheckProduct(models.Model):
first_line = existing_lines[0]
# Calculate the total quantity after addition
- total_quantity = sum(existing_lines.mapped('quantity')) - record.quantity
+ total_quantity = sum(existing_lines.mapped('quantity'))
if total_quantity > total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' sudah melebihi quantity demand."
- ) % (record.product_id.display_name))
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
else:
# Check if the quantity exceeds the allowed total
- if record.quantity > total_qty_in_moves:
+ if record.quantity == total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' sudah melebihi quantity demand."
- ) % (record.product_id.display_name))
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
# Set the quantity to the entered value
record.quantity = record.quantity
+
class BarcodeProduct(models.Model):
_name = 'barcode.product'
_description = 'Barcode Product'
@@ -1423,10 +1972,286 @@ class BarcodeProduct(models.Model):
product_id = fields.Many2one('product.product', string='Product', required=True)
barcode = fields.Char(string='Barcode')
+ def check_duplicate_barcode(self):
+ barcode_product = self.env['product.product'].search([('barcode', '=', self.barcode)])
+
+ if barcode_product:
+ raise UserError('Barcode sudah digunakan {}'.format(barcode_product.display_name))
+
+ barcode_box = self.env['product.product'].search([('barcode_box', '=', self.barcode)])
+
+ if barcode_box:
+ raise UserError('Barcode box sudah digunakan {}'.format(barcode_box.display_name))
+
@api.constrains('barcode')
def send_barcode_to_product(self):
for record in self:
+ record.check_duplicate_barcode()
if record.barcode and not record.product_id.barcode:
record.product_id.barcode = record.barcode
else:
- raise UserError('Barcode sudah terisi') \ No newline at end of file
+ raise UserError('Barcode sudah terisi')
+
+
+class CheckKoli(models.Model):
+ _name = 'check.koli'
+ _description = 'Check Koli'
+ _order = 'picking_id, id'
+ _rec_name = 'koli'
+
+ picking_id = fields.Many2one(
+ 'stock.picking',
+ string='Picking Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ koli = fields.Char(string='Koli')
+ reserved_id = fields.Many2one('stock.picking', string='Reserved Picking')
+ check_koli_progress = fields.Char(
+ string="Progress Check Koli"
+ )
+
+ @api.constrains('koli')
+ def _check_koli_progress(self):
+ for check in self:
+ if check.picking_id:
+ all_checks = self.env['check.koli'].search([('picking_id', '=', check.picking_id.id)], order='id')
+ if all_checks:
+ check_index = list(all_checks).index(check) + 1 # Nomor urut check
+ total_so_koli = len(all_checks)
+ check.check_koli_progress = f"{check_index}/{total_so_koli}" if total_so_koli else "0/0"
+
+
+class ScanKoli(models.Model):
+ _name = 'scan.koli'
+ _description = 'Scan Koli'
+ _order = 'picking_id, id'
+ _rec_name = 'koli_id'
+
+ picking_id = fields.Many2one(
+ 'stock.picking',
+ string='Picking Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ koli_id = fields.Many2one('sales.order.koli', string='Koli')
+ scan_koli_progress = fields.Char(
+ string="Progress Scan Koli",
+ compute="_compute_scan_koli_progress"
+ )
+ code_koli = fields.Char(string='Code Koli')
+
+ @api.onchange('code_koli')
+ def _onchange_code_koli(self):
+ if self.code_koli:
+ koli = self.env['sales.order.koli'].search([('koli_id.koli', '=', self.code_koli)], limit=1)
+ if koli:
+ self.write({'koli_id': koli.id})
+ else:
+ raise UserError('Koli tidak ditemukan')
+
+ # def _compute_scan_koli_progress(self):
+ # for scan in self:
+ # if scan.picking_id:
+ # all_scans = self.env['scan.koli'].search([('picking_id', '=', scan.picking_id.id)], order='id')
+ # if all_scans:
+ # scan_index = list(all_scans).index(scan) + 1 # Nomor urut scan
+ # total_so_koli = scan.picking_id.total_so_koli
+ # scan.scan_koli_progress = f"{scan_index}/{total_so_koli}" if total_so_koli else "0/0"
+
+ @api.onchange('koli_id')
+ def _onchange_koli_compare_with_konfirm_koli(self):
+ if not self.koli_id:
+ return
+
+ if not self.picking_id.konfirm_koli_lines:
+ raise UserError(_('Mapping Koli Harus Diisi!'))
+
+ koli_picking = self.koli_id.picking_id._origin
+
+ konfirm_pick_ids = [
+ line.pick_id._origin
+ for line in self.picking_id.konfirm_koli_lines
+ if line.pick_id
+ ]
+
+ if koli_picking not in konfirm_pick_ids:
+ raise UserError(_('Koli tidak sesuai dengan mapping koli, pastikan picking terkait benar!'))
+
+ @api.constrains('picking_id', 'koli_id')
+ def _check_duplicate_koli(self):
+ for record in self:
+ if record.koli_id:
+ existing_koli = self.search([
+ ('picking_id', '=', record.picking_id.id),
+ ('koli_id', '=', record.koli_id.id),
+ ('id', '!=', record.id)
+ ])
+ if existing_koli:
+ raise ValidationError(f"⚠️ Koli '{record.koli_id.display_name}' sudah discan untuk picking ini!")
+
+ def unlink(self):
+ picking_ids = set(self.mapped('koli_id.picking_id.id'))
+ for scan in self:
+ koli = scan.koli_id.koli_id
+ if koli:
+ koli.reserved_id = False
+
+ for picking_id in picking_ids:
+ remaining_scans = self.env['sales.order.koli'].search_count([
+ ('koli_id.picking_id', '=', picking_id)
+ ])
+
+ delete_koli = len(self.filtered(lambda rec: rec.koli_id.picking_id.id == picking_id))
+
+ if remaining_scans == delete_koli:
+ picking = self.env['stock.picking'].browse(picking_id)
+ picking.linked_out_picking_id = False
+ else:
+ raise UserError(
+ _("Tidak dapat menghapus scan koli, karena masih ada scan koli lain yang tersisa untuk picking ini."))
+
+ for picking_id in picking_ids:
+ self._reset_qty_done_if_no_scan(picking_id)
+
+ # self.check_koli_not_balance()
+
+ return super(ScanKoli, self).unlink()
+
+ @api.onchange('koli_id', 'scan_koli_progress')
+ def onchange_koli_id(self):
+ if not self.koli_id:
+ return
+
+ for scan in self:
+ if scan.koli_id.koli_id.picking_id.group_id.id != scan.picking_id.group_id.id:
+ scan.koli_id.koli_id.reserved_id = scan.picking_id.id.origin
+ scan.koli_id.koli_id.picking_id.linked_out_picking_id = scan.picking_id.id.origin
+
+ def _compute_scan_koli_progress(self):
+ for scan in self:
+ if not scan.picking_id:
+ scan.scan_koli_progress = "0/0"
+ continue
+
+ try:
+ all_scans = self.env['scan.koli'].search([('picking_id', '=', scan.picking_id.id)], order='id')
+ if all_scans:
+ scan_index = list(all_scans).index(scan) + 1
+ total_so_koli = scan.picking_id.total_so_koli or 0
+ scan.scan_koli_progress = f"{scan_index}/{total_so_koli}"
+ else:
+ scan.scan_koli_progress = "0/0"
+ except Exception:
+ # Fallback in case of any error
+ scan.scan_koli_progress = "0/0"
+
+ @api.constrains('picking_id', 'picking_id.total_so_koli')
+ def _check_koli_validation(self):
+ for scan in self.picking_id.scan_koli_lines:
+ scan.koli_id.koli_id.reserved_id = scan.picking_id.id
+ scan.koli_id.koli_id.picking_id.linked_out_picking_id = scan.picking_id.id
+
+ total_scans = len(self.picking_id.scan_koli_lines)
+ if total_scans != self.picking_id.total_so_koli:
+ raise UserError(_("Jumlah scan koli tidak sama dengan total SO koli!"))
+
+ # def check_koli_not_balance(self):
+ # for scan in self:
+ # total_scancs = self.env['scan.koli'].search_count([('picking_id', '=', scan.picking_id.id), ('id', '!=', scan.id)])
+ # if total_scancs != scan.picking_id.total_so_koli:
+ # raise UserError(_("Jumlah scan koli tidak sama dengan total SO koli!"))
+
+ @api.onchange('koli_id')
+ def _onchange_koli_id(self):
+ if not self.koli_id:
+ return
+
+ source_koli_so = self.picking_id.group_id.id
+ source_koli = self.koli_id.picking_id.group_id.id
+
+ if source_koli_so != source_koli:
+ raise UserError(_('Koli tidak sesuai, pastikan picking terkait benar!'))
+
+ @api.constrains('koli_id')
+ def _send_product_from_koli_id(self):
+ if not self.koli_id:
+ return
+
+ koli_count_by_picking = defaultdict(int)
+ for scan in self:
+ koli_count_by_picking[scan.koli_id.picking_id.id] += 1
+
+ for picking_id, total_koli in koli_count_by_picking.items():
+ picking = self.env['stock.picking'].browse(picking_id)
+
+ if total_koli == picking.quantity_koli:
+ pick_moves = self.env['stock.move.line'].search([('picking_id', '=', picking_id)])
+ out_moves = self.env['stock.move.line'].search([('picking_id', '=', picking.linked_out_picking_id.id)])
+
+ for pick_move in pick_moves:
+ corresponding_out_move = out_moves.filtered(lambda m: m.product_id == pick_move.product_id)
+ if corresponding_out_move:
+ corresponding_out_move.qty_done += pick_move.qty_done
+
+ def _reset_qty_done_if_no_scan(self, picking_id):
+ product_bu_pick = self.env['stock.move.line'].search([('picking_id', '=', picking_id)])
+
+ for move in product_bu_pick:
+ product_bu_out = self.env['stock.move.line'].search(
+ [('picking_id', '=', self.picking_id.id), ('product_id', '=', move.product_id.id)])
+ for bu_out in product_bu_out:
+ bu_out.qty_done -= move.qty_done
+ # if remaining_scans == 0:
+ # picking = self.env['stock.picking'].browse(picking_id)
+ # picking.move_line_ids_without_package.write({'qty_done': 0})
+ # picking.message_post(body=f"⚠ qty_done direset ke 0 untuk Picking {picking.name} karena tidak ada scan.koli yang tersisa.")
+
+ # return remaining_scans
+
+
+class KonfirmKoli(models.Model):
+ _name = 'konfirm.koli'
+ _description = 'Konfirm Koli'
+ _order = 'picking_id, id'
+ _rec_name = 'pick_id'
+
+ picking_id = fields.Many2one(
+ 'stock.picking',
+ string='Picking Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ pick_id = fields.Many2one('stock.picking', string='Pick')
+
+ @api.constrains('pick_id')
+ def _check_duplicate_pick_id(self):
+ for rec in self:
+ exist = self.search([
+ ('pick_id', '=', rec.pick_id.id),
+ ('picking_id', '=', rec.picking_id.id),
+ ('id', '!=', rec.id),
+ ])
+
+ if exist:
+ raise UserError(f"⚠️ '{rec.pick_id.display_name}' sudah discan untuk picking ini!")
+
+
+class WarningModalWizard(models.TransientModel):
+ _name = 'warning.modal.wizard'
+ _description = 'Peringatan Koli Belum Diperiksa'
+
+ name = fields.Char(default="⚠️ Perhatian!")
+ message = fields.Text()
+ picking_id = fields.Many2one('stock.picking')
+
+ def action_continue(self):
+ if self.picking_id:
+ return self.picking_id.with_context(skip_koli_check=True).button_validate()
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py
index d4347235..a683d80e 100644
--- a/indoteknik_custom/models/stock_picking_return.py
+++ b/indoteknik_custom/models/stock_picking_return.py
@@ -24,4 +24,15 @@ class ReturnPicking(models.TransientModel):
# if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids:
# raise UserError('Harus Approval Accounting AP untuk melakukan Retur')
- return res \ No newline at end of file
+ return res
+
+class ReturnPickingLine(models.TransientModel):
+ _inherit = 'stock.return.picking.line'
+
+ @api.onchange('quantity')
+ def _onchange_quantity(self):
+ for rec in self:
+ qty_done = rec.move_id.quantity_done
+
+ if rec.quantity > qty_done:
+ raise UserError(f"Quantity yang Anda masukkan tidak boleh melebihi quantity done yaitu: {qty_done}") \ No newline at end of file
diff --git a/indoteknik_custom/models/user_pengajuan_tempo_request.py b/indoteknik_custom/models/user_pengajuan_tempo_request.py
index abcb6f2f..87227764 100644
--- a/indoteknik_custom/models/user_pengajuan_tempo_request.py
+++ b/indoteknik_custom/models/user_pengajuan_tempo_request.py
@@ -110,7 +110,7 @@ class UserPengajuanTempoRequest(models.Model):
pic_tittle = fields.Char(string='Tittle PIC Penerimaan Barang', related='pengajuan_tempo_id.pic_tittle', store=True, readonly=False)
pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Barang', related='pengajuan_tempo_id.pic_mobile', store=True, readonly=False)
pic_name = fields.Char(string='Nama PIC Penerimaan Barang', related='pengajuan_tempo_id.pic_name', store=True, readonly=False)
- street_pengiriman = fields.Char(string="Alamat Perusahaan", related='pengajuan_tempo_id.street_pengiriman', store=True, readonly=False)
+ street_pengiriman = fields.Char(string="Alamat Pengiriman Barang", related='pengajuan_tempo_id.street_pengiriman', store=True, readonly=False)
state_id_pengiriman = fields.Many2one('res.country.state', string='State', related='pengajuan_tempo_id.state_id_pengiriman', store=True, readonly=False)
city_id_pengiriman = fields.Many2one('vit.kota', string='City', related='pengajuan_tempo_id.city_id_pengiriman', store=True, readonly=False)
district_id_pengiriman = fields.Many2one('vit.kecamatan', string='Kecamatan',related='pengajuan_tempo_id.district_id_pengiriman', store=True, readonly=False)
@@ -119,7 +119,7 @@ class UserPengajuanTempoRequest(models.Model):
invoice_pic_tittle = fields.Char(string='Tittle PIC Penerimaan Invoice', related='pengajuan_tempo_id.invoice_pic_tittle', store=True, readonly=False)
invoice_pic_mobile = fields.Char(string='Nomor HP PIC Penerimaan Invoice', related='pengajuan_tempo_id.invoice_pic_mobile', store=True, readonly=False)
invoice_pic = fields.Char(string='Nama PIC Penerimaan Invoice', related='pengajuan_tempo_id.invoice_pic', store=True, readonly=False)
- street_invoice = fields.Char(string="Alamat Perusahaan", related='pengajuan_tempo_id.street_invoice', store=True, readonly=False)
+ street_invoice = fields.Char(string="Alamat Pengiriman Invoice", related='pengajuan_tempo_id.street_invoice', store=True, readonly=False)
state_id_invoice = fields.Many2one('res.country.state', string='State', related='pengajuan_tempo_id.state_id_invoice', store=True, readonly=False)
city_id_invoice = fields.Many2one('vit.kota', string='City', related='pengajuan_tempo_id.city_id_invoice', store=True, readonly=False)
district_id_invoice = fields.Many2one('vit.kecamatan', string='Kecamatan', related='pengajuan_tempo_id.district_id_invoice', store=True, readonly=False)
@@ -365,13 +365,13 @@ class UserPengajuanTempoRequest(models.Model):
@api.onchange('tempo_duration')
def _tempo_duration_change(self):
for tempo in self:
- if tempo.env.user.id not in (7, 688, 28, 377, 12182):
+ if tempo.env.user.id not in (7, 688, 28, 377, 12182, 375):
raise UserError("Durasi tempo hanya bisa di ubah oleh Sales Manager atau Direktur")
@api.onchange('tempo_limit')
def _onchange_tempo_limit(self):
for tempo in self:
- if tempo.env.user.id not in (7, 688, 28, 377, 12182):
+ if tempo.env.user.id not in (7, 688, 28, 377, 12182, 375):
raise UserError("Limit tempo hanya bisa diubah oleh Sales Manager atau Direktur")
def button_approve(self):
for tempo in self:
@@ -381,7 +381,7 @@ class UserPengajuanTempoRequest(models.Model):
if tempo.env.user.id in (688, 28, 7):
raise UserError("Pengajuan tempo harus di approve oleh sales manager terlebih dahulu")
else:
- if tempo.env.user.id not in (377, 12182):
+ if tempo.env.user.id not in (377, 12182, 375):
# if tempo.env.user.id != 12182:
raise UserError("Pengajuan tempo hanya bisa di approve oleh sales manager")
else:
@@ -548,20 +548,7 @@ class UserPengajuanTempoRequest(models.Model):
('name', '=', contact_data['name'])
], limit=1)
- if existing_contact:
- # Pastikan tidak ada duplikasi nama dalam perusahaan yang sama
- duplicate_check = self.env['res.partner'].search([
- ('name', '=', contact_data['name']),
- ('id', '!=', existing_contact.id) # Hindari update yang menyebabkan duplikasi global
- ], limit=1)
-
- if not duplicate_check:
- # Perbarui hanya field yang tidak menyebabkan konflik
- update_data = {k: v for k, v in contact_data.items() if k != 'name'}
- existing_contact.write(update_data)
- else:
- raise UserError(f"Skipping update for {contact_data['name']} due to existing duplicate.")
- else:
+ if not existing_contact:
# Pastikan tidak ada partner lain dengan nama yang sama sebelum membuat baru
duplicate_check = self.env['res.partner'].search([
('name', '=', contact_data['name'])
@@ -695,6 +682,9 @@ class UserPengajuanTempoRequest(models.Model):
('active', 'in', [True, False])
])
+ def _message_get_suggested_recipients(self):
+ return {}
+
def format_currency(self, number):
number = int(number)
return "{:,}".format(number).replace(',', '.') \ No newline at end of file
diff --git a/indoteknik_custom/models/vendor_approval.py b/indoteknik_custom/models/vendor_approval.py
index 01d2e6a2..62fd5368 100644
--- a/indoteknik_custom/models/vendor_approval.py
+++ b/indoteknik_custom/models/vendor_approval.py
@@ -45,7 +45,7 @@ class VendorApproval(models.Model):
def action_approve(self):
for rec in self:
self.check_state_so()
- if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'):
raise UserError('Hanya Merchandiser yang bisa approve')
# Set state menjadi 'done'
@@ -65,8 +65,8 @@ class VendorApproval(models.Model):
def action_reject(self):
for rec in self:
self.check_state_so()
- if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
- raise UserError('Hanya Merchandiser yang bisa cancel')
+ if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'):
+ raise UserError('Hanya Procurement yang bisa cancel')
rec.state = 'cancel'
diff --git a/indoteknik_custom/models/voucher.py b/indoteknik_custom/models/voucher.py
index 101d4bcf..b213a039 100644
--- a/indoteknik_custom/models/voucher.py
+++ b/indoteknik_custom/models/voucher.py
@@ -11,43 +11,47 @@ class Voucher(models.Model):
name = fields.Char(string='Name')
image = fields.Binary(string='Image')
code = fields.Char(string='Code', help='Kode voucher yang akan berlaku untuk pengguna')
+ voucher_category = fields.Many2many('product.public.category', string='Category Voucher',
+ help='Kategori Produk yang dapat menggunakan voucher ini')
description = fields.Text(string='Description')
discount_amount = fields.Float(string='Discount Amount')
- discount_type = fields.Selection(string='Discount Type',
- selection=[
- ('percentage', 'Percentage'),
- ('fixed_price', 'Fixed Price'),
- ],
- help='Select the type of discount:\n'
- '- Percentage: Persentase dari total harga.\n'
- '- Fixed Price: Jumlah tetap yang dikurangi dari harga total.'
- )
- visibility = fields.Selection(string='Visibility',
- selection=[
- ('public', 'Public'),
- ('private', 'Private')
- ],
- help='Select the visibility:\n'
- '- Public: Ditampilkan kepada seluruh pengguna.\n'
- '- Private: Tidak ditampilkan kepada seluruh pengguna.'
- )
+ discount_type = fields.Selection(string='Discount Type',
+ selection=[
+ ('percentage', 'Percentage'),
+ ('fixed_price', 'Fixed Price'),
+ ],
+ help='Select the type of discount:\n'
+ '- Percentage: Persentase dari total harga.\n'
+ '- Fixed Price: Jumlah tetap yang dikurangi dari harga total.'
+ )
+ visibility = fields.Selection(string='Visibility',
+ selection=[
+ ('public', 'Public'),
+ ('private', 'Private')
+ ],
+ help='Select the visibility:\n'
+ '- Public: Ditampilkan kepada seluruh pengguna.\n'
+ '- Private: Tidak ditampilkan kepada seluruh pengguna.'
+ )
start_time = fields.Datetime(string='Start Time')
end_time = fields.Datetime(string='End Time')
- min_purchase_amount = fields.Integer(string='Min. Purchase Amount', help='Nominal minimum untuk dapat menggunakan voucher. Isi 0 jika tidak ada minimum purchase amount')
+ min_purchase_amount = fields.Integer(string='Min. Purchase Amount',
+ help='Nominal minimum untuk dapat menggunakan voucher. Isi 0 jika tidak ada minimum purchase amount')
max_discount_amount = fields.Integer(string='Max. Discount Amount', help='Max nominal terhadap persentase diskon')
order_ids = fields.One2many('sale.order', 'applied_voucher_id', string='Order')
limit = fields.Integer(
- string='Limit',
+ string='Limit',
default=0,
help='Batas penggunaan voucher keseluruhan. Isi dengan angka 0 untuk penggunaan tanpa batas'
)
limit_user = fields.Integer(
- string='Limit User',
+ string='Limit User',
default=0,
help='Batas penggunaan voucher per pengguna. Misalnya, jika diisi dengan angka 1, maka setiap pengguna hanya dapat menggunakan voucher ini satu kali. Isi dengan angka 0 untuk penggunaan tanpa batas'
)
manufacture_ids = fields.Many2many('x_manufactures', string='Brands', help='Voucher appplied only for brand')
- excl_pricelist_ids = fields.Many2many('product.pricelist', string='Excluded Pricelists', help='Hide voucher from selected exclude pricelist')
+ excl_pricelist_ids = fields.Many2many('product.pricelist', string='Excluded Pricelists',
+ help='Hide voucher from selected exclude pricelist')
voucher_line = fields.One2many('voucher.line', 'voucher_id', 'Voucher Line')
terms_conditions = fields.Html('Terms and Conditions')
apply_type = fields.Selection(string='Apply Type', default="all", selection=[
@@ -64,12 +68,40 @@ class Voucher(models.Model):
('person', "Account Individu"),
('company', "Account Company"),
])
+
+ def is_voucher_applicable(self, product_id):
+ if not self.voucher_category:
+ return True
+
+ public_categories = product_id.public_categ_ids
+
+ return bool(set(public_categories.ids) & set(self.voucher_category.ids))
+
+ def is_voucher_applicable_for_category(self, category):
+ if not self.voucher_category:
+ return True
+
+ if category.id in self.voucher_category.ids:
+ return True
+
+ category_path = []
+ current_cat = category
+ while current_cat:
+ category_path.append(current_cat.id)
+ current_cat = current_cat.parent_id
+
+ for voucher_cat in self.voucher_category:
+ if voucher_cat.id in category_path:
+ return True
+
+ return False
+
@api.constrains('description')
def _check_description_length(self):
for record in self:
if record.description and len(record.description) > 120:
raise ValidationError('Deskripsi tidak boleh lebih dari 120 karakter')
-
+
@api.constrains('limit', 'limit_user')
def _check_limit(self):
for rec in self:
@@ -87,7 +119,7 @@ class Voucher(models.Model):
def res_format(self):
datas = [voucher.format() for voucher in self]
return datas
-
+
def format(self):
ir_attachment = self.env['ir.attachment']
data = {
@@ -100,7 +132,7 @@ class Voucher(models.Model):
'remaining_time': self._res_remaining_time(),
}
return data
-
+
def _res_remaining_time(self):
seconds = self._get_remaining_time()
remaining_time = timedelta(seconds=seconds)
@@ -116,14 +148,31 @@ class Voucher(models.Model):
time = minutes
unit = 'menit'
return f'{time} {unit}'
-
+
def _get_remaining_time(self):
calculate_time = self.end_time - datetime.now()
return round(calculate_time.total_seconds())
-
+
def filter_order_line(self, order_line):
+ # import logging
+ # _logger = logging.getLogger(__name__)
+
voucher_manufacture_ids = self.collect_manufacture_ids()
results = []
+
+ if self.voucher_category and len(order_line) > 0:
+ for line in order_line:
+ category_applicable = False
+ for category in line['product_id'].public_categ_ids:
+ if self.is_voucher_applicable_for_category(category):
+ category_applicable = True
+ break
+
+ if not category_applicable:
+ # _logger.info("Cart contains product %s with non-applicable category - voucher %s cannot be used",
+ # line['product_id'].name, self.code)
+ return []
+
for line in order_line:
manufacture_id = line['product_id'].x_manufacture.id or None
if self.apply_type == 'brand' and manufacture_id not in voucher_manufacture_ids:
@@ -132,35 +181,36 @@ class Voucher(models.Model):
product_flashsale = line['product_id']._get_active_flash_sale()
if len(product_flashsale) > 0:
continue
-
+
results.append(line)
-
+
return results
-
+
def calc_total_order_line(self, order_line):
- result = { 'all': 0, 'brand': {} }
+ result = {'all': 0, 'brand': {}}
for line in order_line:
manufacture_id = line['product_id'].x_manufacture.id or None
manufacture_total = result['brand'].get(manufacture_id, 0)
result['brand'][manufacture_id] = manufacture_total + line['subtotal']
result['all'] += line['subtotal']
-
+
return result
-
+
def calc_discount_amount(self, total):
- result = { 'all': 0, 'brand': {} }
+ result = {'all': 0, 'brand': {}}
if self.apply_type in ['all', 'shipping']:
if total['all'] < self.min_purchase_amount:
return result
-
+
if self.discount_type == 'percentage':
decimal_discount = self.discount_amount / 100
discount_all = total['all'] * decimal_discount
- result['all'] = min(discount_all, self.max_discount_amount) if self.max_discount_amount > 0 else discount_all
+ result['all'] = min(discount_all,
+ self.max_discount_amount) if self.max_discount_amount > 0 else discount_all
else:
result['all'] = min(self.discount_amount, total['all'])
-
+
return result
for line in self.voucher_line:
@@ -173,95 +223,141 @@ class Voucher(models.Model):
elif line.discount_type == 'percentage':
decimal_discount = line.discount_amount / 100
discount_brand = total_brand * decimal_discount
- discount_brand = min(discount_brand, line.max_discount_amount) if line.max_discount_amount > 0 else discount_brand
+ discount_brand = min(discount_brand,
+ line.max_discount_amount) if line.max_discount_amount > 0 else discount_brand
else:
discount_brand = min(line.discount_amount, total_brand)
-
+
result['brand'][manufacture_id] = round(discount_brand, 2)
result['all'] += discount_brand
-
+
result['all'] = round(result['all'], 2)
return result
def apply(self, order_line):
- order_line = self.filter_order_line(order_line)
- amount_total = self.calc_total_order_line(order_line)
+
+ filtered_order_line = self.filter_order_line(order_line)
+
+ amount_total = self.calc_total_order_line(filtered_order_line)
+
discount = self.calc_discount_amount(amount_total)
+
return {
'discount': discount,
'total': amount_total,
'type': self.apply_type,
- 'valid_order': order_line
+ 'valid_order': filtered_order_line,
}
-
+
def collect_manufacture_ids(self):
return [x.manufacture_id.id for x in self.voucher_line]
-
+
def calculate_discount(self, price):
if price < self.min_purchase_amount:
return 0
-
+
if self.discount_type == 'fixed_price':
return self.discount_amount
-
+
if self.discount_type == 'percentage':
discount = price * self.discount_amount / 100
max_disc = self.max_discount_amount
return discount if max_disc == 0 else min(discount, max_disc)
-
+
return 0
-
+
def get_active_voucher(self, domain):
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
domain += [
('start_time', '<=', current_time),
('end_time', '>=', current_time),
]
- vouchers = self.search(domain, order='min_purchase_amount ASC')
+ vouchers = self.search(domain, order='min_purchase_amount ASC')
return vouchers
-
+
def generate_tnc(self):
+ def format_currency(amount):
+ formatted_number = '{:,.0f}'.format(amount).replace(',', '.')
+ return f'Rp{formatted_number}'
+
tnc = []
tnc.append('<ol>')
- tnc.append('<li>Voucher hanya berlaku apabila pembelian Pengguna sudah memenuhi syarat dan ketentuan yang tertera pada voucher</li>')
+ tnc.append(
+ '<li>Voucher hanya berlaku apabila pembelian Pengguna sudah memenuhi syarat dan ketentuan yang tertera pada voucher</li>')
tnc.append(f'<li>Voucher berlaku {self._res_remaining_time()} lagi</li>')
tnc.append(f'<li>Voucher tidak bisa digunakan apabila terdapat produk flash sale</li>')
- if len(self.voucher_line) > 0:
- brand_names = ', '.join([x.manufacture_id.x_name or '' for x in self.voucher_line])
- tnc.append(f'<li>Voucher berlaku untuk produk dari brand {brand_names}</li>')
- tnc.append(self.generate_detail_tnc())
+
+ if self.apply_type == 'brand':
+ tnc.append(f'<li>Voucher berlaku untuk produk dari brand terpilih</li>')
+ tnc.append(
+ f'<li>Nominal potongan produk yang bisa didapatkan hingga 10 Juta dengan minimum pembelian 10 Ribu.</li>')
+ elif self.apply_type == 'all' or self.apply_type == 'shipping':
+ if self.voucher_category:
+ category_names = ', '.join([cat.name for cat in self.voucher_category])
+ tnc.append(
+ f'<li>Voucher hanya berlaku untuk produk dalam kategori {category_names} dan sub-kategorinya</li>')
+ tnc.append(
+ f'<li>Voucher tidak dapat digunakan jika ada produk di keranjang yang tidak termasuk dalam kategori tersebut</li>')
+
+ if self.discount_type == 'percentage' and self.apply_type != 'brand':
+ tnc.append(
+ f'<li>Nominal potongan produk yang bisa didapatkan sebesar {self.max_discount_amount}% dengan minimum pembelian {self.min_purchase_amount}</li>')
+ elif self.discount_type == 'percentage' and self.apply_type != 'brand':
+ tnc.append(
+ f'<li>Nominal potongan produk yang bisa didapatkan sebesar {format_currency(self.discount_amount)} dengan minimum pembelian {format_currency(self.min_purchase_amount)}</li>')
+
tnc.append('</ol>')
-
- return ' '.join(tnc)
-
- def generate_detail_tnc(self):
- def format_currency(amount):
- formatted_number = '{:,.0f}'.format(amount).replace(',', '.')
- return f'Rp{formatted_number}'
-
- tnc = []
- if self.apply_type == 'all':
- tnc.append('<li>')
- tnc.append('Nominal potongan yang bisa didapatkan sebesar')
- tnc.append(f'{self.discount_amount}%' if self.discount_type == 'percentage' else format_currency(self.discount_amount))
-
- if self.discount_type == 'percentage' and self.max_discount_amount > 0:
- tnc.append(f'hingga {format_currency(self.max_discount_amount)}')
-
- tnc.append(f'dengan minimum pembelian {format_currency(self.min_purchase_amount)}' if self.min_purchase_amount > 0 else 'tanpa minimum pembelian')
- tnc.append('</li>')
- else:
- for line in self.voucher_line:
- line_tnc = []
- line_tnc.append(f'Nominal potongan produk {line.manufacture_id.x_name} yang bisa didapatkan sebesar')
- line_tnc.append(f'{line.discount_amount}%' if line.discount_type == 'percentage' else format_currency(line.discount_amount))
-
- if line.discount_type == 'percentage' and line.max_discount_amount > 0:
- line_tnc.append(f'hingga {format_currency(line.max_discount_amount)}')
-
- line_tnc.append(f'dengan minimum pembelian {format_currency(line.min_purchase_amount)}' if line.min_purchase_amount > 0 else 'tanpa minimum pembelian')
- line_tnc = ' '.join(line_tnc)
- tnc.append(f'<li>{line_tnc}</li>')
+ # tnc.append(self.generate_detail_tnc())
return ' '.join(tnc)
+ # def generate_detail_tnc(self):
+ # def format_currency(amount):
+ # formatted_number = '{:,.0f}'.format(amount).replace(',', '.')
+ # return f'Rp{formatted_number}'
+ #
+ # tnc = []
+ # if self.apply_type == 'all':
+ # tnc.append('<li>')
+ # tnc.append('Nominal potongan yang bisa didapatkan sebesar')
+ # tnc.append(f'{self.discount_amount}%' if self.discount_type == 'percentage' else format_currency(
+ # self.discount_amount))
+ #
+ # if self.discount_type == 'percentage' and self.max_discount_amount > 0:
+ # tnc.append(f'hingga {format_currency(self.max_discount_amount)}')
+ #
+ # tnc.append(
+ # f'dengan minimum pembelian {format_currency(self.min_purchase_amount)}' if self.min_purchase_amount > 0 else 'tanpa minimum pembelian')
+ # tnc.append('</li>')
+ # else:
+ # for line in self.voucher_line:
+ # line_tnc = []
+ # line_tnc.append(f'Nominal potongan produk {line.manufacture_id.x_name} yang bisa didapatkan sebesar')
+ # line_tnc.append(f'{line.discount_amount}%' if line.discount_type == 'percentage' else format_currency(
+ # line.discount_amount))
+ #
+ # if line.discount_type == 'percentage' and line.max_discount_amount > 0:
+ # line_tnc.append(f'hingga {format_currency(line.max_discount_amount)}')
+ #
+ # line_tnc.append(
+ # f'dengan minimum pembelian {format_currency(line.min_purchase_amount)}' if line.min_purchase_amount > 0 else 'tanpa minimum pembelian')
+ # line_tnc = ' '.join(line_tnc)
+ # tnc.append(f'<li>{line_tnc}</li>')
+ # return ' '.join(tnc)
+
+ # copy semua data kalau diduplicate
+ def copy(self, default=None):
+ default = dict(default or {})
+ voucher_lines = []
+
+ for line in self.voucher_line:
+ voucher_lines.append((0, 0, {
+ 'manufacture_id': line.manufacture_id.id,
+ 'discount_amount': line.discount_amount,
+ 'discount_type': line.discount_type,
+ 'min_purchase_amount': line.min_purchase_amount,
+ 'max_discount_amount': line.max_discount_amount,
+ }))
+
+ default['voucher_line'] = voucher_lines
+ return super(Voucher, self).copy(default)
diff --git a/indoteknik_custom/models/website_user_cart.py b/indoteknik_custom/models/website_user_cart.py
index 44393cf1..a6d08949 100644
--- a/indoteknik_custom/models/website_user_cart.py
+++ b/indoteknik_custom/models/website_user_cart.py
@@ -1,10 +1,11 @@
from odoo import fields, models, api
from datetime import datetime, timedelta
+
class WebsiteUserCart(models.Model):
_name = 'website.user.cart'
_rec_name = 'user_id'
-
+
user_id = fields.Many2one('res.users', string='User')
product_id = fields.Many2one('product.product', string='Product')
program_line_id = fields.Many2one('promotion.program.line', string='Program', help="Apply program")
@@ -18,7 +19,8 @@ class WebsiteUserCart(models.Model):
is_reminder = fields.Boolean(string='Reminder?')
phone_user = fields.Char(string='Phone', related='user_id.mobile')
price = fields.Float(string='Price', compute='_compute_price')
- program_product_id = fields.Many2one('product.product', string='Program Products', compute='_compute_program_product_ids')
+ program_product_id = fields.Many2one('product.product', string='Program Products',
+ compute='_compute_program_product_ids')
@api.depends('program_line_id')
def _compute_program_product_ids(self):
@@ -55,6 +57,12 @@ class WebsiteUserCart(models.Model):
product = self.product_id.v2_api_single_response(self.product_id)
res.update(product)
+ # Add category information
+ res['categories'] = [{
+ 'id': cat.id,
+ 'name': cat.name
+ } for cat in self.product_id.public_categ_ids]
+
# Check if the product's inventory location is in ID 57 or 83
target_locations = [57, 83]
stock_quant = self.env['stock.quant'].search([
@@ -90,7 +98,14 @@ class WebsiteUserCart(models.Model):
def get_products(self):
products = [x.get_product() for x in self]
-
+
+ for i, cart_item in enumerate(self):
+ if cart_item.product_id and i < len(products):
+ products[i]['categories'] = [{
+ 'id': cat.id,
+ 'name': cat.name
+ } for cat in cart_item.product_id.public_categ_ids]
+
return products
def get_product_by_user(self, user_id, selected=False, source=False):
@@ -121,10 +136,10 @@ class WebsiteUserCart(models.Model):
products = products_active.get_products()
return products
-
+
def get_user_checkout(self, user_id, voucher=False, voucher_shipping=False, source=False):
products = self.get_product_by_user(user_id=user_id, selected=True, source=source)
-
+
total_purchase = 0
total_discount = 0
for product in products:
@@ -132,9 +147,9 @@ class WebsiteUserCart(models.Model):
price = product['package_price'] * product['quantity']
else:
price = product['price']['price'] * product['quantity']
-
+
discount_price = price - product['price']['price_discount'] * product['quantity']
-
+
total_purchase += price
total_discount += discount_price
@@ -142,7 +157,7 @@ class WebsiteUserCart(models.Model):
discount_voucher = 0
discount_voucher_shipping = 0
order_line = []
-
+
if voucher or voucher_shipping:
for product in products:
if product['cart_type'] == 'promotion': continue
@@ -153,16 +168,16 @@ class WebsiteUserCart(models.Model):
'qty': product['quantity'],
'subtotal': product['subtotal']
})
-
+
if voucher:
voucher_info = voucher.apply(order_line)
discount_voucher = voucher_info['discount']['all']
subtotal -= discount_voucher
-
+
if voucher_shipping:
voucher_shipping_info = voucher_shipping.apply(order_line)
- discount_voucher_shipping = voucher_shipping_info['discount']['all']
-
+ discount_voucher_shipping = voucher_shipping_info['discount']['all']
+
tax = round(subtotal * 0.11)
grand_total = subtotal + tax
total_weight = sum(x['weight'] * x['quantity'] for x in products)
@@ -179,28 +194,31 @@ class WebsiteUserCart(models.Model):
'kg': total_weight,
'g': total_weight * 1000
},
- 'has_product_without_weight': any(not product.get('weight') or product.get('weight') == 0 for product in products),
+ 'has_product_without_weight': any(
+ not product.get('weight') or product.get('weight') == 0 for product in products),
'products': products
}
return result
-
+
def action_mail_reminder_to_checkout(self, limit=200):
user_ids = self.search([('is_reminder', '=', False)]).mapped('user_id')[:limit]
-
+
for user in user_ids:
latest_cart = self.search([('user_id', '=', user.id)], order='create_date desc', limit=1)
-
+
carts_to_remind = self.search([('user_id', '=', user.id)])
-
+
if latest_cart and not latest_cart.is_reminder:
for cart in carts_to_remind:
check = cart.check_product_flashsale(cart.product_id.id)
- if not cart.program_line_id and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code and check['is_flashsale'] == False:
+ if not cart.program_line_id and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code and \
+ check['is_flashsale'] == False:
cart.is_selected = True
- if cart.program_line_id or check['is_flashsale'] or cart.product_id.default_code and 'BOM' in cart.product_id.default_code:
+ if cart.program_line_id or check[
+ 'is_flashsale'] or cart.product_id.default_code and 'BOM' in cart.product_id.default_code:
cart.is_selected = False
cart.is_reminder = True
-
+
template = self.env.ref('indoteknik_custom.mail_template_user_cart_reminder_to_checkout')
template.send_mail(latest_cart.id, force_send=True)
@@ -234,8 +252,9 @@ class WebsiteUserCart(models.Model):
break
product_discount = subtotal_promo if cart.program_line_id or check['is_flashsale'] else subtotal
- total_discount += product_discount
- if check['is_flashsale'] == False and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code:
+ total_discount += product_discount
+ if check[
+ 'is_flashsale'] == False and cart.product_id.default_code and not 'BOM' in cart.product_id.default_code:
voucher_product = subtotal * (discount_amount / 100.0)
total_voucher += voucher_product
@@ -253,14 +272,15 @@ class WebsiteUserCart(models.Model):
def check_product_flashsale(self, product_id):
product = product_id
current_time = datetime.utcnow()
- found_product = self.env['product.pricelist.item'].search([('product_id', '=', product_id), ('pricelist_id.is_flash_sale', '=', True)])
+ found_product = self.env['product.pricelist.item'].search(
+ [('product_id', '=', product_id), ('pricelist_id.is_flash_sale', '=', True)])
if found_product:
for found in found_product:
pricelist_flashsale = found.pricelist_id
if pricelist_flashsale.start_date <= current_time <= pricelist_flashsale.end_date:
- return {
+ return {
'is_flashsale': True,
'price': found.fixed_price
}
@@ -269,10 +289,9 @@ class WebsiteUserCart(models.Model):
'is_flashsale': False
}
- return {
+ return {
'is_flashsale': False
}
-
# if found_product:
# start_date = found_product.pricelist_id.start_date
@@ -291,26 +310,26 @@ class WebsiteUserCart(models.Model):
# return {
# 'is_flashsale': False
# }
-
+
def get_data_promo(self, program_line_id):
program_line_product = self.env['promotion.product'].search([
('program_line_id', '=', program_line_id)
- ])
-
+ ])
+
program_free_product = self.env['promotion.free_product'].search([
('program_line_id', '=', program_line_id)
- ])
+ ])
return program_line_product, program_free_product
-
+
def get_weight_product(self, program_line_id):
program_line_product = self.env['promotion.product'].search([
('program_line_id', '=', program_line_id)
- ])
-
+ ])
+
program_free_product = self.env['promotion.free_product'].search([
('program_line_id', '=', program_line_id)
- ])
-
+ ])
+
real_weight = 0.0
if program_line_product:
for product in program_line_product:
@@ -321,16 +340,16 @@ class WebsiteUserCart(models.Model):
real_weight += product.product_id.weight
return real_weight
-
+
def get_price_coret(self, program_line_id):
program_line_product = self.env['promotion.product'].search([
('program_line_id', '=', program_line_id)
- ])
-
+ ])
+
program_free_product = self.env['promotion.free_product'].search([
('program_line_id', '=', program_line_id)
- ])
-
+ ])
+
price_coret = 0.0
for product in program_line_product:
price = self.get_price_website(product.product_id.id)
@@ -340,20 +359,22 @@ class WebsiteUserCart(models.Model):
price = self.get_price_website(product.product_id.id)
price_coret += price['price'] * product.qty
- return price_coret
-
+ return price_coret
+
def get_price_website(self, product_id):
- price_website = self.env['product.pricelist.item'].search([('product_id', '=', product_id), ('pricelist_id', '=', 17022)], limit=1)
-
- price_tier = self.env['product.pricelist.item'].search([('product_id', '=', product_id), ('pricelist_id', '=', 17023)], limit=1)
-
+ price_website = self.env['product.pricelist.item'].search(
+ [('product_id', '=', product_id), ('pricelist_id', '=', 17022)], limit=1)
+
+ price_tier = self.env['product.pricelist.item'].search(
+ [('product_id', '=', product_id), ('pricelist_id', '=', 17023)], limit=1)
+
fixed_price = price_website.fixed_price if price_website else 0.0
discount = price_tier.price_discount if price_tier else 0.0
-
+
discounted_price = fixed_price - (fixed_price * discount / 100)
-
+
final_price = discounted_price / 1.11
-
+
return {
'price': final_price,
'web_price': discounted_price
@@ -365,4 +386,4 @@ class WebsiteUserCart(models.Model):
def format_currency(self, number):
number = int(number)
- return "{:,}".format(number).replace(',', '.') \ No newline at end of file
+ return "{:,}".format(number).replace(',', '.')
diff --git a/indoteknik_custom/models/x_banner_banner.py b/indoteknik_custom/models/x_banner_banner.py
index 810bdf39..16d54b02 100755
--- a/indoteknik_custom/models/x_banner_banner.py
+++ b/indoteknik_custom/models/x_banner_banner.py
@@ -25,4 +25,5 @@ class XBannerBanner(models.Model):
('4', '4')
], string='Group by Week')
x_headline_banner = fields.Text(string="Headline Banner")
- x_description_banner = fields.Text(string="Description Banner") \ No newline at end of file
+ x_description_banner = fields.Text(string="Description Banner")
+ x_keyword_banner = fields.Text(string="Keyword Banner") \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 4d0e51eb..601f04c5 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -138,6 +138,7 @@ access_shipment_group,access.shipment.group,model_shipment_group,,1,1,1,1
access_shipment_group_line,access.shipment.group.line,model_shipment_group_line,,1,1,1,1
access_sales_order_reject,access.sales.order.reject,model_sales_order_reject,,1,1,1,1
access_approval_date_doc,access.approval.date.doc,model_approval_date_doc,,1,1,1,1
+access_approval_invoice_date,access.approval.invoice.date,model_approval_invoice_date,,1,1,1,1
access_account_tax,access.account.tax,model_account_tax,,1,1,1,1
access_approval_unreserve,access.approval.unreserve,model_approval_unreserve,,1,1,1,1
access_approval_unreserve_line,access.approval.unreserve.line,model_approval_unreserve_line,,1,1,1,1
@@ -153,9 +154,17 @@ access_va_multi_approve,access.va.multi.approve,model_va_multi_approve,,1,1,1,1
access_va_multi_reject,access.va.multi.reject,model_va_multi_reject,,1,1,1,1
access_vendor_sla,access.vendor_sla,model_vendor_sla,,1,1,1,1
access_check_product,access.check.product,model_check_product,,1,1,1,1
+access_check_bom_product,access.check.bom.product,model_check_bom_product,,1,1,1,1
+access_check_koli,access.check.koli,model_check_koli,,1,1,1,1
+access_scan_koli,access.scan.koli,model_scan_koli,,1,1,1,1
+access_konfirm_koli,access.konfirm.koli,model_konfirm_koli,,1,1,1,1
access_stock_immediate_transfer,access.stock.immediate.transfer,model_stock_immediate_transfer,,1,1,1,1
access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1
access_purchase_order_unlock_wizard,access.purchase.order.unlock.wizard,model_purchase_order_unlock_wizard,,1,1,1,1
+access_sales_order_koli,access.sales.order.koli,model_sales_order_koli,,1,1,1,1
+access_stock_backorder_confirmation,access.stock.backorder.confirmation,model_stock_backorder_confirmation,,1,1,1,1
+access_warning_modal_wizard,access.warning.modal.wizard,model_warning_modal_wizard,,1,1,1,1
+
access_User_pengajuan_tempo_line,access.user.pengajuan.tempo.line,model_user_pengajuan_tempo_line,,1,1,1,1
access_user_pengajuan_tempo,access.user.pengajuan.tempo,model_user_pengajuan_tempo,,1,1,1,1
access_reject_reason_wizard,reject.reason.wizard,model_reject_reason_wizard,,1,1,1,0
@@ -167,4 +176,8 @@ access_barcoding_product_line,access.barcoding.product.line,model_barcoding_prod
access_account_payment_register,access.account.payment.register,model_account_payment_register,,1,1,1,1
access_stock_inventory,access.stock.inventory,model_stock_inventory,,1,1,1,1
access_cancel_reason_order,cancel.reason.order,model_cancel_reason_order,,1,1,1,0
+access_reject_reason_commision,reject.reason.commision,model_reject_reason_commision,,1,1,1,0
access_shipping_option,shipping.option,model_shipping_option,,1,1,1,1
+access_production_purchase_match,access.production.purchase.match,model_production_purchase_match,,1,1,1,1
+access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1
+access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index 17263c3a..46737a40 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -92,6 +92,7 @@
<field name="is_efaktur_exported" optional="hide"/>
<field name="invoice_day_to_due" attrs="{'invisible': [['payment_state', 'in', ('paid', 'in_payment', 'reversed')]]}" optional="hide"/>
<field name="new_invoice_day_to_due" attrs="{'invisible': [['payment_state', 'in', ('paid', 'in_payment', 'reversed')]]}" optional="hide"/>
+ <field name="length_of_payment" optional="hide"/>
<field name="mark_upload_efaktur" optional="hide" widget="badge"
decoration-danger="mark_upload_efaktur == 'belum_upload'"
decoration-success="mark_upload_efaktur == 'sudah_upload'" />
diff --git a/indoteknik_custom/views/approval_invoice_date.xml b/indoteknik_custom/views/approval_invoice_date.xml
new file mode 100644
index 00000000..31f346e7
--- /dev/null
+++ b/indoteknik_custom/views/approval_invoice_date.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="approval_invoice_date_tree" model="ir.ui.view">
+ <field name="name">approval.invoice.date.tree</field>
+ <field name="model">approval.invoice.date</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="number"/>
+ <field name="picking_id"/>
+ <field name="partner_id"/>
+ <field name="sale_id"/>
+ <field name="date_doc_do"/>
+ <field name="date_invoice"/>
+ <field name="state" widget="badge" decoration-danger="state == 'cancel'"
+ decoration-success="state == 'done'"
+ decoration-info="state == 'draft'"/>
+ <field name="approve_date"/>
+ <field name="approve_by"/>
+ <field name="create_uid"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="approval_invoice_date_form" model="ir.ui.view">
+ <field name="name">approval.invoice.date.form</field>
+ <field name="model">approval.invoice.date</field>
+ <field name="arch" type="xml">
+ <form>
+ <header>
+ <button name="button_approve"
+ string="Approve"
+ type="object"
+ attrs="{'invisible': [('state', '=', 'done')]}"
+ />
+ <button name="button_cancel"
+ string="Cancel"
+ type="object"
+ attrs="{'invisible': [('state', '=', 'cancel')]}"
+ />
+ <field name="state" widget="statusbar"
+ statusbar_visible="draft,cancel,done"
+ statusbar_colors='{"cancel":"red", "done":"green"}'/>
+ </header>
+ <sheet string="Approval Invoice Date">
+ <group>
+ <group>
+ <field name="number"/>
+ <field name="picking_id"/>
+ <field name="partner_id"/>
+ <field name="sale_id"/>
+ <field name="move_id"/>
+ <field name="date_doc_do"/>
+ <field name="date_invoice"/>
+ <field name="approve_date"/>
+ <field name="approve_by"/>
+ <field name="create_uid"/>
+ <field name="note" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_approval_invoice_date_filter" model="ir.ui.view">
+ <field name="name">approval.invoice.date.list.select</field>
+ <field name="model">approval.invoice.date</field>
+ <field name="priority" eval="15"/>
+ <field name="arch" type="xml">
+ <search string="Search Approval Invoice Date">
+ <field name="number"/>
+ <field name="partner_id"/>
+ <field name="picking_id"/>
+ <field name="sale_id"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="approval_invoice_date_action" model="ir.actions.act_window">
+ <field name="name">Approval Invoice Date</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">approval.invoice.date</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem id="menu_approval_invoice_date" name="Approval Invoice Date"
+ parent="account.menu_finance_receivables"
+ action="approval_invoice_date_action"
+ sequence="100"
+ />
+
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/barcoding_product.xml b/indoteknik_custom/views/barcoding_product.xml
index c7473d39..b259f1e8 100644
--- a/indoteknik_custom/views/barcoding_product.xml
+++ b/indoteknik_custom/views/barcoding_product.xml
@@ -20,6 +20,7 @@
<tree>
<field name="product_id"/>
<field name="qr_code_variant" widget="image"/>
+ <field name="sequence_with_total" attrs="{'invisible': [['parent.type', 'not in', ('multiparts')]]}"/>
</tree>
</field>
</record>
@@ -34,12 +35,13 @@
<group>
<field name="product_id" required="1"/>
<field name="type" required="1"/>
- <field name="quantity" attrs="{'invisible': [['type', 'in', ('barcoding')]], 'required': [['type', 'not in', ('barcoding')]]}"/>
- <field name="barcode" attrs="{'invisible': [['type', 'in', ('print')]], 'required': [['type', 'not in', ('print')]]}"/>
+ <field name="quantity" attrs="{'invisible': [['type', 'in', ('barcoding','barcoding_box')]], 'required': [['type', 'not in', ('barcoding')]]}"/>
+ <field name="barcode" attrs="{'invisible': [['type', 'in', ('print','multiparts')]], 'required': [['type', 'not in', ('print','multiparts')]]}"/>
+ <field name="qty_pcs_box" attrs="{'invisible': [['type', 'in', ('print','barcoding','multiparts')]], 'required': [['type', 'not in', ('print','barcoding','multiparts')]]}"/>
</group>
</group>
<notebook>
- <page string="Line" attrs="{'invisible': [['type', 'in', ('barcoding')]]}">
+ <page string="Line" attrs="{'invisible': [['type', 'in', ('barcoding','barcoding_box')]]}">
<field name="barcoding_product_line"/>
</page>
</notebook>
diff --git a/indoteknik_custom/views/customer_commision.xml b/indoteknik_custom/views/customer_commision.xml
index bb1628bc..37df16ff 100644
--- a/indoteknik_custom/views/customer_commision.xml
+++ b/indoteknik_custom/views/customer_commision.xml
@@ -11,14 +11,15 @@
<field name="partner_ids" widget="many2many_tags"/>
<field name="commision_percent"/>
<field name="commision_amt" readonly="1"/>
- <field name="status" readonly="1"/>
+ <field name="status" readonly="1" decoration-success="status == 'approved'" widget="badge"
+ optional="show"/>
<field name="payment_status" readonly="1"
- decoration-success="payment_status == 'payment'"
- decoration-danger="payment_status == 'pending'"
- widget="badge"/>
+ decoration-success="payment_status == 'payment'"
+ decoration-danger="payment_status == 'pending'"
+ widget="badge"/>
<field name="brand_ids" widget="many2many_tags"/>
- <field name="grouped_so_number" readonly="1"/>
- <field name="grouped_invoice_number" readonly="1"/>
+ <field name="grouped_so_number" readonly="1" optional="hide"/>
+ <field name="grouped_invoice_number" readonly="1" optional="hide"/>
</tree>
</field>
</record>
@@ -30,10 +31,12 @@
<tree editable="top" create="false">
<field name="partner_id" readonly="1"/>
<field name="invoice_id" readonly="1"/>
+ <field name="sale_order_id" readonly="1"/>
<field name="state" readonly="1"/>
<field name="product_id" readonly="1" optional="hide"/>
<field name="dpp" readonly="1"/>
<field name="total_percent_margin" readonly="1"/>
+ <field name="total_margin_excl_third_party" readonly="1"/>
<field name="tax" readonly="1" optional="hide"/>
<field name="total" readonly="1" optional="hide"/>
</tree>
@@ -45,67 +48,112 @@
<field name="model">customer.commision</field>
<field name="arch" type="xml">
<form>
+ <!-- attrs="{'invisible': [('status', 'in', ['draft','pengajuan1','pengajuan2','pengajuan3','pengajuan4'])]}"-->
<header>
- <button name="action_confirm_customer_commision"
- string="Confirm" type="object"
- options="{}"/>
+ <button name="action_confirm_customer_commision"
+ string="Confirm" type="object"
+ attrs="{'invisible': [('status', 'in', ['approved','reject'])]}"
+ options="{}"/>
+ <button name="action_reject"
+ string="Reject"
+ attrs="{'invisible': [('status', 'in', ['approved','reject'])]}"
+ type="object"/>
+ <button name="button_draft"
+ string="Reset to Draft"
+ attrs="{'invisible': [('status', '!=', 'reject')]}"
+ type="object"/>
<button name="action_confirm_customer_payment"
- string="Konfirmasi Pembayaran" type="object"
- options="{}"
- attrs="{'invisible': [('payment_status', '==', 'payment')], 'readonly': [('payment_status', '=', 'payment')]}"/>
+ string="Konfirmasi Pembayaran" type="object"
+ options="{}"
+ attrs="{'invisible': [('payment_status', '==', 'payment')], 'readonly': [('payment_status', '=', 'payment')]}"/>
+ <field name="status" widget="statusbar"
+ statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,pengajuan4,approved"
+ statusbar_colors='{"reject":"red"}'/>
</header>
<sheet string="Customer Commision">
- <div class="oe_button_box" name="button_box"/>
+ <div class="oe_button_box" name="button_box"/>
+ <group>
<group>
+ <field name="number"/>
+ <field name="date_from"/>
+ <field name="partner_ids" widget="many2many_tags"/>
+ <field name="description"/>
+ <field name="commision_percent"/>
+ <field name="commision_amt"/>
+ <field name="commision_amt_text"/>
+ <field name="grouped_so_number" readonly="1"/>
+ <field name="grouped_invoice_number" readonly="1"/>
+ <field name="approved_by" readonly="1"/>
+ </group>
+ <group>
+ <div>
+ <button name="generate_customer_commision"
+ string="Generate Line"
+ type="object"
+ class="mr-2 oe_highlight"
+ />
+ </div>
+ <field name="date_to"/>
+ <field name="sales_id"/>
+ <field name="commision_type"/>
+ <field name="brand_ids" widget="many2many_tags"/>
+ <field name="notification" readonly="1"/>
+ <!-- <field name="status" readonly="1"/>-->
+ <field name="payment_status" readonly="1"/>
+ <field name="total_dpp"/>
+ </group>
+ </group>
+ <notebook>
+ <page string="Lines">
+ <field name="commision_lines"/>
+ </page>
+ <page string="Other Info" name="customer_commision_info">
<group>
- <field name="number"/>
- <field name="date_from"/>
- <field name="partner_ids" widget="many2many_tags"/>
- <field name="description"/>
- <field name="commision_percent"/>
- <field name="commision_amt"/>
- <field name="grouped_so_number" readonly="1"/>
- <field name="grouped_invoice_number" readonly="1"/>
+ <field name="bank_name"/>
+ <field name="account_name"/>
+ <field name="bank_account"/>
+ <field name="note_transfer"/>
</group>
+ </page>
+ <page string="Finance Notes">
<group>
- <div>
- <button name="generate_customer_commision"
- string="Generate Line"
- type="object"
- class="mr-2 oe_highlight"
- />
- </div>
- <field name="date_to"/>
- <field name="commision_type"/>
- <field name="brand_ids" widget="many2many_tags"/>
- <field name="notification" readonly="1"/>
- <field name="status" readonly="1"/>
- <field name="payment_status" readonly="1" />
- <field name="total_dpp"/>
+ <field name="note_finnance"/>
</group>
- </group>
- <notebook>
- <page string="Lines">
- <field name="commision_lines"/>
- </page>
- <page string="Other Info" name="customer_commision_info">
- <group>
- <field name="bank_name"/>
- <field name="account_name"/>
- <field name="bank_account"/>
- <field name="note_transfer"/>
- </group>
- </page>
- </notebook>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
</form>
</field>
</record>
+ <!-- Wizard for Reject Reason -->
+ <record id="view_reject_reason_wizard_form" model="ir.ui.view">
+ <field name="name">reject.reason.commision.form</field>
+ <field name="model">reject.reason.commision</field>
+ <field name="arch" type="xml">
+ <form string="Reject Reason">
+ <group>
+ <field name="reason_reject" widget="text"/>
+ </group>
+ <footer>
+ <button string="Confirm" type="object" name="confirm_reject" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_reject_reason_wizard" model="ir.actions.act_window">
+ <field name="name">Reject Reason</field>
+ <field name="res_model">reject.reason.commision</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+
<record id="view_customer_commision_filter" model="ir.ui.view">
<field name="name">customer.commision.list.select</field>
<field name="model">customer.commision</field>
@@ -113,7 +161,12 @@
<field name="arch" type="xml">
<search string="Search Customer Commision">
<field name="partner_ids"/>
- </search>
+ <group expand="0" string="Group By">
+ <filter string="Partner" name="group_partner"
+ domain="[]"
+ context="{'group_by':'partner_ids'}"/>
+ </group>
+ </search>
</field>
</record>
@@ -126,17 +179,17 @@
</record>
<menuitem id="menu_customer_commision_acct"
- name="Customer Commision"
- action="customer_commision_action"
- parent="account.menu_finance_entries"
- sequence="113"
+ name="Customer Commision"
+ action="customer_commision_action"
+ parent="account.menu_finance_entries"
+ sequence="113"
/>
<menuitem id="menu_customer_commision_sales"
- name="Customer Commision"
- action="customer_commision_action"
- parent="sale.product_menu_catalog"
- sequence="101"
+ name="Customer Commision"
+ action="customer_commision_action"
+ parent="sale.product_menu_catalog"
+ sequence="101"
/>
<record id="customer_rebate_tree" model="ir.ui.view">
@@ -170,34 +223,34 @@
<field name="arch" type="xml">
<form>
<sheet string="Customer Rebate">
- <div class="oe_button_box" name="button_box"/>
+ <div class="oe_button_box" name="button_box"/>
+ <group>
<group>
- <group>
- <field name="date_from"/>
- <field name="partner_id"/>
- <field name="target_1st"/>
- <field name="target_2nd"/>
- <field name="dpp_q1"/>
- <field name="dpp_q2"/>
- <field name="dpp_q3"/>
- <field name="dpp_q4"/>
- </group>
- <group>
- <field name="date_to"/>
- <field name="description"/>
- <field name="achieve_1"/>
- <field name="achieve_2"/>
- <field name="status_q1"/>
- <field name="status_q2"/>
- <field name="status_q3"/>
- <field name="status_q4"/>
- </group>
+ <field name="date_from"/>
+ <field name="partner_id"/>
+ <field name="target_1st"/>
+ <field name="target_2nd"/>
+ <field name="dpp_q1"/>
+ <field name="dpp_q2"/>
+ <field name="dpp_q3"/>
+ <field name="dpp_q4"/>
+ </group>
+ <group>
+ <field name="date_to"/>
+ <field name="description"/>
+ <field name="achieve_1"/>
+ <field name="achieve_2"/>
+ <field name="status_q1"/>
+ <field name="status_q2"/>
+ <field name="status_q3"/>
+ <field name="status_q4"/>
</group>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids" widget="mail_followers"/>
- <field name="message_ids" widget="mail_thread"/>
- </div>
+ </group>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
</form>
</field>
</record>
@@ -210,16 +263,16 @@
</record>
<menuitem id="menu_customer_rebate_acct"
- name="Customer Rebate"
- action="customer_rebate_action"
- parent="account.menu_finance_entries"
- sequence="114"
+ name="Customer Rebate"
+ action="customer_rebate_action"
+ parent="account.menu_finance_entries"
+ sequence="114"
/>
<menuitem id="menu_customer_rebate_sales"
- name="Customer Rebate"
- action="customer_rebate_action"
- parent="sale.product_menu_catalog"
- sequence="102"
+ name="Customer Rebate"
+ action="customer_rebate_action"
+ parent="sale.product_menu_catalog"
+ sequence="102"
/>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index dfb56100..97bf40bb 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -21,6 +21,16 @@
<field name="number_increment">1</field>
</record>
+ <record id="sequence_invoice_date" model="ir.sequence">
+ <field name="name">Approval Invoice Date</field>
+ <field name="code">approval.invoice.date</field>
+ <field name="active">TRUE</field>
+ <field name="prefix">AID/%(year)s/</field>
+ <field name="padding">5</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ </record>
+
<record id="sequence_vendor_approval" model="ir.sequence">
<field name="name">Vendor Approval</field>
<field name="code">vendor.approval</field>
@@ -55,7 +65,7 @@
<field name="name">Shipment Group</field>
<field name="code">shipment.group</field>
<field name="active">TRUE</field>
- <field name="prefix">SG/%(year)s/</field>
+ <field name="prefix">SGR/%(year)s/</field>
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
diff --git a/indoteknik_custom/views/mrp_production.xml b/indoteknik_custom/views/mrp_production.xml
index f81d65e8..3de52a08 100644
--- a/indoteknik_custom/views/mrp_production.xml
+++ b/indoteknik_custom/views/mrp_production.xml
@@ -5,9 +5,38 @@
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
<field name="arch" type="xml">
+ <button name="button_mark_done" position="after">
+ <button name="create_po_from_manufacturing" type="object" string="Create PO" class="oe_highlight" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('is_po', '=', True)]}"/>
+ </button>
<field name="bom_id" position="after">
<field name="desc"/>
+ <field name="sale_order"/>
+ <field name="is_po"/>
</field>
+ <xpath expr="//form/sheet/notebook/page/field[@name='move_raw_ids']/tree/field[@name='product_uom_qty']" position="before">
+ <field name="vendor_id"/>
+ </xpath>
+ <xpath expr="//form/sheet/notebook/page[@name='miscellaneous']" position="after">
+ <page string="Purchase Match" name="purchase_order_lines_indent">
+ <field name="production_purchase_match"/>
+ </page>
+ <page string="Check Product" name="check_bom_product">
+ <field name="check_bom_product_lines"/>
+ </page>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="check_bom_product_tree" model="ir.ui.view">
+ <field name="name">check.bom.product.tree</field>
+ <field name="model">check.bom.product</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom" decoration-warning="status == 'Pending'" decoration-success="status == 'Done'">
+ <field name="code_product"/>
+ <field name="product_id"/>
+ <field name="quantity"/>
+ <field name="status" readonly="1"/>
+ </tree>
</field>
</record>
@@ -18,7 +47,24 @@
<field name="arch" type="xml">
<field name="product_id" position="after">
<field name="desc"/>
+ <field name="sale_order"/>
+ </field>
+ <field name="state" position="after">
+ <field name="state_reserve" optional="hide"/>
+ <field name="date_reserved" optional="hide"/>
</field>
</field>
</record>
+
+ <record id="production_purchase_match_tree" model="ir.ui.view">
+ <field name="name">production.purchase.match.tree</field>
+ <field name="model">production.purchase.match</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="order_id" readonly="1"/>
+ <field name="vendor" readonly="1"/>
+ <field name="total" readonly="1"/>
+ </tree>
+ </field>
+ </record>
</odoo>
diff --git a/indoteknik_custom/views/product_pricelist.xml b/indoteknik_custom/views/product_pricelist.xml
index 6eff0153..3c2b8b8d 100644
--- a/indoteknik_custom/views/product_pricelist.xml
+++ b/indoteknik_custom/views/product_pricelist.xml
@@ -20,6 +20,7 @@
<field name="banner_top" widget="image" />
<field name="start_date" attrs="{'required': [('is_flash_sale', '=', True)]}" />
<field name="end_date" attrs="{'required': [('is_flash_sale', '=', True)]}" />
+ <field name="number" required="1" />
</group>
</group>
</page>
diff --git a/indoteknik_custom/views/product_product.xml b/indoteknik_custom/views/product_product.xml
index b214dc87..1d04e708 100644
--- a/indoteknik_custom/views/product_product.xml
+++ b/indoteknik_custom/views/product_product.xml
@@ -15,6 +15,7 @@
<field name="qty_onhand_bandengan" optional="hide"/>
<field name="qty_incoming_bandengan" optional="hide"/>
<field name="qty_outgoing_bandengan" optional="hide"/>
+ <field name="qty_outgoing_mo_bandengan" optional="hide"/>
<field name="qty_available_bandengan" optional="hide"/>
<field name="qty_free_bandengan" optional="hide"/>
<field name="qty_rpo" optional="hide"/>
diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml
index af21984a..8f9d1190 100755
--- a/indoteknik_custom/views/product_template.xml
+++ b/indoteknik_custom/views/product_template.xml
@@ -53,6 +53,21 @@
<field name="supplier_taxes_id" position="after">
<field name="supplier_url" widget="url"/>
</field>
+ <notebook position="inside">
+ <page string="Image Carousel">
+ <field name="image_carousel_lines"/>
+ </page>
+ </notebook>
+ </field>
+ </record>
+
+ <record id="image_carousel_tree" model="ir.ui.view">
+ <field name="name">image.carousel.tree</field>
+ <field name="model">image.carousel</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="image" widget="image" width="80"/>
+ </tree>
</field>
</record>
@@ -62,6 +77,8 @@
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<field name="last_update_solr" position="after">
+ <field name="barcode_box" />
+ <field name="qty_pcs_box" />
<field name="clean_website_description" />
<field name="qr_code_variant" widget="image" readonly="True"/>
</field>
diff --git a/indoteknik_custom/views/project_views.xml b/indoteknik_custom/views/project_views.xml
new file mode 100644
index 00000000..3023fa18
--- /dev/null
+++ b/indoteknik_custom/views/project_views.xml
@@ -0,0 +1,13 @@
+<odoo>
+ <record id="view_task_kanban_inherit" model="ir.ui.view">
+ <field name="name">project.task.kanban.inherit</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="project.view_task_kanban"/>
+ <field name="arch" type="xml">
+ <!-- Target field user_id di bagian kanban_bottom_right -->
+ <xpath expr="//div[@class='oe_kanban_bottom_right']/field[@name='user_id']" position="attributes">
+ <attribute name="widget" remove="many2one_avatar_user" />
+ </xpath>
+ </field>
+ </record>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 36c0db13..0fbbb5e7 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -36,7 +36,6 @@
</button>
<button name="button_unlock" position="after">
<button name="create_bill_pelunasan" string="Create Bill Pelunasan" type="object" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ('purchase', 'done')), ('bills_pelunasan_id', '!=', False)]}"/>
-
</button>
<field name="date_order" position="before">
<field name="sale_order_id" attrs="{'readonly': [('state', 'not in', ['draft'])]}"/>
@@ -65,6 +64,7 @@
<field name="payment_term_id"/>
<field name="total_cost_service" attrs="{'required': [('partner_id', 'in', [9688, 29712])]}"/>
<field name="total_delivery_amt" attrs="{'required': [('partner_id', 'in', [9688, 29712])]}"/>
+ <field name="product_bom_id"/>
</field>
<field name="amount_total" position="after">
<field name="total_margin"/>
@@ -139,7 +139,7 @@
</field>
<field name="order_line" position="attributes">
- <attribute name="attrs">{'readonly': ['|', ('state', 'in', ['done', 'cancel']), ('has_active_invoice', '=', True)]}</attribute>
+ <attribute name="attrs">{'readonly': ['|', ('state', 'in', ['purchase', 'done', 'cancel']), ('has_active_invoice', '=', True)]}</attribute>
</field>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" position="attributes">
@@ -307,6 +307,7 @@
<field name="margin_item" optional="hide"/>
<field name="delivery_amt" optional="hide"/>
<field name="margin_deduct" optional="hide"/>
+ <field name="hold_outgoing_so" optional="hide"/>
<field name="margin_so"/>
</tree>
</field>
diff --git a/indoteknik_custom/views/purchase_pricelist.xml b/indoteknik_custom/views/purchase_pricelist.xml
index ca5cd416..409e3b6a 100755
--- a/indoteknik_custom/views/purchase_pricelist.xml
+++ b/indoteknik_custom/views/purchase_pricelist.xml
@@ -20,6 +20,8 @@
<field name="count_brand_vendor" optional="hide"/>
<field name="product_categ_ids" string="Product Category" optional="hide"/>
<field name="is_winner" string="Winner" optional="hide"/>
+ <field name="message_follower_ids" widget="mail_followers" optional="hide"/>
+ <field name="activity_ids" widget="mail_activity" optional="hide"/>
</tree>
</field>
</record>
@@ -52,6 +54,11 @@
</group>
</group>
</sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
</form>
</field>
</record>
diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml
index 16f1bedd..bb1c7643 100644
--- a/indoteknik_custom/views/purchasing_job.xml
+++ b/indoteknik_custom/views/purchasing_job.xml
@@ -17,6 +17,7 @@
<field name="status_apo" invisible="1"/>
<field name="action"/>
<field name="note"/>
+ <field name="date_po"/>
</tree>
</field>
</record>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 0d190f37..0fabf279 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8" ?>
+<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="sale_order_form_view_inherit" model="ir.ui.view">
@@ -16,43 +16,72 @@
type="object"
attrs="{'invisible': [('approval_status', '=', ['approved'])]}"
/>
+ <button name="hold_unhold_qty_outgoing_so"
+ string="Hold/Unhold Outgoing"
+ type="object"
+ attrs="{'invisible': [('state', 'in', ['cancel'])]}"
+ />
+ <button name="ask_retur_cancel_purchasing"
+ string="Ask Cancel Purchasing"
+ type="object"
+ attrs="{'invisible': [('state', 'in', ['cancel'])]}"
+ />
<button name="action_web_approve"
string="Web Approve"
type="object"
attrs="{'invisible': ['|', '|', ('create_uid', '!=', 25), ('web_approval', '!=', False), ('state', '!=', 'draft')]}"
/>
- <button name="indoteknik_custom.action_view_uangmuka_penjualan" string="UangMuka"
- type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/>
+ <button name="indoteknik_custom.action_view_uangmuka_penjualan"
+ string="UangMuka"
+ type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/>
</button>
<field name="payment_term_id" position="after">
<field name="create_uid" invisible="1"/>
<field name="create_date" invisible="1"/>
- <field name="shipping_cost_covered" attrs="{'required': ['|', ('create_date', '&gt;', '2023-06-15'), ('create_date', '=', False)]}"/>
- <field name="shipping_paid_by" attrs="{'required': ['|', ('create_date', '&gt;', '2023-06-15'), ('create_date', '=', False)]}"/>
+ <field name="shipping_cost_covered"
+ attrs="{'required': ['|', ('create_date', '&gt;', '2023-06-15'), ('create_date', '=', False)]}"/>
+ <field name="shipping_paid_by"
+ attrs="{'required': ['|', ('create_date', '&gt;', '2023-06-15'), ('create_date', '=', False)]}"/>
<field name="delivery_amt"/>
+ <field name="ongkir_ke_xpdc"/>
+ <field name="metode_kirim_ke_xpdc"/>
<field name="fee_third_party"/>
+ <field name="biaya_lain_lain"/>
<field name="total_percent_margin"/>
+ <field name="total_margin_excl_third_party" readonly="1"/>
<field name="type_promotion"/>
<label for="voucher_id"/>
<div class="o_row">
- <field name="voucher_id" id="voucher_id" attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}"/>
- <field name="applied_voucher_id" invisible="1" />
- <button name="action_apply_voucher" type="object" string="Apply" confirm="Anda yakin untuk menggunakan voucher?" help="Apply the selected voucher" class="btn-link mb-1 px-0" icon="fa-plus"
- attrs="{'invisible': ['|', '|', ('voucher_id', '=', False), ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}"
+ <field name="voucher_id" id="voucher_id"
+ attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}"/>
+ <field name="applied_voucher_id" invisible="1"/>
+ <button name="action_apply_voucher" type="object" string="Apply"
+ confirm="Anda yakin untuk menggunakan voucher?"
+ help="Apply the selected voucher" class="btn-link mb-1 px-0"
+ icon="fa-plus"
+ attrs="{'invisible': ['|', '|', ('voucher_id', '=', False), ('state', 'not in', ['draft', 'sent']), ('applied_voucher_id', '!=', False)]}"
/>
- <button name="cancel_voucher" type="object" string="Cancel" confirm="Anda yakin untuk membatalkan penggunaan voucher?" help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times"
- attrs="{'invisible': ['|', ('applied_voucher_id', '=', False), ('state', 'not in', ['draft','sent'])]}"
+ <button name="cancel_voucher" type="object" string="Cancel"
+ confirm="Anda yakin untuk membatalkan penggunaan voucher?"
+ help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times"
+ attrs="{'invisible': ['|', ('applied_voucher_id', '=', False), ('state', 'not in', ['draft','sent'])]}"
/>
</div>
<label for="voucher_shipping_id"/>
<div class="o_row">
- <field name="voucher_shipping_id" id="voucher_shipping_id" attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}"/>
- <field name="applied_voucher_shipping_id" invisible="1" />
- <button name="action_apply_voucher_shipping" type="object" string="Apply" confirm="Anda yakin untuk menggunakan voucher?" help="Apply the selected voucher" class="btn-link mb-1 px-0" icon="fa-plus"
- attrs="{'invisible': ['|', '|', ('voucher_id', '=', False), ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}"
+ <field name="voucher_shipping_id" id="voucher_shipping_id"
+ attrs="{'readonly': ['|', ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}"/>
+ <field name="applied_voucher_shipping_id" invisible="1"/>
+ <button name="action_apply_voucher_shipping" type="object" string="Apply"
+ confirm="Anda yakin untuk menggunakan voucher?"
+ help="Apply the selected voucher" class="btn-link mb-1 px-0"
+ icon="fa-plus"
+ attrs="{'invisible': ['|', '|', ('voucher_id', '=', False), ('state', 'not in', ['draft', 'sent']), ('applied_voucher_shipping_id', '!=', False)]}"
/>
- <button name="cancel_voucher_shipping" type="object" string="Cancel" confirm="Anda yakin untuk membatalkan penggunaan voucher?" help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times"
- attrs="{'invisible': ['|', ('applied_voucher_shipping_id', '=', False), ('state', 'not in', ['draft','sent'])]}"
+ <button name="cancel_voucher_shipping" type="object" string="Cancel"
+ confirm="Anda yakin untuk membatalkan penggunaan voucher?"
+ help="Cancel applied voucher" class="btn-link mb-1 px-0" icon="fa-times"
+ attrs="{'invisible': ['|', ('applied_voucher_shipping_id', '=', False), ('state', 'not in', ['draft','sent'])]}"
/>
</div>
<button name="calculate_selling_price"
@@ -60,18 +89,25 @@
type="object"
/>
</field>
+ <field name="approval_status" position="after">
+ <field name="notes"/>
+ </field>
<field name="source_id" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="user_id" position="after">
- <field name="helper_by_id" readonly="1"/>
- <field name="compute_fullfillment" invisible="1"/>
+ <field name="hold_outgoing" readonly="1" />
+ <field name="date_hold" readonly="1" widget="datetime" />
+ <field name="date_unhold" readonly="1" widget="datetime" />
+ <field name="helper_by_id" readonly="1" />
+ <field name="compute_fullfillment" invisible="1" />
</field>
<field name="tag_ids" position="after">
<field name="eta_date_start"/>
<t t-esc="' to '"/>
<field name="eta_date" readonly="1"/>
- <field name="expected_ready_to_ship" />
+ <field name="expected_ready_to_ship"/>
+ <field name="ready_to_ship_status_detail"/>
<field name="flash_sale"/>
<field name="margin_after_delivery_purchase"/>
<field name="percent_margin_after_delivery_purchase"/>
@@ -79,9 +115,9 @@
<field name="pareto_status"/>
</field>
<field name="analytic_account_id" position="after">
- <field name="customer_type" required="1"/>
- <field name="npwp" placeholder='99.999.999.9-999.999' required="1"/>
- <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}"/>
+ <field name="customer_type" readonly="1"/>
+ <field name="npwp" placeholder='99.999.999.9-999.999' readonly="1"/>
+ <field name="sppkp" attrs="{'required': [('customer_type', '=', 'pkp')]}" readonly="1"/>
<field name="email" required="1"/>
<field name="unreserve_id"/>
<field name="due_id" readonly="1"/>
@@ -96,8 +132,9 @@
<field name="partner_shipping_id" position="after">
<field name="real_shipping_id"/>
<field name="real_invoice_id"/>
- <field name="approval_status" />
- <field name="sales_tax_id" domain="[('type_tax_use','=','sale'), ('active', '=', True)]" required="1"/>
+ <field name="approval_status"/>
+ <field name="sales_tax_id"
+ domain="[('type_tax_use','=','sale'), ('active', '=', True)]" required="1"/>
<field name="carrier_id" required="1"/>
<field name="delivery_service_type" readonly="1"/>
<field name="shipping_option_id"/>
@@ -106,20 +143,26 @@
<field name="date_doc_kirim" readonly="1"/>
<field name="notification" readonly="1"/>
</field>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']" position="attributes">
+ <xpath expr="//form/sheet/notebook/page/field[@name='order_line']"
+ position="attributes">
<attribute name="attrs">
- {'readonly': [('state', 'in', ('done','cancel'))]}
+ {'readonly': [('state', 'in', ('done', 'cancel'))]}
</attribute>
</xpath>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree" position="inside">
+ <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree"
+ position="inside">
<field name="desc_updatable" invisible="1"/>
</xpath>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='name']" position="attributes">
+ <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='name']"
+ position="attributes">
<attribute name="modifiers">
{'readonly': [('desc_updatable', '=', False)]}
</attribute>
</xpath>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']" position="attributes">
+ <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_unit']"
+ position="attributes">
<attribute name="attrs">
{
'readonly': [
@@ -131,25 +174,28 @@
</attribute>
</xpath>
<div name="invoice_lines" position="before">
- <div name="vendor_id" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <div name="vendor_id" groups="base.group_no_one"
+ attrs="{'invisible': [('display_type', '!=', False)]}">
<label for="vendor_id"/>
<div name="vendor_id">
- <field name="vendor_id"
- attrs="{'readonly': [('parent.approval_status', '=', 'approved')]}"
- domain="[('parent_id', '=', False)]"
- options="{'no_create': True}" class="oe_inline" />
+ <field name="vendor_id"
+ attrs="{'readonly': [('parent.approval_status', '=', 'approved')]}"
+ domain="[('parent_id', '=', False)]"
+ options="{'no_create': True}" class="oe_inline"/>
</div>
</div>
</div>
-
+
<div name="invoice_lines" position="before">
- <div name="purchase_price" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <div name="purchase_price" groups="base.group_no_one"
+ attrs="{'invisible': [('display_type', '!=', False)]}">
<label for="purchase_price"/>
<field name="purchase_price"/>
</div>
</div>
<div name="invoice_lines" position="before">
- <div name="purchase_tax_id" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <div name="purchase_tax_id" groups="base.group_no_one"
+ attrs="{'invisible': [('display_type', '!=', False)]}">
<label for="purchase_tax_id"/>
<div name="purchase_tax_id">
<field name="purchase_tax_id"/>
@@ -157,22 +203,29 @@
</div>
</div>
<div name="invoice_lines" position="before">
- <div name="item_percent_margin" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <div name="item_percent_margin" groups="base.group_no_one"
+ attrs="{'invisible': [('display_type', '!=', False)]}">
<label for="item_percent_margin"/>
<field name="item_percent_margin"/>
</div>
</div>
<div name="invoice_lines" position="before">
- <div name="price_subtotal" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <div name="price_subtotal" groups="base.group_no_one"
+ attrs="{'invisible': [('display_type', '!=', False)]}">
<label for="price_subtotal"/>
<field name="price_subtotal"/>
</div>
</div>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_total']" position="after">
+ <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_total']"
+ position="after">
<field name="qty_free_bu" optional="hide"/>
- <field name="vendor_id" attrs="{'readonly': [('parent.approval_status', '=', 'approved')], 'invisible': [('display_type', '!=', False)]}" domain="[('parent_id', '=', False)]" options="{'no_create':True}"/>
+ <field name="vendor_id"
+ attrs="{'readonly': [('parent.approval_status', '=', 'approved')], 'invisible': [('display_type', '!=', False)]}"
+ domain="[('parent_id', '=', False)]" options="{'no_create':True}"/>
<field name="vendor_md_id" optional="hide"/>
- <field name="purchase_price" attrs="
+ <field name="purchase_price"
+ attrs="
{
'readonly': [
'|',
@@ -182,44 +235,60 @@
}
"/>
<field name="purchase_price_md" optional="hide"/>
- <field name="purchase_tax_id" attrs="{'readonly': [('parent.approval_status', '!=', False)]}" domain="[('type_tax_use','=','purchase')]" options="{'no_create':True}"/>
+ <field name="purchase_tax_id"
+ attrs="{'readonly': [('parent.approval_status', '!=', False)]}"
+ domain="[('type_tax_use','=','purchase')]" options="{'no_create':True}"/>
<field name="item_percent_margin"/>
<field name="item_margin" optional="hide"/>
<field name="margin_md" optional="hide"/>
<field name="note" optional="hide"/>
<field name="note_procurement" optional="hide"/>
<field name="vendor_subtotal" optional="hide"/>
- <field name="weight" optional="hide"/>
+ <field name="weight" optional="hide"/>
<field name="amount_voucher_disc" string="Voucher" readonly="1" optional="hide"/>
<field name="order_promotion_id" string="Promotion" readonly="1" optional="hide"/>
</xpath>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" position="before">
+ <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']"
+ position="before">
<field name="line_no" readonly="1" optional="hide"/>
</xpath>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='qty_delivered']" position="before">
+ <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='qty_delivered']"
+ position="before">
<field name="qty_reserved" invisible="1"/>
<field name="reserved_from" readonly="1" optional="hide"/>
</xpath>
- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" position="attributes">
+ <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']"
+ position="attributes">
<attribute name="options">{'no_create': True}</attribute>
</xpath>
- <!-- <xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='tax_id']" position="attributes">
+ <!-- <xpath
+ expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='tax_id']"
+ position="attributes">
<attribute name="required">1</attribute>
</xpath> -->
<field name="amount_total" position="after">
<field name="grand_total"/>
- <label for="amount_voucher_disc" string="Voucher" />
+ <label for="amount_voucher_disc" string="Voucher"/>
<div>
- <field class="mb-0" name="amount_voucher_disc" string="Voucher" readonly="1"/>
- <div class="text-right mb-2"><small>*Hanya informasi</small></div>
+ <field class="mb-0" name="amount_voucher_disc" string="Voucher" readonly="1"/>
+ <div class="text-right mb-2">
+ <small>*Hanya informasi</small>
+ </div>
</div>
- <label for="amount_voucher_shipping_disc" string="Voucher Shipping" />
+ <label for="amount_voucher_shipping_disc" string="Voucher Shipping"/>
<div>
- <field class="mb-0" name="amount_voucher_shipping_disc" string="Voucher Shipping" readonly="1"/>
- <div class="text-right mb-2"><small>*Hanya informasi</small></div>
+ <field class="mb-0" name="amount_voucher_shipping_disc"
+ string="Voucher Shipping" readonly="1"/>
+ <div class="text-right mb-2">
+ <small>*Hanya informasi</small>
+ </div>
</div>
<field name="total_margin"/>
<field name="total_percent_margin"/>
+ <field name="total_before_margin"/>
</field>
<field name="effective_date" position="after">
<field name="carrier_id"/>
@@ -229,7 +298,14 @@
</field>
<field name="carrier_id" position="attributes">
<attribute name="attrs">
- {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in', ['cancel','draft'])]}
+ {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in',
+ ['cancel', 'draft'])]}
+ </attribute>
+ </field>
+ <field name="payment_term_id" position="attributes">
+ <attribute name="attrs">
+ {'readonly': [('approval_status', '=', 'approved'), ('state', 'not in',
+ ['cancel', 'draft'])]}
</attribute>
</field>
@@ -259,15 +335,15 @@
<page string="Promotion" name="page_promotion">
<field name="order_promotion_ids" readonly="1">
<tree options="{'no_open': True}">
- <field name="program_line_id" />
- <field name="quantity" />
- <field name="is_applied" />
+ <field name="program_line_id"/>
+ <field name="quantity"/>
+ <field name="is_applied"/>
</tree>
<form>
<group>
- <field name="program_line_id" />
- <field name="quantity" />
- <field name="is_applied" />
+ <field name="program_line_id"/>
+ <field name="quantity"/>
+ <field name="is_applied"/>
</group>
</form>
</field>
@@ -282,36 +358,41 @@
<field name="fulfillment_line_v2" readonly="1"/>
</page>
<page string="Reject Line" name="page_sale_order_reject_line">
- <field name="reject_line" readonly="1"/>
+ <field name="reject_line" readonly="0"/>
+ </page>
+ <page string="Koli" name="page_sales_order_koli_line">
+ <field name="koli_lines" readonly="1"/>
</page>
</page>
</field>
</record>
- <!-- Wizard for Reject Reason -->
- <record id="view_cancel_reason_order_form" model="ir.ui.view">
- <field name="name">cancel.reason.order.form</field>
- <field name="model">cancel.reason.order</field>
- <field name="arch" type="xml">
- <form string="Cancel Reason">
- <group>
- <field name="reason_cancel" widget="selection"/>
- <field name="attachment_bukti" widget="many2many_binary" required="1"/>
- <field name="nomor_so_pengganti" attrs="{'invisible': [('reason_cancel', '!=', 'ganti_quotation')]}"/>
- </group>
- <footer>
- <button string="Confirm" type="object" name="confirm_reject" class="btn-primary"/>
- <button string="Cancel" class="btn-secondary" special="cancel"/>
- </footer>
- </form>
- </field>
- </record>
+ <!-- Wizard for Reject Reason -->
+ <record id="view_cancel_reason_order_form" model="ir.ui.view">
+ <field name="name">cancel.reason.order.form</field>
+ <field name="model">cancel.reason.order</field>
+ <field name="arch" type="xml">
+ <form string="Cancel Reason">
+ <group>
+ <field name="reason_cancel" widget="selection"/>
+ <field name="attachment_bukti" widget="many2many_binary" required="1"/>
+ <field name="nomor_so_pengganti"
+ attrs="{'invisible': [('reason_cancel', '!=', 'ganti_quotation')]}"/>
+ </group>
+ <footer>
+ <button string="Confirm" type="object" name="confirm_reject"
+ class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
- <record id="action_cancel_reason_order" model="ir.actions.act_window">
- <field name="name">Cancel Reason</field>
- <field name="res_model">cancel.reason.order</field>
- <field name="view_mode">form</field>
- <field name="target">new</field>
- </record>
+ <record id="action_cancel_reason_order" model="ir.actions.act_window">
+ <field name="name">Cancel Reason</field>
+ <field name="res_model">cancel.reason.order</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
</data>
<data>
<record id="sale_order_tree_view_inherit" model="ir.ui.view">
@@ -320,12 +401,14 @@
<field name="inherit_id" ref="sale.view_quotation_tree_with_onboarding"/>
<field name="arch" type="xml">
<field name="state" position="after">
- <field name="approval_status" />
+ <field name="approval_status"/>
<field name="client_order_ref"/>
+ <field name="notes"/>
<field name="payment_type" optional="hide"/>
<field name="payment_status" optional="hide"/>
<field name="pareto_status" optional="hide"/>
<field name="shipping_method_picking" optional="hide"/>
+ <field name="hold_outgoing" optional="hide"/>
</field>
</field>
</record>
@@ -335,7 +418,7 @@
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<field name="state" position="after">
- <field name="approval_status" />
+ <field name="approval_status"/>
<field name="client_order_ref"/>
<field name="so_status"/>
<field name="date_status_done"/>
@@ -374,32 +457,67 @@
<field name="email_to">${object.partner_id.email | safe}</field>
<field name="email_cc">${object.partner_id.get_approve_partner_ids("email_comma_sep")}</field>
<field name="body_html" type="html">
- <table border="0" cellpadding="0" cellspacing="0" style="padding: 16px 0; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;">
- <tr><td align="center">
- <table border="0" cellpadding="0" cellspacing="0" width="590" style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
- <tbody>
- <tr>
- <td align="center" style="min-width: 590px;">
- <table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
- <tr><td style="padding-bottom: 24px;">Dear ${(object.partner_id.get_main_parent()).name},</td></tr>
+ <table border="0" cellpadding="0" cellspacing="0"
+ style="padding: 16px 0; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;">
+ <tr>
+ <td align="center">
+ <table border="0" cellpadding="0" cellspacing="0" width="590"
+ style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
+ <tbody>
+ <tr>
+ <td align="center" style="min-width: 590px;">
+ <table border="0" cellpadding="0" cellspacing="0"
+ width="590"
+ style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
+ <tr>
+ <td style="padding-bottom: 24px;">
+ Dear
+ ${(object.partner_id.get_main_parent()).name},</td>
+ </tr>
- <tr><td style="padding-bottom: 16px;">Ini adalah konfirmasi pesanan dari ${object.partner_id.name | safe} untuk nomor pesanan ${object.name} yang memerlukan persetujuan agar dapat diproses.</td></tr>
- <tr><td style="padding-bottom: 16px;">
- <a href="https://indoteknik.com/my/quotations/${object.id}" style="color: white; background-color: #C53030; border: none; border-radius: 6px; padding: 4px 8px; width: fit-content; display: block;">
- Lihat Pesanan
- </a>
- </td></tr>
- <tr><td style="padding-bottom: 16px;">Mohon segera melakukan tinjauan terhadap pesanan ini dan memberikan persetujuan. Terima kasih atas perhatian dan kerjasama Anda. Kami berharap dapat segera melanjutkan proses pesanan ini setelah mendapatkan persetujuan Anda.</td></tr>
+ <tr>
+ <td style="padding-bottom: 16px;">Ini adalah
+ konfirmasi pesanan dari
+ ${object.partner_id.name | safe} untuk nomor
+ pesanan ${object.name} yang memerlukan
+ persetujuan agar dapat diproses.</td>
+ </tr>
+ <tr>
+ <td style="padding-bottom: 16px;">
+ <a
+ href="https://indoteknik.com/my/quotations/${object.id}"
+ style="color: white; background-color: #C53030; border: none; border-radius: 6px; padding: 4px 8px; width: fit-content; display: block;">
+ Lihat Pesanan
+ </a>
+ </td>
+ </tr>
+ <tr>
+ <td style="padding-bottom: 16px;">Mohon segera
+ melakukan tinjauan terhadap pesanan ini dan
+ memberikan persetujuan. Terima kasih atas
+ perhatian dan kerjasama Anda. Kami berharap
+ dapat segera melanjutkan proses pesanan ini
+ setelah mendapatkan persetujuan Anda.</td>
+ </tr>
- <tr><td style="padding-bottom: 2px;">Hormat kami,</td></tr>
- <tr><td style="padding-bottom: 2px;">PT. Indoteknik Dotcom Gemilang</td></tr>
- <tr><td style="padding-bottom: 2px;">sales@indoteknik.com</td></tr>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </td></tr>
+ <tr>
+ <td style="padding-bottom: 2px;">Hormat kami,</td>
+ </tr>
+ <tr>
+ <td style="padding-bottom: 2px;">PT. Indoteknik
+ Dotcom Gemilang</td>
+ </tr>
+ <tr>
+ <td style="padding-bottom: 2px;">
+ sales@indoteknik.com</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
</table>
</field>
</record>
@@ -422,24 +540,38 @@
</data>
<data>
-
- </data>
- <record id="sales_order_fulfillment_v2_tree" model="ir.ui.view">
- <field name="name">sales.order.fulfillment.v2.tree</field>
- <field name="model">sales.order.fulfillment.v2</field>
+ <record id="sales_order_koli_tree" model="ir.ui.view">
+ <field name="name">sales.order.koli.tree</field>
+ <field name="model">sales.order.koli</field>
<field name="arch" type="xml">
- <tree editable="top" create="false">
- <field name="product_id" readonly="1"/>
- <field name="so_qty" readonly="1" optional="show"/>
- <field name="reserved_stock_qty" readonly="1" optional="show"/>
- <field name="delivered_qty" readonly="1" optional="hide"/>
- <field name="po_ids" widget="many2many_tags" readonly="1" optional="show"/>
- <field name="po_qty" readonly="1" optional="show"/>
- <field name="received_qty" readonly="1" optional="show"/>
- <field name="purchaser" readonly="1" optional="hide"/>
+ <tree editable="top" create="false" delete="false">
+ <field name="koli_id" readonly="1"/>
+ <field name="picking_id" readonly="1"/>
+ <field name="state" readonly="1"/>
</tree>
</field>
</record>
+ </data>
+
+ <data>
+
+ </data>
+ <record id="sales_order_fulfillment_v2_tree" model="ir.ui.view">
+ <field name="name">sales.order.fulfillment.v2.tree</field>
+ <field name="model">sales.order.fulfillment.v2</field>
+ <field name="arch" type="xml">
+ <tree editable="top" create="false">
+ <field name="product_id" readonly="1"/>
+ <field name="so_qty" readonly="1" optional="show"/>
+ <field name="reserved_stock_qty" readonly="1" optional="show"/>
+ <field name="delivered_qty" readonly="1" optional="hide"/>
+ <field name="po_ids" widget="many2many_tags" readonly="1" optional="show"/>
+ <field name="po_qty" readonly="1" optional="show"/>
+ <field name="received_qty" readonly="1" optional="show"/>
+ <field name="purchaser" readonly="1" optional="hide"/>
+ </tree>
+ </field>
+ </record>
<data>
<record id="sales_order_fullfillmet_tree" model="ir.ui.view">
<field name="name">sales.order.fullfillment.tree</field>
@@ -463,7 +595,7 @@
<tree editable="top" create="false">
<field name="product_id" readonly="1"/>
<field name="qty_reject" readonly="1"/>
- <field name="reason_reject" readonly="1"/>
+ <field name="reason_reject" readonly="0"/>
</tree>
</field>
</record>
@@ -487,61 +619,79 @@
<field name="email_from">sales@indoteknik.com</field>
<field name="email_to">${object.user_id.login | safe}</field>
<field name="body_html" type="html">
- <table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;">
- <tr><td align="center">
- <table border="0" cellpadding="0" cellspacing="0" width="590" style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
- <!-- HEADER -->
- <tbody>
- <tr>
- <td align="center" style="min-width: 590px;">
- <table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
- <tr>
- <td valign="middle">
- <span></span>
- </td>
- </tr>
- </table>
- </td>
- </tr>
- <!-- CONTENT -->
- <tr>
- <td align="center" style="min-width: 590px;">
- <table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
- <tr><td style="padding-bottom: 24px;">Dear ${salesperson_name},</td></tr>
-
- <tr><td style="padding-bottom: 16px;">Terdapat pesanan dari BP ${business_partner} untuk site ${site} dengan total belanja ${sum_total_amount} dari list SO dibawah ini:</td></tr>
-
- <tr>
- <td>
- <table border="1" cellpadding="5" cellspacing="0">
- <thead>
- <tr>
- <th>Nama Pesanan</th>
- <th>Nama Perusahaan Induk</th>
- <th>Nama Situs</th>
- <th>Total Pembelian</th>
- </tr>
- </thead>
- <tbody>
- ${table_content}
- </tbody>
- </table>
- </td>
- </tr>
-
- <tr>
- <td style="text-align:center;">
- <hr width="100%"
- style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;" />
- </td>
- </tr>
- </table>
- </td>
- </tr>
- <!-- CONTENT -->
- </tbody>
- </table>
- </td></tr>
+ <table border="0" cellpadding="0" cellspacing="0"
+ style="padding-top: 16px; background-color: #F1F1F1; font-family:Inter, Helvetica, Verdana, Arial,sans-serif; line-height: 24px; color: #454748; width: 100%; border-collapse:separate;">
+ <tr>
+ <td align="center">
+ <table border="0" cellpadding="0" cellspacing="0" width="590"
+ style="font-size: 13px; padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
+ <!-- HEADER -->
+ <tbody>
+ <tr>
+ <td align="center" style="min-width: 590px;">
+ <table border="0" cellpadding="0" cellspacing="0"
+ width="590"
+ style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
+ <tr>
+ <td valign="middle">
+ <span></span>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <!-- CONTENT -->
+ <tr>
+ <td align="center" style="min-width: 590px;">
+ <table border="0" cellpadding="0" cellspacing="0"
+ width="590"
+ style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
+ <tr>
+ <td style="padding-bottom: 24px;">Dear
+ ${salesperson_name},</td>
+ </tr>
+
+ <tr>
+ <td style="padding-bottom: 16px;">Terdapat
+ pesanan dari BP ${business_partner} untuk
+ site ${site} dengan total belanja
+ ${sum_total_amount} dari list SO dibawah
+ ini:</td>
+ </tr>
+
+ <tr>
+ <td>
+ <table border="1" cellpadding="5"
+ cellspacing="0">
+ <thead>
+ <tr>
+ <th>Nama Pesanan</th>
+ <th>Nama Perusahaan Induk</th>
+ <th>Nama Situs</th>
+ <th>Total Pembelian</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${table_content}
+ </tbody>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td style="text-align:center;">
+ <hr width="100%"
+ style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <!-- CONTENT -->
+ </tbody>
+ </table>
+ </td>
+ </tr>
</table>
</field>
</record>
diff --git a/indoteknik_custom/views/shipment_group.xml b/indoteknik_custom/views/shipment_group.xml
index e9eec41b..c3f79bda 100644
--- a/indoteknik_custom/views/shipment_group.xml
+++ b/indoteknik_custom/views/shipment_group.xml
@@ -7,6 +7,8 @@
<tree default_order="create_date desc">
<field name="number"/>
<field name="partner_id"/>
+ <field name="carrier_id"/>
+ <field name="total_colly_line"/>
</tree>
</field>
</record>
@@ -16,11 +18,10 @@
<field name="model">shipment.group.line</field>
<field name="arch" type="xml">
<tree editable="bottom">
- <field name="picking_id" required="1"/>
<field name="partner_id" readonly="1"/>
+ <field name="picking_id" required="1"/>
<field name="sale_id" readonly="1"/>
- <field name="shipping_paid_by" readonly="1"/>
- <field name="state" readonly="1"/>
+ <field name="total_colly" readonly="1"/>
</tree>
</field>
</record>
@@ -30,6 +31,13 @@
<field name="model">shipment.group</field>
<field name="arch" type="xml">
<form>
+ <header>
+ <button name="sync_api_shipping"
+ string="Sync API"
+ type="object"
+ attrs="{'invisible': [('carrier_id', 'not in', [151, 173])]}"
+ />
+ </header>
<sheet>
<group>
<group>
@@ -37,6 +45,8 @@
</group>
<group>
<field name="partner_id" readonly="1"/>
+ <field name="carrier_id" readonly="1"/>
+ <field name="total_colly_line" readonly="1"/>
</group>
</group>
<notebook>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index dadd5021..ae77ab9a 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -19,11 +19,14 @@
<field name="note" optional="hide"/>
<field name="date_reserved" optional="hide"/>
<field name="state_reserve" optional="hide"/>
- <field name="final_seq"/>
+ <field name="state_packing" widget="badge" decoration-success="state_packing == 'packing_done'" decoration-danger="state_packing == 'not_packing'" optional="hide"/>
+ <field name="final_seq"/>
+ <field name="state_approve_md" widget="badge" decoration-success="state_approve_md == 'done'" decoration-warning="state_approve_md == 'pending'" optional="hide"/>
<!-- <field name="countdown_hours" optional="hide"/>
<field name="countdown_ready_to_ship" /> -->
</field>
<field name="partner_id" position="after">
+ <field name="area_name" optional="hide"/>
<field name="purchase_representative_id"/>
<field name="status_printed"/>
</field>
@@ -70,12 +73,33 @@
type="object"
attrs="{'invisible': [('carrier_id', '!=', 9)]}"
/>
+ <button name="action_get_kgx_pod"
+ string="Tracking KGX"
+ type="object"
+ attrs="{'invisible': [('carrier_id', '!=', 173)]}"
+ />
+ <button name="button_state_approve_md"
+ string="Approve MD Gudang Selisih"
+ type="object"
+ attrs="{'invisible': [('state_approve_md', 'not in', ['waiting', 'pending'])]}"
+ />
+
+ <button name="button_state_pending_md"
+ string="Pending MD Gudang Selisih"
+ type="object"
+ attrs="{'invisible': [('state_approve_md', 'not in', ['waiting'])]}"
+ />
</button>
<field name="backorder_id" position="after">
+ <field name="shipping_method_so_id"/>
<field name="summary_qty_detail"/>
<field name="count_line_detail"/>
<field name="dokumen_tanda_terima"/>
<field name="dokumen_pengiriman"/>
+ <field name="quantity_koli" attrs="{'invisible': [('location_dest_id', '!=', 60)], 'required': [('location_dest_id', '=', 60)]}"/>
+ <field name="total_mapping_koli" attrs="{'invisible': [('location_id', '!=', 60)]}"/>
+ <field name="total_koli_display" readonly="1" attrs="{'invisible': [('location_id', '!=', 60)]}"/>
+ <field name="linked_out_picking_id" readonly="1" attrs="{'invisible': [('location_id', '=', 60)]}"/>
</field>
<field name="weight_uom_name" position="after">
<group>
@@ -97,12 +121,15 @@
<field name="arrival_time"/>
</field>
<field name="origin" position="after">
+<!-- <field name="show_state_approve_md" invisible="1" optional="hide"/>-->
+ <field name="state_approve_md" widget="badge"/>
<field name="purchase_id"/>
<field name="sale_order"/>
<field name="invoice_status"/>
<field name="date_doc_kirim" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/>
<field name="summary_qty_operation"/>
<field name="count_line_operation"/>
+ <field name="linked_manual_bu_out" attrs="{'invisible': [('location_id', '=', 60)]}" domain="[('picking_type_code', '=', 'outgoing'),('state', 'not in', ['done','cancel']), ('group_id', '=', group_id)]"/>
<field name="account_id"
attrs="{
'readonly': [['state', 'in', ['done', 'cancel']]],
@@ -128,6 +155,7 @@
<field name="approval_status"/>
<field name="approval_receipt_status"/>
<field name="approval_return_status"/>
+ <field name="so_lama"/>
</field>
<field name="product_id" position="before">
<field name="line_no" attrs="{'readonly': 1}" optional="hide"/>
@@ -148,13 +176,15 @@
</group>
</group>
</page>
- <page string="Delivery" name="delivery_order">
+ <page string="Delivery" name="delivery_order" attrs="{'invisible': [('location_dest_id', '=', 60)]}">
<group>
<group>
<field name="notee"/>
<field name="note_logistic"/>
+ <field name="note_info"/>
<field name="responsible" />
<field name="carrier_id"/>
+ <field name="out_code" attrs="{'invisible': [['out_code', '=', False]]}"/>
<field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/>
<field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/>
<field name="driver_departure_date" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/>
@@ -194,27 +224,78 @@
<field name="lalamove_image_url" invisible="1"/>
<field name="lalamove_image_html"/>
</group>
+ <group attrs="{'invisible': [('carrier_id', '!=', 173)]}">
+ <field name="kgx_pod_photo_url" invisible="1"/>
+ <field name="kgx_pod_photo"/>
+ <field name="kgx_pod_signature" invisible="1"/>
+ <field name="kgx_pod_receive_time"/>
+ <field name="kgx_pod_receiver"/>
+ </group>
</group>
</page>
- <page string="Check Product" name="check_product">
+ <page string="Check Product" name="check_product" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}">
<field name="check_product_lines"/>
</page>
<page string="Barcode Product" name="barcode_product" attrs="{'invisible': [('picking_type_code', '!=', 'incoming')]}">
<field name="barcode_product_lines"/>
</page>
+ <page string="Check Koli" name="check_koli" attrs="{'invisible': [('location_dest_id', '!=', 60)]}">
+ <field name="check_koli_lines"/>
+ </page>
+ <page string="Mapping Koli" name="konfirm_koli" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}">
+ <field name="konfirm_koli_lines"/>
+ </page>
+ <page string="Konfirm Koli" name="scan_koli" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}">
+ <field name="scan_koli_lines"/>
+ </page>
</page>
</field>
</record>
+ <record id="scan_koli_tree" model="ir.ui.view">
+ <field name="name">scan.koli.tree</field>
+ <field name="model">scan.koli</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="code_koli"/>
+ <field name="koli_id" options="{'no_create': True}" domain="[('state', '=', 'not_delivered')]"/>
+ <field name="scan_koli_progress"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="konfirm_koli_tree" model="ir.ui.view">
+ <field name="name">konfirm.koli.tree</field>
+ <field name="model">konfirm.koli</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="pick_id" options="{'no_create': True}" required="1" domain="[('picking_type_code', '=', 'internal'), ('group_id', '=', parent.group_id), ('linked_manual_bu_out', '=', parent.id)]"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="check_koli_tree" model="ir.ui.view">
+ <field name="name">check.koli.tree</field>
+ <field name="model">check.koli</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="koli"/>
+ <field name="reserved_id"/>
+ <field name="check_koli_progress"/>
+ </tree>
+ </field>
+ </record>
+
<record id="check_product_tree" model="ir.ui.view">
<field name="name">check.product.tree</field>
<field name="model">check.product</field>
<field name="arch" type="xml">
<tree editable="bottom" decoration-warning="status == 'Pending'" decoration-success="status == 'Done'">
+ <field name="code_product"/>
<field name="product_id"/>
<field name="quantity"/>
- <field name="status"/>
+ <field name="status" readonly="1"/>
</tree>
</field>
</record>
@@ -251,6 +332,35 @@
<field name="purchase_representative_id"/>
</field>
</field>
- </record>
+ </record>
+
+ <record id="view_warning_modal_wizard_form" model="ir.ui.view">
+ <field name="name">warning.modal.wizard.form</field>
+ <field name="model">warning.modal.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Peringatan Koli Belum Diperiksa">
+ <sheet>
+ <div class="oe_title">
+ <h2><span>⚠️ Perhatian!</span></h2>
+ </div>
+ <group>
+ <field name="message" readonly="1" nolabel="1" widget="text"/>
+ </group>
+ </sheet>
+ <footer>
+ <button name="action_continue" type="object" string="Lanjutkan" class="btn-primary"/>
+ <button string="Tutup" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_warning_modal_wizard" model="ir.actions.act_window">
+ <field name="name">Peringatan Koli</field>
+ <field name="res_model">warning.modal.wizard</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_warning_modal_wizard_form"/>
+ <field name="target">new</field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/user_pengajuan_tempo_request.xml b/indoteknik_custom/views/user_pengajuan_tempo_request.xml
index 339ce8db..898d5b2a 100644
--- a/indoteknik_custom/views/user_pengajuan_tempo_request.xml
+++ b/indoteknik_custom/views/user_pengajuan_tempo_request.xml
@@ -426,7 +426,7 @@
<menuitem
id="menu_user_pengajuan_tempo_request"
name="User Pengajuan Tempo Request"
- parent="res_partner_menu_user"
+ parent="account.menu_finance_receivables"
sequence="3"
action="action_user_pengajuan_tempo_request"
/>
diff --git a/indoteknik_custom/views/vendor_payment_term.xml b/indoteknik_custom/views/vendor_payment_term.xml
index e0e96388..7d16b129 100644
--- a/indoteknik_custom/views/vendor_payment_term.xml
+++ b/indoteknik_custom/views/vendor_payment_term.xml
@@ -8,6 +8,8 @@
<field name="display_name"/>
<field name="name"/>
<field name="parent_id"/>
+ <field name="minimum_amount"/>
+ <field name="minimum_amount_tax"/>
<field name="property_supplier_payment_term_id"/>
</tree>
</field>
@@ -23,6 +25,8 @@
<group>
<field name="name"/>
<field name="parent_id" readonly="1"/>
+ <field name="minimum_amount"/>
+ <field name="minimum_amount_tax"/>
<field name="property_supplier_payment_term_id"/>
</group>
</group>
diff --git a/indoteknik_custom/views/voucher.xml b/indoteknik_custom/views/voucher.xml
index ae958f05..78e42969 100755
--- a/indoteknik_custom/views/voucher.xml
+++ b/indoteknik_custom/views/voucher.xml
@@ -27,63 +27,71 @@
<group>
<group>
<field name="image" widget="image" width="120"/>
- <field name="name" required="1" />
- <field name="code" required="1" />
- <field name="visibility" required="1" />
+ <field name="name" required="1"/>
+ <field name="code" required="1"/>
+ <field name="voucher_category" widget="many2many"/>
+ <field name="visibility" required="1"/>
<field name="start_time" required="1"/>
<field name="end_time" required="1"/>
<field name="limit" required="1"/>
<field name="limit_user" required="1"/>
- <field name="apply_type" required="1" />
- <field name="account_type" required="1" />
- <field name="show_on_email" />
- <field name="excl_pricelist_ids" widget="many2many_tags" domain="[('id', 'in', [4, 15037, 15038, 15039, 17023, 17024, 17025, 17026,17027])]"/>
+ <field name="apply_type" required="1"/>
+ <field name="account_type" required="1"/>
+ <field name="show_on_email"/>
+ <field name="excl_pricelist_ids" widget="many2many_tags"
+ domain="[('id', 'in', [4, 15037, 15038, 15039, 17023, 17024, 17025, 17026,17027])]"/>
</group>
- <group string="Discount Settings" attrs="{'invisible': [('apply_type', 'not in', ['all', 'shipping'])]}">
- <field name="min_purchase_amount" widget="monetary" required="1" />
- <field name="discount_type" attrs="{'invisible': [('apply_type','not in', ['all', 'shipping'])], 'required': [('apply_type', 'in', ['all', 'shipping'])]}" />
+ <group string="Discount Settings"
+ attrs="{'invisible': [('apply_type', 'not in', ['all', 'shipping'])]}">
+ <field name="min_purchase_amount" widget="monetary" required="1"/>
+ <field name="discount_type"
+ attrs="{'invisible': [('apply_type','not in', ['all', 'shipping'])], 'required': [('apply_type', 'in', ['all', 'shipping'])]}"/>
- <label for="max_discount_amount" string="Discount Amount" />
+ <label for="max_discount_amount" string="Discount Amount"/>
<div class="d-flex align-items-center">
- <span
- class="mr-1 font-weight-bold"
- attrs="{'invisible': [('discount_type', '!=', 'fixed_price')]}"
+ <span
+ class="mr-1 font-weight-bold"
+ attrs="{'invisible': [('discount_type', '!=', 'fixed_price')]}"
>
Rp
</span>
- <field class="mb-0" name="discount_amount" required="1" />
- <span
- class="ml-1 font-weight-bold"
- attrs="{'invisible': [('discount_type', '!=', 'percentage')]}"
+ <field class="mb-0" name="discount_amount" required="1"/>
+ <span
+ class="ml-1 font-weight-bold"
+ attrs="{'invisible': [('discount_type', '!=', 'percentage')]}"
>
%
</span>
</div>
- <field name="max_discount_amount" widget="monetary" required="1" attrs="{'invisible': [('discount_type', '!=', 'percentage')]}"/>
+ <field name="max_discount_amount" widget="monetary" required="1"
+ attrs="{'invisible': [('discount_type', '!=', 'percentage')]}"/>
</group>
</group>
<notebook>
- <page name="voucher_line" string="Voucher Line" attrs="{'invisible': [('apply_type', '!=', 'brand')]}">
+ <page name="voucher_line" string="Voucher Line"
+ attrs="{'invisible': [('apply_type', '!=', 'brand')]}">
<field name="voucher_line">
<tree editable="bottom">
- <field name="manufacture_id" required="1" />
- <field name="min_purchase_amount" required="1" />
- <field name="discount_type" required="1" />
- <field name="discount_amount" required="1" />
- <field name="max_discount_amount" required="1" attrs="{'readonly': [('discount_type', '!=', 'percentage')]}" />
+ <field name="manufacture_id" required="1"/>
+ <field name="min_purchase_amount" required="1"/>
+ <field name="discount_type" required="1"/>
+ <field name="discount_amount" required="1"/>
+ <field name="max_discount_amount" required="1"
+ attrs="{'readonly': [('discount_type', '!=', 'percentage')]}"/>
</tree>
</field>
</page>
<page name="description" string="Description">
- <label for="description" string="Max 120 characters:" class="font-weight-normal mb-2 oe_edit_only"/>
- <field name="description" placeholder="Insert short description..." />
+ <label for="description" string="Max 120 characters:"
+ class="font-weight-normal mb-2 oe_edit_only"/>
+ <field name="description" placeholder="Insert short description..."/>
</page>
<page name="terms_conditions" string="Terms and Conditions">
- <field name="terms_conditions" />
+ <field name="terms_conditions"/>
</page>
<page name="order_page" string="Orders">
- <field name="order_ids" readonly="1" />
+ <field name="order_ids" readonly="1"/>
</page>
</notebook>
</sheet>
@@ -92,10 +100,10 @@
</record>
<menuitem id="voucher"
- name="Voucher"
- parent="website_sale.menu_catalog"
- sequence="1"
- action="voucher_action"
+ name="Voucher"
+ parent="website_sale.menu_catalog"
+ sequence="1"
+ action="voucher_action"
/>
</data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/x_banner_banner.xml b/indoteknik_custom/views/x_banner_banner.xml
index ec1e38a5..e40568cc 100755
--- a/indoteknik_custom/views/x_banner_banner.xml
+++ b/indoteknik_custom/views/x_banner_banner.xml
@@ -33,6 +33,7 @@
<field name="group_by_week" />
<field name="x_headline_banner" />
<field name="x_description_banner" />
+ <field name="x_keyword_banner" />
<field name="last_update_solr" readonly="1"/>
</group>
<group>