from odoo import fields, models, api, _ from odoo.exceptions import UserError from datetime import datetime, timedelta import logging from odoo.tools.float_utils import float_compare _logger = logging.getLogger(__name__) class SaleOrderLine(models.Model): _inherit = 'sale.order.line' item_margin = fields.Float('Margin', compute='compute_item_margin', help="Total Margin in Sales Order Header") item_before_margin = fields.Float('Before Margin', compute='compute_item_before_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") item_percent_margin_before = fields.Float('%Margin Before', compute='_compute_item_percent_margin_before', help="Total % Margin excluding third party in Sales Order Header") amount_cashback = fields.Float('Cashback Brand', compute='_compute_cashback_brand', help='Cashback from product who has cashback percent in manufacture') initial_discount = fields.Float('Initial Discount') vendor_id = fields.Many2one( 'res.partner', string='Vendor', readonly=True, change_default=True, index=True, tracking=1, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]" ) vendor_md_id = fields.Many2one('res.partner', string='MD Vendor') purchase_price = fields.Float('Purchase', required=True, digits='Product Price', default=0.0) purchase_price_md = fields.Float('MD Purchase') purchase_tax_id = fields.Many2one('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)]) delivery_amt_line = fields.Float('DeliveryAmtLine', compute='compute_delivery_amt_line') fee_third_party_line = fields.Float('FeeThirdPartyLine', compute='compute_fee_third_party_line', default=0) line_no = fields.Integer('No', default=0, copy=False) note = fields.Selection([ ('eta', 'ETA'), ('info_sales', 'Info Sales'), ('info_vendor', 'Info Vendor'), ('penggabungan', 'Penggabungan'), ], string='Note', help="Harap diisi jika ada keterangan tambahan dari Procurement, agar dapat dimonitoring") note_procurement = fields.Char(string='Note Detail', help="Harap diisi jika ada keterangan tambahan dari Procurement, agar dapat dimonitoring") vendor_subtotal = fields.Float(string='Vendor Subtotal', compute="_compute_vendor_subtotal") amount_voucher_disc = fields.Float(string='Voucher Discount') qty_reserved = fields.Float(string='Qty Reserved', compute='_compute_qty_reserved') product_available_quantity = fields.Float(string='Qty pickup by user', ) reserved_from = fields.Char(string='Reserved From', copy=False) item_percent_margin_without_deduction = fields.Float('Margin Without Deduction', compute='_compute_item_margin_without_deduction') weight = fields.Float(string='Weight') md_vendor_id = fields.Many2one('res.partner', string='MD Vendor', readonly=True) margin_md = fields.Float(string='Margin MD') qty_free_bu = fields.Float(string='Free BU', compute='_get_qty_free_bandengan') desc_updatable = fields.Boolean(string='desc boolean', default=True, compute='_get_desc_updatable') is_has_disc = fields.Boolean('Flash Sale', default=False) reserved_percent = fields.Float(string="Reserved %", digits=(16, 2), compute="_compute_reserved_delivered_pie", store=False) 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.state', 'move_ids.reserved_availability', 'move_ids.quantity_done', 'move_ids.picking_type_id' ) 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: for move in line.move_ids: if move.state not in ('done', 'cancel'): reserved_qty += move.reserved_availability or 0.0 continue if move.location_dest_id.usage == 'customer': delivered_qty += move.quantity_done or 0.0 elif move.location_id.usage == 'customer': delivered_qty -= move.quantity_done or 0.0 delivered_qty = max(delivered_qty, 0) line.reserved_percent = min((reserved_qty / order_qty) * 100, 100) if order_qty else 0 line.delivered_percent = min((delivered_qty / order_qty) * 100, 100) if order_qty else 0 line.unreserved_percent = max(100 - line.reserved_percent - line.delivered_percent, 0) def _get_outgoing_incoming_moves(self): outgoing_moves = self.env['stock.move'] incoming_moves = self.env['stock.move'] for move in self.move_ids.filtered( lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id): if move.location_dest_id.usage == "customer": if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund): outgoing_moves |= move elif move.location_id.usage == "customer" and move.to_refund: incoming_moves |= move return outgoing_moves, incoming_moves def _get_desc_updatable(self): for line in self: if line.product_id.id != 417724 and line.product_id.id: line.desc_updatable = False else: line.desc_updatable = True def _get_qty_free_bandengan(self): for line in self: line.qty_free_bu = line.product_id.qty_free_bandengan @api.constrains('note_procurement') def note_procurement_to_apo(self): for line in self: matches_so = self.env['automatic.purchase.sales.match'].search([ ('sale_line_id', '=', line.id), ]) for match_so in matches_so: match_so.note_procurement = line.note_procurement @api.onchange('product_uom', 'product_uom_qty') def product_uom_change(self): if not self.product_uom or not self.product_id: self.price_unit = 0.0 return self.price_unit = self.price_unit def _compute_qty_reserved(self): for line in self: stock_moves = self.env['stock.move.line'].search([ ('product_id', '=', line.product_id.id), ('picking_id.sale_id', '=', line.order_id.id), ('picking_id.state', 'not in', ['cancel', 'done']), ]) reserved_qty = sum(move.product_uom_qty for move in stock_moves) line.qty_reserved = reserved_qty def _compute_reserved_from(self): for line in self: report_stock_forecasted = self.env['report.stock.report_product_product_replenishment'] report_stock_forecasted._get_report_data(False, [line.product_id.id]) def _compute_vendor_subtotal(self): for line in self: if line.purchase_price > 0 and line.product_uom_qty > 0: line.vendor_subtotal = line.purchase_price * line.product_uom_qty else: line.vendor_subtotal = 0 def _compute_item_margin_without_deduction(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ or line.price_unit <= 0 or line.product_uom_qty <= 0 \ or not line.vendor_id: line.item_percent_margin_without_deduction = 0 continue # calculate margin without tax sales_price = line.price_reduce_taxexcl * line.product_uom_qty purchase_price = line.purchase_price if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) if line.amount_cashback > 0: purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price if sales_price > 0: line.item_percent_margin_without_deduction = round((margin_per_item / sales_price), 2) * 100 else: line.item_percent_margin_without_deduction = 0 def _compute_item_percent_margin_before(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ or line.price_unit <= 0 or line.product_uom_qty <= 0 \ or not line.vendor_id: line.item_percent_margin_before = 0 continue sales_price = line.price_reduce_taxexcl * line.product_uom_qty purchase_price = line.purchase_price if line.purchase_tax_id and line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) if line.amount_cashback > 0: purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_before = sales_price - purchase_price if sales_price > 0: line.item_percent_margin_before = round((margin_before / sales_price), 2) * 100 else: line.item_percent_margin_before = 0 def compute_item_margin(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ or line.price_unit <= 0 or line.product_uom_qty <= 0 \ or not line.vendor_id: line.item_margin = 0 line.item_percent_margin = 0 continue # calculate margin without tax sales_price = line.price_reduce_taxexcl * line.product_uom_qty # minus with delivery if covered by indoteknik if line.order_id.shipping_cost_covered == 'indoteknik': sales_price -= line.delivery_amt_line # if line.order_id.fee_third_party > 0: # sales_price -= line.fee_third_party_line purchase_price = line.purchase_price if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) if line.amount_cashback > 0: purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price line.item_margin = margin_per_item if sales_price > 0: line.item_percent_margin = round((margin_per_item / sales_price), 2) * 100 else: line.item_percent_margin = 0 if not line.margin_md: line.margin_md = line.item_percent_margin def compute_item_before_margin(self): for line in self: if not line.product_id or line.product_id.type == 'service' \ or line.price_unit <= 0 or line.product_uom_qty <= 0 \ or not line.vendor_id: line.item_before_margin = 0 continue # calculate margin without tax sales_price = line.price_reduce_taxexcl * line.product_uom_qty purchase_price = line.purchase_price if line.purchase_tax_id.price_include: purchase_price = line.purchase_price / (1 + (line.purchase_tax_id.amount / 100)) if line.amount_cashback > 0: purchase_price = purchase_price - line.amount_cashback purchase_price = purchase_price * line.product_uom_qty margin_per_item = sales_price - purchase_price line.item_before_margin = margin_per_item def _compute_cashback_brand(self): start_date = datetime(2026, 2, 1, 0, 0, 0) for line in self: line.amount_cashback = 0 if not line.product_id: continue if line.order_id.date_order < start_date: continue price, taxes, vendor_id = self._get_purchase_price(line.product_id) cashback_percent = line.product_id.x_manufacture.cashback_percent or 0 if cashback_percent <= 0: continue if line.vendor_id.id != 5571: continue price_tax_excl = price if taxes: tax = self.env['account.tax'].browse(taxes) if tax.price_include: price_tax_excl = price / (1 + (tax.amount / 100)) else: price_tax_excl = price line.amount_cashback = price_tax_excl * cashback_percent # @api.onchange('vendor_id') # def onchange_vendor_id(self): # # TODO : need to change this logic @stephan # if not self.product_id or self.product_id.type == 'service': # return # elif self.product_id.categ_id.id == 34: # finish good / manufacturing only # cost = self.product_id.standard_price # self.purchase_price = cost # elif self.product_id.x_manufacture.override_vendor_id: # # purchase_price = self.env['purchase.pricelist'].search( # # [('vendor_id', '=', self.product_id.x_manufacture.override_vendor_id.id), # # ('product_id', '=', self.product_id.id)], # # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') # price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id) # self.purchase_price = price # self.purchase_tax_id = taxes # # else: # # purchase_price = self.env['purchase.pricelist'].search( # # [('vendor_id', '=', self.vendor_id.id), ('product_id', '=', self.product_id.id)], # # limit=1, order='count_trx_po desc, count_trx_po_vendor desc') # # price, taxes = self._get_valid_purchase_price(purchase_price) # # self.purchase_price = price # # self.purchase_tax_id = taxes # def _calculate_selling_price(self): # rec_purchase_price, rec_taxes, rec_vendor_id = self._get_purchase_price(self.product_id) # state = ['sale', 'done'] # last_so = self.env['sale.order.line'].search([ # ('order_id.partner_id.id', '=', self.order_id.partner_id.id), # ('product_id.id', '=', self.product_id.id), # ('order_id.state', 'in', state) # ], limit=1, order='create_date desc') # # if rec_vendor_id == self.vendor_id and rec_purchase_price == last_so.purchase_price: # # selling_price = last_so.price_unit # # tax_id = last_so.tax_id # if rec_vendor_id == self.vendor_id and rec_purchase_price != last_so.purchase_price: # if rec_taxes.price_include: # selling_price = (rec_purchase_price/1.11) / (1-(last_so.line_item_margin / 100)) # else: # selling_price = rec_purchase_price / (1-(last_so.line_item_margin / 100)) # tax_id = last_so.tax_id # elif rec_vendor_id != last_so.vendor_id: # last_so = self.env['sale.order.line'].search([ # ('order_id.partner_id.id', '=', self.order_id.partner_id.id), # ('product_id.id', '=', self.product_id.id), # ('state', 'in', state), # ('vendor_id', '=', rec_vendor_id) # ], limit=1, order='order_id.date_order desc') # selling_price = last_so.price_unit # tax_id = last_so.tax_id # else: # selling_price = last_so.price_unit # tax_id = last_so.tax_id # self.price_unit = selling_price # self.tax_id = tax_id def _get_purchase_price(self, product_id): 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_purchase_price_by_vendor(self, product_id, vendor_id): purchase_price = self.env['purchase.pricelist'].search( [('product_id', '=', product_id.id), ('vendor_id', '=', vendor_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) default_timestamp = datetime(1970, 1, 1, 0, 0, 0) # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') price = 0 taxes = 24 vendor_id = False 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 or 24 vendor_id = purchase_price.vendor_id.id if delta_time > human_last_update: price = 0 taxes = 24 vendor_id = False 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 or 24 vendor_id = purchase_price.vendor_id.id if delta_time > system_last_update: price = 0 taxes = 24 vendor_id = False return price, taxes, vendor_id @api.onchange('product_id') def product_id_change(self): # need to change purchase price logic @stephan super(SaleOrderLine, self).product_id_change() for line in self: if line.product_id and line.product_id.type == 'product': # query = [('product_id', '=', line.product_id.id)] # if line.product_id.x_manufacture.override_vendor_id: # query = [('product_id', '=', line.product_id.id), # ('vendor_id', '=', line.product_id.x_manufacture.override_vendor_id.id)] # purchase_price = self.env['purchase.pricelist'].search( # query, limit=1, order='count_trx_po desc, count_trx_po_vendor desc') price, taxes, vendor_id = self._get_purchase_price(line.product_id) line.vendor_id = vendor_id line.tax_id = line.order_id.sales_tax_id # price, taxes = line._get_valid_purchase_price(purchase_price) line.purchase_price = price line.purchase_tax_id = taxes attribute_values = line.product_id.product_template_attribute_value_ids.mapped('name') attribute_values_str = ', '.join(attribute_values) if attribute_values else '' line_name = ('[' + line.product_id.default_code + ']' if line.product_id.default_code else '') + ' ' + \ (line.product_id.name if line.product_id.name else '') + ' ' + \ ('(' + attribute_values_str + ')' if attribute_values_str else '') + ' ' + \ (line.product_id.short_spesification if line.product_id.short_spesification else '') line.name = line_name line.weight = line.product_id.weight if line.product_id.id != 417724 and line.product_id.id: line.desc_updatable = False else: line.desc_updatable = True @api.constrains('vendor_id') def _check_vendor_id(self): for line in self: price, taxes, vendor_id = self._get_purchase_price(line.product_id) line.vendor_md_id = vendor_id if vendor_id else None line.margin_md = line.item_percent_margin line.purchase_price_md = price def compute_delivery_amt_line(self): for line in self: try: contribution = round((line.price_total / line.order_id.amount_total), 2) except: contribution = 0 delivery_amt = line.order_id.delivery_amt line.delivery_amt_line = delivery_amt * contribution def compute_fee_third_party_line(self): for line in self: try: contribution = round((line.price_total / line.order_id.amount_total), 2) except: contribution = 0 fee = line.order_id.fee_third_party line.fee_third_party_line = fee * contribution @api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id') def _onchange_discount(self): if not (self.product_id and self.product_uom and self.order_id.partner_id and self.order_id.pricelist_id and self.order_id.pricelist_id.discount_policy == 'without_discount' and self.env.user.has_group('product.group_discount_per_so_line')): return self.discount = 0.0 product = self.product_id.with_context( lang=self.order_id.partner_id.lang, partner=self.order_id.partner_id, quantity=self.product_uom_qty, date=self.order_id.date_order, pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id, fiscal_position=self.env.context.get('fiscal_position') ) product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule( self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) new_list_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) new_list_price = product.web_price if new_list_price != 0: if self.order_id.pricelist_id.currency_id != currency: # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency new_list_price = currency._convert( new_list_price, self.order_id.pricelist_id.currency_id, self.order_id.company_id or self.env.company, self.order_id.date_order or fields.Date.today()) discount = (new_list_price - price) / new_list_price * 100 if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0): self.discount = discount def _get_display_price(self, product): # TO DO: move me in master/saas-16 on sale.order # awa: don't know if it's still the case since we need the "product_no_variant_attribute_value_ids" field now # to be able to compute the full price # it is possible that a no_variant attribute is still in a variant if # the type of the attribute has been changed after creation. no_variant_attributes_price_extra = [ ptav.price_extra for ptav in self.product_no_variant_attribute_value_ids.filtered( lambda ptav: ptav.price_extra and ptav not in product.product_template_attribute_value_ids ) ] if no_variant_attributes_price_extra: product = product.with_context( no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra) ) if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) final_price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule( product or self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) base_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) base_price = product.web_price if currency != self.order_id.pricelist_id.currency_id: base_price = currency._convert( base_price, self.order_id.pricelist_id.currency_id, self.order_id.company_id or self.env.company, self.order_id.date_order or fields.Date.today()) # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) def validate_line(self): for line in self: if line.product_id.id in [385544, 224484, 417724]: raise UserError('Produk Sementara Tidak Bisa Di Confirm atau Ask Approval') if not line.product_id or line.product_id.type == 'service': continue if not line.product_id.product_tmpl_id.sale_ok: raise UserError('Product %s belum bisa dijual, harap hubungi finance' % line.product_id.display_name) if not line.vendor_id or not line.purchase_price and not line.display_type == 'line_note': raise UserError(_('Isi Vendor dan Harga Beli sebelum Request Approval')) @api.depends('state') def _compute_product_updatable(self): for line in self: if line.state == 'draft': line.product_updatable = True # line.desc_updatable = True else: line.product_updatable = False # line.desc_updatable = False @api.onchange('vendor_id') def _onchange_vendor_id_custom(self): self._update_purchase_info() def _update_purchase_info(self): if not self.product_id or self.product_id.type == 'service': return if self.product_id.categ_id.id == 34: self.purchase_price = self.product_id.standard_price self.purchase_tax_id = False elif self.product_id.x_manufacture.override_vendor_id: price, taxes, vendor_id = self._get_purchase_price_by_vendor(self.product_id, self.vendor_id) self.purchase_price = price self.purchase_tax_id = taxes