from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT import logging from dateutil.relativedelta import relativedelta from bs4 import BeautifulSoup from datetime import datetime _logger = logging.getLogger(__name__) class PurchaseOrderLine(models.Model): _inherit = 'purchase.order.line' item_margin = fields.Float( 'Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") item_percent_margin = fields.Float( 'Margin%', compute='compute_item_margin', help="Total % Margin in Sales Order Header") so_item_margin = fields.Float( 'SO Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") so_item_percent_margin = fields.Float( 'SO Margin%', compute='compute_item_margin', help="Total % Margin in Sales Order Header") amount_cashback = fields.Float( 'SO Margin%', compute='_compute_cashback_brand', help="Total % Margin in Sales Order Header") delivery_amt_line = fields.Float('DeliveryAmtLine', compute='compute_delivery_amt_line') line_no = fields.Integer('No', default=0) qty_available = fields.Float('Qty Available', compute='_compute_qty_stock') qty_onhand = fields.Float('Qty On Hand', compute='_compute_qty_stock') qty_incoming = fields.Float('Qty Incoming', compute='_compute_qty_stock') qty_outgoing = fields.Float('Qty Outgoing', compute='_compute_qty_stock') qty_available_store = fields.Float(string='Available') suggest = fields.Char(string='Suggest') price_vendor = fields.Float(string='Price Vendor', compute='compute_price_vendor') so_line_id = fields.Many2one('sale.order.line', string='ID SO Line') so_id = fields.Many2one('sale.order', string='SO') indent = fields.Boolean(string='Indent', help='centang ini jika barang indent') is_ltc = fields.Boolean(string='Sudah di LTC', default=False, help='centang ini jika barang sudah di LTC') note = fields.Char(string='Note') sale_automatic_id = fields.Many2one('sale.order', string='SO') image_small = fields.Binary("Product Image", related="product_id.image_1920") clean_website_description_product = fields.Char(string='Clean Website Description Product', compute='_get_clean_website_description_product') qty_reserved = fields.Float(string='Qty Reserved', compute='_compute_qty_reserved') delete_line = fields.Boolean(string='Delete', default=False, help='centang ini jika anda ingin menghapus line ini') is_edit_product_qty = fields.Boolean(string='Is Edit Product Qty', compute='_compute_is_edit_product_qty') delivery_amt = fields.Float(string='Delivery Amt', compute='_compute_doc_delivery_amt') delivery_amt_per_item = fields.Float(string='Delivery Amt Per Item' , compute='_compute_doc_delivery_amt') contribution_delivery_amt = fields.Float(string='Contribution Delivery Amt', compute='_compute_doc_delivery_amt') cost_service = fields.Float(string='Biaya Jasa', compute='_compute_doc_delivery_amt') cost_service_per_item = fields.Float(string='Biaya Jasa Per Item', compute='_compute_doc_delivery_amt') contribution_cost_service = fields.Float(string='Contribution Cost Service', compute='_compute_doc_delivery_amt') ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt') show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True) price_unit_before = fields.Float(string='Unit Price Before', help="Harga awal yang sebelumnya telah diinputkan") altama_ordered = fields.Float( string='Altama Ordered', default=0.0, copy=False ) altama_delivered = fields.Float( string='Altama Delivered', default=0.0, copy=False ) altama_invoiced = fields.Float( string='Altama Invoiced', default=0.0, copy=False ) description = fields.Text(string='Description', readonly=True, copy=False) docstatus_altama = fields.Text(string='Status Altama', readonly=True, copy=False) @api.onchange('price_unit') def _onchange_price_unit_before(self): if self._origin: self.price_unit_before = self._origin.price_unit def _compute_doc_delivery_amt(self): for line in self: # Inisialisasi nilai default untuk field computed line.delivery_amt = 0.0 line.delivery_amt_per_item = 0.0 line.contribution_delivery_amt = 0.0 line.cost_service = 0.0 line.cost_service_per_item = 0.0 line.contribution_cost_service = 0.0 line.ending_price = line.price_unit * line.product_qty # Ambil nilai dari order_id total_delivery_amt = line.order_id.total_delivery_amt total_cost_service = line.order_id.total_cost_service include_price = line.price_unit * line.product_qty if line.order_id.amount_total > 0: if total_delivery_amt > 0: contributions = include_price / line.order_id.amount_total if line.taxes_id.id == 22: contributions = line.price_subtotal / line.order_id.amount_untaxed contribution = contributions * total_delivery_amt line.delivery_amt = contribution line.delivery_amt_per_item = contribution / line.product_qty line.contribution_delivery_amt = contributions if total_cost_service > 0: contributions = include_price / line.order_id.amount_total if line.taxes_id.id == 22: contributions = line.price_subtotal / line.order_id.amount_untaxed contribution = contributions * total_cost_service line.cost_service = contribution line.cost_service_per_item = contribution / line.product_qty line.contribution_cost_service = contributions if total_delivery_amt > 0 and total_cost_service > 0: line.ending_price = (line.price_unit + line.delivery_amt_per_item + line.cost_service_per_item) * line.product_qty elif total_delivery_amt > 0 and total_cost_service == 0: line.ending_price = (line.price_unit + line.delivery_amt_per_item) * line.product_qty elif total_delivery_amt == 0 and total_cost_service > 0: line.ending_price = (line.price_unit + line.cost_service_per_item) * line.product_qty # @api.constrains('delivery_amt') # def delivery_amt_margin(self): # for line in self: # if line.delivery_amt: # line.delivery_amt_per_item = line.delivery_amt / line.product_qty # line.ending_price = line.price_unit + line.delivery_amt_per_item + line.cost_service # elif line.delivery_amt == 0: # line. delivery_amt_per_item = 0 # if line.cost_service: # line.ending_price = line.price_unit + line.cost_service # else: # line.ending_price = line.price_unit # @api.constrains('cost_service') # def cost_service_margin(self): # for line in self: # if line.cost_service: # line.ending_price = line.price_unit + line.cost_service + line.delivery_amt_per_item # elif line.cost_service == 0: # if line.delivery_amt_per_item: # line.ending_price = line.price_unit + line.delivery_amt_per_item # else: # line.ending_price = line.price_unit def _get_clean_website_description_product(self): for line in self: description = line.product_id.website_description description_clean = BeautifulSoup(description or '', "html.parser").get_text() line.clean_website_description_product = description_clean def _prepare_stock_move_vals(self, picking, price_unit, product_uom_qty, product_uom): self.ensure_one() product = self.product_id.with_context(lang=self.order_id.dest_address_id.lang or self.env.user.lang) description_picking = product._get_description(self.order_id.picking_type_id) if self.product_description_variants: description_picking += "\n" + self.product_description_variants date_planned = self.date_planned or self.order_id.date_planned if self.so_id: sale_id = self.so_id.id else: sale_id = self.so_line_id.order_id.id return { # truncate to 2000 to avoid triggering index limit error # TODO: remove index in master? 'name': (self.name or '')[:2000], 'product_id': self.product_id.id, 'date': date_planned, 'date_deadline': date_planned + relativedelta(days=self.order_id.company_id.po_lead), 'location_id': self.order_id.partner_id.property_stock_supplier.id, 'location_dest_id': (self.orderpoint_id and not (self.move_ids | self.move_dest_ids)) and self.orderpoint_id.location_id.id or self.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': self.order_id.dest_address_id.id, 'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids], 'state': 'draft', 'purchase_line_id': self.id, 'company_id': self.order_id.company_id.id, 'price_unit': price_unit, 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, 'origin': self.order_id.name, 'description_picking': description_picking, 'propagate_cancel': self.propagate_cancel, 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, 'product_uom_qty': product_uom_qty, 'product_uom': product_uom.id, 'sale_id': sale_id, } # @api.constrains('price_unit') def constrains_purchase_price(self): for line in self: matches_so = self.env['purchase.order.sales.match'].search([ ('purchase_order_id', '=', line.order_id.id), ('product_id', '=', line.product_id.id), ]) matches_so.sale_line_id.purchase_price = line.price_unit @api.constrains('product_qty') def constrains_product_qty(self): for line in self: qty_po = 0 matches_so = self.env['purchase.order.sales.match'].search([ ('purchase_order_id', '=', line.order_id.id), ('product_id', '=', line.product_id.id), ]) if not matches_so: continue for matches in matches_so: qty_po += matches.qty_po if qty_po == line.product_qty: continue oldest_date_order = min(matches_so.mapped('sale_id.date_order')) oldest_matches = matches_so.filtered(lambda x: x.sale_id.date_order == oldest_date_order) matches_to_remove = matches_so - oldest_matches if matches_to_remove: matches_to_remove.unlink() def unlink(self): for line in self: mathces_so = self.env['purchase.order.sales.match'].search([ ('purchase_order_id', '=', line.order_id.id), ('product_id', '=', line.product_id.id), ]) mathces_so.unlink() return super(PurchaseOrderLine, self).unlink() def _compute_is_edit_product_qty(self): for line in self: if line.order_id.state in ['draft']: is_valid = True else: is_valid = False line.is_edit_product_qty = is_valid # @api.constrains('product_qty') # def change_qty_po_and_qty_demand(self): # for line in self: # if line.order_id.state in ['draft', 'cancel'] and len(line.order_id.picking_ids) == 0: # continue # for stock_picking in line.order_id.picking_ids: # picking = self.env['stock.move'].search([ # ('picking_id.purchase_id', '=', line.order_id.id), # ('product_id', '=', line.product_id.id) # ]) # if picking: # picking.write({ # 'product_uom_qty': line.product_qty # }) def _compute_qty_reserved(self): for line in self: sale_line = self.env['sale.order.line'].search([ ('product_id', '=', line.product_id.id), ('order_id', '=', line.order_id.sale_order_id.id) ]) reserved_qty = sum(line.qty_reserved for line in sale_line) line.qty_reserved = reserved_qty # so_line.qty_reserved (compute) # so_line.qty_reserved = get from picking_ids where type outgoing and prodid = line.prodid # po_line.qty_reserved = cek dulu apakah ada relasi ke sale order. Jika ada maka ambil sesuai yang ada di sale order (so_line.qty_reserved), # jika tidak maka 0 def suggest_purchasing(self): for line in self: if line.product_id.qty_available_bandengan + line.qty_reserved < line.product_qty: line.suggest = 'harus beli' else: line.suggest = 'masih cukup' def compute_price_vendor(self): for line in self: purchase_pricelist = self.env['purchase.pricelist'].search([ ('product_id', '=', line.product_id.id), ('vendor_id', '=', line.order_id.partner_id.id) ], limit=1) if purchase_pricelist: price_vendor = format(purchase_pricelist.product_price, ".2f") price_vendor = float(price_vendor) line.price_vendor = price_vendor else: line.price_vendor = 0 def _compute_qty_stock(self): for line in self: line.qty_available = line.product_id.qty_available_bandengan line.qty_onhand = line.product_id.qty_onhand_bandengan line.qty_incoming = line.product_id.qty_incoming_bandengan line.qty_outgoing = line.product_id.qty_outgoing_bandengan @api.onchange('product_id') def _onchange_product_custom(self): self._compute_qty_stock() @api.onchange('product_id','product_qty', 'product_uom') def _onchange_quantity(self): res = super(PurchaseOrderLine, self)._onchange_quantity() purchase_pricelist = self.env['purchase.pricelist'].search([ ('product_id', '=', self.product_id.id), ('vendor_id', '=', self.partner_id.id), ], limit=1) price_unit = purchase_pricelist.product_price if not price_unit: product_supplierinfo = self.env['product.supplierinfo'].search([ ('product_tmpl_id', '=', self.product_id.product_tmpl_id.id), ('name', '=', self.partner_id.id) ], limit=1) price_unit = product_supplierinfo.price price_unit, taxes = self._get_valid_purchase_price(purchase_pricelist) self.price_unit = price_unit if purchase_pricelist.taxes_product_id or purchase_pricelist.taxes_system_id: self.taxes_id = taxes return res def _get_valid_purchase_price(self, purchase_price): price = 0 taxes = None human_last_update = purchase_price.human_last_update or datetime.min system_last_update = purchase_price.system_last_update or datetime.min if system_last_update > human_last_update: price = purchase_price.system_price taxes = [purchase.taxes_system_id.id for purchase in purchase_price ] else: price = purchase_price.product_price taxes = [purchase.taxes_product_id.id for purchase in purchase_price ] return price, taxes def compute_item_margin(self): sum_so_margin = sum_sales_price = sum_margin = 0 for line in self: product = line.product_id order = line.order_id # Skip jika tidak ada product_id, produk adalah service, atau tidak ada purchase order terkait if not product or product.type == 'service' or not order: line.so_item_margin = 0 line.so_item_percent_margin = 0 line.item_margin = 0 line.item_percent_margin = 0 continue # Cari semua sale.order.line terkait dengan purchase.order melalui tabel purchase.order.sales.match sales_matches = self.env['purchase.order.sales.match'].search([ ('purchase_order_id', '=', order.id), ('product_id', '=', product.id) ]) total_sales_price = total_margin = total_qty_so = 0 for match in sales_matches: sale_order_line = match.sale_line_id # Hitung harga jual setelah mempertimbangkan biaya tambahan sales_price = sale_order_line.price_reduce_taxexcl * match.qty_so if sale_order_line.order_id.shipping_cost_covered == 'indoteknik': sales_price -= sale_order_line.delivery_amt_line if sale_order_line.order_id.fee_third_party > 0: sales_price -= sale_order_line.fee_third_party_line total_sales_price += sales_price total_margin += sale_order_line.item_margin total_qty_so += match.qty_so # Set margin berdasarkan total dari semua sales order yang terkait line.so_item_margin = total_margin line.so_item_percent_margin = (total_margin / total_sales_price) * 100 if total_sales_price else 0 sum_so_margin += total_margin sum_sales_price += total_sales_price # Hitung harga pembelian dengan mempertimbangkan biaya pengiriman purchase_price = line.price_subtotal if order.delivery_amount > 0: purchase_price += line.delivery_amt_line if line.amount_cashback > 0: purchase_price = purchase_price - line.amount_cashback # Hitung margin dan persentase margin real_item_margin = total_sales_price - purchase_price real_item_percent_margin = (real_item_margin / total_sales_price) * 100 if total_sales_price else 0 # Set nilai margin ke dalam line line.item_margin = real_item_margin line.item_percent_margin = real_item_percent_margin sum_margin += real_item_margin def _compute_cashback_brand(self): start_date = datetime(2026, 2, 1, 0, 0, 0) for line in self: line.amount_cashback = 0.0 product = line.product_id order = line.order_id if not product or not order: continue if order.partner_id.id != 5571: continue sales_matches = self.env['purchase.order.sales.match'].search([ ('purchase_order_id', '=', order.id), ('product_id', '=', product.id) ]) total_cashback = 0.0 for match in sales_matches: so_line = match.sale_line_id so_order = so_line.order_id if not so_order.date_order or so_order.date_order < start_date: continue cashback_percent = product.x_manufacture.cashback_percent or 0.0 if cashback_percent <= 0: continue sales_price = so_line.price_reduce_taxexcl * match.qty_so cashback = sales_price * cashback_percent total_cashback += cashback line.amount_cashback = total_cashback def compute_delivery_amt_line(self): for line in self: if line.product_id.type == 'product': contribution = round((line.price_total / line.order_id.amount_total_without_service), 2) delivery_amt = line.order_id.delivery_amount line.delivery_amt_line = delivery_amt * contribution else: line.delivery_amt_line = 0