Skip to content

Commit

Permalink
BAH-3736, BAH-3751 | Product and Lot price details updation on PO Con…
Browse files Browse the repository at this point in the history
…firmation and Stock Picking (#165)

* BAH-3736 | Add. Extract method to calculate markup price into markup table domain

* BAH-3736 | Refactor. Update MRP only on change of product quantity

* BAH-3736 | Refactor. Use extracted function for markup price calculation

* BAH-3736 | Refactor. Remove unused imports

* BAH-3736 | Add. Show MRP field in product supplier info details

* BAH-3736 | Add. Configuration to enable updation of product price details

* BAH-3736 | Add. Update product price details on purchase order confirmation

* BAH-3736 | Refactor. Establish proper inheritance for setting price details for a lot

* BAH-3736 | Refactor. Extract batch quantity validation to a separate function in picking validate

* BAH-3736 | Add. Update price details for lots on confirmation of a receipt picking

* BAH-3736 | Fix. Use product master mrp when seller mrp is not set

* BAH-3736 | Fix. Update sale price only when markup is applied

* BAH-3736 | Fix. Readonly policies for lot id when existing lot is chosen
  • Loading branch information
mohan-13 authored May 17, 2024
1 parent b97c4a6 commit 24dd22d
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 175 deletions.
12 changes: 11 additions & 1 deletion bahmni_product/views/product_supplierinfo_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<field name="inherit_id" ref="product.product_supplierinfo_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group/group[1]/div" position="after">
<field name="manufacturer"
<field name="manufacturer"
context="{'default_manufacturer': True, 'default_customer': False}"/>
</xpath>
<xpath expr="//group/group[2]/div[2]" position="after">
Expand All @@ -17,4 +17,14 @@
</xpath>
</field>
</record>
<record id="product_supplierinfo_tree_with_mrp" model="ir.ui.view">
<field name="name">product.supplierinfo.with.mrp</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='price']" position="after">
<field name="mrp"/>
</xpath>
</field>
</record>
</odoo>
1 change: 1 addition & 0 deletions bahmni_purchase/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
'data': ['security/ir.model.access.csv',
'views/purchase_views.xml',
'views/product_view.xml',
'views/res_config_inherit.xml',
'views/price_markup_table_view.xml'],
'demo': [],
'qweb': [],
Expand Down
2 changes: 2 additions & 0 deletions bahmni_purchase/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from . import purchase_order_line
from . import product
from . import price_markup_table
from . import purchase_order
from . import res_config_settings
36 changes: 23 additions & 13 deletions bahmni_purchase/models/price_markup_table.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from odoo import fields, models
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError, ValidationError, AccessError, RedirectWarning
from odoo.exceptions import ValidationError


class PriceMarkupTable(models.Model):
Expand All @@ -11,22 +10,33 @@ class PriceMarkupTable(models.Model):
markup_percentage = fields.Float(string="Markup Percentage", default=1)

@api.constrains('lower_price', 'higher_price','markup_percentage')
def _check_fields_values(self):
def _check_fields_values(self):
if self.lower_price < 0 or self.higher_price < 0 or self.markup_percentage < 0:
raise ValidationError('Negative values are not allowed for Minimum Cost, Maximum Cost and Markup Percentage.')
raise ValidationError('Negative values are not allowed for Minimum Cost, Maximum Cost and Markup Percentage.')

if self.markup_percentage > 100:
raise ValidationError('Markup percentage should not exceed 100%. Please enter a valid markup percentage.')
raise ValidationError('Markup percentage should not exceed 100%. Please enter a valid markup percentage.')

if self.lower_price >= self.higher_price:
raise ValidationError('Minimum cost should not be greater than maximum cost.')
# Add any other conditions you need to check
for data in self.env['price.markup.table'].search([]):
if data.lower_price < self.lower_price < data.higher_price and data.id != self.id:
# Add any other conditions you need to check
for data in self.env['price.markup.table'].search([]):
if data.lower_price < self.lower_price < data.higher_price and data.id != self.id:
raise ValidationError('Your minimum cost is available within the range of minimum cost and maximum cost of previous records.')

