diff options
| author | Indoteknik . <it@fixcomart.co.id> | 2025-08-20 13:57:18 +0700 |
|---|---|---|
| committer | Indoteknik . <it@fixcomart.co.id> | 2025-08-20 13:57:18 +0700 |
| commit | 930af568605dd2b0695bc76282f3164b885f3126 (patch) | |
| tree | 8f6e14f3e51378daa66f0fa9f23c32a758b15d46 | |
| parent | caaef86e4c60f026a2b2b7abcad355f2d18366c3 (diff) | |
| parent | 6adef807baa548aa132418d80e21b04cd5e21a68 (diff) | |
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into reminder-tempo-v2
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 3 | ||||
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 32 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 57 | ||||
| -rw-r--r-- | indoteknik_custom/models/sale_order_line.py | 34 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 17 | ||||
| -rw-r--r-- | indoteknik_custom/static/src/js/check_product_barcode.js | 41 | ||||
| -rw-r--r-- | indoteknik_custom/views/assets.xml | 7 | ||||
| -rwxr-xr-x | indoteknik_custom/views/product_template.xml | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/views/purchase_order.xml | 26 | ||||
| -rwxr-xr-x | indoteknik_custom/views/sale_order.xml | 4 |
11 files changed, 190 insertions, 33 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index d1ae681a..85603a33 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -8,9 +8,10 @@ 'author': 'Rafi Zadanly', 'website': '', 'images': ['assets/favicon.ico'], - 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur' ], + 'depends': ['base', 'coupon', 'delivery', 'sale', 'sale_management', 'vit_kelurahan', 'vit_efaktur', 'barcodes'], 'data': [ 'security/ir.model.access.csv', + 'views/assets.xml', 'views/group_partner.xml', 'views/blog_post.xml', 'views/coupon_program.xml', diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index f59bea6b..13e99707 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -1365,4 +1365,5 @@ class ImageCarousel(models.Model): _order = 'product_id, id' product_id = fields.Many2one('product.template', string='Product', required=True, ondelete='cascade', index=True, copy=False) + sequence = fields.Integer("Sequence", default=10) image = fields.Binary(string='Image') diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 103a9131..50913a80 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -103,6 +103,11 @@ class PurchaseOrder(models.Model): string="BU Related Count", compute='_compute_bu_related_count' ) + + bills_related_count = fields.Integer( + string="Bills DP & Pelunasan", + compute="_compute_bills_related_count" + ) manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders') complete_bu_in_count = fields.Integer( @@ -260,6 +265,33 @@ class PurchaseOrder(models.Model): 'target': 'current', } + def action_view_bills(self): + self.ensure_one() + + bill_ids = [] + if self.bills_dp_id: + bill_ids.append(self.bills_dp_id.id) + if self.bills_pelunasan_id: + bill_ids.append(self.bills_pelunasan_id.id) + + return { + 'name': 'Bills (DP & Pelunasan)', + 'type': 'ir.actions.act_window', + 'res_model': 'account.move', + 'view_mode': 'tree,form', + 'target': 'current', + 'domain': [('id', 'in', bill_ids)], + } + + def _compute_bills_related_count(self): + for order in self: + count = 0 + if order.bills_dp_id: + count += 1 + if order.bills_pelunasan_id: + count += 1 + order.bills_related_count = count + # cek payment term def _check_payment_term(self): _logger.info("Check Payment Term Terpanggil") diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 80790ebe..53be999f 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -388,8 +388,37 @@ class SaleOrder(models.Model): string="Unreserved %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False ) + payment_state_custom = fields.Selection([ + ('unpaid', 'Unpaid'), + ('partial', 'Partially Paid'), + ('paid', 'Paid'), + ('no_invoice', 'No Invoice'), + ], string="Payment Status", compute="_compute_payment_state_custom", store=False) + + @api.depends('invoice_ids.payment_state', 'invoice_ids.amount_total', 'invoice_ids.amount_residual') + def _compute_payment_state_custom(self): + for order in self: + invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel') + total = sum(invoices.mapped('amount_total')) + residual = sum(invoices.mapped('amount_residual')) + + if not invoices or total == 0: + order.payment_state_custom = 'no_invoice' + continue + + paid = total - residual + percent_paid = (paid / total) * 100 if total > 0 else 0.0 + + if percent_paid == 100: + order.payment_state_custom = 'paid' + elif percent_paid == 0: + order.payment_state_custom = 'unpaid' + else: + order.payment_state_custom = 'partial' - @api.depends('order_line.reserved_percent', 'order_line.delivered_percent', 'order_line.unreserved_percent') + @api.depends('order_line.move_ids.state', + 'order_line.move_ids.reserved_availability', + 'order_line.move_ids.quantity_done') def _compute_reserved_delivered_pie(self): for order in self: total_qty = sum(order.order_line.mapped('product_uom_qty')) @@ -401,16 +430,23 @@ class SaleOrder(models.Model): if not order_qty: continue - # ambil qty asli dari move, bukan percent agar akurat - pick_moves = line.move_ids.filtered( - lambda m: m.picking_type_id.code == 'internal' and m.state not in ('done', 'cancel') - ) - reserved_qty += sum(pick_moves.mapped('reserved_availability')) + for move in line.move_ids: + if move.state != 'done': + # reserve qty (draft/assigned) + if move.picking_type_id.code == 'internal': + reserved_qty += move.reserved_availability or 0.0 + continue - out_moves = line.move_ids.filtered( - lambda m: m.picking_type_id.code == 'outgoing' and m.state == 'done' - ) - delivered_qty += sum(out_moves.mapped('quantity_done')) + # sudah done → cek alur lokasi + if move.location_dest_id.usage == 'customer': + # barang keluar → delivered + delivered_qty += move.quantity_done + elif move.location_id.usage == 'customer': + # barang balik (return) → kurangi delivered + delivered_qty -= move.quantity_done + + # clamp biar ga minus + delivered_qty = max(delivered_qty, 0) order.reserved_percent = (reserved_qty / total_qty) * 100 order.delivered_percent = (delivered_qty / total_qty) * 100 @@ -418,6 +454,7 @@ class SaleOrder(models.Model): else: order.reserved_percent = order.delivered_percent = order.unreserved_percent = 0 + def _has_ccm(self): if self.id: self.ccm_id = self.env['tukar.guling'].search([('origin', 'ilike', self.name)], limit=1) diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 2a00bac0..2406995d 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -58,31 +58,39 @@ class SaleOrderLine(models.Model): delivered_percent = fields.Float(string="Delivered %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False) unreserved_percent = fields.Float(string="Unreserved %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False) - @api.depends('move_ids') + @api.depends('move_ids.state', 'move_ids.reserved_availability', 'move_ids.quantity_done') def _compute_reserved_delivered_pie(self): for line in self: order_qty = line.product_uom_qty or 0.0 reserved_qty = delivered_qty = 0.0 if order_qty > 0: - # Reserved: hanya dari BU/PICK yang masih aktif - pick_moves = line.move_ids.filtered( - lambda m: m.picking_type_id.code == 'internal' and m.state not in ('done', 'cancel') - ) - reserved_qty = sum(pick_moves.mapped('reserved_availability')) - - # Delivered: hanya dari BU/OUT yang sudah done - out_moves = line.move_ids.filtered( - lambda m: m.picking_type_id.code == 'outgoing' and m.state == 'done' - ) - delivered_qty = sum(out_moves.mapped('quantity_done')) - + for move in line.move_ids: + if move.state != 'done': + # Reserve qty (hanya dari picking internal yang belum selesai) + if move.picking_type_id.code == 'internal': + reserved_qty += move.reserved_availability or 0.0 + continue + + # Kalau sudah done → cek lokasi + if move.location_dest_id.usage == 'customer': + # Barang keluar → tambah delivered + delivered_qty += move.quantity_done + elif move.location_id.usage == 'customer': + # Barang balik dari customer (retur) → kurangi delivered + delivered_qty -= move.quantity_done + + # Jangan sampai delivered minus + delivered_qty = max(delivered_qty, 0) + + # Hitung persentase line.reserved_percent = (reserved_qty / order_qty) * 100 if order_qty else 0 line.delivered_percent = (delivered_qty / order_qty) * 100 if order_qty else 0 line.unreserved_percent = 100 - line.reserved_percent - line.delivered_percent + 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 cb36eb2f..3d04a416 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -2070,6 +2070,8 @@ class CheckProduct(models.Model): _name = 'check.product' _description = 'Check Product' _order = 'picking_id, id' + _inherit = ['barcodes.barcode_events_mixin'] # ⬅️ aktifkan barcode handler + picking_id = fields.Many2one( 'stock.picking', @@ -2084,6 +2086,21 @@ class CheckProduct(models.Model): status = fields.Char(string='Status', compute='_compute_status') code_product = fields.Char(string='Code Product') + def write(self, vals): + if 'code_product' in vals and not self.env.context.get('from_barcode_scan'): + raise UserError("Field Code Product hanya bisa diisi melalui barcode scan.") + res = super().write(vals) + # konsolidasi dll milik Anda tetap jalan + if not self.env.context.get('skip_consolidate'): + self.with_context(skip_consolidate=True)._consolidate_duplicate_lines() + return res + + # Scanner handler + def on_barcode_scanned(self, barcode): + self.ensure_one() + self.with_context(from_barcode_scan=True).write({'code_product': barcode}) + self._onchange_code_product() + @api.onchange('code_product') def _onchange_code_product(self): if not self.code_product: diff --git a/indoteknik_custom/static/src/js/check_product_barcode.js b/indoteknik_custom/static/src/js/check_product_barcode.js new file mode 100644 index 00000000..f7a1bb75 --- /dev/null +++ b/indoteknik_custom/static/src/js/check_product_barcode.js @@ -0,0 +1,41 @@ +odoo.define('indoteknik_custom.prevent_manual_typing', function (require) { + "use strict"; + + console.log("✅ Custom JS from indoteknik_custom loaded!"); + + + const THRESHOLD_MS = 40; // jeda antar karakter dianggap scanner kalau <40ms + let lastTime = 0; + let burstCount = 0; + + function isScannerLike(now) { + const gap = now - lastTime; + lastTime = now; + if (gap < THRESHOLD_MS) { + burstCount += 1; + } else { + burstCount = 1; + } + return burstCount >= 3; // setelah 3 char cepat, dianggap scanner + } + + document.addEventListener('keydown', function (e) { + const t = e.target; + if (!(t instanceof HTMLInputElement)) return; + + // pastikan hanya field code_product + if (t.name !== 'code_product') return; + + const now = performance.now(); + const scanner = isScannerLike(now); + + // enter tetap boleh (scanner biasanya akhiri Enter) + if (e.key === "Enter") return; + + // kalau bukan scanner → blok manual ketikan + if (!scanner) { + e.preventDefault(); + e.stopPropagation(); + } + }, true); +}); diff --git a/indoteknik_custom/views/assets.xml b/indoteknik_custom/views/assets.xml new file mode 100644 index 00000000..4475004e --- /dev/null +++ b/indoteknik_custom/views/assets.xml @@ -0,0 +1,7 @@ +<odoo> + <template id="indoteknik_assets_backend" inherit_id="web.assets_backend" name="Indoteknik Custom Backend Assets"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/indoteknik_custom/static/src/js/check_product_barcode.js"/> + </xpath> + </template> +</odoo> diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml index 8f9d1190..177449f4 100755 --- a/indoteknik_custom/views/product_template.xml +++ b/indoteknik_custom/views/product_template.xml @@ -66,6 +66,7 @@ <field name="model">image.carousel</field> <field name="arch" type="xml"> <tree editable="bottom"> + <field name="sequence" widget="handle"/> <field name="image" widget="image" width="80"/> </tree> </field> diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index 15cdc788..821f3295 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -14,6 +14,16 @@ attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('state', 'not in', ['draft'])]}" /> </div> + <xpath expr="//button[@name='action_view_invoice']" position="after"> + <button type="object" + name="action_view_related_bu" + class="oe_stat_button" + icon="fa-truck" + attrs="{'invisible': [('state', 'in', ['draft', 'sent'])]}"> + <field name="bu_related_count" widget="statinfo" string="BU Related"/> + </button> + <field name="picking_count" invisible="1"/> + </xpath> <xpath expr="//button[@name='action_view_invoice']" position="before"> <field name="is_cab_visible" invisible="1"/> <button type="object" @@ -21,21 +31,19 @@ class="oe_stat_button" icon="fa-book" attrs="{'invisible': [('is_cab_visible', '=', False)]}" - style="width: 200px;"> + > <field name="move_id" widget="statinfo" string="Journal Uang Muka"/> <span class="o_stat_text"> <t t-esc="record.move_id.name"/> </span> </button> - <button type="object" - name="action_view_related_bu" - class="oe_stat_button" - icon="fa-truck" - style="width: 200px;" - attrs="{'invisible': [('state', 'in', ['draft', 'sent'])]}"> - <field name="bu_related_count" widget="statinfo" string="BU Related"/> + <button name="action_view_bills" + type="object" + icon="fa-pencil-square-o" + attrs="{'invisible': [ + ('bills_related_count', '=', 0)]}"> + <field string="Bills DP & Pelunasan" name="bills_related_count" widget="statinfo"/> </button> - <field name="picking_count" invisible="1"/> </xpath> <button id="draft_confirm" position="after"> <button name="po_approve" diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 5fad5700..be31456b 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -483,6 +483,10 @@ <field name="date_kirim_ril"/> <field name="date_driver_departure"/> <field name="date_driver_arrival"/> + <field name="payment_state_custom" widget="badge" + decoration-danger="payment_state_custom == 'unpaid'" + decoration-success="payment_state_custom == 'paid'" + decoration-warning="payment_state_custom == 'partial'"/> <field name="unreserved_percent" widget="percentpie" string="Unreserved"/> <field name="reserved_percent" widget="percentpie" string="Reserved"/> <field name="delivered_percent" widget="percentpie" string="Delivered"/> |
