diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-07-23 15:10:51 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-07-23 15:10:51 +0700 |
| commit | 6f5880c2c78b53177c289175b0e1511196371d58 (patch) | |
| tree | 4b3d5d03e4589d9bc19a94984f7b4f5f0bcfbfda | |
| parent | deb60713ed39979b34083ee094de79fa3afac3b8 (diff) | |
| parent | 87f38f9fcb68f04a2cc8157744622c2d0ebf1eab (diff) | |
<hafid> Fix Conflict
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 1 | ||||
| -rw-r--r-- | indoteknik_custom/models/account_move.py | 101 | ||||
| -rw-r--r-- | indoteknik_custom/models/approval_payment_term.py | 109 | ||||
| -rw-r--r-- | indoteknik_custom/models/commision.py | 5 | ||||
| -rw-r--r-- | indoteknik_custom/models/dunning_run.py | 20 | ||||
| -rw-r--r-- | indoteknik_custom/models/mrp_production.py | 2 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 9 | ||||
| -rw-r--r-- | indoteknik_custom/models/purchasing_job.py | 57 | ||||
| -rw-r--r-- | indoteknik_custom/models/res_partner.py | 15 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 4 | ||||
| -rw-r--r-- | indoteknik_custom/models/sale_order_line.py | 1 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 147 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | indoteknik_custom/views/approval_payment_term.xml | 19 | ||||
| -rw-r--r-- | indoteknik_custom/views/customer_commision.xml | 1 | ||||
| -rw-r--r-- | indoteknik_custom/views/ir_sequence.xml | 4 | ||||
| -rw-r--r-- | indoteknik_custom/views/mail_template_invoice_reminder.xml | 51 | ||||
| -rw-r--r-- | indoteknik_custom/views/purchasing_job.xml | 12 |
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"/> |