if data.lower_price < self.higher_price < data.higher_price and data.id != self.id:
raise ValidationError('Your maximum cost is available within the range of minimum cost and maximum cost of previous records.')
if self.lower_price < data.lower_price < self.higher_price and data.id != self.id:

if self.lower_price < data.lower_price < self.higher_price and data.id != self.id:
raise ValidationError('Your minimum cost is available within the range of minimum cost and maximum cost of previous records.')

def calculate_price_with_markup(self, price):
markup_table_line = self.search([('lower_price', '<', price),
'|', ('higher_price', '>=', price),
('higher_price', '=', 0)], limit=1)
if markup_table_line:
return price + (price * markup_table_line.markup_percentage / 100)
else:
return price


45 changes: 45 additions & 0 deletions bahmni_purchase/models/purchase_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from odoo import models, fields, api


class PurchaseOrder(models.Model):
_inherit = 'purchase.order'

def button_confirm(self):
res = super(PurchaseOrder, self).button_confirm()
if bool(self.env['ir.config_parameter'].sudo().get_param(
'bahmni_purchase.update_product_prices_on_po_confirm')):
for order in self:
for line in order.order_line:
price_unit, sale_price, mrp = self._calculate_prices(line)
self._update_price_for_supplier(line.product_id.product_tmpl_id.id, price_unit, mrp)
self._update_price_for_product(line.product_id, price_unit, sale_price, mrp)
return res

def _calculate_prices(self, purchase_order_line):
price_unit = purchase_order_line.price_unit
mrp = purchase_order_line.mrp
tax = purchase_order_line.price_tax / purchase_order_line.product_qty if purchase_order_line.product_qty > 0 else purchase_order_line.price_tax
# Compute the price_unit,mrp for the template's UoM, because the supplier's UoM is related to that UoM.
if purchase_order_line.product_id.product_tmpl_id.uom_po_id != purchase_order_line.product_uom:
default_uom = purchase_order_line.product_id.product_tmpl_id.uom_po_id
price_unit = purchase_order_line.product_uom._compute_price(price_unit, default_uom)
mrp = purchase_order_line.product_uom._compute_price(mrp, default_uom)
tax = purchase_order_line.product_uom._compute_price(tax, default_uom)
total_cost = price_unit + tax
sale_price = self.env['price.markup.table'].calculate_price_with_markup(total_cost)
# Set sale_price as none if no markup value is added
if sale_price == total_cost:
sale_price = None
return price_unit, sale_price, mrp

def _update_price_for_product(self, product_id, price_unit, sale_price, mrp):
if sale_price:
product_id.write({'standard_price': price_unit, 'list_price': sale_price, 'mrp': mrp})
else:
product_id.write({'standard_price': price_unit, 'mrp': mrp})

def _update_price_for_supplier(self, product_tmpl_id, price_unit, mrp):
seller_info = self.env['product.supplierinfo'].search([('partner_id', '=', self.partner_id.id),
('product_tmpl_id', '=', product_tmpl_id)])
seller_info.mrp = mrp
seller_info.price = price_unit
27 changes: 12 additions & 15 deletions bahmni_purchase/models/purchase_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def onchange_product_id(self):
@api.onchange('product_qty', 'product_uom')
def _onchange_quantity(self):
'''Method to get mrp for product from vendor configuration in product master'''
if not self.product_id:
if not self.product_id or not self.product_uom:
return

