diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-05-27 10:19:09 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-05-27 10:19:09 +0700 |
| commit | f0f414383b3bd34e6fce12e68e171014c08d2a55 (patch) | |
| tree | f9eef4c1331f6507fadc680bdd801656ff9f8ea7 | |
| parent | 431229f2a6f1203fbdfe470229e55da8ebd3ea01 (diff) | |
| parent | d3f530b94569059106164172485aaa9665e80709 (diff) | |
Merge branch 'odoo-backup' into CR/repeat-order
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', '>', '2023-06-15'), ('create_date', '=', False)]}"/> - <field name="shipping_paid_by" attrs="{'required': ['|', ('create_date', '>', '2023-06-15'), ('create_date', '=', False)]}"/> + <field name="shipping_cost_covered" + attrs="{'required': ['|', ('create_date', '>', '2023-06-15'), ('create_date', '=', False)]}"/> + <field name="shipping_paid_by" + attrs="{'required': ['|', ('create_date', '>', '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> |
