summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models
diff options
context:
space:
mode:
authorIndoteknik . <it@fixcomart.co.id>2025-08-09 14:45:48 +0700
committerIndoteknik . <it@fixcomart.co.id>2025-08-09 14:45:48 +0700
commit979ffc90fffe2f09788016376d71a940a28f8fed (patch)
treea5408296eb204a0a5066578664ed0acb13174f27 /indoteknik_custom/models
parent33621956bdb9d807b480eda44ce7f2152f508695 (diff)
parentdf5ff0fc9bcd45c19b74288d8e5cfee018ba5bdd (diff)
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into pum-v2
Diffstat (limited to 'indoteknik_custom/models')
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/account_move.py290
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py9
-rw-r--r--indoteknik_custom/models/approval_payment_term.py1
-rw-r--r--indoteknik_custom/models/commision.py30
-rw-r--r--indoteknik_custom/models/cust_commision.py1
-rw-r--r--indoteknik_custom/models/mrp_production.py8
-rwxr-xr-xindoteknik_custom/models/purchase_order.py152
-rw-r--r--indoteknik_custom/models/res_partner.py5
-rwxr-xr-xindoteknik_custom/models/sale_order.py80
-rw-r--r--indoteknik_custom/models/sale_order_line.py7
-rw-r--r--indoteknik_custom/models/stock_picking.py34
-rw-r--r--indoteknik_custom/models/tukar_guling.py348
-rw-r--r--indoteknik_custom/models/tukar_guling_po.py170
-rw-r--r--indoteknik_custom/models/update_date_planned_po_wizard.py14
15 files changed, 869 insertions, 281 deletions
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 87310614..930e60e7 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -157,3 +157,4 @@ from . import refund_sale_order
from . import down_payment
from . import tukar_guling
from . import tukar_guling_po
+from . import update_date_planned_po_wizard \ No newline at end of file
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 1a6fad1c..fd08ed60 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -94,6 +94,31 @@ class AccountMove(models.Model):
compute='_compute_has_refund_so',
)
+ payment_date = fields.Date(string="Payment Date", compute='_compute_payment_date')
+ partial_payment = fields.Float(string="Partial Payment", compute='compute_partial_payment')
+
+ def compute_partial_payment(self):
+ for move in self:
+ if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial':
+ move.partial_payment = move.amount_total_signed - move.amount_residual_signed
+ else:
+ move.partial_payment = 0
+
+ def _compute_payment_date(self):
+ for move in self:
+ accountPayment = self.env['account.payment']
+
+ payment = accountPayment.search([]).filtered(
+ lambda p: move.id in p.reconciled_invoice_ids.ids
+ )
+
+ if payment:
+ move.payment_date = payment[0].date
+ elif move.reklas_misc_id:
+ move.payment_date = move.reklas_misc_id.date
+ else:
+ move.payment_date = False
+
# def name_get(self):
# result = []
# for move in self:
@@ -109,102 +134,175 @@ class AccountMove(models.Model):
# result.append((move.id, move.display_name))
# return result
- # def send_due_invoice_reminder(self):
- # today = fields.Date.today()
- # target_dates = [
- # today - timedelta(days=7),
- # today - timedelta(days=3),
- # today,
- # today + timedelta(days=3),
- # today + timedelta(days=7),
- # ]
-
- # partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1)
- # if not partner:
- # _logger.info("Partner tidak ditemukan.")
- # return
-
- # invoices = self.env['account.move'].search([
- # ('move_type', '=', 'out_invoice'),
- # ('state', '=', 'posted'),
- # ('payment_state', 'not in', ['paid','in_payment', 'reversed']),
- # ('invoice_date_due', 'in', target_dates),
- # ('partner_id', '=', partner.id),
- # ])
-
- # _logger.info(f"Invoices tahap 1: {invoices}")
-
- # invoices = invoices.filtered(
- # lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower()
- # )
- # _logger.info(f"Invoices tahap 2: {invoices}")
-
- # if not invoices:
- # _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}")
- # return
-
- # grouped = {}
- # for inv in invoices:
- # grouped.setdefault(inv.partner_id, []).append(inv)
-
- # template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
-
- # for partner, invs in grouped.items():
- # if not partner.email:
- # _logger.info(f"Partner {partner.name} tidak memiliki email")
- # continue
-
- # invoice_table_rows = ""
- # for inv in invs:
- # days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
- # invoice_table_rows += f"""
- # <tr>
- # <td>{inv.name}</td>
- # <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td>
- # <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td>
- # <td>{days_to_due}</td>
- # <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td>
- # <td>{inv.ref or '-'}</td>
- # </tr>
- # """
-
- # subject = f"Reminder Invoice Due - {partner.name}"
- # body_html = re.sub(
- # r"<tbody[^>]*>.*?</tbody>",
- # f"<tbody>{invoice_table_rows}</tbody>",
- # template.body_html,
- # flags=re.DOTALL
- # ).replace('${object.name}', partner.name) \
- # .replace('${object.partner_id.name}', partner.name)
- # # .replace('${object.email}', partner.email or '')
-
- # values = {
- # 'subject': subject,
- # 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi
- # 'email_from': 'finance@indoteknik.co.id',
- # 'body_html': body_html,
- # 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id',
- # }
-
- # _logger.info(f"VALUES: {values}")
-
- # template.send_mail(invs[0].id, force_send=True, email_values=values)
-
- # # Default System User
- # user_system = self.env['res.users'].browse(25)
- # system_id = user_system.partner_id.id if user_system else False
- # _logger.info(f"System User: {user_system.name} ({user_system.id})")
- # _logger.info(f"System User ID: {system_id}")
-
- # for inv in invs:
- # inv.message_post(
- # subject=subject,
- # body=body_html,
- # subtype_id=self.env.ref('mail.mt_note').id,
- # author_id=system_id,
- # )
-
- # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice")
+ def send_due_invoice_reminder(self):
+ today = fields.Date.today()
+ target_dates = [
+ today - timedelta(days=7),
+ today - timedelta(days=3),
+ today,
+ today + timedelta(days=3),
+ today + timedelta(days=7),
+ ]
+
+ # --- TESTING ---
+ # partner = self.env['res.partner'].search([('name', 'ilike', 'DIRGANTARA YUDHA ARTHA')], limit=1)
+ # if not partner:
+ # _logger.info("Partner tidak ditemukan.")
+ # return
+ # invoices = self.env['account.move'].search([
+ # ('move_type', '=', 'out_invoice'),
+ # ('state', '=', 'posted'),
+ # ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ # ('invoice_date_due', 'in', target_dates),
+ # ('partner_id', '=', partner.id),
+ # ])
+
+ invoices = self.env['account.move'].search([
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ ('invoice_date_due', 'in', target_dates),
+ ])
+ _logger.info(f"Invoices tahap 1: {invoices}")
+
+ invoices = invoices.filtered(
+ lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower()
+ )
+ _logger.info(f"Invoices tahap 2: {invoices}")
+
+ if not invoices:
+ _logger.info("Tidak ada invoice yang due")
+ return
+
+ invoice_group = {}
+ for inv in invoices:
+ dtd = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
+ invoice_group.setdefault((inv.partner_id, dtd), []).append(inv)
+
+ template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
+
+ for (partner, dtd), invs in invoice_group.items():
+ # Ambil child contact yang di-checklist reminder_invoices
+ reminder_contacts = self.env['res.partner'].search([
+ ('parent_id', '=', partner.id),
+ ('reminder_invoices', '=', True),
+ ('email', '!=', False),
+ ])
+ _logger.info(f"Email Reminder Child {reminder_contacts}")
+
+ if not reminder_contacts:
+ _logger.info(f"Partner {partner.name} tidak memiliki email yang sudah ceklis reminder")
+ continue
+
+ emails = list(filter(None, [partner.email])) + reminder_contacts.mapped('email')
+ if not emails:
+ _logger.info(f"Partner {partner.name} tidak memiliki email yang bisa dikirimi")
+ continue
+
+ email_to = ",".join(emails)
+ _logger.info(f"Email tujuan: {email_to}")
+
+ invoice_table_rows = ""
+ for inv in invs:
+ days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
+ invoice_table_rows += f"""
+ <tr>
+ <td>{inv.partner_id.name}</td>
+ <td>{inv.purchase_order_id.name or '-'}</td>
+ <td>{inv.name}</td>
+ <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td>
+ <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td>
+ <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td>
+ <td>{inv.invoice_payment_term_id.name or '-'}</td>
+ <td>{days_to_due}</td>
+ </tr>
+ """
+
+ days_to_due_message = ""
+ closing_message = ""
+ if dtd < 0:
+ days_to_due_message = (
+ f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {abs(dtd)} hari ke depan, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran "
+ "hubungan kerja sama yang baik antara kedua belah pihak.<br/>"
+ "Mohon konfirmasi apabila pembayaran telah dijadwalkan. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+
+ if dtd == 0:
+ days_to_due_message = (
+ "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status "
+ "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>"
+ "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+
+ if dtd > 0:
+ days_to_due_message = (
+ f"Kami ingin mengingatkan bahwa tagihan anda telah jatuh tempo selama {dtd} hari, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Mohon kesediaan Bapak/Ibu untuk segera melakukan pembayaran guna menghindari keterlambatan "
+ "dan menjaga kelancaran kerja sama yang telah terjalin dengan baik.<br/>"
+ "Apabila pembayaran sudah dilakukan, mohon konfirmasi dan lampirkan bukti transfer agar dapat kami proses lebih lanjut. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+
+ body_html = re.sub(
+ r"<tbody[^>]*>.*?</tbody>",
+ f"<tbody>{invoice_table_rows}</tbody>",
+ template.body_html,
+ flags=re.DOTALL
+ ).replace('${object.name}', partner.name) \
+ .replace('${object.partner_id.name}', partner.name) \
+ .replace('${days_to_due_message}', days_to_due_message) \
+ .replace('${closing_message}', closing_message)
+
+ cc_list = [
+ 'finance@indoteknik.co.id',
+ 'akbar@indoteknik.co.id',
+ 'stephan@indoteknik.co.id',
+ 'darren@indoteknik.co.id'
+ ]
+ sales_email = invs[0].invoice_user_id.partner_id.email if invs[0].invoice_user_id else None
+ if sales_email and sales_email not in cc_list:
+ cc_list.append(sales_email)
+
+ # Siapkan email values
+ values = {
+ 'subject': f"Reminder Invoice Due - {partner.name}",
+ # 'email_to': 'andrifebriyadiputra@gmail.com',
+ 'email_to': email_to,
+ 'email_from': 'finance@indoteknik.co.id',
+ 'email_cc': ",".join(cc_list),
+ 'body_html': body_html,
+ 'reply_to': 'finance@indoteknik.co.id',
+ }
+
+ _logger.info(f"Mengirim email ke: {values['email_to']} CC: {values['email_cc']}")
+ template.send_mail(invs[0].id, force_send=True, email_values=values)
+
+ # Post ke chatter
+ user_system = self.env['res.users'].browse(25)
+ system_id = user_system.partner_id.id if user_system else False
+
+ for inv in invs:
+ inv.message_post(
+ subject=values['subject'],
+ body=body_html,
+ subtype_id=self.env.ref('mail.mt_note').id,
+ author_id=system_id,
+ )
+
+ _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})")
@api.onchange('invoice_date')
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index 4a3f40e2..d354e3e3 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -33,6 +33,7 @@ class DueExtension(models.Model):
counter = fields.Integer(string="Counter", compute='_compute_counter')
approve_by = fields.Many2one('res.users', string="Approve By", readonly=True)
date_approve = fields.Datetime(string="Date Approve", readonly=True)
+
def _compute_counter(self):
for due in self:
due.counter = due.partner_id.counter
@@ -102,6 +103,14 @@ class DueExtension(models.Model):
self.date_approve = datetime.utcnow()
template = self.env.ref('indoteknik_custom.mail_template_due_extension_approve')
template.send_mail(self.id, force_send=True)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sale.order',
+ 'view_mode': 'form',
+ 'res_id': self.order_id.id,
+ 'views': [(False, 'form')],
+ 'target': 'current',
+ }
def generate_due_line(self):
partners = self.partner_id.get_child_ids()
diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py
index 6c857b45..4cf9a4c8 100644
--- a/indoteknik_custom/models/approval_payment_term.py
+++ b/indoteknik_custom/models/approval_payment_term.py
@@ -39,6 +39,7 @@ class ApprovalPaymentTerm(models.Model):
('rejected', 'Rejected')],
default='waiting_approval_sales_manager', tracking=True)
reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True)
+ reject_reason = fields.Text('Reject Reason', tracking=True)
sale_order_ids = fields.Many2many(
'sale.order',
string='Sale Orders',
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index 26b5df37..6d538b83 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -208,16 +208,15 @@ class CustomerCommision(models.Model):
('pending', 'Pending'),
('payment', 'Payment'),
], string='Payment Status', copy=False, readonly=True, tracking=3, default='pending')
- note_finnance = fields.Text('Notes Finnance')
+ note_finnance = fields.Text('Notes Finance')
reason_reject = fields.Char(string='Reason Reaject', tracking=True, track_visibility='onchange')
approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always')
grouped_so_number = fields.Char(string='Group SO Number', compute='_compute_grouped_numbers')
grouped_invoice_number = fields.Char(string='Group Invoice Number', compute='_compute_grouped_numbers')
- sales_id = fields.Many2one('res.users', string="Sales", tracking=True, default=lambda self: self.env.user,
- domain=lambda self: [
- ('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman').id)])
+ sales_id = fields.Many2one('res.users', string="Sales", tracking=True, required=True,
+ domain=[('groups_id', 'in', [94]),('id', '!=', 15710)])
date_approved_sales = fields.Datetime(string="Date Approved Sales", tracking=True)
date_approved_marketing = fields.Datetime(string="Date Approved Marketing", tracking=True)
@@ -400,6 +399,27 @@ class CustomerCommision(models.Model):
# result = super(CustomerCommision, self).create(vals)
# return result
+ def _fill_note_finance(self):
+ for rec in self:
+ fee_percent = rec.commision_percent or 0.0
+ dpp = rec.total_dpp or 0.0
+
+ fee = dpp * fee_percent / 100
+ pph21 = 0.5 * fee * 0.05
+ fee_net = fee - pph21
+ rec.note_finnance = (
+ "Kelengkapan data penerima fee sudah lengkap (NPWP dan KTP)\n"
+ f"Perhitungan Fee ({fee_percent:.0f}%) dari nilai DPP pada Invoice terlampir sudah\n"
+ f"sesuai yaitu Rp {fee:,.0f}\n"
+ "Sesuai PMK No. 168 tahun 2023, komisi fee dikenakan PPH 21\n"
+ "sebesar :\n"
+ f"= 50% x Penghasilan Bruto x 5%\n"
+ f"= 50% x Rp {fee:,.0f} x 5%\n"
+ f"= Rp {pph21:,.0f}\n"
+ "Sehingga fee bersih sebesar\n"
+ f"= Rp {fee:,.0f} - Rp {pph21:,.0f}\n"
+ f"= Rp {fee_net:,.0f}"
+ )
def action_confirm_customer_commision(self):
jakarta_tz = pytz.timezone('Asia/Jakarta')
now = datetime.now(jakarta_tz)
@@ -426,6 +446,8 @@ class CustomerCommision(models.Model):
elif self.status == 'pengajuan4' and self.env.user.id == 1272:
for line in self.commision_lines:
line.invoice_id.is_customer_commision = True
+ if self.commision_type == 'fee':
+ self._fill_note_finance()
self.status = 'approved'
self.approved_by = (self.approved_by + ', ' if self.approved_by else '') + self.env.user.name
self.date_approved_accounting = now_naive
diff --git a/indoteknik_custom/models/cust_commision.py b/indoteknik_custom/models/cust_commision.py
index c3105cfd..05c68935 100644
--- a/indoteknik_custom/models/cust_commision.py
+++ b/indoteknik_custom/models/cust_commision.py
@@ -34,4 +34,3 @@ class CustCommision(models.Model):
for rec in duplicate_partner:
if self.commision_type == rec.commision_type:
raise UserError('Partner already exists')
- \ No newline at end of file
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index 7977bdf7..91da0597 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -156,7 +156,7 @@ class MrpProduction(models.Model):
'order_id': new_po.id
}])
- new_po.button_confirm()
+ # new_po.button_confirm()
self.is_po = True
@@ -247,7 +247,7 @@ class CheckBomProduct(models.Model):
@api.constrains('production_id', 'product_id')
def _check_product_bom_validation(self):
for record in self:
- if record.production_id.sale_order.state not in ['sale', 'done']:
+ if record.production_id.sale_order and record.production_id.sale_order.state not in ['sale', 'done']:
raise UserError((
"SO harus diconfirm terlebih dahulu."
))
@@ -273,13 +273,13 @@ class CheckBomProduct(models.Model):
if existing_lines:
total_quantity = sum(existing_lines.mapped('quantity'))
- if total_quantity < total_qty_in_moves:
+ if total_quantity > total_qty_in_moves:
raise UserError((
"Quantity Product '%s' kurang dari quantity demand."
) % (record.product_id.display_name))
else:
# Check if the quantity exceeds the allowed total
- if record.quantity < total_qty_in_moves:
+ if record.quantity > total_qty_in_moves:
raise UserError((
"Quantity Product '%s' kurang dari quantity demand."
) % (record.product_id.display_name))
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 45134939..103a9131 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -92,6 +92,11 @@ class PurchaseOrder(models.Model):
is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible')
+ reason_change_date_planned = fields.Selection([
+ ('delay', 'Delay By Vendor'),
+ ('urgent', 'Urgent Delivery'),
+ ], string='Reason Change Date Planned', tracking=True)
+
# picking_ids = fields.One2many('stock.picking', 'purchase_id', string='Pickings')
bu_related_count = fields.Integer(
@@ -100,9 +105,68 @@ class PurchaseOrder(models.Model):
)
manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders')
+ complete_bu_in_count = fields.Integer(
+ string="Complete BU In Count",
+ compute='_compute_complete_bu_in_count'
+ )
+
+ def _compute_complete_bu_in_count(self):
+ for order in self:
+ if order.state not in ['done', 'cancel']:
+ order.complete_bu_in_count = 1
+ else:
+ relevant_pickings = order.picking_ids.filtered(
+ lambda p: p.state != 'done'
+ and p.state != 'cancel'
+ and p.picking_type_code == 'incoming'
+ and p.origin == order.name
+ and p.name.startswith('BU/IN')
+ )
+ order.complete_bu_in_count = len(relevant_pickings)
+
def _has_vcm(self):
if self.id:
self.vcm_id = self.env['tukar.guling.po'].search([('origin', '=', self.name)], limit=1)
+
+ @api.depends('order_line.date_planned')
+ def _compute_date_planned(self):
+ """ date_planned = the earliest date_planned across all order lines. """
+ for order in self:
+ order.date_planned = False
+
+ @api.constrains('date_planned')
+ def constrains_date_planned(self):
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'):
+ raise ValidationError("Hanya dapat diisi oleh Purchasing")
+
+ base_bu = self.env['stock.picking'].search([
+ ('name', 'ilike', 'BU/'),
+ ('origin', 'ilike', rec.name),
+ ('group_id', '=', rec.group_id.id),
+ ('state', 'not in', ['cancel','done'])
+ ])
+
+ for bu in base_bu:
+ bu.write({
+ 'scheduled_date': rec.date_planned,
+ 'reason_change_date_planned': rec.reason_change_date_planned
+ })
+
+ rec.sync_date_planned_to_so()
+
+ def sync_date_planned_to_so(self):
+ for line in self.order_sales_match_line:
+ other_sales_match = self.env['purchase.order.sales.match'].search([
+ # ('product_id', '=', line.product_id.id),
+ ('sale_id', '=', line.sale_id.id),
+ # ('sale_line_id', '=', line.sale_line_id.id)
+ ])
+
+ dates = [d for d in other_sales_match.mapped('purchase_order_id.date_planned') if d]
+ if dates:
+ date_planned = max(dates)
+ line.sale_id.write({'et_products': date_planned, 'reason_change_date_planned': line.purchase_order_id.reason_change_date_planned})
@api.depends('name')
def _compute_bu_related_count(self):
@@ -677,13 +741,6 @@ class PurchaseOrder(models.Model):
for order in self:
order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids)
- # def _compute_has_active_invoice(self):
- # for order in self:
- # related_invoices = order.invoice_ids.filtered(
- # lambda inv: inv.purchase_order_id.id == order.id and inv.move_type == 'in_invoice' and inv.state != 'cancel'
- # )
- # order.has_active_invoice = bool(related_invoices)
-
def add_product_to_pricelist(self):
i = 0
for line in self.order_line:
@@ -766,16 +823,16 @@ class PurchaseOrder(models.Model):
"""
purchase_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id)
- def _compute_date_planned(self):
- for order in self:
- if order.date_approve:
- leadtime = order.partner_id.leadtime
- current_time = order.date_approve
- delta_time = current_time + timedelta(days=leadtime)
- delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
- order.date_planned = delta_time
- else:
- order.date_planned = False
+ # def _compute_date_planned(self):
+ # for order in self:
+ # if order.date_approve:
+ # leadtime = order.partner_id.leadtime
+ # current_time = order.date_approve
+ # delta_time = current_time + timedelta(days=leadtime)
+ # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
+ # order.date_planned = delta_time
+ # else:
+ # order.date_planned = False
def action_create_invoice(self):
res = super(PurchaseOrder, self).action_create_invoice()
@@ -959,6 +1016,9 @@ class PurchaseOrder(models.Model):
if self.amount_untaxed >= 50000000 and not self.env.user.id == 21:
raise UserError("Hanya Rafly Hanggara yang bisa approve")
+
+ if not self.date_planned:
+ raise UserError("Receipt Date harus diisi")
if self.total_percent_margin < self.total_so_percent_margin:
self.env.user.notify_danger(
@@ -975,7 +1035,7 @@ class PurchaseOrder(models.Model):
# )
if not self.from_apo:
- if (not self.matches_so or not self.sale_order_id) and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not self.manufacturing_id:
+ if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader:
raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager")
send_email = False
@@ -1010,10 +1070,10 @@ class PurchaseOrder(models.Model):
self.approve_by = self.env.user.id
# override date planned added with two days
- leadtime = self.partner_id.leadtime
- delta_time = current_time + timedelta(days=leadtime)
- delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
- self.date_planned = delta_time
+ # leadtime = self.partner_id.leadtime
+ # delta_time = current_time + timedelta(days=leadtime)
+ # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
+ # self.date_planned = delta_time
self.date_deadline_ref_date_planned()
self.unlink_purchasing_job_state()
@@ -1023,9 +1083,9 @@ class PurchaseOrder(models.Model):
# Tambahan: redirect ke BU hanya untuk single PO yang berhasil dikonfirmasi
_logger.info("Jumlah PO: %s | State: %s", len(self), self.state)
- if len(self) == 1:
- _logger.info("Redirecting ke BU")
- return self.action_view_related_bu()
+ # if len(self) == 1:
+ # _logger.info("Redirecting ke BU")
+ # return self.action_view_related_bu()
return res
@@ -1391,6 +1451,20 @@ class PurchaseOrder(models.Model):
# Tambahkan pemanggilan method untuk handle pricelist system update
self._handle_pricelist_system_update(vals)
return res
+
+ def action_open_change_date_wizard(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'change.date.planned.wizard',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'default_purchase_id': self.id,
+ 'default_new_date_planned': self.date_planned,
+ }
+ }
+
def _handle_pricelist_system_update(self, vals):
if 'order_line' in vals or any(key in vals for key in ['state', 'approval_status']):
@@ -1479,4 +1553,32 @@ class PurchaseOrderUnlockWizard(models.TransientModel):
order.approval_status_unlock = 'pengajuanFinance'
return {'type': 'ir.actions.act_window_close'}
+class ChangeDatePlannedWizard(models.TransientModel):
+ _name = 'change.date.planned.wizard'
+ _description = 'Change Date Planned Wizard'
+
+ purchase_id = fields.Many2one('purchase.order', string="Purchase Order", required=True)
+ new_date_planned = fields.Datetime(string="New Date Planned") # <- harus DTTM biar match
+ old_date_planned = fields.Datetime(string="Current Planned Date", related='purchase_id.date_planned', readonly=True)
+ reason = fields.Selection([
+ ('delay', 'Delay By Vendor'),
+ ('urgent', 'Urgent Delivery'),
+ ], string='Reason')
+ date_changed = fields.Boolean(string="Date Changed", compute="_compute_date_changed")
+
+ @api.depends('old_date_planned', 'new_date_planned')
+ def _compute_date_changed(self):
+ for rec in self:
+ rec.date_changed = (
+ rec.old_date_planned and rec.new_date_planned and
+ rec.old_date_planned != rec.new_date_planned
+ )
+
+ def confirm_change(self):
+ self.purchase_id.write({
+ 'date_planned': self.new_date_planned,
+ 'reason_change_date_planned': self.reason,
+ })
+
+
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index 236df16f..f260f58e 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -27,6 +27,11 @@ class ResPartner(models.Model):
# Referensi
supplier_ids = fields.Many2many('user.pengajuan.tempo.line', string="Suppliers")
+ reminder_invoices = fields.Boolean(
+ string='Reminder Invoice?',
+ help='Centang jika kontak ini harus menerima email pengingat invoice.', default=False
+ )
+
# informasi perusahaan
name_tempo = fields.Many2one('res.partner', string='Nama Perusahaan',tracking=True)
industry_id_tempo = fields.Many2one('res.partner.industry', 'Customer Industry', readonly=True)
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 7be0e8ff..e71e3830 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -163,7 +163,7 @@ class SaleOrder(models.Model):
carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3)
have_visit_service = fields.Boolean(string='Have Visit Service', compute='_have_visit_service',
help='To compute is customer get visit service')
- delivery_amt = fields.Float(string='Delivery Amt', copy=False)
+ delivery_amt = fields.Float(string='Delivery Amt', copy=False, tracking=True)
shipping_cost_covered = fields.Selection([
('indoteknik', 'Indoteknik'),
('customer', 'Customer')
@@ -350,7 +350,7 @@ class SaleOrder(models.Model):
date_unhold = fields.Datetime(string='Date Unhold', tracking=True, readonly=True, help='Waktu ketika SO di Unhold'
)
- et_products = fields.Datetime(string='ET Products', compute='_compute_et_products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.")
+ et_products = fields.Datetime(string='ET Products', help="Leadtime produk berdasarkan SLA vendor, tanpa logistik.", tracking=True)
eta_date_reserved = fields.Datetime(
string="Date Reserved",
@@ -381,6 +381,11 @@ class SaleOrder(models.Model):
if self.id:
self.ccm_id = self.env['tukar.guling'].search([('origin', 'ilike', self.name)], limit=1)
+ reason_change_date_planned = fields.Selection([
+ ('delay', 'Delay By Vendor'),
+ ('urgent', 'Urgent Delivery'),
+ ], string='Reason Change Date Planned', tracking=True)
+
@api.depends('order_line.product_id', 'date_order')
def _compute_et_products(self):
jakarta = pytz.timezone("Asia/Jakarta")
@@ -2154,7 +2159,12 @@ class SaleOrder(models.Model):
# if order.validate_partner_invoice_due():
# return self._create_notification_action('Notification',
# 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
-
+
+ if not order.with_context(ask_approval=True)._is_request_to_own_team_leader():
+ return self._create_notification_action(
+ 'Peringatan',
+ 'Hanya bisa konfirmasi SO tim Anda.'
+ )
if order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
return self._create_approval_notification('Pimpinan')
@@ -2164,6 +2174,12 @@ class SaleOrder(models.Model):
self.check_limit_so_to_invoice()
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
+ elif order._requires_approval_team_sales():
+ self.check_product_bom()
+ self.check_credit_limit()
+ self.check_limit_so_to_invoice()
+ order.approval_status = 'approved'
+ return self._create_approval_notification('Team Sales')
raise UserError("Bisa langsung Confirm")
@@ -2380,12 +2396,20 @@ class SaleOrder(models.Model):
return self._create_notification_action('Notification',
'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
+ if not order._is_request_to_own_team_leader():
+ return self._create_notification_action(
+ 'Warning',
+ 'Hanya bisa konfirmasi SO tim Anda.'
+ )
if order._requires_approval_margin_leader():
order.approval_status = 'pengajuan2'
return self._create_approval_notification('Pimpinan')
elif order._requires_approval_margin_manager():
order.approval_status = 'pengajuan1'
return self._create_approval_notification('Sales Manager')
+ elif order._requires_approval_team_sales():
+ order.approval_status = 'approved'
+ return self._create_approval_notification('Team Sales')
order.approval_status = 'approved'
order._set_sppkp_npwp_contact()
@@ -2486,8 +2510,36 @@ class SaleOrder(models.Model):
return self.total_percent_margin <= 15 and not self.env.user.is_leader
def _requires_approval_margin_manager(self):
- return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader
- # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager
+ return 15 < self.total_percent_margin < 18 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader
+
+ def _requires_approval_team_sales(self):
+ return (
+ 18 <= self.total_percent_margin <= 24
+ and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra
+ and not self.env.user.is_sales_manager
+ and not self.env.user.is_leader
+ )
+
+
+ def _is_request_to_own_team_leader(self):
+ user = self.env.user
+
+ # Pengecualian Pak Akbar & Darren
+ if user.is_leader or user.is_sales_manager:
+ return True
+
+ if user.id in (3401, 20, 3988): # admin (fida, nabila, ninda)
+ return True
+
+ if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988):
+ return True
+
+ salesperson_id = self.user_id.id
+ approver_id = user.id
+ team_leader_id = self.team_id.user_id.id
+
+ return salesperson_id == approver_id or approver_id == team_leader_id
+
def _create_approval_notification(self, approval_role):
title = 'Warning'
@@ -3083,6 +3135,24 @@ class SaleOrder(models.Model):
except:
pass
+ #payment term vals
+ if 'payment_term_id' in vals and any(
+ order.approval_status in ['pengajuan1', 'pengajuan2', 'approved'] for order in self):
+ raise UserError(
+ "Payment Term tidak dapat diubah karena Sales Order sedang dalam proses approval atau sudah diapprove.")
+
+ if 'payment_term_id' in vals:
+ 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:
+ 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."
+ )
+
+
res = super(SaleOrder, self).write(vals)
# Update before margin setelah write
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 5e9fc362..64b9f9bc 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -1,6 +1,10 @@
from odoo import fields, models, api, _
from odoo.exceptions import UserError
from datetime import datetime, timedelta
+import logging
+from odoo.tools.float_utils import float_compare
+
+_logger = logging.getLogger(__name__)
class SaleOrderLine(models.Model):
@@ -49,6 +53,9 @@ class SaleOrderLine(models.Model):
qty_free_bu = fields.Float(string='Free BU', compute='_get_qty_free_bandengan')
desc_updatable = fields.Boolean(string='desc boolean', default=True, compute='_get_desc_updatable')
+ is_has_disc = fields.Boolean('Flash Sale', default=False)
+
+
def _get_outgoing_incoming_moves(self):
outgoing_moves = self.env['stock.move']
incoming_moves = self.env['stock.move']
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 3e152f10..82f81642 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -303,6 +303,10 @@ class StockPicking(models.Model):
approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date')
last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False)
update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD')
+ reason_change_date_planned = fields.Selection([
+ ('delay', 'Delay By Vendor'),
+ ('urgent', 'Urgent Delivery'),
+ ], string='Reason Change Date Planned', tracking=True)
def _get_kgx_awb_number(self):
"""Menggabungkan name dan origin untuk membuat AWB Number"""
@@ -806,6 +810,7 @@ class StockPicking(models.Model):
self.biteship_tracking_id = data.get("courier", {}).get("tracking_id", "")
self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "")
self.delivery_tracking_no = self.biteship_waybill_id
+ self.biteship_shipping_price = data.get("price", 0.0)
waybill_id = self.biteship_waybill_id
@@ -1055,16 +1060,23 @@ class StockPicking(models.Model):
self.sale_id.date_doc_kirim = self.date_doc_kirim
def action_assign(self):
- res = super(StockPicking, self).action_assign()
- for move in self:
- # if not move.sale_id.hold_outgoing and move.location_id.id != 57 and move.location_dest_id.id != 60:
- # TODO cant skip hold outgoing cause of not singleton method
- current_time = datetime.datetime.utcnow()
- move.real_shipping_id = move.sale_id.real_shipping_id
- move.date_availability = current_time
- # self.check_state_reserve()
+ if self.env.context.get('default_picking_type_id'):
+ pickings_to_assign = self.filtered(
+ lambda p: not (p.sale_id and p.sale_id.hold_outgoing)
+ )
+ else:
+ pickings_to_assign = self
+
+ res = super(StockPicking, pickings_to_assign).action_assign()
+
+ current_time = datetime.datetime.utcnow()
+ for picking in pickings_to_assign:
+ picking.real_shipping_id = picking.sale_id.real_shipping_id
+ picking.date_availability = current_time
+
return res
+
def ask_approval(self):
if self.env.user.is_accounting:
raise UserError("Bisa langsung Validate")
@@ -1380,6 +1392,12 @@ class StockPicking(models.Model):
self.send_mail_bills()
if 'BU/PUT' in self.name:
self.automatic_reserve_product()
+
+ if self.tukar_guling_id:
+ self.tukar_guling_id.update_doc_state()
+ elif self.tukar_guling_po_id:
+ self.tukar_guling_po_id.update_doc_state()
+
return res
def automatic_reserve_product(self):
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index 43bc156e..6aedb70e 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -5,7 +5,8 @@ from datetime import datetime
_logger = logging.getLogger(__name__)
-#TODO
+
+# TODO
# 1. tracking status dokumen BU [X]
# 2. ganti nama dokumen
# 3. Tracking ketika create dokumen [X]
@@ -20,7 +21,7 @@ class TukarGuling(models.Model):
_order = 'date desc, id desc'
_rec_name = 'name'
_inherit = ['mail.thread', 'mail.activity.mixin']
-
+
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
origin = fields.Char(string='Origin SO')
if_so = fields.Boolean('Is SO', default=True)
@@ -31,7 +32,7 @@ class TukarGuling(models.Model):
'tukar_guling_id',
string='Transfers'
)
- # origin_so = fields.Many2one('sale.order', string='Origin SO')
+ origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so')
name = fields.Char('Number', required=True, copy=False, readonly=True, default='New')
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
operations = fields.Many2one(
@@ -62,6 +63,7 @@ class TukarGuling(models.Model):
('approval_sales', ' Approval Sales'),
('approval_finance', 'Approval Finance'),
('approval_logistic', 'Approval Logistic'),
+ ('approved', 'Waiting for Operations'),
('done', 'Done'),
('cancel', 'Canceled')
], default='draft', tracking=True, required=True)
@@ -72,6 +74,47 @@ class TukarGuling(models.Model):
date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True)
date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True)
+ val_inv_opt = fields.Selection([
+ ('tanpa_cancel', 'Tanpa Cancel Invoice'),
+ ('cancel_invoice', 'Cancel Invoice'),
+ ], tracking=3, string='Invoice Option')
+
+ is_has_invoice = fields.Boolean('Has Invoice?', compute='_compute_is_has_invoice', readonly=True, default=False)
+
+ invoice_id = fields.Many2many('account.move', string='Invoice Ref', readonly=True)
+
+ @api.depends('origin', 'operations')
+ def _compute_origin_so(self):
+ for rec in self:
+ rec.origin_so = False
+ origin_str = rec.origin or rec.operations.origin
+ if origin_str:
+ so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1)
+ rec.origin_so = so.id if so else False
+
+ @api.depends('origin')
+ def _compute_is_has_invoice(self):
+ for rec in self:
+ invoices = self.env['account.move'].search([
+ ('invoice_origin', 'ilike', rec.origin),
+ ('move_type', '=', 'out_invoice'), # hanya invoice
+ ('state', 'not in', ['draft', 'cancel'])
+ ])
+ if invoices:
+ rec.is_has_invoice = True
+ rec.invoice_id = invoices
+ else:
+ rec.is_has_invoice = False
+
+ def set_opt(self):
+ if not self.val_inv_opt and self.is_has_invoice == True:
+ raise UserError("Kalau sudah ada invoice Return Invoice Option harus diisi!")
+ for rec in self:
+ if rec.val_inv_opt == 'cancel_invoice' and self.is_has_invoice == True:
+ raise UserError("Tidak bisa mengubah Return karena sudah ada invoice dan belum di cancel.")
+ elif rec.val_inv_opt == 'tanpa_cancel' and self.is_has_invoice == True:
+ continue
+
# @api.onchange('operations')
# def get_partner_id(self):
# if self.operations and self.operations.partner_id and self.operations.partner_id.name:
@@ -98,7 +141,7 @@ class TukarGuling(models.Model):
@api.onchange('operations')
def _onchange_operations(self):
"""Auto-populate lines ketika operations dipilih"""
- if self.operations.picking_type_id.id not in [29,30]:
+ if self.operations.picking_type_id.id not in [29, 30]:
raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
for rec in self:
if rec.operations and rec.operations.picking_type_id.id == 30:
@@ -110,8 +153,6 @@ class TukarGuling(models.Model):
if self.line_ids and from_return_picking:
# Hanya update origin, jangan ubah lines
- if self.operations.origin:
- self.origin = self.operations.origin
_logger.info("📌 Menggunakan product lines dari return wizard, tidak populate ulang.")
# 🚀 Tapi tetap populate mapping koli jika BU/OUT
@@ -143,6 +184,7 @@ class TukarGuling(models.Model):
# Set origin dari operations
if self.operations.origin:
self.origin = self.operations.origin
+ self.origin_so = self.operations.group_id.id
# Auto-populate lines dari move_ids operations
lines_data = []
@@ -217,7 +259,6 @@ class TukarGuling(models.Model):
self.origin = False
-
def action_populate_lines(self):
"""Manual button untuk populate lines - sebagai alternatif"""
self.ensure_one()
@@ -257,7 +298,7 @@ class TukarGuling(models.Model):
def _check_product_lines(self):
"""Constraint: Product lines harus ada jika state bukan draft"""
for record in self:
- if record.state in ('approval_sales', 'approval_logistic', 'approval_finance',
+ if record.state in ('approval_sales', 'approval_logistic', 'approval_finance', 'approved',
'done') and not record.line_ids:
raise ValidationError("Product lines harus diisi sebelum submit atau approve!")
@@ -281,36 +322,38 @@ class TukarGuling(models.Model):
return True
- def _is_already_returned(self, picking):
- return self.env['stock.picking'].search_count([
- ('origin', '=', 'Return of %s' % picking.name),
- ('state', '!=', 'cancel')
- ]) > 0
+ # def _is_already_returned(self, picking):
+ # return self.env['stock.picking'].search_count([
+ # ('origin', '=', 'Return of %s' % picking.name),
+ # ('state', '!=', 'cancel')
+ # ]) > 0
+
+ # def _check_invoice_on_revisi_so(self):
+ # for record in self:
+ # if record.return_type == 'revisi_so' and record.origin:
+ # invoices = self.env['account.move'].search([
+ # ('invoice_origin', 'ilike', record.origin),
+ # ('state', 'not in', ['draft', 'cancel'])
+ # ])
+ # if invoices:
+ # raise ValidationError(
+ # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin
+ # )
- @api.constrains('return_type', 'operations')
- def _check_invoice_on_revisi_so(self):
- for record in self:
- if record.return_type == 'revisi_so' and record.origin:
- invoices = self.env['account.move'].search([
- ('invoice_origin', 'ilike', record.origin),
- ('state', 'not in', ['draft', 'cancel'])
- ])
- if invoices:
- raise ValidationError(
- _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin
- )
@api.model
def create(self, vals):
- # Generate sequence number
if not vals.get('name') or vals['name'] == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling')
- # Auto-fill origin from operations
- if not vals.get('origin') and vals.get('operations'):
+ if vals.get('operations'):
picking = self.env['stock.picking'].browse(vals['operations'])
if picking.origin:
vals['origin'] = picking.origin
+ # Find matching SO
+ so = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1)
+ if so:
+ vals['origin_so'] = so.id
if picking.partner_id:
vals['partner_id'] = picking.partner_id.id
@@ -318,6 +361,10 @@ class TukarGuling(models.Model):
res.message_post(body=_("CCM Created By %s") % self.env.user.name)
return res
+ res = super(TukarGuling, self).create(vals)
+ res.message_post(body=_("CCM Created By %s") % self.env.user.name)
+ return res
+
def copy(self, default=None):
if default is None:
default = {}
@@ -345,9 +392,9 @@ class TukarGuling(models.Model):
def write(self, vals):
self.ensure_one()
- if self.operations.picking_type_id.id not in [29,30]:
+ if self.operations.picking_type_id.id not in [29, 30]:
raise UserError("❌ Picking type harus BU/OUT atau BU/PICK")
- self._check_invoice_on_revisi_so()
+ # self._check_invoice_on_revisi_so()
operasi = self.operations.picking_type_id.id
tipe = self.return_type
pp = vals.get('return_type', tipe)
@@ -376,24 +423,33 @@ class TukarGuling(models.Model):
# if self.state == 'done':
# raise UserError ("Tidak Boleh delete ketika sudahh done")
for record in self:
- if record.state == 'done':
+ if record.state == 'approved' or record.state == 'done':
raise UserError(
- "Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu jika ingin menghapus")
- ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'done')
+ "Tidak bisa hapus pengajuan jika sudah Approved, set ke draft terlebih dahulu jika ingin menghapus")
+ ongoing_bu = self.picking_ids.filtered(lambda p: p.state != 'approved')
for picking in ongoing_bu:
picking.action_cancel()
return super(TukarGuling, self).unlink()
def action_view_picking(self):
self.ensure_one()
- action = self.env.ref('stock.action_picking_tree_all').read()[0]
- pickings = self.picking_ids
- if len(pickings) > 1:
- action['domain'] = [('id', 'in', pickings.ids)]
- elif pickings:
- action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
- action['res_id'] = pickings.id
- return action
+
+ # picking_origin = f"Return of {self.operations.name}"
+ returs = self.env['stock.picking'].search([
+ ('tukar_guling_id', '=', self.id),
+ ])
+
+ if not returs:
+ raise UserError("Doc Retrun Not Found")
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Delivery Pengajuan Retur SO',
+ 'res_model': 'stock.picking',
+ 'view_mode': 'tree,form',
+ 'domain': [('id', 'in', returs.ids)],
+ 'target': 'current',
+ }
def action_draft(self):
"""Reset to draft state"""
@@ -434,40 +490,100 @@ class TukarGuling(models.Model):
linked_bu_out = picking.linked_manual_bu_out
if linked_bu_out and linked_bu_out.state == 'done':
raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT suda Done!")
- if self._is_already_returned(self.operations):
- raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
+ # if self._is_already_returned(self.operations):
+ # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
if self.operations.picking_type_id.id == 29:
- for line in self.line_ids:
- mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
- total_qty = sum(l.qty_return for l in mapping_lines)
- if total_qty != line.product_uom_qty:
- raise UserError(
- _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name)
-
- self._check_invoice_on_revisi_so()
+ # Cek apakah ada BU/PICK di origin
+ origin = self.operations.origin
+ has_bu_pick = self.env['stock.picking'].search_count([
+ ('origin', '=', origin),
+ ('picking_type_id', '=', 30),
+ ('state', '!=', 'cancel')
+ ]) > 0
+
+ if has_bu_pick:
+ for line in self.line_ids:
+ mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
+ total_qty = sum(l.qty_return for l in mapping_lines)
+ if total_qty != line.product_uom_qty:
+ raise UserError(
+ _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name
+ )
+ # self._check_invoice_on_revisi_so()
self._validate_product_lines()
if self.state != 'draft':
raise UserError("Submit hanya bisa dilakukan dari Draft.")
self.state = 'approval_sales'
+ def update_doc_state(self):
+ # 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([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 29),
+ ])
+ done_out = self.env['stock.picking'].search_count([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 29),
+ ('state', '=', 'done'),
+ ])
+ if self.state == 'approved' and total_out > 0 and done_out == total_out:
+ self.state = 'done'
+
+ # OUT revisi SO
+ elif self.operations.picking_type_id.id == 29 and self.return_type == 'revisi_so':
+ total_ort = self.env['stock.picking'].search_count([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 74),
+ ])
+ done_ort = self.env['stock.picking'].search_count([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 74),
+ ('state', '=', 'done'),
+ ])
+ if self.state == 'approved' and total_ort > 0 and done_ort == total_ort:
+ self.state = 'done'
+
+ # PICK revisi SO
+ elif self.operations.picking_type_id.id == 30 and self.return_type == 'revisi_so':
+ done_ort = self.env['stock.picking'].search([
+ ('tukar_guling_id', '=', self.id),
+ ('picking_type_id', '=', 74),
+ ('state', '=', 'done'),
+ ])
+ if self.state == 'approved' and done_ort:
+ self.state = 'done'
+ else:
+ raise UserError("Tidak bisa menentukan jenis retur.")
+
def action_approve(self):
self.ensure_one()
self._validate_product_lines()
- self._check_invoice_on_revisi_so()
+ # self._check_invoice_on_revisi_so()
self._check_not_allow_tukar_guling_on_bu_pick()
operasi = self.operations.picking_type_id.id
tipe = self.return_type
if self.operations.picking_type_id.id == 29:
- for line in self.line_ids:
- mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
- total_qty = sum(l.qty_return for l in mapping_lines)
- if total_qty != line.product_uom_qty:
- raise UserError(
- _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name)
+ # Cek apakah ada BU/PICK di origin
+ origin = self.operations.origin
+ has_bu_pick = self.env['stock.picking'].search_count([
+ ('origin', '=', origin),
+ ('picking_type_id', '=', 30),
+ ('state', '!=', 'cancel')
+ ]) > 0
+
+ if has_bu_pick:
+ for line in self.line_ids:
+ mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id)
+ total_qty = sum(l.qty_return for l in mapping_lines)
+ if total_qty != line.product_uom_qty:
+ raise UserError(
+ _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name
+ )
if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done':
raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done")
@@ -495,13 +611,15 @@ class TukarGuling(models.Model):
elif rec.state == 'approval_finance':
if not rec.env.user.has_group('indoteknik_custom.group_role_fat'):
raise UserError("Hanya Finance Manager yang boleh approve tahap ini.")
+ # rec._check_invoice_on_revisi_so()
+ rec.set_opt()
rec.state = 'approval_logistic'
rec.date_finance = now
elif rec.state == 'approval_logistic':
if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'):
raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.")
- rec.state = 'done'
+ rec.state = 'approved'
rec._create_pickings()
rec.date_logistic = now
@@ -533,6 +651,23 @@ class TukarGuling(models.Model):
def _create_pickings(self):
_logger.info("🛠 Starting _create_pickings()")
+
+ def _force_locations(picking, from_loc, to_loc):
+ picking.write({
+ 'location_id': from_loc,
+ 'location_dest_id': to_loc,
+ })
+ for move in picking.move_lines:
+ move.write({
+ 'location_id': from_loc,
+ 'location_dest_id': to_loc,
+ })
+ for move_line in move.move_line_ids:
+ move_line.write({
+ 'location_id': from_loc,
+ 'location_dest_id': to_loc,
+ })
+
for record in self:
if not record.operations:
raise UserError("BU/OUT dari field operations tidak ditemukan.")
@@ -555,36 +690,53 @@ class TukarGuling(models.Model):
### ======== SRT dari BU/OUT =========
srt_return_lines = []
- for prod in mapping_koli.mapped('product_id'):
- qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
- move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
- if not move:
- raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}")
- srt_return_lines.append((0, 0, {
- 'product_id': prod.id,
- 'quantity': qty_total,
- 'move_id': move.id,
- }))
- _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
+ if mapping_koli:
+ for prod in mapping_koli.mapped('product_id'):
+ qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod))
+ move = bu_out.move_lines.filtered(lambda m: m.product_id == prod)
+ if not move:
+ raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}")
+ srt_return_lines.append((0, 0, {
+ 'product_id': prod.id,
+ 'quantity': qty_total,
+ 'move_id': move.id,
+ }))
+ _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}")
+
+ elif not mapping_koli:
+ for line in record.line_ids:
+ move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id)
+ if not move:
+ raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}")
+ srt_return_lines.append((0, 0, {
+ 'product_id': line.product_id.id,
+ 'quantity': line.product_uom_qty,
+ 'move_id': move.id,
+ }))
+ _logger.info(
+ f"📟 SRT line (fallback line_ids): {line.product_id.display_name} | qty={line.product_uom_qty}")
srt_picking = None
if srt_return_lines:
+ # Tentukan tujuan lokasi berdasarkan ada/tidaknya mapping_koli
+ dest_location_id = BU_OUTPUT_LOCATION_ID if mapping_koli else BU_STOCK_LOCATION_ID
+
srt_wizard = self.env['stock.return.picking'].with_context({
'active_id': bu_out.id,
'default_location_id': PARTNER_LOCATION_ID,
- 'default_location_dest_id': BU_OUTPUT_LOCATION_ID,
+ 'default_location_dest_id': dest_location_id,
'from_ui': False,
}).create({
'picking_id': bu_out.id,
'location_id': PARTNER_LOCATION_ID,
- 'original_location_id': BU_OUTPUT_LOCATION_ID,
'product_return_moves': srt_return_lines
})
+
srt_vals = srt_wizard.create_returns()
srt_picking = self.env['stock.picking'].browse(srt_vals['res_id'])
+ _force_locations(srt_picking, PARTNER_LOCATION_ID, dest_location_id)
+
srt_picking.write({
- 'location_id': PARTNER_LOCATION_ID,
- 'location_dest_id': BU_OUTPUT_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
@@ -597,12 +749,11 @@ class TukarGuling(models.Model):
### ======== ORT dari BU/PICK =========
ort_pickings = []
is_retur_from_bu_pick = record.operations.picking_type_id.id == 30
- picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id') or line.product_uom_qty
+ picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id')
for pick in picks_to_return:
ort_return_lines = []
if is_retur_from_bu_pick:
- # Ambil dari tukar.guling.line
for line in record.line_ids:
move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id)
if not move:
@@ -613,9 +764,9 @@ class TukarGuling(models.Model):
'quantity': line.product_uom_qty,
'move_id': move.id,
}))
- _logger.info(f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}")
+ _logger.info(
+ f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}")
else:
- # Ambil dari mapping koli
for mk in mapping_koli.filtered(lambda m: m.pick_id == pick):
move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id)
if not move:
@@ -626,7 +777,8 @@ class TukarGuling(models.Model):
'quantity': mk.qty_return,
'move_id': move.id,
}))
- _logger.info(f"📟 ORT (mapping koli) | {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}")
+ _logger.info(
+ f"📟 ORT (mapping koli) | {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}")
if ort_return_lines:
ort_wizard = self.env['stock.return.picking'].with_context({
@@ -637,27 +789,27 @@ class TukarGuling(models.Model):
}).create({
'picking_id': pick.id,
'location_id': BU_OUTPUT_LOCATION_ID,
- 'original_location_id': BU_STOCK_LOCATION_ID,
'product_return_moves': ort_return_lines
})
+
ort_vals = ort_wizard.create_returns()
ort_picking = self.env['stock.picking'].browse(ort_vals['res_id'])
+ _force_locations(ort_picking, BU_OUTPUT_LOCATION_ID, BU_STOCK_LOCATION_ID)
+
ort_picking.write({
- 'location_id': BU_OUTPUT_LOCATION_ID,
- 'location_dest_id': BU_STOCK_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
})
+
created_returns.append(ort_picking)
ort_pickings.append(ort_picking)
_logger.info(f"✅ ORT created: {ort_picking.name}")
record.message_post(
body=f"📦 <b>{ort_picking.name}</b> created by <b>{self.env.user.name}</b> (state: <b>{ort_picking.state}</b>)")
- ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ========
+ ### ======== BU/PICK & BU/OUT Baru dari SRT/ORT ========
if record.return_type == 'tukar_guling':
-
# BU/PICK Baru dari ORT
for ort_p in ort_pickings:
return_lines = []
@@ -683,19 +835,18 @@ class TukarGuling(models.Model):
}).create({
'picking_id': ort_p.id,
'location_id': BU_STOCK_LOCATION_ID,
- 'original_location_id': BU_OUTPUT_LOCATION_ID,
'product_return_moves': return_lines
})
bu_pick_vals = bu_pick_wizard.create_returns()
new_pick = self.env['stock.picking'].browse(bu_pick_vals['res_id'])
+ _force_locations(new_pick, BU_STOCK_LOCATION_ID, BU_OUTPUT_LOCATION_ID)
+
new_pick.write({
- 'location_id': BU_STOCK_LOCATION_ID,
- 'location_dest_id': BU_OUTPUT_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
})
- new_pick.action_assign() # Penting agar bisa trigger check koli
+ new_pick.action_assign()
new_pick.action_confirm()
created_returns.append(new_pick)
_logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}")
@@ -723,14 +874,13 @@ class TukarGuling(models.Model):
}).create({
'picking_id': srt_picking.id,
'location_id': BU_OUTPUT_LOCATION_ID,
- 'original_location_id': PARTNER_LOCATION_ID,
'product_return_moves': return_lines
})
bu_out_vals = bu_out_wizard.create_returns()
new_out = self.env['stock.picking'].browse(bu_out_vals['res_id'])
+ _force_locations(new_out, BU_OUTPUT_LOCATION_ID, PARTNER_LOCATION_ID)
+
new_out.write({
- 'location_id': BU_OUTPUT_LOCATION_ID,
- 'location_dest_id': PARTNER_LOCATION_ID,
'group_id': bu_out.group_id.id,
'tukar_guling_id': record.id,
'sale_order': record.origin
@@ -808,18 +958,17 @@ class StockPicking(models.Model):
message = _(
"📦 <b>%s</b> Validated by <b>%s</b> Status Changed <b>%s</b> at <b>%s</b>."
) % (
- picking.name,
- # picking.picking_type_id.name,
- picking.env.user.name,
- picking.state,
- fields.Datetime.now().strftime("%d/%m/%Y %H:%M")
- )
+ picking.name,
+ # picking.picking_type_id.name,
+ picking.env.user.name,
+ picking.state,
+ fields.Datetime.now().strftime("%d/%m/%Y %H:%M")
+ )
picking.tukar_guling_id.message_post(body=message)
return res
-
class TukarGulingMappingKoli(models.Model):
_name = 'tukar.guling.mapping.koli'
_description = 'Mapping Koli di Tukar Guling'
@@ -830,6 +979,7 @@ class TukarGulingMappingKoli(models.Model):
qty_done = fields.Float(string='Qty Done BU PICK')
qty_return = fields.Float(string='Qty diretur')
sequence = fields.Integer(string='Sequence', default=10)
+
@api.constrains('qty_return')
def _check_qty_return_editable(self):
for rec in self:
@@ -840,4 +990,4 @@ class TukarGulingMappingKoli(models.Model):
for rec in self:
if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']:
raise UserError("Tidak bisa menghapus Mapping Koli karena status Tukar Guling bukan Draft atau Cancel.")
- return super(TukarGulingMappingKoli, self).unlink() \ No newline at end of file
+ return super(TukarGulingMappingKoli, self).unlink()
diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py
index 14f2cc96..03d7668f 100644
--- a/indoteknik_custom/models/tukar_guling_po.py
+++ b/indoteknik_custom/models/tukar_guling_po.py
@@ -49,10 +49,53 @@ class TukarGulingPO(models.Model):
('approval_purchase', 'Approval Purchasing'),
('approval_finance', 'Approval Finance'),
('approval_logistic', 'Approval Logistic'),
+ ('approved', 'Waiting for Operations'),
('done', 'Done'),
('cancel', 'Cancel'),
], string='Status', default='draft', tracking=3)
+ val_bil_opt = fields.Selection([
+ ('tanpa_cancel', 'Tanpa Cancel Bill'),
+ ('cancel_bill', 'Cancel Bill'),
+ ], tracking=3, string='Bill Option')
+
+ is_has_bill = fields.Boolean('Has Bill?', compute='_compute_is_has_bill', readonly=True, default=False)
+
+ bill_id = fields.Many2many('account.move', string='Bill Ref', readonly=True)
+ origin_po = fields.Many2one('purchase.order', string='Origin PO', compute='_compute_origin_po')
+
+ @api.depends('origin', 'operations')
+ def _compute_origin_po(self):
+ for rec in self:
+ rec.origin_po = False
+ origin_str = rec.origin or rec.operations.origin
+ if origin_str:
+ so = self.env['purchase.order'].search([('name', '=', origin_str)], limit=1)
+ rec.origin_po = so.id if so else False
+
+ @api.depends('origin')
+ def _compute_is_has_bill(self):
+ for rec in self:
+ bills = self.env['account.move'].search([
+ ('invoice_origin', 'ilike', rec.origin),
+ ('move_type', '=', 'in_invoice'), # hanya vendor bill
+ ('state', 'not in', ['draft', 'cancel'])
+ ])
+ if bills:
+ rec.is_has_bill = True
+ rec.bill_id = bills
+ else:
+ rec.is_has_bill = False
+
+ def set_opt(self):
+ if not self.val_bil_opt and self.is_has_bill == True:
+ raise UserError("Kalau sudah ada bill Return Bill Option harus diisi!")
+ for rec in self:
+ if rec.val_bil_opt == 'cancel_bill' and self.is_has_bill == True:
+ raise UserError("Tidak bisa mengubah Return karena sudah ada bill dan belum di cancel.")
+ elif rec.val_bil_opt == 'tanpa_cancel' and self.is_has_bill == True:
+ continue
+
@api.model
def create(self, vals):
# Generate sequence number
@@ -73,19 +116,18 @@ class TukarGulingPO(models.Model):
return res
- @api.constrains('return_type', 'operations')
- def _check_bill_on_revisi_po(self):
- for record in self:
- if record.return_type == 'revisi_po' and record.origin:
- bills = self.env['account.move'].search([
- ('invoice_origin', 'ilike', record.origin),
- ('move_type', '=', 'in_invoice'), # hanya vendor bill
- ('state', 'not in', ['draft', 'cancel'])
- ])
- if bills:
- raise ValidationError(
- _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill.") % record.origin
- )
+ # def _check_bill_on_revisi_po(self):
+ # for record in self:
+ # if record.return_type == 'revisi_po' and record.origin:
+ # bills = self.env['account.move'].search([
+ # ('invoice_origin', 'ilike', record.origin),
+ # ('move_type', '=', 'in_invoice'), # hanya vendor bill
+ # ('state', 'not in', ['draft', 'cancel'])
+ # ])
+ # if bills:
+ # raise ValidationError(
+ # _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin
+ # )
@api.onchange('operations')
def _onchange_operations(self):
@@ -101,6 +143,7 @@ class TukarGulingPO(models.Model):
# Hanya update origin, jangan ubah lines
if self.operations.origin:
self.origin = self.operations.origin
+ self.origin_po = self.operations.group_id.id
return
if from_return_picking:
@@ -245,12 +288,12 @@ class TukarGulingPO(models.Model):
return True
- def _is_already_returned(self, picking):
- return self.env['stock.picking'].search_count([
- ('origin', '=', 'Return of %s' % picking.name),
- # ('returned_from_id', '=', picking.id),
- ('state', 'not in', ['cancel', 'draft']),
- ]) > 0
+ # def _is_already_returned(self, picking):
+ # return self.env['stock.picking'].search_count([
+ # ('origin', '=', 'Return of %s' % picking.name),
+ # # ('returned_from_id', '=', picking.id),
+ # ('state', 'not in', ['cancel', 'draft']),
+ # ]) > 0
def copy(self, default=None):
if default is None:
@@ -280,7 +323,7 @@ class TukarGulingPO(models.Model):
def write(self, vals):
if self.operations.picking_type_id.id not in [75, 28]:
raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!")
- self._check_bill_on_revisi_po()
+ # self._check_bill_on_revisi_po()
tipe = vals.get('return_type', self.return_type)
if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling':
@@ -311,7 +354,7 @@ class TukarGulingPO(models.Model):
def unlink(self):
for record in self:
- if record.state == 'done':
+ if record.state == 'done' or record.state == 'approved':
raise UserError("Tidak bisa hapus pengajuan jika sudah done, set ke draft terlebih dahulu")
ongoing_bu = self.po_picking_ids.filtered(lambda p: p.state != 'done')
for picking in ongoing_bu:
@@ -320,14 +363,23 @@ class TukarGulingPO(models.Model):
def action_view_picking(self):
self.ensure_one()
- action = self.env.ref('stock.action_picking_tree_all').read()[0]
- pickings = self.po_picking_ids
- if len(pickings) > 1:
- action['domain'] = [('id', 'in', pickings.ids)]
- elif pickings:
- action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
- action['res_id'] = pickings.id
- return action
+
+ # picking_origin = f"Return of {self.operations.name}"
+ returs = self.env['stock.picking'].search([
+ ('tukar_guling_po_id', '=', self.id),
+ ])
+
+ if not returs:
+ raise UserError("Doc Retrun Not Found")
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Delivery Pengajuan Retur PO',
+ 'res_model': 'stock.picking',
+ 'view_mode': 'tree,form',
+ 'domain': [('id', 'in', returs.ids)],
+ 'target': 'current',
+ }
def action_draft(self):
"""Reset to draft state"""
@@ -339,7 +391,7 @@ class TukarGulingPO(models.Model):
def action_submit(self):
self.ensure_one()
- self._check_bill_on_revisi_po()
+ # self._check_bill_on_revisi_po()
self._validate_product_lines()
self._check_not_allow_tukar_guling_on_bu_input()
@@ -365,8 +417,8 @@ class TukarGulingPO(models.Model):
if pick_id not in [75, 28]:
raise UserError("❌ Tidak bisa retur bukan BU/INPUT atau BU/PUT!")
- if self._is_already_returned(self.operations):
- raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
+ # if self._is_already_returned(self.operations):
+ # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.")
if self.state != 'draft':
raise UserError("Submit hanya bisa dilakukan dari Draft.")
@@ -375,7 +427,7 @@ class TukarGulingPO(models.Model):
def action_approve(self):
self.ensure_one()
self._validate_product_lines()
- self._check_bill_on_revisi_po()
+ # self._check_bill_on_revisi_po()
self._check_not_allow_tukar_guling_on_bu_input()
if not self.operations:
@@ -389,26 +441,66 @@ class TukarGulingPO(models.Model):
# Cek hak akses berdasarkan state
for rec in self:
if rec.state == 'approval_purchase':
- if not rec.env.user.has_group('indoteknik_custom.group_role_sales'):
- raise UserError("Hanya Sales Manager yang boleh approve tahap ini.")
+ if not rec.env.user.has_group('indoteknik_custom.group_role_purchasing'):
+ raise UserError("Hanya Purchasing yang boleh approve tahap ini.")
rec.state = 'approval_finance'
rec.date_purchase = now
elif rec.state == 'approval_finance':
if not rec.env.user.has_group('indoteknik_custom.group_role_fat'):
- raise UserError("Hanya Finance Manager yang boleh approve tahap ini.")
+ raise UserError("Hanya Finance yang boleh approve tahap ini.")
+ # rec._check_bill_on_revisi_po()
+ rec.set_opt()
rec.state = 'approval_logistic'
rec.date_finance = now
elif rec.state == 'approval_logistic':
if not rec.env.user.has_group('indoteknik_custom.group_role_logistic'):
- raise UserError("Hanya Logistic Manager yang boleh approve tahap ini.")
- rec.state = 'done'
+ raise UserError("Hanya Logistic yang boleh approve tahap ini.")
+ rec.state = 'approved'
rec._create_pickings()
rec.date_logistic = now
else:
raise UserError("Status ini tidak bisa di-approve.")
+ def update_doc_state(self):
+ # bu input rev po
+ if self.operations.picking_type_id.id == 28 and self.return_type == 'revisi_po':
+ prt = self.env['stock.picking'].search([
+ ('tukar_guling_po_id', '=', self.id),
+ ('state', '=', 'done'),
+ ('picking_type_id.id', '=', 76)
+ ])
+ if self.state == 'approved' and prt:
+ self.state = 'done'
+ # bu put rev po
+ elif self.operations.picking_type_id.id == 75 and self.return_type == 'revisi_po':
+ total_prt = self.env['stock.picking'].search_count([
+ ('tukar_guling_po_id', '=', self.id),
+ ('picking_type_id.id', '=', 76)
+ ])
+ prt = self.env['stock.picking'].search_count([
+ ('tukar_guling_po_id', '=', self.id),
+ ('state', '=', 'done'),
+ ('picking_type_id.id', '=', 76)
+ ])
+ if self.state == 'approved' and total_prt > 0 and prt == total_prt:
+ self.state = 'done'
+ # bu put tukar guling
+ elif self.operations.picking_type_id.id == 75 and self.return_type == 'tukar_guling':
+ total_put = self.env['stock.picking'].search_count([
+ ('tukar_guling_po_id', '=', self.id),
+ ('picking_type_id.id', '=', 75)
+ ])
+ put = self.env['stock.picking'].search_count([
+ ('tukar_guling_po_id', '=', self.id),
+ ('state', '=', 'done'),
+ ('picking_type_id.id', '=', 75)
+ ])
+ if self.state == 'aproved' and total_put > 0 and put == total_put:
+ self.state = 'done'
+
+
def action_cancel(self):
self.ensure_one()
# if self.state == 'done':
@@ -416,7 +508,7 @@ class TukarGulingPO(models.Model):
user = self.env.user
if not (
- user.has_group('indoteknik_custom.group_role_sales') or
+ user.has_group('indoteknik_custom.group_role_purchasing') or
user.has_group('indoteknik_custom.group_role_fat') or
user.has_group('indoteknik_custom.group_role_logistic')
):
diff --git a/indoteknik_custom/models/update_date_planned_po_wizard.py b/indoteknik_custom/models/update_date_planned_po_wizard.py
new file mode 100644
index 00000000..a0d241c8
--- /dev/null
+++ b/indoteknik_custom/models/update_date_planned_po_wizard.py
@@ -0,0 +1,14 @@
+from odoo import models, fields, api
+
+class PurchaseOrderUpdateDateWizard(models.TransientModel):
+ _name = 'purchase.order.update.date.wizard'
+ _description = 'Wizard to Update Receipt Date on Purchase Order Lines'
+
+ date_planned = fields.Datetime(string="New Receipt Date", required=True)
+
+ def action_update_date(self):
+ active_ids = self.env.context.get('active_ids', [])
+ orders = self.env['purchase.order'].browse(active_ids)
+ for order in orders:
+ order.write({'date_planned': self.date_planned})
+ return {'type': 'ir.actions.act_window_close'}