seller = self.product_id._select_seller(
Expand All @@ -33,19 +33,16 @@ def _onchange_quantity(self):
if seller or not self.date_planned:
self.date_planned = self._get_date_planned(seller).strftime(DTF)

if not seller:
self.mrp = self.product_id.mrp
return
if not seller or (seller and seller.mrp == 0):
mrp = self.product_id.product_tmpl_id.mrp
if self.product_id.product_tmpl_id.uom_po_id != self.product_uom:
default_uom = self.product_id.product_tmpl_id.uom_po_id
mrp = default_uom._compute_price(mrp, self.product_uom)
else:
mrp = seller.mrp
if mrp and self.product_uom and seller.product_uom != self.product_uom:
mrp = seller.product_uom._compute_price(mrp, self.product_uom)
if mrp and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
mrp = seller.currency_id.compute(mrp, self.order_id.currency_id)
self.manufacturer = seller.manufacturer.id
price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0
mrp = self.env['account.tax']._fix_tax_included_price_company(seller.mrp, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0
if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id)
if mrp and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
mrp = seller.currency_id.compute(mrp, self.order_id.currency_id)
if seller and self.product_uom and seller.product_uom != self.product_uom:
price_unit = seller.product_uom._compute_price(price_unit, self.product_uom)
if mrp and self.product_uom and seller.product_uom != self.product_uom:
mrp = seller.product_uom._compute_price(mrp, self.product_uom)
self.price_unit = price_unit
self.mrp = mrp
24 changes: 24 additions & 0 deletions bahmni_purchase/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from odoo import models, fields, api


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

update_product_prices_on_po_confirmation = fields.Boolean(
string="Update price details of the product on Purchase Order Confirmation",
config_parameter="bahmni_purchase.update_product_prices_on_po_confirm")

def set_values(self):
res = super(ResConfigSettings, self).set_values()
self.env['ir.config_parameter'].sudo().set_param('bahmni_purchase.update_product_prices_on_po_confirm',
self.update_product_prices_on_po_confirmation)
return res

@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
ICPSudo = self.env['ir.config_parameter'].sudo()
res.update(
update_product_prices_on_po_confirmation=ICPSudo.get_param('bahmni_purchase.update_product_prices_on_po_confirm'),
)
return res
29 changes: 29 additions & 0 deletions bahmni_purchase/views/res_config_inherit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="res_config_settings_view_bahmni_payments" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.bahmni.payments</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="1"/>
<field name="inherit_id" ref="bahmni_sale.res_config_settings_view_form_i"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='bahmni-custom-configs']" position="inside">
<div class="row mt16 o_settings_container" name="bahmni_purchase_configurations">
<h2 style="text-align: center;">Purchase Configurations</h2>
<div class="col-12 col-lg-6 o_setting_box" id="variant_options">
<div class="o_setting_left_pane">
<field name="update_product_prices_on_po_confirmation"/>
</div>
<div class="o_setting_right_pane">
<label for="update_product_prices_on_po_confirmation"/>
<div class="text-muted">
Enabling this config will update the Cost, Sale Price, MRP of the product from the PO on confirmation
</div>
</div>

</div>
</div>

</xpath>
</field>
</record>
</odoo>
112 changes: 38 additions & 74 deletions bahmni_stock/models/stock_move_line.py
Original file line number Diff line number Diff line change
@@ -1,109 +1,73 @@

import datetime
from dateutil import tz
from collections import Counter, defaultdict

from collections import defaultdict
from odoo import models, fields, api
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DTF

from odoo.exceptions import UserError, ValidationError, AccessError, RedirectWarning


class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
_inherit = 'stock.move.line'

sale_price = fields.Float(string="Sale Price")
mrp = fields.Float(string="MRP")
cost_price = fields.Float(string="Cost Price")
balance = fields.Float(string="Balance")
cost_price = fields.Float(string="Cost Price")
balance = fields.Float(string="Balance")
existing_lot_id = fields.Many2one(
'stock.lot', 'Lot/Serial Number',
domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True)

@api.onchange('product_id','qty_done')
def _onchange_balance_qty(self):
def _onchange_balance_qty(self):
if self.location_id and self.product_id:
self.balance = (sum([stock.inventory_quantity_auto_apply for stock in self.env['stock.quant'].search([('location_id', '=', self.location_id.id),('product_id', '=', self.product_id.id)])])) - self.qty_done

@api.onchange('existing_lot_id')
def _onchange_existing_lot_id(self):
if self.existing_lot_id:
self.lot_name = self.existing_lot_id.name
self.expiration_date = self.existing_lot_id.expiration_date
def _onchange_existing_lot_id(self):
if self.existing_lot_id:
self.lot_name = self.existing_lot_id.name
self.expiration_date = self.existing_lot_id.expiration_date
else:
self.lot_name = ''
self.lot_name = ''
self.expiration_date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time)

@api.constrains('mrp')
def _check_fields_values(self):
for rec in self:
if rec.mrp > 0.00:
if rec.mrp > 0.00:
if rec.sale_price > rec.mrp:
raise ValidationError('Sales Price should not be greater than MRP Rate.')
else:
pass

