summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-07-23 15:10:51 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-07-23 15:10:51 +0700
commit6f5880c2c78b53177c289175b0e1511196371d58 (patch)
tree4b3d5d03e4589d9bc19a94984f7b4f5f0bcfbfda
parentdeb60713ed39979b34083ee094de79fa3afac3b8 (diff)
parent87f38f9fcb68f04a2cc8157744622c2d0ebf1eab (diff)
<hafid> Fix Conflict
-rwxr-xr-xindoteknik_custom/__manifest__.py1
-rw-r--r--indoteknik_custom/models/account_move.py101
-rw-r--r--indoteknik_custom/models/approval_payment_term.py109
-rw-r--r--indoteknik_custom/models/commision.py5
-rw-r--r--indoteknik_custom/models/dunning_run.py20
-rw-r--r--indoteknik_custom/models/mrp_production.py2
-rwxr-xr-xindoteknik_custom/models/purchase_order.py9
-rw-r--r--indoteknik_custom/models/purchasing_job.py57
-rw-r--r--indoteknik_custom/models/res_partner.py15
-rwxr-xr-xindoteknik_custom/models/sale_order.py4
-rw-r--r--indoteknik_custom/models/sale_order_line.py1
-rw-r--r--indoteknik_custom/models/stock_picking.py147
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv3
-rw-r--r--indoteknik_custom/views/approval_payment_term.xml19
-rw-r--r--indoteknik_custom/views/customer_commision.xml1
-rw-r--r--indoteknik_custom/views/ir_sequence.xml4
-rw-r--r--indoteknik_custom/views/mail_template_invoice_reminder.xml51
-rw-r--r--indoteknik_custom/views/purchasing_job.xml12
18 files changed, 460 insertions, 101 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 13f0399b..93c437af 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -97,6 +97,7 @@
'views/mail_template_po.xml',
'views/mail_template_efaktur.xml',
'views/mail_template_invoice_po.xml',
+ 'views/mail_template_invoice_reminder.xml',
'views/price_group.xml',
'views/mrp_production.xml',
'views/apache_solr.xml',
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 7bb71e03..db52f398 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -9,12 +9,15 @@ import PyPDF2
import os
import re
from terbilang import Terbilang
+from collections import defaultdict
+from odoo.tools.misc import formatLang
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
+ _description = 'Account Move'
invoice_day_to_due = fields.Integer(string="Day to Due", compute="_compute_invoice_day_to_due")
bill_day_to_due = fields.Date(string="Day to Due", compute="_compute_bill_day_to_due")
date_send_fp = fields.Datetime(string="Tanggal Kirim Faktur Pajak")
@@ -106,6 +109,104 @@ 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")
+
+
@api.onchange('invoice_date')
def _onchange_invoice_date(self):
if self.invoice_date:
diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py
index da71b7e4..6c857b45 100644
--- a/indoteknik_custom/models/approval_payment_term.py
+++ b/indoteknik_custom/models/approval_payment_term.py
@@ -31,8 +31,103 @@ class ApprovalPaymentTerm(models.Model):
approve_leader = fields.Boolean('Approve Pimpinan', tracking=True, copy=False)
reason = fields.Text('Reason', tracking=True)
approve_date = fields.Datetime('Approve Date')
- state = fields.Selection([('waiting_approval', 'Waiting Approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval', tracking=True)
+ state = fields.Selection([
+ ('waiting_approval_sales_manager', 'Waiting Approval Sales Manager'),
+ ('waiting_approval_finance', 'Waiting Approval Finance'),
+ ('waiting_approval_leader', 'Waiting Approval Leader'),
+ ('approved', 'Approved'),
+ ('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)
+ sale_order_ids = fields.Many2many(
+ 'sale.order',
+ string='Sale Orders',
+ copy=False,
+ tracking=True
+ )
+
+ total = fields.Char(
+ string='Sale Order Totals',
+ compute='_compute_total'
+ )
+
+ grand_total = fields.Float(string='Grand Total', compute="_compute_grand_total")
+
+ change_log_688 = fields.Text(string="Change Log", readonly=True, copy=False)
+
+ def write(self, vals):
+ # Ambil nilai lama sebelum perubahan
+ old_values_dict = {
+ rec.id: rec.read(vals.keys())[0]
+ for rec in self
+ }
+
+ res = super().write(vals)
+
+ self._track_changes_for_user_688(vals, old_values_dict)
+ return res
+
+ def _track_changes_for_user_688(self, vals, old_values_dict):
+ if self.env.user.id != 688:
+ return
+
+ for rec in self:
+ changes = []
+ old_values = old_values_dict.get(rec.id, {})
+
+ for field_name, new_value in vals.items():
+ if field_name not in rec._fields or field_name == 'change_log_688':
+ continue
+
+ field = rec._fields[field_name]
+ old_value = old_values.get(field_name)
+
+ field_label = field.string # Ambil label user-friendly
+
+ # Relational field
+ if field.type == 'many2one':
+ old_id = old_value[0] if old_value else False
+ is_different = old_id != new_value
+ if is_different:
+ old_display = old_value[1] if old_value else 'False'
+ new_display = rec.env[field.comodel_name].browse(new_value).display_name if new_value else 'False'
+ changes.append(f"[{field_label}] dari '{old_display}' ke '{new_display}'")
+
+ else:
+ # Float khusus
+ if field.type == 'float':
+ is_different = not self._float_equal(old_value, new_value)
+ else:
+ is_different = old_value != new_value
+
+ if is_different:
+ changes.append(f"[{field_label}] dari '{old_value}' ke '{new_value}'")
+
+ if changes:
+ timestamp = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ rec.change_log_688 = f"{timestamp} - Perubahan oleh Widya:\n" + "\n".join(changes)
+
+
+ @staticmethod
+ def _float_equal(val1, val2, eps=1e-6):
+ try:
+ return abs(float(val1 or 0.0) - float(val2 or 0.0)) < eps
+ except Exception:
+ return False
+
+ def _compute_grand_total(self):
+ for rec in self:
+ grand_total = sum(order.amount_total for order in rec.sale_order_ids)
+ rec.grand_total = grand_total
+
+ def _compute_total(self):
+ for rec in self:
+ totals_list = []
+ for order in rec.sale_order_ids:
+ formatted_total = "{:,.2f}".format(order.amount_total)
+ totals_list.append(f"{order.name}: {formatted_total}")
+
+ rec.total = "\n".join(totals_list) if totals_list else "No Sale Orders"
@api.constrains('partner_id')
@@ -48,20 +143,22 @@ class ApprovalPaymentTerm(models.Model):
user = self.env.user
is_it = user.has_group('indoteknik_custom.group_role_it')
- if user.id == 19 or is_it:
+ if (not user.id ==7 and user.id == 19 and not self.approve_sales_manager) or (is_it and not self.approve_sales_manager):
self.approve_sales_manager = True
+ self.state = 'waiting_approval_finance'
return
- if user.id == 688 or is_it:
+ if (not user.id ==7 and user.id == 688 and not self.approve_finance) or (is_it and not self.approve_finance):
self.approve_finance = True
+ self.state = 'waiting_approval_leader'
return
- if (user.id == 7 and self.approve_finance) or is_it:
+ if (user.id == 7 and self.approve_finance) or (is_it and not self.approve_leader):
self.approve_leader = True
- if not self.approve_finance or not is_it:
+ if not self.approve_finance and not is_it:
raise UserError('Harus Approval Finance!!')
- if not self.approve_leader or not is_it:
+ if not self.approve_leader and not is_it:
raise UserError('Harus Approval Pimpinan!!')
if user.id == 7:
diff --git a/indoteknik_custom/models/commision.py b/indoteknik_custom/models/commision.py
index 97184cdb..26b5df37 100644
--- a/indoteknik_custom/models/commision.py
+++ b/indoteknik_custom/models/commision.py
@@ -193,6 +193,7 @@ class CustomerCommision(models.Model):
commision_amt_text = fields.Char(string='Amount Text', compute='compute_delivery_amt_text')
total_cashback_text = fields.Char(string='Cashback Text', compute='compute_total_cashback_text')
total_dpp = fields.Float(string='Total DPP', compute='_compute_total_dpp')
+ biaya_lain_lain = fields.Float(string='Biaya Lain-lain')
commision_type = fields.Selection([
('fee', 'Fee'),
('cashback', 'Cashback'),
@@ -363,13 +364,13 @@ class CustomerCommision(models.Model):
else:
self.cashback = 0
self.total_commision = 0
-
+
def _compute_total_dpp(self):
for data in self:
total_dpp = 0
for line in data.commision_lines:
total_dpp = total_dpp + line.dpp
- data.total_dpp = total_dpp
+ data.total_dpp = total_dpp - data.biaya_lain_lain
@api.model
def create(self, vals):
diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py
index bb53fc0c..5a6aebac 100644
--- a/indoteknik_custom/models/dunning_run.py
+++ b/indoteknik_custom/models/dunning_run.py
@@ -92,10 +92,23 @@ class DunningRun(models.Model):
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('partner_id', '=', partner.id),
- # ('amount_residual_signed', '>', 0),
('date_kirim_tukar_faktur', '=', False),
]
- invoices = self.env['account.move'].search(query, order='invoice_date')
+ invoices = self.env['account.move'].search(query)
+
+ # sort full berdasarkan tahun, bulan, nomor
+ def invoice_key(x):
+ try:
+ parts = x.name.split('/')
+ tahun = int(parts[1])
+ bulan = int(parts[2])
+ nomor = int(parts[3])
+ return (tahun, bulan, nomor)
+ except Exception:
+ return (0, 0, 0)
+
+ invoices = sorted(invoices, key=invoice_key)
+
count = 0
for invoice in invoices:
self.env['dunning.run.line'].create([{
@@ -123,8 +136,9 @@ class DunningRunLine(models.Model):
_name = 'dunning.run.line'
_description = 'Dunning Run Line'
# _order = 'dunning_id, id'
- _order = 'invoice_id desc, id'
+ _order = 'invoice_number asc, id'
+ invoice_number = fields.Char('Invoice Number', related='invoice_id.name')
dunning_id = fields.Many2one('dunning.run', string='Dunning Ref', required=True, ondelete='cascade', index=True, copy=False)
partner_id = fields.Many2one('res.partner', string='Customer')
invoice_id = fields.Many2one('account.move', string='Invoice')
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index 85b8405f..7977bdf7 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -156,6 +156,8 @@ class MrpProduction(models.Model):
'order_id': new_po.id
}])
+ new_po.button_confirm()
+
self.is_po = True
return po_ids
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 4dc26d74..5b9e1acb 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -672,6 +672,13 @@ 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:
@@ -963,7 +970,7 @@ class PurchaseOrder(models.Model):
# )
if not self.from_apo:
- if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader:
+ 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:
raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager")
send_email = False
diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py
index ea2f46cb..db733b5a 100644
--- a/indoteknik_custom/models/purchasing_job.py
+++ b/indoteknik_custom/models/purchasing_job.py
@@ -26,6 +26,46 @@ class PurchasingJob(models.Model):
purchase_representative_id = fields.Many2one('res.users', string="Purchase Representative", readonly=True)
note = fields.Char(string="Note Detail")
date_po = fields.Datetime(string='Date PO', copy=False)
+ so_number = fields.Text(string='SO Number', copy=False)
+ check_pj = fields.Boolean(compute='_get_check_pj', string='Linked')
+
+ def action_open_job_detail(self):
+ self.ensure_one()
+ Seen = self.env['purchasing.job.seen']
+ seen = Seen.search([
+ ('user_id', '=', self.env.uid),
+ ('product_id', '=', self.product_id.id)
+ ], limit=1)
+
+ if seen:
+ seen.so_snapshot = self.so_number
+ seen.seen_date = fields.Datetime.now()
+ else:
+ Seen.create({
+ 'user_id': self.env.uid,
+ 'product_id': self.product_id.id,
+ 'so_snapshot': self.so_number,
+ })
+
+ return {
+ 'name': 'Purchasing Job Detail',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'v.purchasing.job',
+ 'res_id': self.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
+
+
+ @api.depends('so_number')
+ def _get_check_pj(self):
+ for rec in self:
+ seen = self.env['purchasing.job.seen'].search([
+ ('user_id', '=', self.env.uid),
+ ('product_id', '=', rec.product_id.id)
+ ], limit=1)
+ rec.check_pj = bool(seen and seen.so_snapshot == rec.so_number)
+
def unlink(self):
# Example: Delete related records from the underlying model
@@ -66,6 +106,7 @@ class PurchasingJob(models.Model):
max(pjs.status_apo::text) AS status_apo,
max(pjs.note::text) AS note,
max(pjs.date_po::text) AS date_po,
+ pmp.so_number,
CASE
WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco', 'RYU (Sparepart)') THEN 27
WHEN sub.vendor_id = 9688 THEN 397
@@ -83,7 +124,7 @@ class PurchasingJob(models.Model):
group by vso.product_id
) sub ON sub.product_id = pmp.product_id
WHERE pmp.action = 'kurang'::text AND sub.vendor_id IS NOT NULL
- GROUP BY pmp.product_id, pmp.brand, pmp.item_code, pmp.product, pmp.action, sub.vendor_id;
+ GROUP BY pmp.product_id, pmp.brand, pmp.item_code, pmp.product, pmp.action, sub.vendor_id, pmp.so_number;
""" % self._table)
def open_form_multi_generate_request_po(self):
@@ -197,3 +238,17 @@ class OutstandingSales(models.Model):
and sp.name like '%OUT%'
)
""")
+
+class PurchasingJobSeen(models.Model):
+ _name = 'purchasing.job.seen'
+ _description = 'User Seen SO Snapshot'
+ _rec_name = 'product_id'
+
+ user_id = fields.Many2one('res.users', required=True, ondelete='cascade')
+ product_id = fields.Many2one('product.product', required=True, ondelete='cascade')
+ so_snapshot = fields.Text("Last Seen SO")
+ seen_date = fields.Datetime(default=fields.Datetime.now)
+
+ _sql_constraints = [
+ ('user_product_unique', 'unique(user_id, product_id)', 'User already tracked this product.')
+ ]
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index 52947128..236df16f 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -165,26 +165,13 @@ class ResPartner(models.Model):
"this feature", tracking=3)
telegram_id = fields.Char(string="Telegram")
avg_aging= fields.Float(string='Average Aging')
- payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', compute="_compute_payment_difficulty", inverse = "_inverse_payment_difficulty", tracking=3)
+ payment_difficulty = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3)
payment_history_url = fields.Text(string='Payment History URL')
# no compute
# payment_diff = fields.Selection([('bermasalah', 'Bermasalah'),('sulit', 'Sulit'),('agak_sulit', 'Agak Sulit'),('normal', 'Normal')], string='Payment Difficulty', tracking=3)
# tidak terpakai
- @api.depends('parent_id.payment_difficulty')
- def _compute_payment_difficulty(self):
- for partner in self:
- if partner.parent_id:
- partner.payment_difficulty = partner.parent_id.payment_difficulty
-
- # tidak terpakai
- def _inverse_payment_difficulty(self):
- for partner in self:
- if not partner.parent_id:
- partner.child_ids.write({
- 'payment_difficulty': partner.payment_difficulty
- })
@api.model
def _default_payment_term(self):
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 8d40bfb5..995cafba 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -3056,6 +3056,10 @@ class SaleOrder(models.Model):
if picking.state == 'assigned':
picking.carrier_id = vals['carrier_id']
+ for picking in order.picking_ids:
+ if picking.state not in ['done', 'cancel', 'assigned']:
+ picking.write({'carrier_id': vals['carrier_id']})
+
try:
helper_ids = self._get_helper_ids()
if str(self.env.user.id) in helper_ids:
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 291940ed..2a0160e8 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
+
item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header")
item_before_margin = fields.Float('Before Margin', compute='compute_item_before_margin',
help="Total Margin in Sales Order Header")
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 69718c7e..0efffd2f 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -20,8 +20,10 @@ _logger = logging.getLogger(__name__)
_biteship_url = "https://api.biteship.com/v1"
biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
+
+
# biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
-
+
class StockPicking(models.Model):
_inherit = 'stock.picking'
@@ -119,7 +121,7 @@ class StockPicking(models.Model):
waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill')
purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id',
string="Purchase Representative")
- carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method')
+ carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3)
shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status")
date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
status_printed = fields.Selection([
@@ -278,16 +280,22 @@ class StockPicking(models.Model):
biteship_driver_plate_number = fields.Char('Biteship Driver Plate Number')
biteship_courier_link = fields.Char('Biteship Courier Link')
biteship_shipping_status = fields.Char('Biteship Shipping Status', help="Status pengiriman dari Biteship")
- biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', help="Harga pengiriman dari Biteship")
+ biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id',
+ help="Harga pengiriman dari Biteship")
currency_id = fields.Many2one('res.currency', related='sale_id.currency_id', string='Currency', readonly=True)
final_seq = fields.Float(string='Remaining Time')
- shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', help="Shipping Method yang digunakan di SO")
- shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', related='sale_id.shipping_option_id' , help="Shipping Option yang digunakan di SO")
+ shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id',
+ help="Shipping Method yang digunakan di SO", tracking=3)
+ shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option',
+ related='sale_id.shipping_option_id',
+ help="Shipping Option yang digunakan di SO", tracking=3)
select_shipping_option_so = fields.Selection([
('biteship', 'Biteship'),
('custom', 'Custom'),
- ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO")
- state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status')
+ ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO",
+ tracking=3)
+ state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')],
+ string='Packing Status')
approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date')
last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False)
update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD')
@@ -298,7 +306,7 @@ class StockPicking(models.Model):
if not self.name or not self.origin:
return False
return f"{self.name}"
-
+
def _download_pod_photo(self, url):
"""Mengunduh foto POD dari URL"""
try:
@@ -307,48 +315,53 @@ class StockPicking(models.Model):
return base64.b64encode(response.content)
except Exception as e:
raise UserError(f"Gagal mengunduh foto POD: {str(e)}")
-
+
def _parse_datetime(self, dt_str):
"""Parse datetime string dari format KGX"""
try:
from datetime import datetime
- # Hilangkan timezone jika ada masalah parsing
+
+ if not dt_str:
+ return False
+
if '+' in dt_str:
dt_str = dt_str.split('+')[0]
+
return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S')
except ValueError:
return False
-
+
+
def action_get_kgx_pod(self, shipment=False):
self.ensure_one()
-
+
awb_number = shipment or self._get_kgx_awb_number()
if not awb_number:
raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin")
-
+
url = "https://kgx.co.id/get_detail_awb"
headers = {'Content-Type': 'application/json'}
- payload = {"params" : {'awb_number': awb_number}}
-
+ payload = {"params": {'awb_number': awb_number}}
+
try:
response = requests.post(url, headers=headers, data=json.dumps(payload))
response.raise_for_status()
data = response.json()
-
+
if data.get('result', {}).get('data', []):
pod_data = data['result']['data'][0].get('connote_pod', {})
photo_url = pod_data.get('photo')
-
+
self.kgx_pod_photo_url = photo_url
self.kgx_pod_signature = pod_data.get('signature')
self.kgx_pod_receiver = pod_data.get('receiver')
self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive'))
self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive'))
-
+
return data
else:
raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}")
-
+
except requests.exceptions.RequestException as e:
raise UserError(f"Gagal mengambil data POD: {str(e)}")
@@ -695,8 +708,9 @@ class StockPicking(models.Model):
raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}")
if self.sale_id.select_shipping_option == 'custom':
- raise UserError("Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.")
-
+ raise UserError(
+ "Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.")
+
def is_courier_need_coordinates(service_code):
return service_code in [
"instant", "same_day", "instant_car",
@@ -793,11 +807,11 @@ class StockPicking(models.Model):
self.message_post(
body=f"Biteship berhasil dilakukan.<br/>"
- f"Kurir: {self.carrier_id.name}<br/>"
- f"Tracking ID: {self.biteship_tracking_id or '-'}<br/>"
- f"Resi: {waybill_id or '-'}<br/>"
- f"Reference: {self.name}<br/>"
- f"SO: {self.sale_id.name}",
+ f"Kurir: {self.carrier_id.name}<br/>"
+ f"Tracking ID: {self.biteship_tracking_id or '-'}<br/>"
+ f"Resi: {waybill_id or '-'}<br/>"
+ f"Reference: {self.name}<br/>"
+ f"SO: {self.sale_id.name}",
message_type="comment"
)
@@ -939,6 +953,9 @@ class StockPicking(models.Model):
pending_section = None
# Invoice values.
invoice_vals = order._prepare_invoice()
+ invoice_date = self.date_done
+ invoice_vals['date'] = invoice_date
+ invoice_vals['invoice_date'] = invoice_date
# Invoice line values (keep only necessary sections).
for line in self.move_ids_without_package:
po_line = self.env['purchase.order.line'].search(
@@ -1304,7 +1321,6 @@ class StockPicking(models.Model):
)
)
-
self.validation_minus_onhand_quantity()
self.responsible = self.env.user.id
# self.send_koli_to_so()
@@ -1355,7 +1371,7 @@ class StockPicking(models.Model):
if 'BU/PUT' in self.name:
self.automatic_reserve_product()
return res
-
+
def automatic_reserve_product(self):
if self.state == 'done':
po = self.env['purchase.order'].search([
@@ -1373,11 +1389,12 @@ class StockPicking(models.Model):
continue
invoice = self.env['account.move'].search(
- [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), ('move_type', '=', 'out_invoice')], limit=1)
+ [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']),
+ ('move_type', '=', 'out_invoice')], limit=1)
if not invoice:
continue
-
+
if not picking.so_lama and invoice and (not picking.date_doc_kirim or not invoice.invoice_date):
raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!")
@@ -1563,25 +1580,25 @@ class StockPicking(models.Model):
new_picking.state_packing = 'packing_done'
self._use_faktur(vals)
self.sync_sale_line(vals)
- for picking in self:
- # Periksa apakah kondisi terpenuhi saat data diubah
- if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and
- vals.get('location_dest_id', picking.location_dest_id.id) == 58):
- if 'name' in vals or picking.name.startswith('BU/IN/'):
- name_to_modify = vals.get('name', picking.name)
- if name_to_modify.startswith('BU/IN/'):
- vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1)
-
- if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and
- vals.get('location_id', picking.location_id.id) == 58):
- name_to_modify = vals.get('name', picking.name)
- if name_to_modify.startswith('BU/INT'):
- new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1)
- # Periksa apakah nama sudah ada
- if self.env['stock.picking'].search_count(
- [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0:
- new_name = f"{new_name}-DUP"
- vals['name'] = new_name
+ # for picking in self:
+ # # Periksa apakah kondisi terpenuhi saat data diubah
+ # if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and
+ # vals.get('location_dest_id', picking.location_dest_id.id) == 58):
+ # if 'name' in vals or picking.name.startswith('BU/IN/'):
+ # name_to_modify = vals.get('name', picking.name)
+ # if name_to_modify.startswith('BU/IN/'):
+ # vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1)
+
+ # if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and
+ # vals.get('location_id', picking.location_id.id) == 58):
+ # name_to_modify = vals.get('name', picking.name)
+ # if name_to_modify.startswith('BU/INT'):
+ # new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1)
+ # # Periksa apakah nama sudah ada
+ # if self.env['stock.picking'].search_count(
+ # [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0:
+ # new_name = f"{new_name}-DUP"
+ # vals['name'] = new_name
return super(StockPicking, self).write(vals)
def _use_faktur(self, vals):
@@ -1661,7 +1678,7 @@ class StockPicking(models.Model):
self.ensure_one()
order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1)
-
+
sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', order.name)], limit=1)
product_shipped = []
@@ -1676,11 +1693,12 @@ class StockPicking(models.Model):
'delivery_order': {
'name': self.name,
'carrier': self.carrier_id.name or '-',
- 'service' : order.delivery_service_type or '-',
+ 'service': order.delivery_service_type or '-',
'receiver_name': '',
'receiver_city': ''
},
- 'delivered_date': self.driver_departure_date.strftime('%d %b %Y') if self.driver_departure_date != False else '-',
+ 'delivered_date': self.driver_departure_date.strftime(
+ '%d %b %Y') if self.driver_departure_date != False else '-',
'delivered': False,
'status': self.shipping_status,
'waybill_number': self.delivery_tracking_no or '-',
@@ -1703,7 +1721,7 @@ class StockPicking(models.Model):
elif sale_order_delay.status == 'early':
day_start = day_start - sale_order_delay.days_delayed
day_end = day_end - sale_order_delay.days_delayed
-
+
eta_start = order.date_order + timedelta(days=day_start)
eta_end = order.date_order + timedelta(days=day_end)
formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}"
@@ -1732,7 +1750,7 @@ class StockPicking(models.Model):
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
-
+
manifests = []
try:
@@ -1769,9 +1787,9 @@ class StockPicking(models.Model):
"manifests": [],
"delivered": False
}
- except Exception as e :
+ except Exception as e:
_logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}")
- return { 'error': str(e) }
+ return {'error': str(e)}
# ACTION GET TRACKING MANUAL BITESHIP
# def action_sync_biteship_tracking(self):
@@ -1855,7 +1873,6 @@ class StockPicking(models.Model):
return description_map.get(status, f"Status '{status}' diterima dari Biteship")
-
def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None):
self.ensure_one()
updated_fields = {}
@@ -1916,7 +1933,7 @@ class StockPicking(models.Model):
# Hindari log duplikat
if not self._has_existing_log(log_line):
- biteship_user = self.env['res.users'].sudo().browse(15710) # ID live
+ biteship_user = self.env['res.users'].sudo().browse(15710) # ID live
# biteship_user = self.env['res.users'].sudo().browse(15710) # ID user (cek di db)
self.sudo().message_post(
body=log_line,
@@ -1928,7 +1945,6 @@ class StockPicking(models.Model):
self.write(updated_fields)
_logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}")
-
def _has_existing_log(self, log_line):
self.ensure_one()
self.env.cr.execute("""
@@ -1995,8 +2011,7 @@ class StockPicking(models.Model):
days_end = self.sale_id.estimated_arrival_days or (self.sale_id.estimated_arrival_days + 3)
start_date = self.sale_id.create_date + datetime.timedelta(days=days_start)
end_date = self.sale_id.create_date + datetime.timedelta(days=days_end)
-
-
+
add_day_start = 0
add_day_end = 0
sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', self.sale_id.name)], limit=1)
@@ -2005,11 +2020,11 @@ class StockPicking(models.Model):
add_day_start = sale_order_delay.days_delayed
add_day_end = sale_order_delay.days_delayed
elif sale_order_delay.status == 'early':
- add_day_start = -abs(sale_order_delay.days_delayed)
- add_day_end = -abs(sale_order_delay.days_delayed)
-
- fastest_eta = start_date +datetime.timedelta(days=add_day_start + add_day_start)
-
+ add_day_start = -abs(sale_order_delay.days_delayed)
+ add_day_end = -abs(sale_order_delay.days_delayed)
+
+ fastest_eta = start_date + datetime.timedelta(days=add_day_start + add_day_start)
+
longest_eta = end_date + datetime.timedelta(days=add_day_end)
format_time = '%d %b %Y'
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index f3764177..a35f7d3a 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -184,4 +184,5 @@ access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1
access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1
access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,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 \ No newline at end of file
+access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1
+access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1
diff --git a/indoteknik_custom/views/approval_payment_term.xml b/indoteknik_custom/views/approval_payment_term.xml
index 44bd99f8..cc9db914 100644
--- a/indoteknik_custom/views/approval_payment_term.xml
+++ b/indoteknik_custom/views/approval_payment_term.xml
@@ -9,11 +9,19 @@
<field name="partner_id"/>
<field name="parent_id"/>
<field name="property_payment_term_id"/>
+ <field name="create_date" optional="hide"/>
<field name="approve_date" optional="hide"/>
<field name="approve_sales_manager" optional="hide"/>
<field name="approve_finance" optional="hide"/>
<field name="approve_leader" optional="hide"/>
<field name="create_uid" optional="hide"/>
+ <field name="sale_order_ids" optional="hide" widget="many2many_tags"/>
+ <field name="total" optional="hide"/>
+ <field name="grand_total" optional="hide"/>
+ <field name="state" widget="badge" decoration-danger="state == 'rejected'"
+ decoration-success="state == 'approved'"
+ decoration-info="state in ['waiting_approval_sales_manager', 'waiting_approval_finance', 'waiting_approval_leader']"/>
+ <field name="change_log_688" optional="hide"/>
</tree>
</field>
</record>
@@ -44,10 +52,10 @@
<field name="number" readonly="1"/>
<field name="partner_id"/>
<field name="parent_id" readonly="1"/>
- <field name="blocking_stage" attrs="{'readonly': [('number', '=', False)]}"/>
- <field name="warning_stage" attrs="{'readonly': [('number', '=', False)]}"/>
- <field name="property_payment_term_id" attrs="{'readonly': [('number', '=', False)]}"/>
- <field name="active_limit" attrs="{'readonly': [('number', '=', False)]}"/>
+ <field name="blocking_stage" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/>
+ <field name="warning_stage" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/>
+ <field name="property_payment_term_id" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/>
+ <field name="active_limit" attrs="{'readonly': ['|', ('number', '=', False), ('state', 'in', ['approved','rejected'])]}"/>
</group>
<group>
<field name="reason"/>
@@ -56,6 +64,9 @@
<field name="approve_sales_manager" readonly="1"/>
<field name="approve_finance" readonly="1"/>
<field name="approve_leader" readonly="1"/>
+ <field name="sale_order_ids" widget="many2many_tags"/>
+ <field name="total"/>
+ <field name="grand_total"/>
</group>
</group>
</sheet>
diff --git a/indoteknik_custom/views/customer_commision.xml b/indoteknik_custom/views/customer_commision.xml
index 1c17bf63..514e6284 100644
--- a/indoteknik_custom/views/customer_commision.xml
+++ b/indoteknik_custom/views/customer_commision.xml
@@ -103,6 +103,7 @@
<field name="notification" readonly="1"/>
<!-- <field name="status" readonly="1"/>-->
<field name="payment_status" readonly="1"/>
+ <field name="biaya_lain_lain"/>
<field name="total_dpp"/>
</group>
</group>
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index a4c98e3f..06d7ef13 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -154,7 +154,7 @@
<record id="sequence_approval_payment_term" model="ir.sequence">
<field name="name">Approval Payment Term</field>
<field name="code">approval.payment.term</field>
- <field name="prefix">APP/%(year)s/</field>
+ <field name="prefix">APT/%(year)s/</field>
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
@@ -164,7 +164,7 @@
<record id="sequence_commision_fee" model="ir.sequence">
<field name="name">Customer Commision Fee</field>
<field name="code">customer.commision.fee</field>
- <field name="prefix">FE/%(year)s/</field>
+ <field name="prefix">CC/%(year)s/</field>
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
diff --git a/indoteknik_custom/views/mail_template_invoice_reminder.xml b/indoteknik_custom/views/mail_template_invoice_reminder.xml
new file mode 100644
index 00000000..21055eb0
--- /dev/null
+++ b/indoteknik_custom/views/mail_template_invoice_reminder.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="0">
+ <record id="mail_template_invoice_due_reminder" model="mail.template">
+ <field name="name">Invoice Reminder: Due Date Notification</field>
+ <field name="model_id" ref="account.model_account_move"/>
+ <field name="subject">Reminder Invoice Due - ${object.name}</field>
+ <field name="email_from">finance@indoteknik.co.id</field>
+ <field name="email_to">andrifebriyadiputra@gmail.com</field>
+ <field name="body_html" type="html">
+ <div>
+ <p><b>Dear ${object.name},</b></p>
+
+ <p>Berikut adalah daftar invoice Anda yang mendekati atau telah jatuh tempo:</p>
+
+ <table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; width: 100%; font-size: 12px">
+ <thead>
+ <tr style="background-color: #f2f2f2;" align="left">
+ <th>Invoice Number</th>
+ <th>Tanggal Invoice</th>
+ <th>Jatuh Tempo</th>
+ <th>Sisa Hari</th>
+ <th>Total</th>
+ <th>Referensi</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+
+ <p>Mohon bantuan dan kerjasamanya agar tetap bisa bekerjasama dengan baik</p>
+ <p>Terima Kasih.</p>
+ <br/>
+ <br/>
+ <p><b>Best Regards,
+ <br/>
+ <br/>
+ Widya R.<br/>
+ Dept. Finance<br/>
+ PT. INDOTEKNIK DOTCOM GEMILANG<br/>
+ <img src="https://erp.indoteknik.com/api/image/ir.attachment/datas/2135765" alt="Indoteknik" style="max-width: 18%; height: auto;"></img><br/>
+ <a href="https://wa.me/6285716970374" target="_blank">+62-857-1697-0374</a> |
+ <a href="mailto:finance@indoteknik.co.id">finance@indoteknik.co.id</a>
+ </b></p>
+
+ </div>
+ </field>
+ <field name="auto_delete" eval="True"/>
+ </record>
+ </data>
+</odoo>
diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml
index bb1c7643..e3866d84 100644
--- a/indoteknik_custom/views/purchasing_job.xml
+++ b/indoteknik_custom/views/purchasing_job.xml
@@ -4,7 +4,7 @@
<field name="name">v.purchasing.job.tree</field>
<field name="model">v.purchasing.job</field>
<field name="arch" type="xml">
- <tree create="false" multi_edit="1">
+ <tree decoration-info="(check_pj == False)" create="false" multi_edit="1">
<field name="product_id"/>
<field name="vendor_id"/>
<field name="purchase_representative_id"/>
@@ -18,6 +18,15 @@
<field name="action"/>
<field name="note"/>
<field name="date_po"/>
+ <field name="so_number"/>
+ <field name="check_pj" invisible="1"/>
+ <button name="action_open_job_detail"
+ string="📄"
+ type="object"
+ icon="fa-file"
+ attrs="{'invisible': [('check_pj','=',True)]}"
+ context="{}"/>
+
</tree>
</field>
</record>
@@ -41,6 +50,7 @@
<field name="item_code"/>
<field name="product"/>
<field name="action"/>
+ <field name="so_number"/>
</group>
<group>
<field name="onhand"/>