summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <it@fixcomart.co.id>2025-10-15 15:13:19 +0700
committerFIN-IT_AndriFP <it@fixcomart.co.id>2025-10-15 15:13:19 +0700
commit787ce986cc652bd92d0e477c11853eaef871b4e5 (patch)
tree5b9df5e357e301d84554618e3592bdfd389c8178
parentc04dd5a6a38ba35b30a39ed6596514679067e541 (diff)
parentee6d5d4433a397ca5cdf3474f29bcbd0b2553736 (diff)
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into matches-so-v2
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py11
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py32
-rwxr-xr-xindoteknik_custom/__manifest__.py1
-rwxr-xr-xindoteknik_custom/models/__init__.py2
-rw-r--r--indoteknik_custom/models/automatic_purchase.py161
-rw-r--r--indoteknik_custom/models/domain_apo.py12
-rw-r--r--indoteknik_custom/models/letter_receivable.py29
-rw-r--r--indoteknik_custom/models/mrp_production.py16
-rw-r--r--indoteknik_custom/models/partial_delivery.py257
-rw-r--r--indoteknik_custom/models/refund_sale_order.py32
-rwxr-xr-xindoteknik_custom/models/sale_order.py100
-rw-r--r--indoteknik_custom/models/sale_order_line.py62
-rw-r--r--indoteknik_custom/models/sj_tele.py75
-rw-r--r--indoteknik_custom/models/stock_move.py1
-rw-r--r--indoteknik_custom/models/stock_picking.py86
-rw-r--r--indoteknik_custom/models/user_pengajuan_tempo_request.py2
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv9
-rw-r--r--indoteknik_custom/views/account_move_line.xml4
-rw-r--r--indoteknik_custom/views/domain_apo.xml46
-rw-r--r--indoteknik_custom/views/letter_receivable.xml16
-rwxr-xr-xindoteknik_custom/views/sale_order.xml54
-rw-r--r--indoteknik_custom/views/stock_move_line.xml3
-rw-r--r--indoteknik_custom/views/stock_picking.xml11
23 files changed, 780 insertions, 242 deletions
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index 1a75c830..741dc0b1 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])
@@ -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 7f878ad2..2ec1ec2a 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -125,29 +125,9 @@ 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',
- 'indraprtama60@gmail.com'
- }
-
- 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
-
- # 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
# ===== Cari picking by id / picking_code =====
picking_data = False
@@ -161,10 +141,12 @@ class StockPicking(controller.Controller):
return self.response(code=403, description='picking not found')
params = {
- 'sj_documentation': sj_document,
- 'paket_documentation': paket_document,
'driver_arrival_date': datetime.utcnow(),
}
+ if sj_document:
+ params['sj_documentation'] = sj_document
+ if paket_document:
+ params['paket_documentation'] = paket_document
if dispatch_document:
params['dispatch_documentation'] = dispatch_document
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/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py
index ffe14491..a98e46a1 100644
--- a/indoteknik_custom/models/letter_receivable.py
+++ b/indoteknik_custom/models/letter_receivable.py
@@ -69,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):
@@ -193,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:
@@ -362,10 +374,11 @@ class SuratPiutang(models.Model):
'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))),
+ 'email_cc': ",".join(sorted(set(cc_list))),
+ # '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',
+ 'reply_to': 'finance@indoteknik.co.id',
}
template.with_context(mail_post_autofollow=False).send_mail(
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index b39995b5..30956082 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):
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/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index de9870f6..47565dfc 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -304,12 +304,14 @@ class RefundSaleOrder(models.Model):
('state', '=', 'posted'),
])
- misc = self.env['account.move'].search([
- ('ref', 'ilike', invoices.mapped('name')[0]),
- ('ref', 'not ilike', 'reklas'),
- ('journal_id', '=', 13),
- ('state', '=', 'posted'),
- ])
+ misc = self.env['account.move']
+ if invoices:
+ misc = self.env['account.move'].search([
+ ('ref', 'ilike', invoices.mapped('name')[0]),
+ ('ref', 'not ilike', 'reklas'),
+ ('journal_id', '=', 13),
+ ('state', '=', 'posted'),
+ ])
moves2 = self.env['account.move']
if so_ids:
so_names = self.env['sale.order'].browse(so_ids).mapped('name')
@@ -571,7 +573,9 @@ class RefundSaleOrder(models.Model):
domain = [
('journal_id', '=', 11),
('state', '=', 'posted'),
- ('ref', 'ilike', 'dp')
+ '|',
+ ('ref', 'ilike', 'dp'),
+ ('ref', 'ilike', 'payment'),
]
domain += ['|'] * (len(so_names) - 1)
for n in so_names:
@@ -641,12 +645,14 @@ class RefundSaleOrder(models.Model):
('journal_id', '=', 7),
('state', '=', 'posted'),
])
- misc = self.env['account.move'].search([
- ('ref', 'ilike', all_invoices.mapped('name')[0]),
- ('ref', 'not ilike', 'reklas'),
- ('journal_id', '=', 13),
- ('state', '=', 'posted'),
- ])
+ misc = self.env['account.move']
+ if all_invoices:
+ misc = self.env['account.move'].search([
+ ('ref', 'ilike', all_invoices.mapped('name')[0]),
+ ('ref', 'not ilike', 'reklas'),
+ ('journal_id', '=', 13),
+ ('state', '=', 'posted'),
+ ])
moves2 = self.env['account.move']
if so_ids:
so_records = self.env['sale.order'].browse(so_ids)
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 663cba58..a5e2f7c4 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'):
@@ -1984,10 +1984,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 +2000,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 +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")
@@ -2871,6 +2871,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 +2914,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 +3230,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 +3332,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 +3365,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..3ef4b877 100644
--- a/indoteknik_custom/models/sj_tele.py
+++ b/indoteknik_custom/models/sj_tele.py
@@ -19,41 +19,18 @@ class SjTele(models.Model):
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
-
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}'
- data = self.search([], order='create_date asc', limit=15)
+ 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 +51,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 +60,38 @@ 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
+
+ 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..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/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 16e235da..4772c433 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -176,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')
@@ -191,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']:
@@ -205,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'
@@ -511,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
@@ -1082,9 +1105,12 @@ class StockPicking(models.Model):
def ask_approval(self):
- if self.env.user.is_accounting:
- raise UserError("Bisa langsung Validate")
- if self.env.user.is_logistic_approver and self.location_id.id == 57 or self.location_id== 57:
+ # 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")
@@ -1110,9 +1136,7 @@ class StockPicking(models.Model):
if line.qty_done <= 0:
raise UserError("Qty tidak boleh 0")
pick.approval_status = 'pengajuan1'
- if pick.location_id.id == 57:
- pick.approval_status = 'pengajuan2'
- return
+
def ask_receipt_approval(self):
if self.env.user.is_logistic_approver:
@@ -1315,10 +1339,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_logistic_approver and self.location_id.id == 57:
+ # 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:
+ 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:
@@ -1327,7 +1357,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'
@@ -1344,7 +1374,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:
@@ -1360,22 +1390,19 @@ class StockPicking(models.Model):
)
self.validation_minus_onhand_quantity()
- loc = self.location_id
- if loc.id == 57 and not self.env.user.is_logistic_approver and self.approval_status in ['pengajuan2']:
- raise UserError ("Harus Ask Approval Logistik")
self.responsible = self.env.user.id
# self.send_koli_to_so()
if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name:
self.check_koli()
res = super(StockPicking, self).button_validate()
-
+
# 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
@@ -2552,9 +2579,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/user_pengajuan_tempo_request.py b/indoteknik_custom/models/user_pengajuan_tempo_request.py
index 8ed92fc8..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')
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index ea6670eb..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
@@ -198,7 +201,7 @@ 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
diff --git a/indoteknik_custom/views/account_move_line.xml b/indoteknik_custom/views/account_move_line.xml
index 017a9eda..3a20388e 100644
--- a/indoteknik_custom/views/account_move_line.xml
+++ b/indoteknik_custom/views/account_move_line.xml
@@ -6,9 +6,9 @@
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
- <xpath expr="//page[@id='aml_tab']/field[@name='line_ids']" position="attributes">
+ <!-- <xpath expr="//page[@id='aml_tab']/field[@name='line_ids']" position="attributes">
<attribute name="attrs">{'readonly': [('refund_id','!=',False)]}</attribute>
- </xpath>
+ </xpath> -->
<xpath expr="//page[@id='aml_tab']/field[@name='line_ids']/tree/field[@name='currency_id']" position="before">
<field name="is_required" invisible="1"/>
</xpath>
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 98186862..3241d5f1 100644
--- a/indoteknik_custom/views/letter_receivable.xml
+++ b/indoteknik_custom/views/letter_receivable.xml
@@ -128,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/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>