From 17cd1c4b3a5e685498d16e6d1af1bb990ec543e8 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 8 Oct 2025 14:48:49 +0700 Subject: remove approval status validation for bayar sekarang --- indoteknik_api/controllers/api_v1/sale_order.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 1c87400f..5d6a4117 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -231,11 +231,10 @@ class SaleOrder(controller.Controller): for so in filtered_orders_paginated: item = request.env['sale.order'].api_v1_single_response(so) - approval_ok = (so.approval_status in ('pengajuan1', 'pengajuan2')) source_ok = _is_website_order(so) term_ok = bool(so.payment_term_id and so.payment_term_id.id == CBD_PAYMENT_TERM_ID) pay_status = (getattr(so, 'payment_status', '') or '').strip().lower() - eligible = bool(approval_ok and source_ok and term_ok and pay_status in ALLOWED_CONTINUE) + eligible = bool(source_ok and term_ok and pay_status in ALLOWED_CONTINUE) redirect_url = getattr(so, 'payment_link_midtrans', '') or '' @@ -285,7 +284,6 @@ class SaleOrder(controller.Controller): pay_status = (getattr(sale_order, 'payment_status', '') or '').strip().lower() eligible = ( - sale_order.approval_status in ('pengajuan1', 'pengajuan2') and _is_website_order(sale_order) and sale_order.payment_term_id and sale_order.payment_term_id.id == CBD_PAYMENT_TERM_ID and pay_status in ALLOWED_CONTINUE -- cgit v1.2.3 From c5642f4f6c4f0969475d863bee7243a83b9290dc Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 8 Oct 2025 14:55:04 +0700 Subject: partial --- indoteknik_custom/__manifest__.py | 1 + indoteknik_custom/models/__init__.py | 2 + indoteknik_custom/models/automatic_purchase.py | 161 +++++++++++---------- indoteknik_custom/models/domain_apo.py | 12 ++ indoteknik_custom/models/partial_delivery.py | 189 +++++++++++++++++++++++++ indoteknik_custom/models/sale_order.py | 13 +- indoteknik_custom/models/stock_move.py | 1 + indoteknik_custom/security/ir.model.access.csv | 3 + indoteknik_custom/views/domain_apo.xml | 46 ++++++ indoteknik_custom/views/sale_order.xml | 48 ++++++- indoteknik_custom/views/stock_picking.xml | 1 + 11 files changed, 394 insertions(+), 83 deletions(-) create mode 100644 indoteknik_custom/models/domain_apo.py create mode 100644 indoteknik_custom/models/partial_delivery.py create mode 100644 indoteknik_custom/views/domain_apo.xml diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index b083be70..d1229ffe 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -186,6 +186,7 @@ # 'views/reimburse.xml', 'views/sj_tele.xml', 'views/close_tempo_mail_template.xml', + 'views/domain_apo.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 6dc61277..5ac4d6ca 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -160,3 +160,5 @@ 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 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/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/partial_delivery.py b/indoteknik_custom/models/partial_delivery.py new file mode 100644 index 00000000..c9d2ba5c --- /dev/null +++ b/indoteknik_custom/models/partial_delivery.py @@ -0,0 +1,189 @@ +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 + + @api.onchange('picking_id') + def _onchange_picking_id(self): + """Generate lines whenever picking_id is changed""" + lines = [] + if self.picking_id: + moves = self.picking_id.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, + })) + self.line_ids = lines + + + 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.")) + + if selected_lines.selected_qty > selected_lines.reserved_qty: + raise UserError(_("Jumlah produk yang dipilih melebihi jumlah reserved.")) + + 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: + move = line.move_id + move._do_unreserve() + + # kalau cuma selected tanpa isi qty, otomatis set selected_qty = reserved_qty + if line.selected and not line.selected_qty: + line.selected_qty = line.reserved_qty + + # MODE 1 → Prioritas kalau ada selected_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 + # split move + 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: + # full pindah + move.write({'picking_id': new_picking.id, 'partial': True}) + + + + # Confirm & assign DO baru + new_picking.action_confirm() + new_picking.action_assign() + + # Reassign DO lama biar sisa qty ke-update + picking.action_assign() + + # --- 🔢 Rename picking baru dengan format "/(Nomor urut)" --- + 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) + if suffix_number == 0: + suffix_number = 1 + else: + suffix_number += 1 + + new_name = f"{picking.name}/{suffix_number}" + new_picking.name = new_name + + # --- 💬 Post message ke SO --- + if picking.origin: + sale_order = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1) + if sale_order: + sale_order.message_post( + body=f"Partial Delivery Created: {new_picking.name} " + f"oleh {self.env.user.name}", + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + # --- 📝 Log di DO baru --- + new_picking.message_post( + body=f"Partial Picking created 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") + diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 3aaae12d..57217894 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -1813,10 +1813,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'): @@ -2241,7 +2241,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 +2324,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") @@ -3328,12 +3328,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) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index d6505a86..b7db8775 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -19,6 +19,7 @@ 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) # @api.model_create_multi # def create(self, vals_list): diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index a1adc90a..a6175b21 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 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 @@ + + + apo.domain.config.tree + apo.domain.config + + + + + + + + + + + + + apo.domain.config.form + apo.domain.config + +
+ + + + + + + + + +
+
+
+ + + Domain APO + ir.actions.act_window + apo.domain.config + tree,form + + + +
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 8d56bbbd..eeb51eaa 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -7,6 +7,12 @@