summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIndoteknik . <it@fixcomart.co.id>2025-08-20 13:57:18 +0700
committerIndoteknik . <it@fixcomart.co.id>2025-08-20 13:57:18 +0700
commit930af568605dd2b0695bc76282f3164b885f3126 (patch)
tree8f6e14f3e51378daa66f0fa9f23c32a758b15d46
parentcaaef86e4c60f026a2b2b7abcad355f2d18366c3 (diff)
parent6adef807baa548aa132418d80e21b04cd5e21a68 (diff)
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into reminder-tempo-v2
-rwxr-xr-xindoteknik_custom/__manifest__.py3
-rwxr-xr-xindoteknik_custom/models/product_template.py1
-rwxr-xr-xindoteknik_custom/models/purchase_order.py32
-rwxr-xr-xindoteknik_custom/models/sale_order.py57
-rw-r--r--indoteknik_custom/models/sale_order_line.py34
-rw-r--r--indoteknik_custom/models/stock_picking.py17
-rw-r--r--indoteknik_custom/static/src/js/check_product_barcode.js41
-rw-r--r--indoteknik_custom/views/assets.xml7
-rwxr-xr-xindoteknik_custom/views/product_template.xml1
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml26
-rwxr-xr-xindoteknik_custom/views/sale_order.xml4
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 &amp; 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"/>