summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py137
-rwxr-xr-xindoteknik_custom/models/__init__.py2
-rw-r--r--indoteknik_custom/models/stock_location.py40
-rw-r--r--indoteknik_custom/models/stock_move.py71
-rw-r--r--indoteknik_custom/models/stock_picking.py81
-rw-r--r--indoteknik_custom/models/stock_quant.py6
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv3
-rw-r--r--indoteknik_custom/views/stock_location.xml25
-rw-r--r--indoteknik_custom/views/stock_quant.xml14
9 files changed, 353 insertions, 26 deletions
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index c19812f5..94d0035f 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -307,3 +307,140 @@ class StockPicking(controller.Controller):
}
)
+
+ @http.route(prefix + 'locator/picking', auth='public', methods=['GET', 'OPTIONS'])
+ @controller.Controller.must_authorized()
+ def get_picking_by_name(self, **kw):
+ name = str(kw.get('name'))
+ if not name:
+ return self.response({'status': 'error', 'message': 'Picking name is required'})
+
+ picking = request.env['stock.picking'].search([('name', 'ilike', name)], limit=1)
+ if 'BU/INPUT/' in name:
+ if picking.state != 'done':
+ return self.response({'status': 'error', 'message': 'BU Input nya belum done'})
+ move_dest = picking.move_ids_without_package[0]
+ picking = move_dest.move_dest_ids.filtered(lambda m: m.state not in ['done', 'cancel']).picking_id
+
+ if not picking:
+ return self.response({'status': 'error', 'message': 'Picking not found'})
+
+ lines = []
+ for move in picking.move_line_ids_without_package:
+ lines.append({
+ 'move_id': move.id,
+ 'product_id': move.product_id.id,
+ 'product_name': move.product_id.display_name,
+ 'product_uom_qty': move.product_uom_qty,
+ 'qty_done': move.qty_done,
+ 'uom_name': move.product_uom_id.name,
+ 'source_location': move.location_id.complete_name,
+ 'dest_location': move.location_dest_id.complete_name,
+ })
+
+ data = {
+ 'status': 'success',
+ 'picking': {
+ 'id': picking.id,
+ 'name': picking.name,
+ 'type_code': picking.picking_type_id.code,
+ 'source_location': picking.location_id.complete_name,
+ 'dest_location': picking.location_dest_id.complete_name,
+ 'state': picking.state,
+ 'lines': lines,
+ }
+ }
+
+ return self.response(data)
+
+ @http.route('/api/v1/locator/picking/update', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
+ @controller.Controller.must_authorized()
+ def update_picking_lines(self, **kw):
+ picking_id = int(kw.get('picking_id'))
+ lines_str = kw.get('lines', '[]')
+ try:
+ updates = json.loads(lines_str)
+ except:
+ updates = []
+
+ picking = request.env['stock.picking'].sudo().browse(picking_id)
+ if not picking.exists():
+ return self.response({'status': 'error', 'message': 'Picking not found'})
+
+ for line in updates:
+ move = request.env['stock.move.line'].sudo().browse(line['move_id'])
+ if not move.exists():
+ continue
+
+ location_id, location_dest_id = self.get_location_locator(line)
+
+ for move_line in move:
+ move_line.qty_done = line.get('qty_done', move_line.qty_done)
+ if 'source_location' in line:
+ move_line.location_id = location_id
+ if 'dest_location' in line:
+ move_line.location_dest_id = location_dest_id
+
+ return self.response({'status': 'success', 'message': 'Picking updated'})
+
+ @http.route('/api/v1/locator/picking/validate', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
+ @controller.Controller.must_authorized()
+ def validate_picking(self, **kw):
+ try:
+ picking_id = int(kw.get('picking_id'))
+ picking = request.env['stock.picking'].sudo().browse(picking_id)
+
+ if not picking.exists():
+ return self.response({
+ 'status': 'error',
+ 'message': 'Picking not found'
+ })
+
+ action = picking.button_validate()
+ backorder = None
+
+ if isinstance(action, dict) and action.get('res_model') == 'stock.backorder.confirmation':
+
+ ctx = action.get('context', {}) or {}
+
+ pick_ids = ctx.get('default_pick_ids') or []
+
+ if pick_ids and isinstance(pick_ids[0], (tuple, list)):
+ pick_ids = [p[1] for p in pick_ids]
+
+ Wizard = request.env['stock.backorder.confirmation'].with_context({
+ **ctx,
+ "default_pick_ids": pick_ids,
+ "button_validate_picking_ids": pick_ids,
+ }).sudo()
+
+ wizard = Wizard.create({})
+
+ # --- Step 4: Jalankan wizard.process() → Odoo create backorder ---
+ wizard.process()
+
+ # --- Step 5: Ambil backorder ---
+ backorder = request.env['stock.picking'].sudo().search([
+ ('backorder_id', '=', picking.id)
+ ], limit=1)
+
+ # --- FINAL RESPONSE ---
+ return self.response({
+ 'status': 'success',
+ 'picking_name': picking.name,
+ 'validated': True,
+ 'backorder_created': bool(backorder),
+ 'backorder_name': backorder.name if backorder else None,
+ })
+
+ except Exception as e:
+ return self.response({
+ 'status': 'error',
+ 'message': str(e)
+ })
+
+
+ def get_location_locator(self, line):
+ location_id = request.env['stock.location'].sudo().search([('complete_name', '=', line.get('source_location'))], limit=1).id
+ location_dest_id = request.env['stock.location'].sudo().search([('complete_name', '=', line.get('dest_location'))], limit=1).id
+ return location_id, location_dest_id \ No newline at end of file
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index a14c766e..adc80b20 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -163,5 +163,7 @@ from . import letter_receivable
from . import sj_tele
from . import partial_delivery
from . import domain_apo
+from . import stock_location
+from . import stock_quant
from . import uom_uom
from . import commission_internal
diff --git a/indoteknik_custom/models/stock_location.py b/indoteknik_custom/models/stock_location.py
new file mode 100644
index 00000000..d075ff2a
--- /dev/null
+++ b/indoteknik_custom/models/stock_location.py
@@ -0,0 +1,40 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class StockLocation(models.Model):
+ _inherit = 'stock.location'
+
+ rack_level = fields.Integer(
+ string='Sequence',
+ default=1,
+ help='Indicates the vertical rack level (1 = lowest, 4 = highest).'
+ )
+
+ # level = fields.Integer(
+ # string='Rack Level',
+ # default=1,
+ # help='Indicates the vertical rack level (1 = lowest, 4 = highest).'
+ # )
+
+ is_locked = fields.Boolean(
+ string="Locked",
+ default=False,
+ help="Jika dicentang, lokasi ini tidak dapat digunakan untuk reservasi atau penerimaan barang."
+ )
+
+ @api.constrains('rack_level')
+ def _check_rack_level(self):
+ for rec in self:
+ if rec.rack_level < 1 or rec.rack_level > 4:
+ raise ValidationError(_("Rack level harus antara 1 sampai 4."))
+
+ @api.constrains('is_locked')
+ def _sync_locked_quant(self):
+ Quant = self.env['stock.quant']
+ for rec in self:
+ quants = Quant.search([('location_id', '=', rec.id)])
+ if quants:
+ if rec.is_locked:
+ quants.write({'note': 'Locked'})
+ else:
+ quants.write({'note': False})
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index cac88287..320c175f 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -2,6 +2,8 @@ from odoo import fields, models, api
from odoo.tools.misc import format_date, OrderedSet
from odoo.exceptions import UserError
import logging
+from odoo.tools.float_utils import float_compare, float_is_zero, float_round
+
_logger = logging.getLogger(__name__)
@@ -20,7 +22,6 @@ class StockMove(models.Model):
hold_outgoingg = fields.Boolean('Hold Outgoing', default=False)
product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly=True)
partial = fields.Boolean('Partial?', default=False)
-
# Ambil product uom dari SO line
@api.model
def create(self, vals):
@@ -28,7 +29,66 @@ class StockMove(models.Model):
sale_line = self.env['sale.order.line'].browse(vals['sale_line_id'])
vals['product_uom'] = sale_line.product_uom.id
return super().create(vals)
+
+ def _update_reserved_quantity(
+ self, need, available_quantity, location_id,
+ lot_id=None, package_id=None, owner_id=None, strict=True
+ ):
+ self.ensure_one()
+ picking = self.picking_id
+ if picking and 'BU/PICK' in (picking.name or ''):
+ _logger.info(f"[LocatorLogic] Running custom locator logic for {picking.name}")
+
+ # Ambil semua lokasi anak dari source location (ex: BU/Stock)
+ locations = self.env['stock.location'].search([
+ ('id', 'child_of', self.location_id.id),
+ ('usage', '=', 'internal'),
+ ('is_locked', '=', False),
+ # ('id', '!=', 57),
+ ], order='rack_level asc')
+
+ total_reserved = 0.0
+ remaining_need = need
+
+ for loc in locations:
+ if remaining_need <= 0:
+ break
+
+ quants = self.env['stock.quant']._gather(self.product_id, loc)
+ for quant in quants:
+ if quant.available_quantity <= 0:
+ continue
+
+ qty_to_take = min(quant.available_quantity, remaining_need)
+ _logger.info(
+ f"[LocatorLogic] Reserving {qty_to_take}/{remaining_need} "
+ f"from {loc.display_name} (avail={quant.available_quantity})"
+ )
+
+ reserved_now = super(StockMove, self)._update_reserved_quantity(
+ qty_to_take, quant.available_quantity, quant.location_id,
+ lot_id, package_id, owner_id, strict
+ )
+
+ total_reserved += reserved_now
+ remaining_need -= reserved_now
+
+ if remaining_need <= 0:
+ break
+
+ if total_reserved > 0:
+ _logger.info(f"[LocatorLogic] Total reserved: {total_reserved} / {need}")
+ return total_reserved
+ else:
+ _logger.info("[LocatorLogic] No available stock found in unlocked locations by level order.")
+ return 0
+
+ return super(StockMove, self)._update_reserved_quantity(
+ need, available_quantity, location_id, lot_id, package_id, owner_id, strict
+ )
+
+
# @api.model_create_multi
# def create(self, vals_list):
# moves = super(StockMove, self).create(vals_list)
@@ -288,4 +348,11 @@ class StockMoveLine(models.Model):
move = self.env['stock.move'].browse(vals['move_id'])
if move.product_uom:
vals['product_uom_id'] = move.product_uom.id
- return super().create(vals) \ No newline at end of file
+ return super().create(vals)
+ def _action_done(self):
+ for line in self:
+ if line.location_dest_id and line.location_dest_id.is_locked:
+ raise UserError(f"Lokasi '{line.location_dest_id.display_name}' sedang dikunci dan tidak bisa menerima barang.")
+ if line.location_id and line.location_id.is_locked:
+ raise UserError(f"Lokasi '{line.location_id.display_name}' sedang dikunci dan tidak bisa reserve barang.")
+ return super(StockMoveLine, self)._action_done()
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index e7686b75..5a793382 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -2349,33 +2349,44 @@ class CheckProduct(models.Model):
self.quantity = 1
def unlink(self):
- # Get all affected pickings before deletion
+ """
+ Override unlink untuk:
+ 1. Simpan picking dan product_id yang terdampak sebelum delete.
+ 2. Hapus record check.product.
+ 3. Reset quantity_done untuk product yang udah gak ada di check_product_lines.
+ 4. Sync ulang product yang masih ada.
+ """
+ # Step 1: ambil data sebelum hapus
pickings = self.mapped('picking_id')
-
- # Store product_ids that will be deleted
deleted_product_ids = self.mapped('product_id')
- # Perform the deletion
+ # Step 2: hapus record
result = super(CheckProduct, self).unlink()
- # After deletion, update moves for affected pickings
+ # Step 3: update masing-masing picking
for picking in pickings:
- # For products that were completely removed (no remaining check.product lines)
+ # pastikan picking masih valid (kadang record udah kehapus bareng cascade)
+ if not picking.exists():
+ continue
+
remaining_product_ids = picking.check_product_lines.mapped('product_id')
removed_product_ids = deleted_product_ids - remaining_product_ids
- # Set quantity_done to 0 for moves of completely removed products
- moves_to_reset = picking.move_ids_without_package.filtered(
- lambda move: move.product_id in removed_product_ids
- )
- for move in moves_to_reset:
- move.quantity_done = 0.0
+ # Reset quantity_done untuk produk yang udah gak ada
+ if removed_product_ids:
+ moves_to_reset = picking.move_line_ids_without_package.filtered(
+ lambda m: m.product_id in removed_product_ids
+ )
+ for move_line in moves_to_reset:
+ move_line.qty_done = 0.0
- # Also sync remaining products in case their totals changed
- self._sync_check_product_to_moves(picking)
+ # Step 4: sync ulang product yang masih ada
+ if remaining_product_ids:
+ self._sync_check_product_to_moves(picking)
return result
+
@api.depends('quantity')
def _compute_status(self):
for record in self:
@@ -2407,19 +2418,41 @@ class CheckProduct(models.Model):
def _sync_check_product_to_moves(self, picking):
"""
- Sinkronisasi quantity_done di move_ids_without_package
- dengan total quantity dari check.product berdasarkan product_id.
+ Sinkronisasi qty_done di move_line_ids_without_package
+ berdasarkan total quantity dari check_product_lines per product_id,
+ dan distribusikan ke move_line berdasarkan urutan rack_level ascending.
"""
for product_id in picking.check_product_lines.mapped('product_id'):
- # Totalkan quantity dari semua baris check.product untuk product_id ini
- total_quantity = sum(
- line.quantity for line in
- picking.check_product_lines.filtered(lambda line: line.product_id == product_id)
+ # Hitung total quantity dari check_product_lines untuk produk ini
+ remaining_qty = sum(
+ line.quantity
+ for line in picking.check_product_lines.filtered(lambda l: l.product_id == product_id)
)
- # Update quantity_done di move yang relevan
- moves = picking.move_ids_without_package.filtered(lambda move: move.product_id == product_id)
- for move in moves:
- move.quantity_done = total_quantity
+
+ # Ambil move_line untuk product_id ini dan urutkan berdasarkan rack_level ASC
+ move_lines = picking.move_line_ids_without_package.filtered(
+ lambda m: m.product_id == product_id
+ )
+ move_lines = sorted(
+ move_lines,
+ key=lambda m: m.location_id.rack_level or 0 # kalau rack_level kosong, dianggap 0
+ )
+
+ for move_line in move_lines:
+ if remaining_qty <= 0:
+ move_line.qty_done = 0.0
+ continue
+
+ needed = move_line.product_uom_qty
+ assigned_qty = min(needed, remaining_qty)
+
+ move_line.qty_done = assigned_qty
+ remaining_qty -= assigned_qty
+
+ # (Opsional) Log biar bisa dilacak
+ # _logger.info(f"[SYNC] {picking.name} - {product_id.display_name}: Sisa {remaining_qty}")
+
+
def _consolidate_duplicate_lines(self):
"""
diff --git a/indoteknik_custom/models/stock_quant.py b/indoteknik_custom/models/stock_quant.py
new file mode 100644
index 00000000..05335115
--- /dev/null
+++ b/indoteknik_custom/models/stock_quant.py
@@ -0,0 +1,6 @@
+from odoo import _, api, fields, models
+
+class StockQuant(models.Model):
+ _inherit = 'stock.quant'
+
+ note = fields.Char(string="Note") \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index bc8dc2a4..f6dcd4fc 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -213,4 +213,7 @@ access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,
access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1
access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1
access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
+
+access_stock_location,access.stock.location,model_stock_location,,1,1,1,1
+access_stock_quant,access.stock.quant,model_stock_quant,,1,1,1,1
access_stock_picking_sj_document,stock.picking.sj.document,model_stock_picking_sj_document,base.group_user,1,1,1,1
diff --git a/indoteknik_custom/views/stock_location.xml b/indoteknik_custom/views/stock_location.xml
index 82ab2bc5..af6706e9 100644
--- a/indoteknik_custom/views/stock_location.xml
+++ b/indoteknik_custom/views/stock_location.xml
@@ -5,4 +5,29 @@
<field name="location_id">3</field>
<field name="usage">inventory</field>
</record>
+
+ <record id="view_location_tree_inherit_rack_level" model="ir.ui.view">
+ <field name="name">stock.location.tree.rack_level</field>
+ <field name="model">stock.location</field>
+ <field name="inherit_id" ref="stock.view_location_tree2"/>
+ <field name="arch" type="xml">
+ <field name="complete_name" position="after">
+ <field name="rack_level"/>
+ <field name="is_locked"/>
+ </field>
+ </field>
+ </record>
+
+ <!-- Tambahin field rack_level di Form View -->
+ <record id="view_location_form_inherit_rack_level" model="ir.ui.view">
+ <field name="name">stock.location.form.rack_level</field>
+ <field name="model">stock.location</field>
+ <field name="inherit_id" ref="stock.view_location_form"/>
+ <field name="arch" type="xml">
+ <field name="usage" position="after">
+ <field name="rack_level"/>
+ <field name="is_locked" widget="boolean_toggle"/>
+ </field>
+ </field>
+ </record>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/stock_quant.xml b/indoteknik_custom/views/stock_quant.xml
index 107f75f3..a665529e 100644
--- a/indoteknik_custom/views/stock_quant.xml
+++ b/indoteknik_custom/views/stock_quant.xml
@@ -9,6 +9,20 @@
<field name="inventory_quantity" position="after">
<field name="reserved_quantity" readonly="1" />
</field>
+ <field name="value" position="after">
+ <field name="note" readonly="1" />
+ </field>
+ </field>
+ </record>
+
+ <record id="stock_view_stock_quant_tree_inherited" model="ir.ui.view">
+ <field name="name">stock.view_stock_quant_tree_inherited</field>
+ <field name="model">stock.quant</field>
+ <field name="inherit_id" ref="stock.view_stock_quant_tree"/>
+ <field name="arch" type="xml">
+ <field name="value" position="after">
+ <field name="note" readonly="1" />
+ </field>
</field>
</record>
</data>