diff options
38 files changed, 1280 insertions, 505 deletions
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 1a75c830..cff1921d 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -728,6 +728,7 @@ class SaleOrder(controller.Controller): if params['value']['type'] == 'sale_order': parameters['approval_status'] = 'pengajuan1' + # parameters['approval_status'] = False _logger.info("Setting approval_status to 'pengajuan1'") sale_order = request.env['sale.order'].with_context(from_website_checkout=True).create([parameters]) @@ -788,7 +789,7 @@ class SaleOrder(controller.Controller): order_line.product_id_change() order_line.weight = order_line.product_id.weight - order_line.onchange_vendor_id() + order_line._onchange_vendor_id_custom() _logger.info(f"After onchanges - Price: {order_line.price_unit}, Disc: {order_line.discount}") elif cart['cart_type'] == 'promotion': @@ -808,6 +809,14 @@ class SaleOrder(controller.Controller): sale_order.apply_promotion_program() sale_order.add_free_product(promotions) + # Pastikan baris hasil promo/bonus ditandai supaya bisa di-skip voucher + promo_lines = sale_order.order_line.filtered( + lambda l: getattr(l, 'order_promotion_id', False) or (l.price_unit or 0.0) == 0.0 + ) + if promo_lines: + promo_lines.write({'is_has_disc': True}) + _logger.info(f"[PROMO_MARK] Marked {len(promo_lines)} promo/free lines as is_has_disc=True") + voucher_code = params['value']['voucher'] if voucher_code: _logger.info(f"Processing voucher: {voucher_code}") @@ -816,7 +825,7 @@ class SaleOrder(controller.Controller): voucher_shipping = request.env['voucher'].search( [('code', '=', voucher_code), ('apply_type', 'in', ['shipping'])], limit=1) - if voucher and len(promotions) == 0: + if voucher: _logger.info("Applying regular voucher") sale_order.voucher_id = voucher.id sale_order.apply_voucher() diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py index a4a9cf80..fe82e665 100644 --- a/indoteknik_api/controllers/api_v1/stock_picking.py +++ b/indoteknik_api/controllers/api_v1/stock_picking.py @@ -1,5 +1,5 @@ from .. import controller -from odoo import http +from odoo import http, fields from odoo.http import request, Response from pytz import timezone from datetime import datetime @@ -8,7 +8,6 @@ import logging _logger = logging.getLogger(__name__) -_logger = logging.getLogger(__name__) class StockPicking(controller.Controller): prefix = '/api/v1/' @@ -125,30 +124,10 @@ class StockPicking(controller.Controller): @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, scanid, **kw): - sj_document = kw.get('sj_document', False) - paket_document = kw.get('paket_document', False) - dispatch_document = kw.get('dispatch_document', False) - - # ===== Role by EMAIL ===== - driver_emails = { - 'driverindoteknik@gmail.com', - 'sulistianaridwan8@gmail.com', - } - dispatch_emails = { - 'rahmat.afiudin@gmail.com', - 'it@fixcomart.co.id' - } - - login = (request.env.user.login or '').lower() - is_dispatch_user = login in dispatch_emails - is_driver_user = (login in driver_emails) and not is_dispatch_user - - # ===== Validasi minimal ===== - if not sj_document or not paket_document: - return self.response(code=400, description='dispatch_document wajib untuk role dispatch login= %s' % login) - - # if is_dispatch_user and not dispatch_document and not is_driver_user: - # return self.response(code=400, description='dispatch_document wajib untuk role dispatch login= %s' % login) + sj_document = kw.get('sj_document') if 'sj_document' in kw else None + paket_document = kw.get('paket_document') if 'paket_document' in kw else None + dispatch_document = kw.get('dispatch_document') if 'dispatch_document' in kw else None + self_pu= kw.get('self_pu') if 'self_pu' in kw else None # ===== Cari picking by id / picking_code ===== picking_data = False @@ -161,17 +140,21 @@ class StockPicking(controller.Controller): if not picking_data: return self.response(code=403, description='picking not found') - params = { - 'sj_documentation': sj_document, - 'paket_documentation': paket_document, - 'driver_arrival_date': datetime.utcnow(), - } + params = {} + if sj_document: + params['sj_documentation'] = sj_document + if self_pu: + params['driver_arrival_date'] = datetime.utcnow() + if paket_document: + params['paket_documentation'] = paket_document + params['driver_arrival_date'] = datetime.utcnow() if dispatch_document: params['dispatch_documentation'] = dispatch_document picking_data.write(params) return self.response({'name': picking_data.name}) + @http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False) def update_status_from_biteship(self, **kw): _logger.info("Biteship Webhook: Request received at controller start (type='json').") diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 3c3c9a95..392e848d 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -167,6 +167,7 @@ 'report/report_picking.xml', 'report/report_sale_order.xml', 'report/report_surat_piutang.xml', + 'report/report_tutup_tempo.xml', 'report/purchase_report.xml', 'views/vendor_sla.xml', 'views/coretax_faktur.xml', @@ -184,6 +185,8 @@ 'views/letter_receivable_mail_template.xml', # 'views/reimburse.xml', 'views/sj_tele.xml', + 'views/close_tempo_mail_template.xml', + 'views/domain_apo.xml', 'views/sourcing.xml' ], 'demo': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index cb110342..36a992d2 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -160,4 +160,6 @@ from . import update_date_planned_po_wizard from . import unpaid_invoice_view from . import letter_receivable from . import sj_tele +from . import partial_delivery +from . import domain_apo from . import sourcing_job_order diff --git a/indoteknik_custom/models/account_asset.py b/indoteknik_custom/models/account_asset.py index bd5f9adb..211ab229 100644 --- a/indoteknik_custom/models/account_asset.py +++ b/indoteknik_custom/models/account_asset.py @@ -4,6 +4,10 @@ from odoo.exceptions import AccessError, UserError, ValidationError class AccountAsset(models.Model): _inherit = 'account.asset.asset' + asset_type = fields.Selection(string='Tipe Aset', selection=[ + ('aset_gudang', ' Aset Gudang'), + ('aset_kantor', 'Aset Kantor'), + ], tracking=True ) def action_close_asset(self): for asset in self: diff --git a/indoteknik_custom/models/account_payment.py b/indoteknik_custom/models/account_payment.py index 11c664eb..d2d3d175 100644 --- a/indoteknik_custom/models/account_payment.py +++ b/indoteknik_custom/models/account_payment.py @@ -42,7 +42,7 @@ class AccountPayment(models.Model): def allocate_invoices(self): for payment in self: - if self. + # if self. for line in payment.payment_line: invoice = line.account_move_id move_lines = payment.line_ids.filtered(lambda line: line.account_internal_type in ('receivable', 'payable')) diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 449bd90b..e45305db 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -56,6 +56,7 @@ class ApprovalPaymentTerm(models.Model): change_log_688 = fields.Text(string="Change Log", readonly=True, copy=False) + def write(self, vals): # Ambil nilai lama sebelum perubahan old_values_dict = { diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py index 83a7cb3c..d9ec17f4 100644 --- a/indoteknik_custom/models/automatic_purchase.py +++ b/indoteknik_custom/models/automatic_purchase.py @@ -93,7 +93,7 @@ class AutomaticPurchase(models.Model): counter = 0 for vendor in vendor_ids: - self.create_po_by_vendor(vendor['partner_id'][0]) + self.create_po_for_vendor(vendor['partner_id'][0]) # param_header = { # 'partner_id': vendor['partner_id'][0], @@ -191,95 +191,106 @@ class AutomaticPurchase(models.Model): if qty_pj > qty_outgoing_pj: raise UserError('Qty yang anda beli lebih dari qty outgoing. %s' %id_po) - def create_po_by_vendor(self, vendor_id): + def create_po_for_vendor(self, vendor_id): current_time = datetime.now() name = "/PJ/" if not self.apo_type == 'reordering' else "/A/" - PRODUCT_PER_PO = 20 auto_purchase_line = self.env['automatic.purchase.line'] - # Domain untuk semua baris dengan vendor_id tertentu - domain = [ + config = self.env['apo.domain.config'].search([ + ('vendor_id', '=', vendor_id) + ], limit=1) + + base_domain = [ ('automatic_purchase_id', '=', self.id), ('partner_id', '=', vendor_id), ('qty_purchase', '>', 0) ] - # Tambahkan domain khusus untuk brand_id 22 dan 564 - special_brand_domain = domain + [('brand_id', 'in', [22, 564])] - regular_domain = domain + [('brand_id', 'not in', [22, 564])] - - # Fungsi untuk membuat PO berdasarkan domain tertentu - def create_po_for_domain(domain, special_payment_term=False): - products_len = auto_purchase_line.search_count(domain) - page = math.ceil(products_len / PRODUCT_PER_PO) - - for i in range(page): - # Buat PO baru - param_header = { - 'partner_id': vendor_id, - '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, - 'from_apo': True, - 'note_description': 'Automatic PO' - } + # Kalau vendor punya brand spesial → bikin domain sesuai config + if config and config.is_special: + special_brand_domain = base_domain + [('brand_id', 'in', config.brand_ids.ids)] + self._create_po_for_domain( + vendor_id, special_brand_domain, name, PRODUCT_PER_PO, current_time, config.payment_term_id, special=config.is_special + ) - new_po = self.env['purchase.order'].create([param_header]) + # Regular domain (selain brand spesial) + regular_domain = base_domain + if config and config.is_special and config.brand_ids: + regular_domain = base_domain + [('brand_id', 'not in', config.brand_ids.ids)] - # Set payment_term_id khusus jika diperlukan - if special_payment_term: - new_po.payment_term_id = 29 - else: - new_po.payment_term_id = new_po.partner_id.property_supplier_payment_term_id + self._create_po_for_domain( + vendor_id, regular_domain, name, PRODUCT_PER_PO, current_time, config.payment_term_id + ) - new_po.name = new_po.name + name + str(i + 1) - self.env['automatic.purchase.match'].create([{ - 'automatic_purchase_id': self.id, - 'order_id': new_po.id - }]) + def _create_po_for_domain(self, vendor_id, domain, name, PRODUCT_PER_PO, current_time, payment_term_id, special=False): + auto_purchase_line = self.env['automatic.purchase.line'] + products_len = auto_purchase_line.search_count(domain) + page = math.ceil(products_len / PRODUCT_PER_PO) + + for i in range(page): + # Buat header PO + param_header = { + 'partner_id': vendor_id, + 'currency_id': 12, + 'user_id': self.env.user.id, + 'company_id': 1, + 'picking_type_id': 28, + 'date_order': current_time, + 'from_apo': True, + 'note_description': 'Automatic PO' + } + new_po = self.env['purchase.order'].create(param_header) + + # Set payment term + new_po.payment_term_id = payment_term_id.id if special else ( + new_po.partner_id.property_supplier_payment_term_id + ) + + new_po.name = new_po.name + name + str(i + 1) + + self.env['automatic.purchase.match'].create([{ + 'automatic_purchase_id': self.id, + 'order_id': new_po.id + }]) + + # Ambil lines + lines = auto_purchase_line.search( + domain, + offset=i * PRODUCT_PER_PO, + limit=PRODUCT_PER_PO + ) + + # Pre-fetch sales_match biar ga search per line + sales_matches = self.env['automatic.purchase.sales.match'].search([ + ('automatic_purchase_id', '=', self.id), + ('product_id', 'in', lines.mapped('product_id').ids), + ]) + match_map = {sm.product_id.id: sm for sm in sales_matches} + + for line in lines: + product = line.product_id + sales_match = match_map.get(product.id) + param_line = { + 'order_id': new_po.id, + 'product_id': product.id, + 'product_qty': line.qty_purchase, + 'qty_available_store': product.qty_available_bandengan, + 'suggest': product._get_po_suggest(line.qty_purchase), + 'product_uom_qty': line.qty_purchase, + 'price_unit': line.last_price, + 'ending_price': line.last_price, + 'taxes_id': [(6, 0, [line.taxes_id.id])] if line.taxes_id else False, + 'so_line_id': sales_match.sale_line_id.id if sales_match else None, + 'so_id': sales_match.sale_id.id if sales_match else None + } + new_po_line = self.env['purchase.order.line'].create(param_line) + line.current_po_id = new_po.id + line.current_po_line_id = new_po_line.id + + self.create_purchase_order_sales_match(new_po) - # Ambil baris sesuai halaman - lines = auto_purchase_line.search( - domain, - offset=i * PRODUCT_PER_PO, - limit=PRODUCT_PER_PO - ) - - for line in lines: - product = line.product_id - sales_match = self.env['automatic.purchase.sales.match'].search([ - ('automatic_purchase_id', '=', self.id), - ('product_id', '=', product.id), - ]) - param_line = { - 'order_id': new_po.id, - 'product_id': product.id, - 'product_qty': line.qty_purchase, - 'qty_available_store': product.qty_available_bandengan, - 'suggest': product._get_po_suggest(line.qty_purchase), - 'product_uom_qty': line.qty_purchase, - 'price_unit': line.last_price, - 'ending_price': line.last_price, - 'taxes_id': [line.taxes_id.id] if line.taxes_id else None, - 'so_line_id': sales_match[0].sale_line_id.id if sales_match else None, - 'so_id': sales_match[0].sale_id.id if sales_match else None - } - new_po_line = self.env['purchase.order.line'].create([param_line]) - line.current_po_id = new_po.id - line.current_po_line_id = new_po_line.id - - self.create_purchase_order_sales_match(new_po) - - # Buat PO untuk special brand - if vendor_id == 23: - create_po_for_domain(special_brand_domain, special_payment_term=True) - - # Buat PO untuk regular domain - create_po_for_domain(regular_domain, "") def update_purchase_price_so_line(self, apo): diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py index 6d538b83..a937e2d0 100644 --- a/indoteknik_custom/models/commision.py +++ b/indoteknik_custom/models/commision.py @@ -428,22 +428,22 @@ class CustomerCommision(models.Model): 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.id == 19 or self.env.user.has_group('indoteknik_custom.group_role_it')): self.status = 'pengajuan2' 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 == 216: + elif self.status == 'pengajuan2' and (self.env.user.id == 216 or self.env.user.has_group('indoteknik_custom.group_role_it')): 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: + elif self.status == 'pengajuan3' and (self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_it')): 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: + elif self.status == 'pengajuan4' and (self.env.user.id == 1272 or self.env.user.has_group('indoteknik_custom.group_role_it')): for line in self.commision_lines: line.invoice_id.is_customer_commision = True if self.commision_type == 'fee': diff --git a/indoteknik_custom/models/domain_apo.py b/indoteknik_custom/models/domain_apo.py new file mode 100644 index 00000000..585dd24c --- /dev/null +++ b/indoteknik_custom/models/domain_apo.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ApoDomainConfig(models.Model): + _name = 'apo.domain.config' + _description = 'Automatic Purchase Domain Config' + + name = fields.Char(string="Config Name", required=True) + vendor_id = fields.Many2one('res.partner', string="Vendor", required=True, domain=[('supplier_rank', '>', 0)]) + brand_ids = fields.Many2many('x_manufactures', string="Special Brands") + payment_term_id = fields.Many2one('account.payment.term', string="Payment Term") + is_special = fields.Boolean(string="Special Vendor?", default=False) diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 16034938..a98e46a1 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -23,6 +23,7 @@ class SuratPiutang(models.Model): tujuan_nama = fields.Char(string="Nama Tujuan", tracking=True) tujuan_email = fields.Char(string="Email Tujuan", tracking=True) perihal = fields.Selection([ + ('tutup_tempo', 'Surat Penutupan Pembayaran Tempo'), ('penagihan', 'Surat Resmi Penagihan'), ('sp1', 'Surat Peringatan Piutang ke-1'), ('sp2', 'Surat Peringatan Piutang ke-2'), @@ -36,6 +37,7 @@ class SuratPiutang(models.Model): ("sent", "Approved & Sent") ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) + due_date = fields.Date(string="Tanggal Jatuh Tempo", tracking=True, default= fields.Date.today) seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim") periode_invoices_terpilih = fields.Char( string="Periode Invoices Terpilih", @@ -67,6 +69,18 @@ class SuratPiutang(models.Model): "sp1": "sp2", "sp2": "sp3", } + + def action_select_all_lines(self): + for rec in self: + if not rec.line_ids: + raise UserError(_("Tidak ada invoice line untuk dipilih.")) + rec.line_ids.write({'selected': True}) + + def action_unselect_all_lines(self): + for rec in self: + if not rec.line_ids: + raise UserError(_("Tidak ada invoice line untuk dihapus seleksinya.")) + rec.line_ids.write({'selected': False}) @api.onchange('partner_id') def _onchange_partner_id_domain(self): @@ -191,17 +205,17 @@ class SuratPiutang(models.Model): line.amount_residual or 0.0 for line in rec.line_ids if line.selected ) - @api.constrains("tujuan_email") - def _check_email_format(self): - for rec in self: - if rec.tujuan_email and not mail.single_email_re.match(rec.tujuan_email): - raise ValidationError(_("Format email tidak valid: %s") % rec.tujuan_email) + # @api.constrains("tujuan_email") + # def _check_email_format(self): + # for rec in self: + # if rec.tujuan_email and not mail.single_email_re.match(rec.tujuan_email): + # raise ValidationError(_("Format email tidak valid: %s") % rec.tujuan_email) def action_approve(self): wib = pytz.timezone('Asia/Jakarta') now_wib = datetime.now(wib) - sales_manager_ids = [10] # ganti dengan ID user Sales Manager + sales_manager_ids = [19] # ganti dengan ID user Sales Manager pimpinan_user_ids = [7] # ganti dengan ID user Pimpinan for rec in self: @@ -228,20 +242,27 @@ class SuratPiutang(models.Model): continue # === Surat penagihan biasa (langsung Pimpinan approve) === - if rec.perihal == "penagihan": + if rec.perihal in ("tutup_tempo", "penagihan"): # if self.env.user.id not in pimpinan_user_ids: # raise UserError("Hanya Pimpinan yang boleh menyetujui surat penagihan.") rec.state = "sent" now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) rec.send_date = now_utc rec.action_send_letter() - rec.message_post(body="Surat Penagihan disetujui dan berhasil dikirim.") + rec.message_post(body=f"{rec.perihal_label} disetujui dan berhasil dikirim.") self.env.user.notify_info( message=f"Surat piutang {rec.name} berhasil dikirim ke {rec.partner_id.name} ({rec.tujuan_email})", title="Informasi", sticky=False ) + + def action_print(self): + self.ensure_one() + if self.perihal == 'tutup_tempo': + return self.env.ref('indoteknik_custom.action_report_surat_tutup_tempo').report_action(self) + else: + return self.env.ref('indoteknik_custom.action_report_surat_piutang').report_action(self) def action_send_letter(self): self.ensure_one() @@ -253,64 +274,79 @@ class SuratPiutang(models.Model): if not self.tujuan_email: raise UserError(_("Email tujuan harus diisi.")) - template = self.env.ref('indoteknik_custom.letter_receivable_mail_template') - # today = fields.Date.today() - - month_map = { - 1: "Januari", 2: "Februari", 3: "Maret", 4: "April", - 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus", - 9: "September", 10: "Oktober", 11: "November", 12: "Desember", - } - target_date = (self.send_date or fields.Datetime.now()).date() + timedelta(days=7) - self.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}" - - perihal_map = { - 'penagihan': 'Surat Resmi Penagihan', - 'sp1': 'Surat Peringatan Pertama (I)', - 'sp2': 'Surat Peringatan Kedua (II)', - 'sp3': 'Surat Peringatan Ketiga (III)', - } - perihal_text = perihal_map.get(self.perihal, self.perihal or '') - - invoice_table_rows = "" - grand_total = 0 - for line in selected_lines: - # days_to_due = (line.invoice_date_due - today).days if line.invoice_date_due else 0 - grand_total += line.amount_residual - invoice_table_rows += f""" - <tr> - <td>{line.invoice_number or '-'}</td> - <td>{self.partner_id.name or '-'}</td> - <td>{fields.Date.to_string(line.invoice_date) or '-'}</td> - <td>{fields.Date.to_string(line.invoice_date_due) or '-'}</td> - <td>{line.new_invoice_day_to_due}</td> - <td>{line.ref or '-'}</td> - <td>{formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)}</td> - <td>{line.payment_term_id.name or '-'}</td> - </tr> - """ - - invoice_table_footer = f""" - <tfoot> - <tr style="font-weight:bold; background-color:#f9f9f9;"> - <td colspan="6" align="right">Grand Total</td> - <td>{formatLang(self.env, grand_total, currency_obj=self.currency_id, monetary=True)}</td> - <td colspan="2"></td> + template = None + report = None + body_html = None + subject = None + + # Logika untuk memilih template dan report berdasarkan 'perihal' + if self.perihal == 'tutup_tempo': + template = self.env.ref('indoteknik_custom.close_tempo_mail_template') + report = self.env.ref('indoteknik_custom.action_report_surat_tutup_tempo') + due_date_str = self.due_date.strftime('%d %B %Y') if self.due_date else 'yang telah ditentukan' + body_html = template.body_html \ + .replace('${object.partner_id.name}', self.partner_id.name or '') \ + .replace('${object.due_date}', due_date_str or '') + subject = f"Pemberitahuan Penutupan Pembayaran Tempo – {self.partner_id.name}" + else: + template = self.env.ref('indoteknik_custom.letter_receivable_mail_template') + + month_map = { + 1: "Januari", 2: "Februari", 3: "Maret", 4: "April", + 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus", + 9: "September", 10: "Oktober", 11: "November", 12: "Desember", + } + target_date = (self.send_date or fields.Datetime.now()).date() + timedelta(days=7) + self.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}" + + perihal_map = { + 'penagihan': 'Surat Resmi Penagihan', + 'sp1': 'Surat Peringatan Pertama (I)', + 'sp2': 'Surat Peringatan Kedua (II)', + 'sp3': 'Surat Peringatan Ketiga (III)', + } + perihal_text = perihal_map.get(self.perihal, self.perihal or '') + + invoice_table_rows = "" + grand_total = 0 + for line in selected_lines: + grand_total += line.amount_residual + invoice_table_rows += f""" + <tr> + <td>{line.invoice_number or '-'}</td> + <td>{self.partner_id.name or '-'}</td> + <td>{fields.Date.to_string(line.invoice_date) or '-'}</td> + <td>{fields.Date.to_string(line.invoice_date_due) or '-'}</td> + <td>{line.new_invoice_day_to_due}</td> + <td>{line.ref or '-'}</td> + <td>{formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)}</td> + <td>{line.payment_term_id.name or '-'}</td> </tr> - </tfoot> - """ - # inject table rows ke template - body_html = re.sub( - r"<tbody[^>]*>.*?</tbody>", - f"<tbody>{invoice_table_rows}</tbody>{invoice_table_footer}", - template.body_html, - flags=re.DOTALL - ).replace('${object.name}', self.name or '') \ - .replace('${object.partner_id.name}', self.partner_id.name or '') \ - .replace('${object.seven_days_after_sent_date}', self.seven_days_after_sent_date or '') \ - .replace('${object.perihal}', perihal_text or '') - - report = self.env.ref('indoteknik_custom.action_report_surat_piutang') + """ + + invoice_table_footer = f""" + <tfoot> + <tr style="font-weight:bold; background-color:#f9f9f9;"> + <td colspan="6" align="right">Grand Total</td> + <td>{formatLang(self.env, grand_total, currency_obj=self.currency_id, monetary=True)}</td> + <td colspan="2"></td> + </tr> + </tfoot> + """ + + body_html = re.sub( + r"<tbody[^>]*>.*?</tbody>", + f"<tbody>{invoice_table_rows}</tbody>{invoice_table_footer}", + template.body_html, + flags=re.DOTALL + ).replace('${object.name}', self.name or '') \ + .replace('${object.partner_id.name}', self.partner_id.name or '') \ + .replace('${object.seven_days_after_sent_date}', self.seven_days_after_sent_date or '') \ + .replace('${object.perihal}', perihal_text or '') + + report = self.env.ref('indoteknik_custom.action_report_surat_piutang') + subject = perihal_map.get(self.perihal, self.perihal or '') + " - " + (self.partner_id.name or '') + pdf_content, _ = report._render_qweb_pdf([self.id]) attachment_base64 = base64.b64encode(pdf_content) @@ -335,12 +371,12 @@ class SuratPiutang(models.Model): cc_list.append(sales_email) values = { - # 'subject': template.subject.replace('${object.name}', self.name or ''), - 'subject': perihal_map.get(self.perihal, self.perihal or '') + " - " + (self.partner_id.name or ''), + 'subject': subject, # Menggunakan subject yang sudah ditentukan di atas 'email_to': self.tujuan_email, 'email_from': 'finance@indoteknik.co.id', 'email_cc': ",".join(sorted(set(cc_list))), - 'body_html': body_html, + # 'email_cc': 'finance@indoteknik.co.id', # testing + 'body_html': body_html, # Menggunakan body_html yang sudah ditentukan di atas 'attachments': [(attachment.name, attachment.datas)], 'reply_to': 'finance@indoteknik.co.id', } @@ -352,7 +388,7 @@ class SuratPiutang(models.Model): ) _logger.info( - f"Surat Piutang {self.name} terkirim ke {self.tujuan_email} " + f"{self.name} terkirim ke {self.tujuan_email} " f"({self.partner_id.name}), total {len(selected_lines)} invoice." ) @@ -453,6 +489,18 @@ class SuratPiutang(models.Model): body=f"Line Invoices diperbarui. Total line saat ini: {len(rec.line_ids)}" ) + @api.onchange('perihal', 'partner_id') + def _onchange_perihal_tutup_tempo(self): + if self.perihal == 'tutup_tempo': + for line in self.line_ids: + if line.new_invoice_day_to_due < -30: + line.selected = True + else: + line.selected = False + else: + for line in self.line_ids: + line.selected = False + @api.model def create(self, vals): # Generate nomor surat otomatis @@ -462,7 +510,7 @@ class SuratPiutang(models.Model): bulan_romawi = ["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII"][today.month-1] tahun = today.strftime("%y") vals["name"] = f"{seq}/LO/FAT/IDG/{bulan_romawi}/{tahun}" - if vals.get("perihal") == "penagihan": + if vals.get("perihal") in ("tutup_tempo", "penagihan"): vals["state"] = "waiting_approval_pimpinan" else: vals["state"] = "waiting_approval_sales" diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py index b39995b5..02679458 100644 --- a/indoteknik_custom/models/mrp_production.py +++ b/indoteknik_custom/models/mrp_production.py @@ -21,6 +21,22 @@ class MrpProduction(models.Model): ], 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) + def action_cancel(self): + for production in self: + moves_with_forecast = production.move_raw_ids.filtered( + lambda m: m.reserved_availability > 0 + ) + + if moves_with_forecast: + # bikin list produk per baris + product_list = "\n".join( + "- %s" % p.display_name for p in moves_with_forecast.mapped('product_id') + ) + raise UserError(_( + "You cannot cancel this Manufacturing Order because the following raw materials " + "still have forecast availability:\n\n%s" % product_list + )) + return super(MrpProduction, self).action_cancel() @api.constrains('check_bom_product_lines') def constrains_check_bom_product_lines(self): @@ -292,6 +308,9 @@ class CheckBomProduct(models.Model): if not self.code_product: return + if self.production_id.qty_producing == 0: + raise UserError("Isi dan Save dahulu Quantity To Produce yang diinginkan!") + # Cari product berdasarkan default_code, barcode, atau barcode_box product = self.env['product.product'].search([ '|', diff --git a/indoteknik_custom/models/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py new file mode 100644 index 00000000..4df7da1e --- /dev/null +++ b/indoteknik_custom/models/partial_delivery.py @@ -0,0 +1,257 @@ +from odoo import fields, models, api, _ +from odoo.exceptions import UserError, ValidationError +from datetime import datetime, timedelta, timezone, time +import logging, random, string, requests, math, json, re, qrcode, base64 + +_logger = logging.getLogger(__name__) + +class PartialDeliveryWizard(models.TransientModel): + _name = 'partial.delivery.wizard' + _description = 'Partial Delivery Wizard' + + sale_id = fields.Many2one('sale.order') + picking_ids = fields.Many2many('stock.picking') + picking_id = fields.Many2one( + 'stock.picking', + string='Delivery Order', + domain="[('id','in',picking_ids), ('state', 'not in', ('done', 'cancel')), ('name', 'like', 'BU/PICK/%')]" + ) + line_ids = fields.One2many('partial.delivery.wizard.line', 'wizard_id') + + # @api.model + # def default_get(self, fields_list): + # res = super().default_get(fields_list) + # picking_ids_ctx = self.env.context.get('default_picking_ids') + # lines = [] + # if picking_ids_ctx: + # if isinstance(picking_ids_ctx, list) and picking_ids_ctx and isinstance(picking_ids_ctx[0], tuple): + # picking_ids = picking_ids_ctx[0][2] + # else: + # picking_ids = picking_ids_ctx + + # pickings = self.env['stock.picking'].browse(picking_ids) + # moves = pickings.move_ids_without_package.filtered(lambda m: m.reserved_availability > 0) + + # for move in moves: + # lines.append((0, 0, { + # 'product_id': move.product_id.id, + # 'reserved_qty': move.reserved_availability, + # 'move_id': move.id, + # })) + # res['line_ids'] = lines + # return res + + def action_select_all(self): + if len(self.line_ids) == 0: + raise UserError(_("Tidak ada produk yang dipilih.")) + for line in self.line_ids: + line.selected = True + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'view_mode': 'form', + 'res_id': self.id, + 'target': 'new', + } + + def action_unselect_all(self): + if len(self.line_ids) == 0: + raise UserError(_("Tidak ada produk yang dipilih.")) + for line in self.line_ids: + line.selected = False + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'view_mode': 'form', + 'res_id': self.id, + 'target': 'new', + } + + @api.onchange('picking_id') + def _onchange_picking_id(self): + """Generate lines whenever picking_id is changed""" + if not self.picking_id: + self.line_ids = [(5, 0, 0)] + return + + if self.line_ids: + self.line_ids.unlink() + + moves = self.picking_id.move_lines or self.picking_id.move_ids_without_package + moves = moves.filtered(lambda m: m.product_id and m.reserved_availability > 0) + + if not moves: + _logger.warning(f"[PartialDeliveryWizard] Tidak ada move line di picking {self.picking_id.name}") + return + + for move in moves: + reserved_qty = move.reserved_availability or 0.0 + ordered_qty = move.sale_line_id.product_uom_qty if move.sale_line_id else 0.0 + + self.env['partial.delivery.wizard.line'].create({ + 'wizard_id': self.id, + 'product_id': move.product_id.id, + 'reserved_qty': reserved_qty, + # 'selected_qty': reserved_qty, + 'move_id': move.id, + 'sale_line_id': move.sale_line_id.id if move.sale_line_id else False, + }) + + _logger.info( + f"[PartialDeliveryWizard] ✅ Created line for {move.product_id.display_name} " + f"(reserved={reserved_qty}, move_id={move.id})" + ) + + + def action_confirm_partial_delivery(self): + self.ensure_one() + StockPicking = self.env['stock.picking'] + + picking = self.picking_id + if not picking: + raise UserError(_("Tidak ada picking yang dipilih.")) + + if picking.state != "assigned": + raise UserError(_("Picking harus dalam status Ready (assigned).")) + + lines_by_qty = self.line_ids.filtered(lambda l: l.selected_qty > 0) + lines_by_selected = self.line_ids.filtered(lambda l: l.selected and not l.selected_qty) + selected_lines = lines_by_qty | lines_by_selected # gabung dua domain hasil filter + + if not selected_lines: + raise UserError(_("Tidak ada produk yang dipilih atau diisi jumlahnya.")) + + # 🧠 Cek apakah semua move di DO sudah muncul di wizard dan semua dipilih + picking_move_ids = picking.move_ids_without_package.ids + wizard_move_ids = self.line_ids.mapped('move_id').ids + + # Semua move DO muncul di wizard, dan semua baris dipilih + full_selected = ( + set(picking_move_ids) == set(wizard_move_ids) + and len(selected_lines) == len(self.line_ids) + and all( + (line.selected_qty or line.reserved_qty) >= line.reserved_qty + for line in selected_lines + ) + ) + + if full_selected: + # 💡 Gak perlu bikin picking baru, langsung ubah state_reserve + picking.write({'state_reserve': 'partial'}) + + picking.message_post( + body=f"<b>Full Picking Confirmed</b> via wizard partial delivery oleh {self.env.user.name} (tanpa DO baru)", + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + return { + "type": "ir.actions.act_window", + "res_model": "stock.picking", + "view_mode": "form", + "res_id": picking.id, + "target": "current", + "effect": { + "fadeout": "slow", + "message": f"✅ Semua produk dari DO ini dikirim penuh — tidak dibuat DO baru.", + "type": "rainbow_man", + }, + } + + # 🧩 Kalau bukan full selected, lanjut bikin DO baru + new_picking = StockPicking.create({ + 'origin': picking.origin, + 'partner_id': picking.partner_id.id, + 'picking_type_id': picking.picking_type_id.id, + 'location_id': picking.location_id.id, + 'location_dest_id': picking.location_dest_id.id, + 'company_id': picking.company_id.id, + 'state_reserve': 'partial', + }) + + for line in selected_lines: + if line.selected_qty > line.reserved_qty: + raise UserError(_("Jumlah produk %s yang dipilih melebihi jumlah reserved.") % line.product_id.display_name) + move = line.move_id + move._do_unreserve() + + if line.selected and not line.selected_qty: + line.selected_qty = line.reserved_qty + + if line.selected_qty > 0: + if line.selected_qty > move.product_uom_qty: + raise UserError(_( + f"Qty kirim ({line.selected_qty}) untuk {move.product_id.display_name} melebihi qty move ({move.product_uom_qty})." + )) + + if line.selected_qty < move.product_uom_qty: + qty_to_keep = move.product_uom_qty - line.selected_qty + new_move = move.copy(default={ + 'product_uom_qty': line.selected_qty, + 'picking_id': new_picking.id, + 'partial': True, + }) + move.write({'product_uom_qty': qty_to_keep}) + else: + move.write({'picking_id': new_picking.id, 'partial': True}) + + new_picking.action_confirm() + new_picking.action_assign() + picking.action_assign() + + existing_partials = self.env['stock.picking'].search([ + ('origin', '=', picking.origin), + ('state_reserve', '=', 'partial'), + ('id', '!=', new_picking.id), + ], order='name asc') + + suffix_number = len(existing_partials) + 1 + new_picking.name = f"{picking.name}/{suffix_number}" + + if picking.origin: + sale_order = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1) + if sale_order: + sale_order.message_post( + body=f"<b>Partial Delivery Created:</b> <a href=# data-oe-model='stock.picking' data-oe-id='{new_picking.id}'>{new_picking.name}</a> " + f"oleh {self.env.user.name}", + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + new_picking.message_post( + body=f"<b>Partial Picking created</b> dari {picking.name} oleh {self.env.user.name}", + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + return { + "type": "ir.actions.act_window", + "res_model": "stock.picking", + "view_mode": "form", + "res_id": new_picking.id, + "target": "current", + "effect": { + "fadeout": "slow", + "message": f"🚚 Partial Delivery {new_picking.name} berhasil dibuat!", + "type": "rainbow_man", + }, + } + +class PartialDeliveryWizardLine(models.TransientModel): + _name = 'partial.delivery.wizard.line' + _description = 'Partial Delivery Wizard Line' + + wizard_id = fields.Many2one('partial.delivery.wizard') + product_id = fields.Many2one('product.product', string="Product") + reserved_qty = fields.Float(string="Reserved Qty") + selected_qty = fields.Float(string="Send Qty") + move_id = fields.Many2one('stock.move') + selected = fields.Boolean(string="Select") + sale_line_id = fields.Many2one('sale.order.line', string="SO Line", related='move_id.sale_line_id') + ordered_qty = fields.Float(related='sale_line_id.product_uom_qty', string="Ordered Qty") + + @api.onchange('selected') + def onchange_selected(self): + if self.selected: + self.selected_qty = self.reserved_qty + diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index b34ec926..e79417aa 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -1069,6 +1069,21 @@ class PurchaseOrder(models.Model): ) % order.name) def button_confirm(self): + if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan + if '/PJ/' in self.name: + low_margin_lines = self.order_sales_match_line.filtered( + lambda match: match.so_header_margin <= 15.0 + ) + price_change_detected = any(line.price_unit_before for line in self.order_line) + if low_margin_lines and price_change_detected: + # raise UserError("Matches SO terdapat item dengan header margin SO <= 15%. Approval Pimpinan diperlukan.") + raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line yang Matches SO item memiliki header margin SO <= 15%") + # else: + # is_po_manual = '/A/' not in self.name and '/MO/' not in self.name + # if is_po_manual: + # if not self.order_sales_match_line: + # raise UserError("Tidak ada matches SO, Approval Pimpinan diperlukan.") + self._check_assets_note() # self._check_payment_term() # check payment term res = super(PurchaseOrder, self).button_confirm() @@ -1077,7 +1092,7 @@ class PurchaseOrder(models.Model): self.check_different_vendor_so_po() # self.check_data_vendor() - if self.amount_untaxed >= 50000000 and not self.env.user.id == 21: + if self.amount_untaxed >= 50000000 and not self.env.user.id in (21, 7): raise UserError("Hanya Rafly Hanggara yang bisa approve") if not self.date_planned: @@ -1405,63 +1420,50 @@ class PurchaseOrder(models.Model): ('product_id', '=', line.product_id.id), ('order_id', '=', line.purchase_order_id.id) ], limit=1) - sale_order_line = line.sale_line_id - if not sale_order_line: - sale_order_line = self.env['sale.order.line'].search([ - ('product_id', '=', line.product_id.id), - ('order_id', '=', line.sale_id.id) - ], limit=1, order='price_reduce_taxexcl') + sale_order_line = line.sale_line_id or self.env['sale.order.line'].search([ + ('product_id', '=', line.product_id.id), + ('order_id', '=', line.sale_id.id) + ], limit=1, order='price_reduce_taxexcl') if sale_order_line and po_line: - so_margin = (line.qty_po / line.qty_so) * sale_order_line.item_margin + qty_so = line.qty_so or 0 + qty_po = line.qty_po or 0 + + # Hindari division by zero + so_margin = (qty_po / qty_so) * sale_order_line.item_margin if qty_so > 0 else 0 sum_so_margin += so_margin - sales_price = sale_order_line.price_reduce_taxexcl * line.qty_po + sales_price = sale_order_line.price_reduce_taxexcl * qty_po if sale_order_line.order_id.shipping_cost_covered == 'indoteknik': - sales_price -= (sale_order_line.delivery_amt_line / sale_order_line.product_uom_qty) * line.qty_po + sales_price -= (sale_order_line.delivery_amt_line / sale_order_line.product_uom_qty) * qty_po if sale_order_line.order_id.fee_third_party > 0: - sales_price -= (sale_order_line.fee_third_party_line / sale_order_line.product_uom_qty) * line.qty_po + sales_price -= (sale_order_line.fee_third_party_line / sale_order_line.product_uom_qty) * qty_po sum_sales_price += sales_price - purchase_price = po_line.price_subtotal if po_line.ending_price > 0: if po_line.taxes_id.id == 22: - ending_price = po_line.ending_price / 1.11 - purchase_price = ending_price + purchase_price = po_line.ending_price / 1.11 else: purchase_price = po_line.ending_price if line.purchase_order_id.delivery_amount > 0: - purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * line.qty_po + purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * qty_po if line.purchase_order_id.delivery_amt > 0: purchase_price += line.purchase_order_id.delivery_amt + real_item_margin = sales_price - purchase_price sum_margin += real_item_margin - if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0: + # Akumulasi hasil akhir + if sum_sales_price != 0: self.total_so_margin = sum_so_margin self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 - else: - self.total_margin = 0 - self.total_percent_margin = 0 - self.total_so_margin = 0 - self.total_so_percent_margin = 0 - - - if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0: - self.total_so_margin = sum_so_margin - self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 - self.total_margin = sum_margin - self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 + self.total_margin = self.total_percent_margin = 0 + self.total_so_margin = self.total_so_percent_margin = 0 - else: - self.total_margin = 0 - self.total_percent_margin = 0 - self.total_so_margin = 0 - self.total_so_percent_margin = 0 def compute_amt_total_without_service(self): for order in self: diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index a3c3a33b..8c72887d 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -51,6 +51,12 @@ class PurchaseOrderLine(models.Model): contribution_cost_service = fields.Float(string='Contribution Cost Service', compute='_compute_doc_delivery_amt') ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt') show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True) + price_unit_before = fields.Float(string='Unit Price Before', help="Harga awal yang sebelumnya telah diinputkan") + + @api.onchange('price_unit') + def _onchange_price_unit_before(self): + if self._origin: + self.price_unit_before = self._origin.price_unit def _compute_doc_delivery_amt(self): for line in self: diff --git a/indoteknik_custom/models/purchase_order_sales_match.py b/indoteknik_custom/models/purchase_order_sales_match.py index 084b93f7..ea25a3b1 100644 --- a/indoteknik_custom/models/purchase_order_sales_match.py +++ b/indoteknik_custom/models/purchase_order_sales_match.py @@ -29,6 +29,11 @@ class PurchaseOrderSalesMatch(models.Model): 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') bu_pick = fields.Many2one('stock.picking', string='BU Pick', compute='compute_bu_pick') + so_header_margin = fields.Float( + related='sale_id.total_percent_margin', + string='SO Header Margin %', + readonly=True + ) def compute_bu_pick(self): for rec in self: diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index 47565dfc..e6547a88 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -753,10 +753,11 @@ class RefundSaleOrder(models.Model): line_vals = [] for so in self.sale_order_ids: for line in so.order_line: - if line.qty_delivered == 0: + barang_kurang = line.product_uom_qty - line.qty_delivered + if line.qty_delivered == 0 or barang_kurang > 0: line_vals.append((0, 0, { 'product_id': line.product_id.id, - 'quantity': line.product_uom_qty, + 'quantity': barang_kurang, 'from_name': so.name, 'prod_id': so.id, 'reason': '', diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 663cba58..5c8f34c6 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -399,20 +399,21 @@ class SaleOrder(models.Model): ) def action_open_partial_delivery_wizard(self): - self.ensure_one() - pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name) - return { - 'type': 'ir.actions.act_window', - 'name': 'Partial Delivery', - 'res_model': 'partial.delivery.wizard', - 'view_mode': 'form', - 'target': 'new', - 'context': { - 'default_sale_id': self.id, - # kasih langsung list of int biar ga ribet di wizard - 'default_picking_ids': pickings.ids, - } - } + raise UserError("Fitur ini sedang dalam pengembangan") + # self.ensure_one() + # pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.name and 'BU/PICK/' in p.name) + # return { + # 'type': 'ir.actions.act_window', + # 'name': 'Partial Delivery', + # 'res_model': 'partial.delivery.wizard', + # 'view_mode': 'form', + # 'target': 'new', + # 'context': { + # 'default_sale_id': self.id, + # # kasih langsung list of int biar ga ribet di wizard + # 'default_picking_ids': pickings.ids, + # } + # } @api.depends('partner_id.is_cbd_locked') @@ -1813,10 +1814,10 @@ class SaleOrder(models.Model): # rec.commitment_date = rec.expected_ready_to_ship - @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship + @api.onchange('expected_ready_to_ship') 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'): @@ -1984,10 +1985,10 @@ class SaleOrder(models.Model): # raise UserError('Kelurahan Real Delivery Address harus diisi') def generate_payment_link_midtrans_sales_order(self): - # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox - # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox - midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production - midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production + midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox + midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox + # midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production + # midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production so_number = self.name so_number = so_number.replace('/', '-') @@ -2000,8 +2001,8 @@ class SaleOrder(models.Model): } # ==== ENV ==== - # check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox - check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production + check_url = f'https://api.sandbox.midtrans.com/v2/{so_number}/status' # dev - sandbox + # check_url = f'https://api.midtrans.com/v2/{so_number}/status' # production # ============================================= check_response = requests.get(check_url, headers=headers) @@ -2241,7 +2242,7 @@ class SaleOrder(models.Model): raise UserError("Payment Term pada Master Data Customer harus diisi") if not partner.active_limit and term_days > 0: raise UserError("Credit Limit pada Master Data Customer harus diisi") - if order.payment_term_id != partner.property_payment_term_id: + if order.payment_term_id != partner.property_payment_term_id and not order.partner_id.id == 29179: raise UserError("Payment Term berbeda pada Master Data Customer") if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp: raise UserError("NPWP berbeda pada Master Data Customer") @@ -2324,7 +2325,7 @@ class SaleOrder(models.Model): raise UserError("Payment Term pada Master Data Customer harus diisi") if not partner.active_limit and term_days > 0: raise UserError("Credit Limit pada Master Data Customer harus diisi") - if order.payment_term_id != partner.property_payment_term_id: + if order.payment_term_id != partner.property_payment_term_id and not order.partner_id.id == 29179: raise UserError("Payment Term berbeda pada Master Data Customer") if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp: raise UserError("NPWP berbeda pada Master Data Customer") @@ -2871,6 +2872,7 @@ class SaleOrder(models.Model): def action_apply_voucher(self): for line in self.order_line: if line.order_promotion_id: + _logger.warning(f"[CHECKOUT FAILED] Produk promo ditemukan: {line.product_id.display_name}") raise UserError('Voucher tidak dapat digabung dengan promotion program') voucher = self.voucher_id @@ -2913,42 +2915,82 @@ class SaleOrder(models.Model): self.apply_voucher_shipping() def apply_voucher(self): + def _is_promo_line(line): + # TRUE jika baris tidak boleh kena voucher + if getattr(line, 'order_promotion_id', False): + return True # baris dari program promo + if (line.price_unit or 0.0) == 0.0: + return True # free item + if getattr(line, 'is_has_disc', False): + return True # sudah promo/flashsale/berdiskon + if (line.discount or 0.0) >= 100.0: + return True # safety + return False + + # --- LOOP 1: susun input untuk voucher.apply() --- order_line = [] for line in self.order_line: + if _is_promo_line(line): + continue order_line.append({ 'product_id': line.product_id, 'price': line.price_unit, 'discount': line.discount, 'qty': line.product_uom_qty, - 'subtotal': line.price_subtotal + 'subtotal': line.price_subtotal, }) + + if not order_line: + return + voucher = self.voucher_id.apply(order_line) + # --- LOOP 2: tulis hasilnya HANYA ke non-promo --- for line in self.order_line: + if _is_promo_line(line): + continue + line.initial_discount = line.discount voucher_type = voucher['type'] - used_total = voucher['total'][voucher_type] - used_discount = voucher['discount'][voucher_type] + total_map = voucher['total'][voucher_type] + discount_map = voucher['discount'][voucher_type] - manufacture_id = line.product_id.x_manufacture.id if voucher_type == 'brand': - used_total = used_total.get(manufacture_id) - used_discount = used_discount.get(manufacture_id) + m_id = line.product_id.x_manufacture.id + used_total = (total_map or {}).get(m_id) + used_discount = (discount_map or {}).get(m_id) + else: + used_total = total_map + used_discount = discount_map - if not used_total or not used_discount: + if not used_total or not used_discount or (line.product_uom_qty or 0.0) == 0.0: continue line_contribution = line.price_subtotal / used_total line_voucher = used_discount * line_contribution - line_voucher_item = line_voucher / line.product_uom_qty + per_item_voucher = line_voucher / line.product_uom_qty + + has_ppn_11 = any(tax.id == 23 for tax in line.tax_id) + base_unit = line.price_unit / 1.11 if has_ppn_11 else line.price_unit - 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 + new_disc_value = base_unit * line.discount / 100 + per_item_voucher + new_disc_pct = (new_disc_value / base_unit) * 100 line.amount_voucher_disc = line_voucher - line.discount = line_voucher_item + line.discount = new_disc_pct + + _logger.info( + "[VOUCHER_APPLIED] SO=%s voucher=%s type=%s line_id=%s product=%s qty=%s discount_pct=%.2f amount_voucher=%s", + self.name, + getattr(self.voucher_id, "code", None), + voucher.get("type"), + line.id, + line.product_id.display_name, + line.product_uom_qty, + line.discount, + line.amount_voucher_disc, + ) self.amount_voucher_disc = voucher['discount']['all'] self.applied_voucher_id = self.voucher_id @@ -3189,6 +3231,10 @@ class SaleOrder(models.Model): # order._auto_set_shipping_from_website() order._compute_etrts_date() order._validate_expected_ready_ship_date() + # for line in order.order_line: + # updated_vals = line._update_purchase_info() + # if updated_vals: + # line.write(updated_vals) # order._validate_delivery_amt() # order._check_total_margin_excl_third_party() # order._update_partner_details() @@ -3287,12 +3333,15 @@ class SaleOrder(models.Model): for order in self: partner = order.partner_id.parent_id or order.partner_id customer_payment_term = partner.property_payment_term_id - if vals['payment_term_id'] != customer_payment_term.id: + if vals['payment_term_id'] != customer_payment_term.id and not order.partner_id.id == 29179: raise UserError( f"Payment Term berbeda pada Master Data Customer. " f"Harap ganti ke '{customer_payment_term.name}' " f"sesuai dengan payment term yang terdaftar pada customer." ) + + if order.partner_id.id == 29179 and vals['payment_term_id'] not in [25,28]: + raise UserError(_("Pilih payment term 60 hari atau 30 hari.")) res = super(SaleOrder, self).write(vals) @@ -3317,6 +3366,12 @@ class SaleOrder(models.Model): if any(field in vals for field in ["order_line", "client_order_ref"]): self._calculate_etrts_date() + # for order in self: + # for line in order.order_line: + # updated_vals = line._update_purchase_info() + # if updated_vals: + # line.write(updated_vals) + return res def button_refund(self): diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 1f2ea1fb..1df1a058 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -247,29 +247,29 @@ class SaleOrderLine(models.Model): 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 - if not self.product_id or self.product_id.type == 'service': - return - elif self.product_id.categ_id.id == 34: # finish good / manufacturing only - cost = self.product_id.standard_price - self.purchase_price = cost - elif self.product_id.x_manufacture.override_vendor_id: - # purchase_price = self.env['purchase.pricelist'].search( - # [('vendor_id', '=', self.product_id.x_manufacture.override_vendor_id.id), - # ('product_id', '=', self.product_id.id)], - # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') - price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id) - self.purchase_price = price - self.purchase_tax_id = taxes - # else: - # purchase_price = self.env['purchase.pricelist'].search( - # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)], - # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') - # price, taxes = self._get_valid_purchase_price(purchase_price) - # self.purchase_price = price - # self.purchase_tax_id = taxes + # @api.onchange('vendor_id') + # def onchange_vendor_id(self): + # # TODO : need to change this logic @stephan + # if not self.product_id or self.product_id.type == 'service': + # return + # elif self.product_id.categ_id.id == 34: # finish good / manufacturing only + # cost = self.product_id.standard_price + # self.purchase_price = cost + # elif self.product_id.x_manufacture.override_vendor_id: + # # purchase_price = self.env['purchase.pricelist'].search( + # # [('vendor_id', '=', self.product_id.x_manufacture.override_vendor_id.id), + # # ('product_id', '=', self.product_id.id)], + # # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') + # price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id) + # self.purchase_price = price + # self.purchase_tax_id = taxes + # # else: + # # purchase_price = self.env['purchase.pricelist'].search( + # # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)], + # # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') + # # price, taxes = self._get_valid_purchase_price(purchase_price) + # # self.purchase_price = price + # # self.purchase_tax_id = taxes # def _calculate_selling_price(self): # rec_purchase_price, rec_taxes, rec_vendor_id = self._get_purchase_price(self.product_id) @@ -512,3 +512,19 @@ class SaleOrderLine(models.Model): else: line.product_updatable = False # line.desc_updatable = False + + @api.onchange('vendor_id') + def _onchange_vendor_id_custom(self): + self._update_purchase_info() + + def _update_purchase_info(self): + if not self.product_id or self.product_id.type == 'service': + return + + if self.product_id.categ_id.id == 34: + self.purchase_price = self.product_id.standard_price + self.purchase_tax_id = False + elif self.product_id.x_manufacture.override_vendor_id: + price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id) + self.purchase_price = price + self.purchase_tax_id = taxes diff --git a/indoteknik_custom/models/sj_tele.py b/indoteknik_custom/models/sj_tele.py index d44aa338..53ba26fc 100644 --- a/indoteknik_custom/models/sj_tele.py +++ b/indoteknik_custom/models/sj_tele.py @@ -18,42 +18,24 @@ class SjTele(models.Model): sale_name = fields.Char(string='Sale Name') create_date = fields.Datetime(string='Create Date') date_doc_kirim = fields.Datetime(string='Tanggal Kirim SJ') - - # @api.model - # def run_pentaho_carte(self): - # carte = "http://127.0.0.1:8080" - # job_kjb = r"C:/Users/Indoteknik/Desktop/tes.kjb" - # params = {"job": job_kjb, "level": "Basic", "block": "Y"} - # try: - # r = requests.get( - # f"{carte}/kettle/executeJob/", - # params=params, - # auth=("cluster", "cluster"), - # timeout=900, - # ) - # r.raise_for_status() - # # kalau Carte mengembalikan <result>ERROR</result>, anggap gagal - # if "<result>ERROR</result>" in r.text: - # raise UserError(f"Carte error: {r.text}") - # except Exception as e: - # _logger.exception("Carte call failed: %s", e) - # raise UserError(f"Gagal memanggil Carte: {e}") - - # time.sleep(3) - - # self.env['sj.tele'].sudo().woi() - - # return True + is_sent = fields.Boolean(default=False) def woi(self): bot_mqdd = '8203414501:AAHy_XwiUAVrgRM2EJzW7sZx9npRLITZpb8' chat_id_mqdd = '-1003087280519' api_base = f'https://api.telegram.org/bot{bot_mqdd}' + # bot_testing = '8306689189:AAHEFe5xwAkapoQ8xKoNZs-6gVfv3kO3kaU' + # chat_id_testing = '-4920864331' + # api_testing = f'https://api.telegram.org/bot{bot_testing}' + + # Select Data + data = self.search([('is_sent', '=', False)], order='create_date asc') - data = self.search([], order='create_date asc', limit=15) + # Old + # data = self.search([], order='create_date asc') if not data: - text = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n✅ tidak ada data (semua sudah tercatat)." + text = "✅ tidak ada data (semua sudah tercatat)." try: r = requests.post(api_base + "/sendMessage", json={'chat_id': chat_id_mqdd, 'text': text}, @@ -74,7 +56,6 @@ class SjTele(models.Model): dttm = (rec.picking_id.date_doc_kirim if (rec.picking_id and rec.picking_id.date_doc_kirim) else getattr(rec, 'date_doc_kirim', None)) - # format header tanggal (string), tanpa konversi Waktu/WIB if dttm: date_header = dttm if isinstance(dttm, str) else fields.Datetime.to_string(dttm) date_header = date_header[:10] @@ -84,19 +65,41 @@ class SjTele(models.Model): if name: groups.setdefault(date_header, []).append(f"- ({pid}) - {name} - {so}") - # build output berurutan per tanggal for header_date, items in groups.items(): lines.append(header_date) lines.extend(items) - header = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n" - text = header + "\n".join(lines) - - try: - r = requests.post(api_base + "/sendMessage", - json={'chat_id': chat_id_mqdd, 'text': text}) - r.raise_for_status() - except Exception as e: - _logger.exception("Gagal kirim Telegram: %s", e) - return True
\ No newline at end of file + BUB = 20 # jumlah baris per bubble + total = (len(lines) + BUB - 1) // BUB # total bubble + + for i in range(0, len(lines), BUB): + body = "\n".join(lines[i:i + BUB]) + bagian = (i // BUB) + 1 + text = f"{header}Lampiran ke {bagian}/{total}\n{body}" + try: + r = requests.post( + api_base + "/sendMessage", + json={'chat_id': chat_id_mqdd, 'text': text}, + timeout=20 + ) + r.raise_for_status() + except Exception as e: + _logger.exception("Gagal kirim Telegram (batch %s-%s): %s", i + 1, min(i + BUB, len(lines)), e) + time.sleep(5) # jeda kecil biar rapi & aman rate limit + + # Set sent = true ketika sudah terkirim + data.write({'is_sent': True}) + + return True + + # header = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n" + # text = header + "\n".join(lines) + # + # try: + # r = requests.post(api_base + "/sendMessage", + # json={'chat_id': chat_id_mqdd, 'text': text}) + # r.raise_for_status() + # except Exception as e: + # _logger.exception("Gagal kirim Telegram: %s", e) + # return True
\ No newline at end of file diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index d6505a86..1da2befe 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -19,6 +19,15 @@ class StockMove(models.Model): vendor_id = fields.Many2one('res.partner' ,string='Vendor') hold_outgoingg = fields.Boolean('Hold Outgoing', default=False) product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly=True) + partial = fields.Boolean('Partial?', default=False) + + # Ambil product uom dari SO line + @api.model + def create(self, vals): + if vals.get('sale_line_id'): + sale_line = self.env['sale.order.line'].browse(vals['sale_line_id']) + vals['product_uom'] = sale_line.product_uom.id + return super().create(vals) # @api.model_create_multi # def create(self, vals_list): @@ -177,3 +186,12 @@ class StockMoveLine(models.Model): line_no = fields.Integer('No', default=0) note = fields.Char('Note') manufacture = fields.Many2one('x_manufactures', string="Brands", related="product_id.x_manufacture", store=True) + + # Ambil uom dari stock move + @api.model + def create(self, vals): + if 'move_id' in vals and 'product_uom_id' not in vals: + move = self.env['stock.move'].browse(vals['move_id']) + if move.product_uom: + vals['product_uom_id'] = move.product_uom.id + return super().create(vals)
\ No newline at end of file diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index b27e6b5d..d6096cc0 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -97,6 +97,7 @@ class StockPicking(models.Model): approval_status = fields.Selection([ ('pengajuan1', 'Approval Accounting'), + ('pengajuan2', 'Approval Logistic'), ('approved', 'Approved'), ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Internal Use") @@ -152,6 +153,7 @@ class StockPicking(models.Model): state_reserve = fields.Selection([ ('waiting', 'Waiting For Fullfilment'), ('ready', 'Ready to Ship'), + ('partial', 'Ready to Ship Partial'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.") @@ -174,6 +176,29 @@ class StockPicking(models.Model): linked_manual_bu_out = fields.Many2one('stock.picking', string='BU Out', copy=False) area_name = fields.Char(string="Area", compute="_compute_area_name") + is_bu_iu = fields.Boolean('Is BU/IU', compute='_compute_is_bu_iu', default=False, copy=False, readonl=True) + + @api.depends('name') + def _compute_is_bu_iu(self): + for record in self: + if 'BU/IU' in record.name: + record.is_bu_iu = True + else: + record.is_bu_iu = False + + def action_bu_iu_to_pengajuan2(self): + for rec in self: + if not rec.is_bu_iu or not rec.is_internal_use: + raise UserError(_("Tombol ini hanya untuk dokumen BU/IU - Internal Use.")) + if rec.approval_status == False: + raise UserError("Harus Ask Approval terlebih dahulu") + if rec.approval_status in ['pengajuan1'] and self.env.user.is_accounting: + rec.approval_status = 'pengajuan2' + rec.message_post(body=_("Status naik ke Approval Logistik oleh %s") % self.env.user.display_name) + if rec.approval_status in ['pengajuan1', 'pengajuan2', ''] and not self.env.user.is_accounting: + raise UserError("Tombol hanya untuk accounting") + + return True # def _get_biteship_api_key(self): # # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test') @@ -189,7 +214,7 @@ class StockPicking(models.Model): # def write(self, vals): # if 'linked_manual_bu_out' in vals: # for record in self: - # if (record.picking_type_code == 'internal' + # 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']: @@ -203,8 +228,8 @@ class StockPicking(models.Model): # @api.model # def create(self, vals): # record = super().create(vals) - # if (record.picking_type_code == 'internal' - # and 'BU/PICK/' in record.name + # 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' @@ -393,7 +418,7 @@ class StockPicking(models.Model): deadline = kirim_date + timedelta(days=1) deadline = deadline.replace(hour=10, minute=0, second=0) - if now > deadline: + if now > deadline and not self.so_lama: raise ValidationError( _("Anda tidak dapat mengubah Tanggal Kirim setelah jam 10:00 pada hari berikutnya!") ) @@ -439,15 +464,15 @@ class StockPicking(models.Model): 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") + # @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 + # picking.driver_departure_date = now @api.depends('total_so_koli') def _compute_total_so_koli(self): @@ -509,7 +534,7 @@ class StockPicking(models.Model): # rts_days = rts.days # rts_hours = divmod(rts.seconds, 3600) - # estimated_by_erts = rts.total_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 @@ -1080,9 +1105,16 @@ class StockPicking(models.Model): def ask_approval(self): - if self.env.user.is_accounting: + # if self.env.user.is_accounting: + # if self.env.user.is_accounting and self.location_id.id == 57 or self.location_id == 57 and self.approval_status in ['pengajuan1', ''] and 'BU/IU' in self.name and self.approval_status == 'pengajuan1': + # raise UserError("Bisa langsung set ke approval logistik") + if self.env.user.is_accounting and self.approval_status == "pengajuan2" and 'BU/IU' in self.name: + raise UserError("Tidak perlu ask approval sudah approval logistik") + if self.env.user.is_logistic_approver and self.location_id.id == 57 or self.location_id== 57 and self.approval_status == 'pengajuan2' and 'BU/IU' in self.name: raise UserError("Bisa langsung Validate") + + # for calendar distribute only # if self.is_internal_use: # stock_move_lines = self.env['stock.move.line'].search([ @@ -1105,6 +1137,7 @@ class StockPicking(models.Model): raise UserError("Qty tidak boleh 0") pick.approval_status = 'pengajuan1' + def ask_receipt_approval(self): if self.env.user.is_logistic_approver: raise UserError('Bisa langsung validate tanpa Ask Receipt') @@ -1270,6 +1303,9 @@ class StockPicking(models.Model): and self.create_date > threshold_datetime and not self.so_lama): raise UserError(_("Tidak ada scan koli! Harap periksa kembali.")) + + if 'BU/OUT/' in self.name: + self.driver_departure_date = datetime.datetime.utcnow() # 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")) @@ -1306,7 +1342,16 @@ class StockPicking(models.Model): if self.picking_type_id.code == 'incoming' and self.group_id.id == False and self.is_internal_use == False: raise UserError(_('Tidak bisa Validate jika tidak dari Document SO / PO')) - if self.is_internal_use and not self.env.user.is_accounting: + # if self.is_internal_use and not self.env.user.is_logistic_approver and self.location_id.id == 57 and self.approval_status == 'pengajuan2': + # raise UserError("Harus di Approve oleh Logistik") + + if self.is_internal_use and self.approval_status in ['pengajuan1', '', False] and 'BU/IU' in self.name and self.is_bu_iu == True: + raise UserError("Tidak Bisa Validate, set approval status ke approval logistik terlebih dahhulu") + + if self.is_internal_use and not self.env.user.is_logistic_approver and self.approval_status in ['pengajuan2'] and self.is_bu_iu == True and 'BU/IU' in self.name: + raise UserError("Harus di Approve oleh Logistik") + + if self.is_internal_use and not self.env.user.is_accounting and self.approval_status in ['pengajuan1', '', False] and self.is_bu_iu == False: raise UserError("Harus di Approve oleh Accounting") if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver: @@ -1315,7 +1360,7 @@ class StockPicking(models.Model): 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") - if self.is_internal_use: + if self.is_internal_use and self.approval_status == 'pengajuan2': self.approval_status = 'approved' elif self.picking_type_id.code == 'incoming': self.approval_receipt_status = 'approved' @@ -1332,7 +1377,7 @@ class StockPicking(models.Model): current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.date_reserved = current_time - + # Validate Qty Demand Can't higher than Qty Product if self.location_dest_id.id == 58 and 'BU/INPUT/' in self.name: for move in self.move_ids_without_package: @@ -1353,14 +1398,14 @@ class StockPicking(models.Model): if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name: self.check_koli() res = super(StockPicking, self).button_validate() - + # Penambahan link PO di Stock Journal untuk Picking BD for picking in self: if picking.name and 'BD/' in picking.name and picking.purchase_id: stock_journal = self.env['account.move'].search([ ('ref', 'ilike', picking.name + '%'), - ('journal_id', '=', 3) # Stock Journal ID - ], limit = 1) + ('journal_id', '=', 3) # Stock Journal ID + ], limit=1) if stock_journal: stock_journal.write({ 'purchase_order_id': picking.purchase_id.id @@ -2537,9 +2582,22 @@ class ScanKoli(models.Model): 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 + corresponding_out_moves = out_moves.filtered(lambda m: m.product_id == pick_move.product_id) + + if len(corresponding_out_moves) == 1: + corresponding_out_moves.qty_done += pick_move.qty_done + + elif len(corresponding_out_moves) > 1: + qty_koli = pick_move.qty_done + for out_move in corresponding_out_moves: + if qty_koli <= 0: + break + # ambil sesuai kebutuhan atau sisa qty + qty_to_assign = min(qty_koli, out_move.product_uom_qty) + out_move.qty_done += qty_to_assign + qty_koli -= qty_to_assign + + def _reset_qty_done_if_no_scan(self, picking_id): product_bu_pick = self.env['stock.move.line'].search([('picking_id', '=', picking_id)]) diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index c683f75a..cb630a04 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -538,6 +538,11 @@ class TukarGuling(models.Model): self.state = 'approval_sales' def update_doc_state(self): + bu_pick = self.env['stock.picking'].search([ + ('origin', '=', self.operations.origin), + ('name', 'ilike', 'BU/PICK'), + ]) + # OUT tukar guling if self.operations.picking_type_id.id == 29 and self.return_type == 'tukar_guling': total_out = self.env['stock.picking'].search_count([ @@ -552,7 +557,26 @@ class TukarGuling(models.Model): if self.state == 'approved' and total_out > 0 and done_out == total_out: self.state = 'done' - # OUT revisi SO + #SO Lama (gk ada bu pick) + elif self.operations.picking_type_id.id == 29 and self.return_type == 'retur_so' and not bu_pick: + # so_lama = self.env['sale.order'].search([ + # ('name', '=', self.operations.origin), + # ('state', '=', 'done'), + # ('group_id.name', '=', self.operations.origin) + # ]) + total_ort = self.env['stock.picking'].search_count([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 74), + ]) + done_srt = self.env['stock.picking'].search([ + ('tukar_guling_id', '=', self.id), + ('picking_type_id', '=', 73), + ('state', '=', 'done') + ]) + if self.state == 'approved' and total_ort == 0 and done_srt and not bu_pick: + self.state = 'done' + + # OUT retur SO elif self.operations.picking_type_id.id == 29 and self.return_type == 'retur_so': total_ort = self.env['stock.picking'].search_count([ ('tukar_guling_id', '=', self.id), diff --git a/indoteknik_custom/models/user_pengajuan_tempo_request.py b/indoteknik_custom/models/user_pengajuan_tempo_request.py index 600381c0..6e8498f7 100644 --- a/indoteknik_custom/models/user_pengajuan_tempo_request.py +++ b/indoteknik_custom/models/user_pengajuan_tempo_request.py @@ -365,7 +365,7 @@ 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, 375): + if tempo.env.user.id not in (7, 688, 28, 377, 12182, 375, 19): raise UserError("Durasi tempo hanya bisa di ubah oleh Sales Manager atau Direktur") @api.onchange('tempo_limit') @@ -381,7 +381,8 @@ 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 (375, 19): + # if tempo.env.user.id not in (375, 19): + if tempo.env.user.id != 19: # if tempo.env.user.id != 12182: raise UserError("Pengajuan tempo hanya bisa di approve oleh sales manager") else: diff --git a/indoteknik_custom/report/report_surat_piutang copy.xml b/indoteknik_custom/report/report_surat_piutang copy.xml deleted file mode 100644 index cb5762f3..00000000 --- a/indoteknik_custom/report/report_surat_piutang copy.xml +++ /dev/null @@ -1,149 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <data> - - <!-- External Layout tanpa company --> - <template id="external_layout_no_company"> - <!-- HEADER --> - <div class="header"> - <img t-att-src="'https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521'" - class="img img-fluid w-100"/> - </div> - - <!-- CONTENT --> - <div class="content mt-5 mb-5 ml-3 mr-3"> - <t t-raw="0"/> - </div> - - <!-- FOOTER --> - <div class="footer"> - <img t-att-src="'https://erp.indoteknik.com/api/image/ir.attachment/datas/2498529'" - style="height:60px;"/> - </div> - </template> - - <!-- Report Action --> - <record id="action_report_surat_piutang" model="ir.actions.report"> - <field name="name">Surat Peringatan Piutang</field> - <field name="model">surat.piutang</field> - <field name="report_type">qweb-pdf</field> - <field name="report_name">indoteknik_custom.report_surat_piutang_formal_custom</field> - <field name="report_file">indoteknik_custom.report_surat_piutang_formal_custom</field> - <field name="binding_model_id" ref="model_surat_piutang"/> - <field name="binding_type">report</field> - </record> - - <!-- QWeb Template Surat --> - <template id="report_surat_piutang_formal_custom"> - <t t-call="indoteknik_custom.external_layout_no_company"> - <t t-set="doc" t-value="docs[0] if docs else None"/> - - <!-- SURAT CONTENT --> - <main class="o_report_layout_standard" style="font-size:12pt; font-family: Arial, sans-serif;"> - - <!-- Header Surat --> - <div class="row mb-3"> - <div class="col-6"> - Ref. No: <t t-esc="doc.name or '-'"/> - </div> - <div class="col-6 text-right"> - Jakarta, <t t-esc="doc.send_date and doc.send_date.strftime('%d %B %Y') or '-'"/> - </div> - </div> - - <!-- Tujuan --> - <div class="mb-3"> - <strong>Kepada Yth.</strong><br/> - <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/><br/> - <t t-esc="doc.partner_id.street if doc and doc.partner_id else '-'"/><br/> - <t t-esc="doc.partner_id.country_id.name if doc and doc.partner_id and doc.partner_id.country_id else '-'"/> - </div> - - <!-- UP & Perihal --> - <div class="mb-4"> - U.P. : <t t-esc="doc.tujuan_nama or '-'"/><br/> - <strong>Perihal:</strong> <t t-esc="doc.perihal or '-'"/> - </div> - - <!-- Isi Surat --> - <div class="mb-3">Dengan Hormat,</div> - <div class="mb-3">Yang bertanda tangan di bawah ini menyampaikan sebagai berikut:</div> - - <div class="mb-3 text-justify"> - Namun, bersama surat ini kami ingin mengingatkan bahwa hingga tanggal surat ini dibuat, masih terdapat tagihan yang belum diselesaikan oleh pihak - <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/> periode bulan <t t-esc="doc.periode_invoices_terpilih or '-'"/>, berdasarkan data korespondensi dan laporan keuangan yang kami kelola, - <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/> (“Saudara”) masih mempunyai tagihan yang telah jatuh tempo dan belum dibayarkan sejumlah - <t t-esc="doc.grand_total_text or '-'"/> (“Tagihan”). - </div> - - <div class="mb-3">Berikut kami lampirkan Rincian Tagihan yang telah Jatuh Tempo:</div> - - <!-- Tabel Invoice --> - <table class="table table-sm table-bordered mb-4"> - <thead class="thead-light"> - <tr> - <th>Invoice Number</th> - <th>Invoice Date</th> - <th>Due Date</th> - <th class="text-center">Day to Due</th> - <th>Reference</th> - <th class="text-right">Amount Due</th> - <th>Payment Terms</th> - </tr> - </thead> - <tbody> - <t t-foreach="doc.line_ids.filtered(lambda l: l.selected)" t-as="line"> - <tr> - <td><t t-esc="line.invoice_number or '-'"/></td> - <td><t t-esc="line.invoice_date and line.invoice_date.strftime('%d-%m-%Y') or '-'"/></td> - <td><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y') or '-'"/></td> - <td class="text-center"><t t-esc="line.new_invoice_day_to_due or '-'"/></td> - <td><t t-esc="line.ref or '-'"/></td> - <td class="text-right"><t t-esc="line.amount_residual or '-'"/></td> - <td><t t-esc="line.payment_term_id.name or '-'"/></td> - </tr> - </t> - </tbody> - <tfoot> - <tr class="font-weight-bold"> - <td colspan="6" class="text-right"> - GRAND TOTAL INVOICE YANG BELUM DIBAYAR DAN TELAH JATUH TEMPO - </td> - <td class="text-right"> - <t t-esc="doc.grand_total or '-'"/> (<t t-esc="doc.grand_total_text or '-'"/>) - </td> - </tr> - </tfoot> - </table> - - <!-- Isi Penutup --> - <div class="mb-3"> - Kami belum menerima konfirmasi pelunasan ataupun pembayaran sebagian dari total kewajiban tersebut. Kami sangat terbuka untuk berdiskusi serta mencari solusi terbaik agar kerja sama tetap berjalan baik. - </div> - - <div class="mb-3"> - Oleh karena itu, kami mohon perhatian dan itikad baik dari pihak <t t-esc="doc.partner_id.name if doc and doc.partner_id else '-'"/> untuk segera melakukan pelunasan atau memberikan informasi terkait rencana pembayaran paling lambat dalam waktu 7 (tujuh) hari kerja sejak surat ini diterima. - </div> - - <div class="mb-3"> - Jika dalam waktu yang telah ditentukan belum ada penyelesaian atau tanggapan, kami akan mempertimbangkan untuk melanjutkan proses sesuai ketentuan yang berlaku. - </div> - - <div class="mb-4"> - Demikian kami sampaikan. Atas perhatian dan kerja samanya, kami ucapkan terima kasih. - </div> - - <div class="mb-2">Hormat kami,</div> - - <!-- TTD --> - <div class="mt-5"> - <img t-att-src="'https://erp.indoteknik.com/api/image/ir.attachment/datas/2851919'" style="width:200px; height:auto;"/><br/> - <div>Nama: Akbar Prabawa<br/>Jabatan: General Manager</div> - </div> - - </main> - </t> - </template> - - </data> -</odoo> diff --git a/indoteknik_custom/report/report_surat_piutang.xml b/indoteknik_custom/report/report_surat_piutang.xml index 62db7982..f41ae604 100644 --- a/indoteknik_custom/report/report_surat_piutang.xml +++ b/indoteknik_custom/report/report_surat_piutang.xml @@ -10,8 +10,8 @@ <field name="report_name">indoteknik_custom.report_surat_piutang</field> <field name="report_file">indoteknik_custom.report_surat_piutang</field> <field name="print_report_name">'%s - %s' % (object.perihal_label or '', object.partner_id.name or '')</field> - <field name="binding_model_id" ref="model_surat_piutang"/> - <field name="binding_type">report</field> + <!-- <field name="binding_model_id" ref="model_surat_piutang"/> + <field name="binding_type">report</field> --> </record> <template id="external_layout_surat_piutang"> @@ -216,7 +216,7 @@ <div style="height:120px; position: relative;"> <t t-if="doc.perihal != 'penagihan'"> <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2851919" - style="width:300px; height:auto; margin-top:-40px;"/> + style="width:300px; height:auto; margin-top:-40px; margin-left:-20px;"/> </t> <t t-else=""> <div style="height:100px;"></div> diff --git a/indoteknik_custom/report/report_tutup_tempo.xml b/indoteknik_custom/report/report_tutup_tempo.xml new file mode 100644 index 00000000..1aa1367d --- /dev/null +++ b/indoteknik_custom/report/report_tutup_tempo.xml @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + + <record id="action_report_surat_tutup_tempo" model="ir.actions.report"> + <field name="name">Surat Penutupan Tempo</field> + <field name="model">surat.piutang</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">indoteknik_custom.report_surat_tutup_tempo</field> + <field name="report_file">indoteknik_custom.report_surat_tutup_tempo</field> + <field name="print_report_name">'%s - %s' % (object.perihal_label or '', object.partner_id.name or '')</field> + <!-- <field name="binding_model_id" ref="model_surat_piutang"/> + <field name="binding_type">report</field> --> + </record> + + <template id="external_layout_surat_tutup_tempo"> + <t t-call="web.html_container"> + <div class="header"> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2498521" + style="width:100%; display: block;"/> + </div> + <div class="article" style="margin: 0 1.5cm 0 1.5cm; "> + <t t-raw="0"/> + </div> + <div class="footer"> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2859765" + style="width:100%; display: block;"/> + </div> + </t> + </template> + + <template id="report_surat_tutup_tempo"> + <t t-call="web.html_container"> + <t t-foreach="docs" t-as="doc"> + <t t-call="indoteknik_custom.report_surat_tutup_tempo_document" + t-lang="doc.partner_id.lang"/> + </t> + </t> + </template> + + <template id="report_surat_tutup_tempo_document"> + <t t-call="indoteknik_custom.external_layout_surat_tutup_tempo"> + <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/> + <div class="page"> + + <div class="row mb3"> + <div class="col-6"> + Ref. No: <t t-esc="doc.name or '-'"/> + </div> + <div class="col-6 text-right"> + Jakarta, <t t-esc="doc.send_date and doc.send_date.strftime('%d %B %Y') or '-'"/> + </div> + </div> + <br/> + <div class="mb3" style="max-width:500px; word-wrap:break-word; white-space:normal;"> + <strong>Kepada Yth.</strong><br/> + <strong><t t-esc="doc.partner_id.name or '-'"/></strong><br/> + <span style="display:inline-block; max-width:400px; word-wrap:break-word; white-space:normal;"> + <t t-esc="doc.partner_id.street or ''"/> + </span><br/> + <u>Republik Indonesia</u> + </div> + <br/> + <table style="margin-left:2cm;"> + <tr style="font-weight: bold;"> + <td style="padding-right:10px;">U.P.</td> + <td style="white-space: nowrap;">: <t t-esc="doc.tujuan_nama or '-'"/></td> + </tr> + <tr style="font-weight: bold;"> + <td style="padding-right:10px;">Perihal</td> + <td>: <u><t t-esc="doc.perihal_label or '-'"/></u></td> + </tr> + </table> + <br/> + <p><strong>Dengan Hormat,</strong></p> + <t t-set="selected_lines" t-value="doc.line_ids.filtered(lambda l: l.selected)"/> + <t t-set="line_count" t-value="len(selected_lines)"/> + <t t-if="line_count == 1"> + <t t-set="line" t-value="selected_lines[0]"/> + <p class="text-justify"> + Berdasarkan catatan kami, pembayaran atas invoice + <strong><t t-esc="line.invoice_number"/></strong> + yang jatuh tempo pada tanggal + <strong><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d %B %Y')"/></strong> + telah melewati batas waktu 30 (tiga puluh) hari. Sehubungan dengan hal tersebut, bersama ini kami sampaikan kebijakan perusahaan sebagai berikut: + </p> + </t> + + <t t-else=""> + <p class="text-justify"> + Berdasarkan catatan kami, pembayaran atas beberapa invoice yang telah melewati batas waktu 30 (tiga puluh) hari adalah sebagai berikut: + </p> + + <table class="table table-sm" style="font-size:13px; border:1px solid #000; margin-top:16px; margin-bottom:16px;"> + <thead style="background:#f5f5f5;"> + <tr> + <th style="border:1px solid #000; padding:4px; font-weight: bold;">Invoice</th> + <th style="border:1px solid #000; padding:4px; font-weight: bold;">Due Date</th> + <th style="border:1px solid #000; padding:4px; font-weight: bold;" class="text-center">Day to Due</th> + </tr> + </thead> + <tbody> + <tr t-foreach="selected_lines" t-as="line"> + <td style="border:1px solid #000; padding:4px;"><t t-esc="line.invoice_number"/></td> + <td style="border:1px solid #000; padding:4px;"><t t-esc="line.invoice_date_due and line.invoice_date_due.strftime('%d-%m-%Y')"/></td> + <td style="border:1px solid #000; padding:4px;" class="text-center"><t t-esc="line.new_invoice_day_to_due"/></td> + </tr> + </tbody> + </table> + + <p class="text-justify"> + Sehubungan dengan hal tersebut, bersama ini kami sampaikan kebijakan perusahaan sebagai berikut: + </p> + </t> + + <ol style="padding-left: 1.5em; margin-bottom: 1em;"> + <li class="text-justify" style="margin-bottom: 0.5em;"> + Secara sistem, akun akan otomatis terkunci (locked) apabila pembayaran telah melewati 30 (tiga puluh) hari dari tanggal jatuh tempo. + </li> + <li class="text-justify" style="margin-bottom: 0.5em;"> + Payment term yang semula Tempo akan otomatis berubah menjadi <strong>Cash Before Delivery (CBD)</strong>. + </li> + <li class="text-justify"> + Apabila Bapak/Ibu telah melakukan konfirmasi pembayaran atau memberikan informasi lanjutan terkait pelunasan, maka payment term dapat dibukakan kembali menjadi Tempo berdasarkan pengajuan dari tim Sales kami. + </li> + </ol> + + <p class="text-justify"> + Kebijakan ini kami terapkan untuk menjaga kelancaran proses transaksi serta memastikan hubungan kerja sama dapat terus berjalan dengan baik. + </p> + + <p class="text-justify"> + Atas perhatian dan kerja samanya kami ucapkan terima kasih. + </p> + <div class="mt32" style="page-break-inside: avoid;"> + <p>Hormat kami,<br/> + <strong>PT. Indoteknik Dotcom Gemilang</strong> + </p> + <div style="height:120px; position: relative;"> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2869838" + style="width:300px; height:auto; margin-left:-20px;"/> + </div> + <table style="margin-top:10px;"> + <tr style="border-top:1px solid #000; font-weight: bold;"> + <td style="padding-right:50px; white-space: nowrap;">Nama</td> + <td>: Stephan Christianus</td> + </tr> + <tr style="font-weight: bold;"> + <td style="padding-right:50px; white-space: nowrap;">Jabatan</td> + <td>: FAT Manager</td> + </tr> + </table> + </div> + </div> + </t> + </template> + </data> +</odoo>
\ 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 17372e48..b9934d7a 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -185,6 +185,9 @@ access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_match access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,1,1,1,1 access_purchase_order_update_date_wizard,access.purchase.order.update.date.wizard,model_purchase_order_update_date_wizard,,1,1,1,1 access_change_date_planned_wizard,access.change.date.planned.wizard,model_change_date_planned_wizard,,1,1,1,1 +access_partial_delivery_wizard,access.partial.delivery.wizard,model_partial_delivery_wizard,,1,1,1,1 +access_partial_delivery_wizard_line,access.partial.delivery.wizard.line,model_partial_delivery_wizard_line,,1,1,1,1 +access_apo_domain_config,access.apo.domain.config,model_apo_domain_config,base.group_user,1,1,1,1 access_refund_sale_order,access.refund.sale.order,model_refund_sale_order,base.group_user,1,1,1,1 access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1 @@ -198,9 +201,9 @@ access_tukar_guling_mapping_koli_all_users,tukar.guling.mapping.koli.all.users,m access_sync_promise_date_wizard,access.sync.promise.date.wizard,model_sync_promise_date_wizard,base.group_user,1,1,1,1 access_sync_promise_date_wizard_line,access.sync.promise.date.wizard.line,model_sync_promise_date_wizard_line,base.group_user,1,1,1,1 access_change_date_planned_wizard,access.change.date.planned.wizard,model_change_date_planned_wizard,,1,1,1,1 -access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,base.group_user,1,1,1,1 -access_surat_piutang_user,surat.piutang user,model_surat_piutang,base.group_user,1,1,1,1 -access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,base.group_user,1,1,1,1 +access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,,1,1,1,1 +access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1 +access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1 access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 access_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_system,1,1,1,1 access_sourcing_job_order_line_user,sourcing.job.order.line,model_sourcing_job_order_line,base.group_user,1,1,1,1 diff --git a/indoteknik_custom/views/account_asset_views.xml b/indoteknik_custom/views/account_asset_views.xml index 90c53623..776ab51f 100644 --- a/indoteknik_custom/views/account_asset_views.xml +++ b/indoteknik_custom/views/account_asset_views.xml @@ -12,6 +12,9 @@ type="object" /> </button> + <field name="invoice_id" position="after"> + <field name="asset_type"/> + </field> </field> </record> </data> diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml index b0b99689..090c9b5c 100644 --- a/indoteknik_custom/views/approval_payment_term.xml +++ b/indoteknik_custom/views/approval_payment_term.xml @@ -32,6 +32,10 @@ <field name="arch" type="xml"> <form> <header> + <!-- <button name="button_closing_mail" + string="Send Closing by Email" + type="object" + /> --> <button name="button_approve" string="Approve" type="object" diff --git a/indoteknik_custom/views/close_tempo_mail_template.xml b/indoteknik_custom/views/close_tempo_mail_template.xml new file mode 100644 index 00000000..0f16d3ef --- /dev/null +++ b/indoteknik_custom/views/close_tempo_mail_template.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="0"> + <record id="close_tempo_mail_template" model="mail.template"> + <field name="name">Surat Penutupan Tempo</field> + <field name="model_id" ref="indoteknik_custom.model_surat_piutang"/> + <field name="subject">${object.perihal_label} - ${object.partner_id.name}</field> + <field name="email_from">finance@indoteknik.co.id</field> + <field name="email_to">${object.tujuan_email}</field> + <field name="body_html" type="html"> + <div style="font-family:Arial, sans-serif; font-size:13px; color:#333;"> + <div><b>Dengan hormat,</b></div> + <br/> + <div>Kepada Yth.</div> + <div><b>Manajemen ${object.partner_id.name}</b></div> + <br/> + <div> + Bersama email ini, kami sampaikan surat pemberitahuan resmi terkait <strong>penutupan pembayaran tempo</strong> + yang selama ini berlaku bagi ${object.partner_id.name}. + </div> + <br/> + + <div> + <b>Terhitung mulai tanggal ${object.due_date}</b>, + seluruh transaksi dengan ${object.partner_id.name} akan diberlakukan dengan sistem pembayaran + <b>Cash Before Delivery (CBD)</b>. + </div> + + <p> + Adapun surat resminya kami lampirkan, apabila diperlukan klarifikasi atau penyesuaian terkait kebijakan ini, + kami terbuka untuk mendiskusikannya lebih lanjut. + </p> + <br/> + <p> + Atas perhatian dan kerja samanya, kami ucapkan terima kasih. + </p> + + <br/><br/> + <p> + <b> + Best Regards,<br/><br/> + Widya R.<br/> + Dept. Finance<br/> + PT. INDOTEKNIK DOTCOM GEMILANG<br/> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" + alt="Indoteknik" style="max-width:18%; height:auto;"/><br/> + <a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> | + <a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a> + </b> + </p> + </div> + </field> + <field name="auto_delete" eval="True"/> + </record> + </data> +</odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/domain_apo.xml b/indoteknik_custom/views/domain_apo.xml new file mode 100644 index 00000000..1dae473d --- /dev/null +++ b/indoteknik_custom/views/domain_apo.xml @@ -0,0 +1,46 @@ +<odoo> + <record id="view_apo_domain_config_tree" model="ir.ui.view"> + <field name="name">apo.domain.config.tree</field> + <field name="model">apo.domain.config</field> + <field name="arch" type="xml"> + <tree> + <field name="name"/> + <field name="vendor_id"/> + <field name="brand_ids"/> + <field name="is_special"/> + <field name="payment_term_id"/> + </tree> + </field> + </record> + + <record id="view_apo_domain_config_form" model="ir.ui.view"> + <field name="name">apo.domain.config.form</field> + <field name="model">apo.domain.config</field> + <field name="arch" type="xml"> + <form> + <sheet> + <group> + <field name="name"/> + <field name="vendor_id"/> + <field name="brand_ids"/> + <field name="is_special"/> + <field name="payment_term_id"/> + </group> + </sheet> + </form> + </field> + </record> + + <record id="domain_apo_action" model="ir.actions.act_window"> + <field name="name">Domain APO</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">apo.domain.config</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="menu_automatic_purchase" + name="Domain APO" + action="domain_apo_action" + parent="menu_monitoring_in_purchase" + sequence="200"/> +</odoo> diff --git a/indoteknik_custom/views/letter_receivable.xml b/indoteknik_custom/views/letter_receivable.xml index 98ea7768..3241d5f1 100644 --- a/indoteknik_custom/views/letter_receivable.xml +++ b/indoteknik_custom/views/letter_receivable.xml @@ -26,6 +26,7 @@ <form string="Surat Piutang"> <header> <field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval_sales,waiting_approval_pimpinan,sent"/> + <button name="action_print" string="Print" type="object" /> <button name="action_approve" type="object" string="Approve" @@ -47,6 +48,12 @@ <div class="alert alert-info" role="alert" style="height: 40px; margin-bottom:0px;" + attrs="{'invisible': ['|', ('perihal', '!=', 'tutup_tempo'), ('state', '!=', 'waiting_approval_pimpinan')]}"> + <strong>Info!</strong> Surat penutupan tempo telah diajukan & surat otomatis terkirim bila telah di approve. + </div> + <div class="alert alert-info" + role="alert" + style="height: 40px; margin-bottom:0px;" attrs="{'invisible': ['|', ('perihal', '!=', 'penagihan'), ('state', '!=', 'waiting_approval_pimpinan')]}"> <strong>Info!</strong> Surat resmi penagihan telah diajukan & surat otomatis terkirim bila telah di approve. </div> @@ -60,7 +67,7 @@ <div class="alert alert-info" role="alert" style="margin-bottom:0px;" - attrs="{'invisible': ['|', ('perihal', '=', 'penagihan'), ('state', '!=', 'waiting_approval_pimpinan')]}"> + attrs="{'invisible': ['|', ('perihal', 'in', ['penagihan', 'tutup_tempo']), ('state', '!=', 'waiting_approval_pimpinan')]}"> <strong>Info!</strong> Surat peringatan piutang ini sedang menunggu persetujuan dari <b>Pimpinan</b>. Silakan hubungi Pimpinan terkait untuk melakukan approval agar surat dapat terkirim otomatis ke customer. </div> @@ -81,6 +88,7 @@ <field name="tujuan_nama" attrs="{'readonly':[('state','=','sent')]}"/> <field name="tujuan_email" attrs="{'readonly':[('state','=','sent')]}"/> <field name="perihal" attrs="{'readonly':[('state','=','sent')]}"/> + <field name="due_date" attrs="{'invisible': [('perihal', '!=', 'tutup_tempo')]}"/> <field name="partner_id" options="{'no_create': True}" attrs="{'readonly':[('state','=','sent')]}"/> </group> <group> @@ -120,11 +128,23 @@ </div> </div> <div> + <button name="action_select_all_lines" + type="object" + string="Select All" + class="btn btn-secondary" + icon="fa-check-square"/> + <button name="action_unselect_all_lines" + type="object" + string="Unselect All" + class="btn btn-secondary" + icon="fa-square-o" + style="margin-left:5px;"/> <button name="action_refresh_lines" string="Refresh Invoices" type="object" - class="btn-primary" - style="margin-left:10px;" + class="btn btn-secondary" + icon="fa-refresh" + style="margin-left:5px;" help="Refresh Invoices agar data tetap update"/> </div> </div> diff --git a/indoteknik_custom/views/mail_template_closing_apt.xml b/indoteknik_custom/views/mail_template_closing_apt.xml new file mode 100644 index 00000000..5df2813b --- /dev/null +++ b/indoteknik_custom/views/mail_template_closing_apt.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="0"> + <record id="mail_template_closing_apt" model="mail.template"> + <field name="name"></field> + <field name="model_id" ref="indoteknik_custom.model_approval_payment_term"/> + <field name="subject"></field> + <field name="email_from">finance@indoteknik.co.id</field> + <field name="email_to"></field> + <field name="body_html" type="html"> + <div style="font-family:Arial, sans-serif; font-size:13px; color:#333;"> + <p> + <b> + Best Regards,<br/><br/> + Widya R.<br/> + Dept. Finance<br/> + PT. INDOTEKNIK DOTCOM GEMILANG<br/> + <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" + alt="Indoteknik" style="max-width:18%; height:auto;"/><br/> + <a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> | + <a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a> + </b> + </p> + <!-- <p style="font-size:11px; color:#777;"> + <i>Email ini dikirim secara otomatis. Abaikan jika pembayaran telah dilakukan.</i> + </p> --> + </div> + </field> + <field name="auto_delete" eval="True"/> + </record> + </data> +</odoo> diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 7feec934..09d901b9 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -144,6 +144,7 @@ <field name="cost_service_per_item" optional="hide"/> <field name="contribution_cost_service" optional="hide"/> <field name="ending_price" optional="hide"/> + <field name="price_unit_before" readonly="1" optional="hide" force_save="1"/> <!-- <field name="suggest" readonly="1"/> --> </field> <field name="product_id" position="before"> @@ -390,6 +391,7 @@ <field name="hold_outgoing_so" optional="hide"/> <field name="bu_pick" optional="hide"/> <field name="margin_so"/> + <field name="so_header_margin" optional="hide"/> </tree> </field> </record> diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 8d56bbbd..82daa36f 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -7,6 +7,12 @@ <field name="inherit_id" ref="sale.view_order_form"/> <field name="arch" type="xml"> <button id="action_confirm" position="after"> + <button name="action_open_partial_delivery_wizard" + string="Partial Delivery" + type="object" + class="oe_highlight" + attrs="{'invisible': [('state','!=','sale')]}"/> + <button name="calculate_line_no" string="Create No" type="object" @@ -365,9 +371,13 @@ </field> <field name="payment_term_id" position="attributes"> <attribute name="attrs"> - {'readonly': ['|', ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']), - ('state', 'not in', - ['cancel', 'draft'])]} + { + 'readonly': [ + '|', + ('approval_status', 'in', ['pengajuan1', 'pengajuan2', 'approved']), + ('state', 'not in', ['cancel', 'draft']) + ] + } </attribute> </field> @@ -502,6 +512,44 @@ </field> </record> + <record id="view_partial_delivery_wizard_form" model="ir.ui.view"> + <field name="name">partial.delivery.wizard.form</field> + <field name="model">partial.delivery.wizard</field> + <field name="arch" type="xml"> + <form string="Partial Delivery Wizard"> + <group> + <!-- Field ini WAJIB ada walau invisible --> + <field name="picking_ids" invisible="1"/> + + <field name="picking_id"/> + </group> + + <separator string="Products"/> + <div class="oe_button_box" name="select_all_box"> + <button name="action_select_all" string="✅ Select All" type="object" class="btn-primary"/> + <button name="action_unselect_all" string="❌ Unselect All" type="object" class="btn-secondary"/> + </div> + + <field name="line_ids" context="{'default_wizard_id': active_id}" widget="many2many"> + <tree editable="bottom"> + <field name="selected"/> + <field name="product_id"/> + <field name="ordered_qty" readonly="1"/> + <field name="reserved_qty" readonly="1"/> + <field name="selected_qty"/> + </tree> + </field> + + <footer> + <button string="Confirm" type="object" name="action_confirm_partial_delivery" class="btn-primary"/> + <button string="Cancel" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> + + + <record id="sale_order_multi_update_ir_actions_server" model="ir.actions.server"> <field name="name">Mark As Cancel</field> <field name="model_id" ref="sale.model_sale_order"/> diff --git a/indoteknik_custom/views/stock_move_line.xml b/indoteknik_custom/views/stock_move_line.xml index 94c0bf53..ac8d3dbe 100644 --- a/indoteknik_custom/views/stock_move_line.xml +++ b/indoteknik_custom/views/stock_move_line.xml @@ -20,6 +20,9 @@ <field name="product_id" position="after"> <field name="manufacture"/> </field> + <field name="qty_done" position="after"> + <field name="product_uom_qty"/> + </field> </field> </record> </odoo> diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 21762202..44ab6355 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -45,6 +45,14 @@ <field name="model">stock.picking</field> <field name="inherit_id" ref="stock.view_picking_form"/> <field name="arch" type="xml"> + <!-- Tambahkan tombol custom: tampil hanya saat BU/IU + pengajuan1 --> + <xpath expr="//header" position="inside"> + <button name="action_bu_iu_to_pengajuan2" + type="object" + string="Approve by Accounting" + class="btn-primary" + attrs="{'invisible': [('is_bu_iu', '=', False), ('approval_status', 'in', ['pengajuan2', False, 'false', ''])]}"/> + </xpath> <button name="action_confirm" position="before"> <button name="ask_approval" string="Ask Approval" @@ -158,6 +166,8 @@ <field name="purchase_id"/> <field name="sale_order"/> <field name="invoice_status"/> + <field name="is_bu_iu" /> + <field name="approval_status" attrs="{'invisible': [('is_bu_iu', '=', False)]}"/> <field name="date_doc_kirim" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/> <field name="summary_qty_operation"/> <field name="count_line_operation"/> @@ -197,6 +207,7 @@ <field name="product_uom" position="after"> <field name="sale_id" attrs="{'readonly': 1}" optional="hide"/> <field name="print_barcode" optional="hide"/> + <field name="partial" widget="boolean_toggle" optional="hide"/> <field name="qr_code_variant" widget="image" optional="hide"/> <field name="barcode" optional="hide"/> </field> |