@api.onchange('cost_price')
def _check_cost_price_values(self):
if self.cost_price > 0.00:
markup_table_line = self.env['price.markup.table'].search([('lower_price', '<', self.cost_price),
'|', ('higher_price', '>=', self.cost_price),
('higher_price', '=', 0)],limit=1)
if markup_table_line:
self.sale_price = self.cost_price + (self.cost_price * markup_table_line.markup_percentage / 100)
else:
self.sale_price = self.cost_price
else:
pass

def _check_cost_price_values(self):
if self.cost_price > 0.00:
self.sale_price = self.env['price.markup.table'].calculate_price_with_markup(self.cost_price)

@api.model
def default_get(self, fields):
res = super(StockMoveLine, self).default_get(fields)
cost_value = 0.00
move_ids = self.env['stock.move'].search([('picking_id', '=', res.get('picking_id')),('product_id', '=', res.get('product_id'))],limit=1)
if move_ids:
cost_value = move_ids.purchase_line_id.price_unit + (move_ids.purchase_line_id.price_tax / move_ids.purchase_line_id.product_qty)
cost_value_per_unit = float(cost_value) * float(move_ids.purchase_line_id.product_uom.factor)
if cost_value_per_unit > 0.00:
markup_table_line = self.env['price.markup.table'].search([('lower_price', '<', cost_value_per_unit),
'|', ('higher_price', '>=', cost_value_per_unit),
('higher_price', '=', 0)],limit=1)
if markup_table_line:
res.update({'cost_price': cost_value_per_unit,'sale_price': cost_value_per_unit + (cost_value_per_unit * markup_table_line.markup_percentage / 100)})
else:
res.update({'cost_price': cost_value_per_unit,'sale_price': cost_value_per_unit })
associated_purchase_line = move_ids.purchase_line_id
if associated_purchase_line:
res.update({'mrp': associated_purchase_line.product_uom._compute_price(associated_purchase_line.mrp, self.product_uom_id)})
total_cost_value = associated_purchase_line.price_unit + (associated_purchase_line.price_tax / associated_purchase_line.product_qty)
cost_value_per_unit = associated_purchase_line.product_uom._compute_price(total_cost_value, self.product_uom_id)
if cost_value_per_unit > 0.00:
res.update({'cost_price': cost_value_per_unit,
'sale_price': self.env['price.markup.table'].calculate_price_with_markup(cost_value_per_unit)
})
else:
pass
return res

def _create_and_assign_production_lot(self):
""" Creates and assign new production lots for move lines."""
lot_vals = []
# It is possible to have multiple time the same lot to create & assign,
# so we handle the case with 2 dictionaries.
key_to_index = {} # key to index of the lot
key_to_mls = defaultdict(lambda: self.env['stock.move.line']) # key to all mls
for ml in self:
ml.product_id.write({'standard_price': ml.cost_price,'list_price': ml.sale_price,'mrp': ml.mrp})
key = (ml.company_id.id, ml.product_id.id, ml.lot_name, ml.cost_price, ml.sale_price, ml.mrp, ml.expiration_date)
key_to_mls[key] |= ml
if ml.tracking != 'lot' or key not in key_to_index:
key_to_index[key] = len(lot_vals)
lot_vals.append(ml._get_value_production_lot())
lots = self.env['stock.lot'].create(lot_vals)
for key, mls in key_to_mls.items():
lot = lots[key_to_index[key]].with_prefetch(lots._ids) # With prefetch to reconstruct the ones broke by accessing by index
mls.write({'lot_id': lot.id})


# This function is overridden to set price details for the lot to help markup feature.
# This will be called on receive products
def _get_value_production_lot(self):
self.ensure_one()

return {
'company_id': self.company_id.id,
'name': self.lot_name,
'product_id': self.product_id.id,
res = super(StockMoveLine,self)._get_value_production_lot()
res.update({
'cost_price': self.cost_price,
'sale_price': self.sale_price,
'mrp': self.mrp,
'expiration_date': self.expiration_date
}

'mrp': self.mrp
})
return res
Loading

0 comments on commit 24dd22d

Please sign in to comment.