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__) class StockMove(models.Model): _inherit = 'stock.move' line_no = fields.Integer('No', default=0) sale_id = fields.Many2one('sale.order', string='SO') print_barcode = fields.Boolean( string="Print Barcode", default=lambda self: self.product_id.print_barcode, ) qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant') barcode = fields.Char(string='Barcode', related='product_id.barcode') vendor_id = fields.Many2one('res.partner' ,string='Vendor') 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): if vals.get('sale_line_id'): 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) # for move in moves: # if move.product_id and move.location_id.id == 58 and move.location_dest_id.id == 57 and move.picking_type_id.id == 75: # po_line = self.env['purchase.order.line'].search([ # ('product_id', '=', move.product_id.id), # ('order_id.name', '=', move.origin) # ], limit=1) # if po_line: # move.write({'purchase_line_id': po_line.id}) # return moves @api.constrains('product_id') def constrains_product_to_fill_vendor(self): for rec in self: if rec.product_id and rec.bom_line_id: if rec.product_id.x_manufacture.override_vendor_id: rec.vendor_id = rec.product_id.x_manufacture.override_vendor_id.id else: purchase_pricelist = self.env['purchase.pricelist'].search( [('product_id', '=', rec.product_id.id), ('is_winner', '=', True)], limit=1) if purchase_pricelist: rec.vendor_id = purchase_pricelist.vendor_id.id def _compute_qr_code_variant(self): for rec in self: if rec.picking_id.picking_type_code == 'outgoing' and rec.picking_id and rec.picking_id.origin and rec.picking_id.origin.startswith('SO/'): rec.qr_code_variant = rec.product_id.qr_code_variant rec.print_barcode = True if rec.print_barcode and rec.print_barcode == True and rec.product_id and rec.product_id.qr_code_variant: rec.qr_code_variant = rec.product_id.qr_code_variant else: rec.qr_code_variant = False def write(self, vals): res = super(StockMove, self).write(vals) if 'print_barcode' in vals: for line in self: if line.product_id: line.product_id.print_barcode = vals['print_barcode'] return res def _do_unreserve(self, product=None, quantity=False): moves_to_unreserve = OrderedSet() for move in self: if move.state == 'cancel' or (move.state == 'done' and move.scrapped): continue elif move.state == 'done': raise UserError("You cannot unreserve a stock move that has been set to 'Done'.") if product and move.product_id != product: continue # Skip moves that don't match the specified product moves_to_unreserve.add(move.id) moves_to_unreserve = self.env['stock.move'].browse(moves_to_unreserve) ml_to_update, ml_to_unlink = OrderedSet(), OrderedSet() moves_not_to_recompute = OrderedSet() for ml in moves_to_unreserve.move_line_ids: if product and ml.product_id != product: continue # Only affect the specified product if quantity and quantity > 0: # Only reduce by the specified quantity if it is greater than zero ml_to_update.add(ml.id) remaining_qty = ml.product_uom_qty - quantity ml.write({'product_uom_qty': remaining_qty if remaining_qty > 0 else 0}) quantity = 0 # Set to zero to prevent further unreserving in the same loop elif ml.qty_done: ml_to_update.add(ml.id) else: ml_to_unlink.add(ml.id) moves_not_to_recompute.add(ml.move_id.id) ml_to_update, ml_to_unlink = self.env['stock.move.line'].browse(ml_to_update), self.env['stock.move.line'].browse(ml_to_unlink) moves_not_to_recompute = self.env['stock.move'].browse(moves_not_to_recompute) ml_to_unlink.unlink() (moves_to_unreserve - moves_not_to_recompute)._recompute_state() return True def _prepare_account_move_line_from_mr(self, po_line, qty, move=False): po_line.ensure_one() aml_currency = move and move.currency_id or po_line.currency_id date = move and move.date or fields.Date.today() res = { 'display_type': po_line.display_type, 'sequence': po_line.sequence, 'name': '%s: %s' % (po_line.order_id.name, po_line.name), 'product_id': po_line.product_id.id, 'product_uom_id': po_line.product_uom.id, 'quantity': qty, 'price_unit': po_line.currency_id._convert(po_line.price_unit, aml_currency, po_line.company_id, date, round=False), 'tax_ids': [(6, 0, po_line.taxes_id.ids)], 'analytic_account_id': po_line.account_analytic_id.id, 'analytic_tag_ids': [(6, 0, po_line.analytic_tag_ids.ids)], 'purchase_line_id': po_line.id, } if not move: return res if self.currency_id == move.company_id.currency_id: currency = False else: currency = move.currency_id res.update({ 'move_id': move.id, 'currency_id': currency and currency.id or False, 'date_maturity': move.invoice_date_due, 'partner_id': move.partner_id.id, }) return res def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost): self.ensure_one() if self.picking_id.is_internal_use: AccountMove = self.env['account.move'].with_context(default_journal_id=journal_id) # 538 is static id for "Biaya Umum Lain-Lain" on account.account model # 440 is static id for "PPN Keluaran" on account.account model debit_account_id = self.picking_id.account_id.id if self.picking_id.account_id.id else 538 tax = cost * (11 / 100) move_lines = self._prepare_account_move_line(qty, cost, credit_account_id, debit_account_id, description) move_lines += self._prepare_account_move_line(qty, tax, 440, debit_account_id, description) if move_lines: date = self._context.get('force_period_date', fields.Date.context_today(self)) new_account_move = AccountMove.sudo().create({ 'journal_id': journal_id, 'line_ids': move_lines, 'date': date, 'ref': description, 'stock_move_id': self.id, 'stock_valuation_layer_ids': [(6, None, [svl_id])], 'move_type': 'entry', }) new_account_move._post() return True return super(StockMove, self)._create_account_move_line(credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost) class StockMoveLine(models.Model): _inherit = 'stock.move.line' line_no = fields.Integer('No', default=0) note = fields.Char('Note') manufacture = fields.Many2one('x_manufactures', string="Brands", related="product_id.x_manufacture", store=True) outstanding_qty = fields.Float( string='Outstanding Qty', compute='_compute_delivery_line_status', store=True ) delivery_status = fields.Selection([ ('none', 'No Movement'), ('partial', 'Partial'), ('partial_final', 'Partial Final'), ('full', 'Full'), ], string='Delivery Status', compute='_compute_delivery_line_status', store=True) @api.depends('qty_done', 'product_uom_qty', 'picking_id.state') def _compute_delivery_line_status(self): for line in self: line.outstanding_qty = 0.0 line.delivery_status = 'none' if not line.picking_id or line.picking_id.picking_type_id.code != 'outgoing': continue total_qty = line.move_id.product_uom_qty or 0 done_qty = line.qty_done or 0 line.outstanding_qty = max(total_qty - done_qty, 0) if total_qty == 0: continue if done_qty >= total_qty: line.delivery_status = 'full' elif done_qty < total_qty: line.delivery_status = 'partial' elif 0 < done_qty < total_qty: backorder_exists = self.env['stock.picking'].search_count([ ('group_id', '=', line.picking_id.group_id.id), ('name', 'ilike', 'BU/OUT'), ('id', '!=', line.picking_id.id), ('state', 'in', ['done', 'assigned']), ]) if backorder_exists: line.delivery_status = 'partial' if done_qty >= total_qty: line.delivery_status = 'partial_final' # Ambil uom dari stock move @api.model def create(self, vals): if 'move_id' in vals and 'product_uom_id' not in vals: 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) 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()