summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/mrp_production.py
diff options
context:
space:
mode:
Diffstat (limited to 'indoteknik_custom/models/mrp_production.py')
-rw-r--r--indoteknik_custom/models/mrp_production.py478
1 files changed, 476 insertions, 2 deletions
diff --git a/indoteknik_custom/models/mrp_production.py b/indoteknik_custom/models/mrp_production.py
index 54d90256..8179fe56 100644
--- a/indoteknik_custom/models/mrp_production.py
+++ b/indoteknik_custom/models/mrp_production.py
@@ -1,10 +1,484 @@
-from odoo import fields, models, api, _
+from odoo import models, fields, api, tools, _
+from datetime import datetime, timedelta
+import math
+import logging
from odoo.exceptions import AccessError, UserError, ValidationError
class MrpProduction(models.Model):
_inherit = 'mrp.production'
+ check_bom_product_lines = fields.One2many('check.bom.product', 'production_id', string='Check Product', auto_join=True, copy=False)
desc = fields.Text(string='Description')
+ sale_order = fields.Many2one('sale.order', string='Sale Order', copy=False)
+ production_purchase_match = fields.One2many('production.purchase.match', 'production_id', string='Purchase Matches', auto_join=True)
+ is_po = fields.Boolean(string='Is PO')
+ state_reserve = fields.Selection([
+ ('waiting', 'Waiting For Fullfilment'),
+ ('ready', 'Ready to Ship'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled'),
+ ], string='Status Reserve', tracking=True, copy=False, help="The current state of the stock picking.")
+ date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
+
+
+ @api.constrains('check_bom_product_lines')
+ def constrains_check_bom_product_lines(self):
+ for rec in self:
+ if len(rec.check_bom_product_lines) > 0:
+ rec.qty_producing = rec.product_qty
+
+ def button_mark_done(self):
+ """Override button_mark_done untuk mengirim pesan ke Sale Order jika state berubah menjadi 'confirmed'."""
+ if self._name != 'mrp.production':
+ return super(MrpProduction, self).button_mark_done()
+
+ result = super(MrpProduction, self).button_mark_done()
+
+ for record in self:
+ if len(record.check_bom_product_lines) < 1:
+ raise UserError("Check Product Tidak Boleh Kosong")
+ if not record.sale_order:
+ raise UserError("Sale Order Tidak Boleh Kosong")
+ if record.sale_order and record.state == 'confirmed':
+ message = _("Manufacturing order telah dibuat dengan nomor %s") % (record.name)
+ record.sale_order.message_post(body=message)
+
+ return result
+
+ def action_confirm(self):
+ """Override action_confirm untuk mengirim pesan ke Sale Order jika state berubah menjadi 'confirmed'."""
+ if self._name != 'mrp.production':
+ return super(MrpProduction, self).action_confirm()
+
+ result = super(MrpProduction, self).action_confirm()
+
+ for record in self:
+ # if len(record.check_bom_product_lines) < 1:
+ # raise UserError("Check Product Tidak Boleh Kosong")
+ if record.sale_order and record.state == 'confirmed':
+ message = _("Manufacturing order telah dibuat dengan nomor %s") % (record.name)
+ record.sale_order.message_post(body=message)
+
+ return result
+
+
+ def create_po_from_manufacturing(self):
+ if not self.state == 'confirmed':
+ raise UserError('Harus Di Approve oleh Merchandiser')
+
+ if self.is_po == True:
+ raise UserError('Sudah pernah di buat PO')
+
+ if not self.move_raw_ids:
+ raise UserError('Tidak ada Lines, belum bisa create PO')
+ # if self.is_po:
+ # raise UserError('Sudah pernah di create PO')
+
+ vendor_ids = self.env['stock.move'].read_group([
+ ('raw_material_production_id', '=', self.id),
+ ('vendor_id', '!=', False)
+ ], fields=['vendor_id'], groupby=['vendor_id'])
+
+ po_ids = []
+ for vendor in vendor_ids:
+ result_po = self.create_po_by_vendor(vendor['vendor_id'][0])
+ po_ids += result_po
+ return {
+ 'name': _('Purchase Order'),
+ 'view_mode': 'tree,form',
+ 'res_model': 'purchase.order',
+ 'target': 'current',
+ 'type': 'ir.actions.act_window',
+ 'domain': [('id', 'in', po_ids)],
+ }
- \ No newline at end of file
+
+ def create_po_by_vendor(self, vendor_id):
+ current_time = datetime.now()
+
+ PRODUCT_PER_PO = 20
+
+ stock_move = self.env['stock.move']
+
+ param_header = {
+ 'partner_id': vendor_id,
+ # 'partner_ref': self.sale_order_id.name,
+ 'currency_id': 12,
+ 'user_id': self.env.user.id,
+ 'company_id': 1, # indoteknik dotcom gemilang
+ 'picking_type_id': 28, # indoteknik bandengan receipts
+ 'date_order': current_time,
+ 'product_bom_id': self.product_id.id,
+ # 'sale_order_id': self.sale_order_id.id,
+ 'note_description': 'from Manufacturing Order'
+ }
+
+ domain = [
+ ('raw_material_production_id', '=', self.id),
+ ('vendor_id', '=', vendor_id),
+ ('state', 'in', ['waiting','confirmed','partially_available'])
+ ]
+
+ products_len = stock_move.search_count(domain)
+ page = math.ceil(products_len / PRODUCT_PER_PO)
+ po_ids = []
+ # i start from zero (0)
+ for i in range(page):
+ new_po = self.env['purchase.order'].create([param_header])
+ new_po.name = new_po.name + "/MO/" + str(i + 1)
+ po_ids.append(new_po.id)
+ lines = stock_move.search(
+ domain,
+ offset=i * PRODUCT_PER_PO,
+ limit=PRODUCT_PER_PO
+ )
+ tax = [22]
+
+ for line in lines:
+ product = line.product_id
+ price, taxes, vendor = self._get_purchase_price(product)
+
+ param_line = {
+ 'order_id' : new_po.id,
+ 'product_id': product.id,
+ 'product_qty': line.product_uom_qty if line.state in ['confirmed', 'waiting'] else line.product_uom_qty - line.forecast_availability,
+ 'product_uom_qty': line.product_uom_qty if line.state in ['confirmed', 'waiting'] else line.product_uom_qty - line.forecast_availability,
+ 'name': product.display_name,
+ 'price_unit': price if price else 0.0,
+ 'taxes_id': [taxes] if taxes else [],
+ }
+ new_po_line = self.env['purchase.order.line'].create([param_line])
+
+ self.env['production.purchase.match'].create([{
+ 'production_id': self.id,
+ 'order_id': new_po.id
+ }])
+
+ self.is_po = True
+
+ return po_ids
+
+ def _get_purchase_price(self, product_id):
+ override_vendor = product_id.x_manufacture.override_vendor_id
+ query = [('product_id', '=', product_id.id),
+ ('vendor_id', '=', override_vendor.id)]
+ purchase_price = self.env['purchase.pricelist'].search(query, limit=1)
+ if purchase_price:
+ return self._get_valid_purchase_price(purchase_price)
+ else:
+ purchase_price = self.env['purchase.pricelist'].search(
+ [('product_id', '=', product_id.id),
+ ('is_winner', '=', True)],
+ limit=1)
+
+ return self._get_valid_purchase_price(purchase_price)
+
+ def _get_valid_purchase_price(self, purchase_price):
+ current_time = datetime.now()
+ delta_time = current_time - timedelta(days=365)
+ # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
+
+ price = 0
+ taxes = ''
+ vendor_id = ''
+ human_last_update = purchase_price.human_last_update or datetime.min
+ system_last_update = purchase_price.system_last_update or datetime.min
+
+ if purchase_price.taxes_product_id.type_tax_use == 'purchase':
+ price = purchase_price.product_price
+ taxes = purchase_price.taxes_product_id.id
+ vendor_id = purchase_price.vendor_id.id
+ if delta_time > human_last_update:
+ price = 0
+ taxes = ''
+ vendor_id = ''
+
+ if system_last_update > human_last_update:
+ if purchase_price.taxes_system_id.type_tax_use == 'purchase':
+ price = purchase_price.system_price
+ taxes = purchase_price.taxes_system_id.id
+ vendor_id = purchase_price.vendor_id.id
+ if delta_time > system_last_update:
+ price = 0
+ taxes = ''
+ vendor_id = ''
+
+ return price, taxes, vendor_id
+
+class CheckBomProduct(models.Model):
+ _name = 'check.bom.product'
+ _description = 'Check Product'
+ _order = 'production_id, id'
+
+ production_id = fields.Many2one(
+ 'mrp.production',
+ string='Bom Reference',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ copy=False,
+ )
+ product_id = fields.Many2one('product.product', string='Product')
+ quantity = fields.Float(string='Quantity')
+ status = fields.Char(string='Status', compute='_compute_status')
+ code_product = fields.Char(string='Code Product')
+
+ @api.constrains('production_id')
+ def _check_missing_components(self):
+ for mo in self:
+ required = mo.production_id.move_raw_ids.mapped('product_id')
+ entered = mo.production_id.check_bom_product_lines.mapped('product_id')
+ missing = required - entered
+
+ # Jika HTML tidak bekerja sama sekali, gunakan format text biasa yang rapi
+ if missing:
+ product_list = "\n- " + "\n- ".join(p.display_name for p in missing)
+ raise UserError(
+ "⚠️ Komponen Wajib Diisi\n\n"
+ "Produk berikut harus ditambahkan:\n"
+ f"{product_list}\n\n"
+ "Silakan lengkapi terlebih dahulu."
+ )
+
+ @api.constrains('production_id', 'product_id')
+ def _check_product_bom_validation(self):
+ for record in self:
+ if not record.production_id or not record.product_id:
+ continue
+
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+
+ if not moves:
+ raise UserError((
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
+
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ # Find existing lines for the same product, excluding the current line
+ existing_lines = record.production_id.check_bom_product_lines.filtered(
+ lambda line: line.product_id == record.product_id
+ )
+
+ if existing_lines:
+ total_quantity = sum(existing_lines.mapped('quantity'))
+
+ if total_quantity < total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' kurang dari quantity demand."
+ ) % (record.product_id.display_name))
+ else:
+ # Check if the quantity exceeds the allowed total
+ if record.quantity < total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' kurang dari quantity demand."
+ ) % (record.product_id.display_name))
+
+ # Set the quantity to the entered value
+ record.quantity = record.quantity
+
+ @api.onchange('code_product')
+ def _onchange_code_product(self):
+ if not self.code_product:
+ return
+
+ # Cari product berdasarkan default_code, barcode, atau barcode_box
+ product = self.env['product.product'].search([
+ '|',
+ ('default_code', '=', self.code_product),
+ '|',
+ ('barcode', '=', self.code_product),
+ ('barcode_box', '=', self.code_product)
+ ], limit=1)
+
+ if not product:
+ raise UserError("Product tidak ditemukan")
+
+ # Jika scan barcode_box, set quantity sesuai qty_pcs_box
+ if product.barcode_box == self.code_product:
+ self.product_id = product.id
+ self.quantity = product.qty_pcs_box
+ self.code_product = product.default_code or product.barcode
+ # return {
+ # 'warning': {
+ # 'title': 'Info',8994175025871
+
+ # 'message': f'Product box terdeteksi. Quantity di-set ke {product.qty_pcs_box}'
+ # }
+ # }
+ else:
+ # Jika scan biasa
+ self.product_id = product.id
+ self.code_product = product.default_code or product.barcode
+ self.quantity = 1
+
+ def unlink(self):
+ # Get all affected pickings before deletion
+ productions = self.mapped('production_id')
+
+ # Store product_ids that will be deleted
+ deleted_product_ids = self.mapped('product_id')
+
+ # Perform the deletion
+ result = super(CheckBomProduct, self).unlink()
+
+ # After deletion, update moves for affected pickings
+ for production in productions:
+ # For products that were completely removed (no remaining check.bom.product lines)
+ remaining_product_ids = production.check_bom_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 = production.move_raw_ids.filtered(
+ lambda move: move.product_id in removed_product_ids
+ )
+ for move in moves_to_reset:
+ move.quantity_done = 0.0
+
+ production.qty_producing = 0
+
+ # Also sync remaining products in case their totals changed
+ self._sync_check_product_to_moves(production)
+
+ return result
+
+ @api.depends('quantity')
+ def _compute_status(self):
+ for record in self:
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ if record.quantity < total_qty_in_moves:
+ record.status = 'Pending'
+ else:
+ record.status = 'Done'
+
+
+ def create(self, vals):
+ # Create the record
+ record = super(CheckBomProduct, self).create(vals)
+ # Ensure uniqueness after creation
+ if not self.env.context.get('skip_consolidate'):
+ record.with_context(skip_consolidate=True)._consolidate_duplicate_lines()
+ return record
+
+ def write(self, vals):
+ # Write changes to the record
+ result = super(CheckBomProduct, self).write(vals)
+ # Ensure uniqueness after writing
+ if not self.env.context.get('skip_consolidate'):
+ self.with_context(skip_consolidate=True)._consolidate_duplicate_lines()
+ return result
+
+ def _sync_check_product_to_moves(self, production):
+ """
+ Sinkronisasi quantity_done di move_raw_ids
+ dengan total quantity dari check.bom.product berdasarkan product_id.
+ """
+ for product_id in production.check_bom_product_lines.mapped('product_id'):
+ # Totalkan quantity dari semua baris check.bom.product untuk product_id ini
+ total_quantity = sum(
+ line.quantity for line in production.check_bom_product_lines.filtered(lambda line: line.product_id == product_id)
+ )
+ # Update quantity_done di move yang relevan
+ moves = production.move_raw_ids.filtered(lambda move: move.product_id == product_id)
+ for move in moves:
+ move.quantity_done = total_quantity
+
+ def _consolidate_duplicate_lines(self):
+ """
+ Consolidate duplicate lines with the same product_id under the same production_id
+ and sync the total quantity to related moves.
+ """
+ for production in self.mapped('production_id'):
+ lines_to_remove = self.env['check.bom.product'] # Recordset untuk menyimpan baris yang akan dihapus
+ product_lines = production.check_bom_product_lines.filtered(lambda line: line.product_id)
+
+ # Group lines by product_id
+ product_groups = {}
+ for line in product_lines:
+ product_groups.setdefault(line.product_id.id, []).append(line)
+
+ for product_id, lines in product_groups.items():
+ if len(lines) > 1:
+ # Consolidate duplicate lines
+ first_line = lines[0]
+ total_quantity = sum(line.quantity for line in lines)
+
+ # Update the first line's quantity
+ first_line.with_context(skip_consolidate=True).write({'quantity': total_quantity})
+
+ # Add the remaining lines to the lines_to_remove recordset
+ lines_to_remove |= self.env['check.bom.product'].browse([line.id for line in lines[1:]])
+
+ # Perform unlink after consolidation
+ if lines_to_remove:
+ lines_to_remove.unlink()
+
+ # Sync total quantities to moves
+ self._sync_check_product_to_moves(production)
+
+ @api.onchange('product_id', 'quantity')
+ def check_product_validity(self):
+ for record in self:
+ if not record.production_id or not record.product_id:
+ continue
+
+ # Filter moves related to the selected product
+ moves = record.production_id.move_raw_ids.filtered(
+ lambda move: move.product_id.id == record.product_id.id
+ )
+
+ if not moves:
+ raise UserError((
+ "The product '%s' tidak ada di operations. "
+ ) % record.product_id.display_name)
+
+ total_qty_in_moves = sum(moves.mapped('product_uom_qty'))
+
+ # Find existing lines for the same product, excluding the current line
+ existing_lines = record.production_id.check_bom_product_lines.filtered(
+ lambda line: line.product_id == record.product_id
+ )
+
+ if existing_lines:
+ # Get the first existing line
+ first_line = existing_lines[0]
+
+ # Calculate the total quantity after addition
+ total_quantity = sum(existing_lines.mapped('quantity'))
+
+ if total_quantity > total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
+ else:
+ # Check if the quantity exceeds the allowed total
+ if record.quantity == total_qty_in_moves:
+ raise UserError((
+ "Quantity Product '%s' sudah melebihi quantity demand."
+ ) % (record.product_id.display_name))
+
+ # Set the quantity to the entered value
+ record.quantity = record.quantity
+
+
+class ProductionPurchaseMatch(models.Model):
+ _name = 'production.purchase.match'
+ _order = 'production_id, id'
+
+ production_id = fields.Many2one('mrp.production', string='Ref', required=True, ondelete='cascade', index=True, copy=False)
+ order_id = fields.Many2one('purchase.order', string='Purchase Order')
+ vendor = fields.Char(string='Vendor', compute='_compute_info_po')
+ total = fields.Float(string='Total', compute='_compute_info_po')
+
+ def _compute_info_po(self):
+ for match in self:
+ match.vendor = match.order_id.partner_id.name
+ match.total = match.order_id.amount_total
+