New module bemade_margin_vendor_pricelist.

Calculates margins based vendor pricing, average inventory cost, or a
blend of both, depending on the situation.
This commit is contained in:
Marc Durepos 2023-10-18 20:53:35 -04:00
parent 085b0af099
commit a988b6d035
5 changed files with 207 additions and 4 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -21,13 +21,11 @@
'version': '15.0.0.0.1',
'summary': 'Enables calculation of sales margins based on vendor pricelists.',
'description': """Adds a new method for calculating """,
'category': '',
'author': 'Bemade Inc.',
'website': 'https://www.bemade.org',
'license': 'OPL-1',
'depends': [''],
'data': [''],
'demo': [''],
'depends': ['sale_stock_margin'],
'data': ['models/views/sale_order.xml'],
'installable': True,
'auto_install': False,
}

View file

@ -0,0 +1 @@
from . import sale_order

View file

@ -0,0 +1,153 @@
from odoo import models, fields, api, _
from odoo.tools.float_utils import float_is_zero, float_compare
class SaleOrder(models.Model):
_inherit = 'sale.order'
margin_actual = fields.Monetary("Margin", compute='_compute_margin_actual',
store=False)
margin_percent_actual = fields.Float('Margin (%)', compute='_compute_margin_actual',
store=False,
group_operator='avg')
@api.depends('order_line.margin_actual', 'amount_untaxed')
def _compute_margin_actual(self):
for order in self:
order.margin_actual = sum(order.order_line.mapped('margin_actual'))
order.margin_percent_actual = order.amount_untaxed \
and order.margin_actual / order.amount_untaxed
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
purchase_price_vendor = fields.Float(compute='_compute_purchase_price_vendor',
string="Vendor Price", groups="base.group_user",
digits='Product Price')
margin_percent_vendor = fields.Float(string='Margin (%) on Vendor Price',
groups='base.group_user', group_operator='avg',
compute='_compute_margin_vendor')
margin_vendor = fields.Float(string='Margin on Vendor Price',
groups='base.group_user', digits='Product Price',
compute='_compute_margin_vendor')
purchase_price_actual = fields.Float(compute="_compute_actual_margins",
digits='Product Price',
groups="base.group_user",
string="Purchase Price")
margin_actual = fields.Float(compute="_compute_actual_margins",
digits='Product Price',
groups="base.group_user",
string="Margin")
margin_percent_actual = fields.Float(compute="_compute_actual_margins",
groups="base.group_user",
string="Margin (%)")
@api.depends('purchase_price', 'purchase_price_vendor', 'move_ids.product_id',
'move_ids.product_id.qty_available', 'move_ids.state',
'qty_to_deliver')
def _compute_actual_margins(self):
""" We want to use the margin based on average inventory valuation when the
sale order line will be completely fulfilled (or has been fulfilled) from stock.
For product not yet in stock we want to use the vendor price. We can also have
blended calculations (partly on vendor price, partly on stock valuation). This
occurs when an order has been or would be partially fulfilled from available
stock.
:return:
"""
non_product_lines = self.filtered(lambda r: not r.product_id)
non_product_lines.purchase_price_actual = 0.0
non_product_lines.margin_actual = 0.0
non_product_lines.margin_percent_actual = 0.0
for line in self - non_product_lines:
stock_missing = line._determine_missing_stock()
if float_is_zero(stock_missing,
precision_rounding=line.product_uom.rounding):
# everything is coming from stock, use inventory valuation
line.purchase_price_actual = line.purchase_price
elif float_compare(line.product_uom_qty, stock_missing,
precision_rounding=line.product_uom.rounding) == 0:
# everything is coming from the vendor, use vendor pricing
line.purchase_price_actual = line.purchase_price_vendor
else:
# we have a mix, use blended pricing
qty_from_stock = line.product_uom_qty - stock_missing
line.purchase_price_actual = \
(stock_missing * line.purchase_price_vendor
+ qty_from_stock * line.purchase_price) \
/ line.product_uom_qty
line.margin_actual = line.price_subtotal - (
line.purchase_price_actual * line.product_uom_qty)
line.margin_percent_actual = line.price_subtotal \
and line.margin_actual / line.price_subtotal
def _determine_missing_stock(self) -> float:
""" Compute how much stock is missing to meet an order line's demand. In the
case of a quotation line, available stock is checked as if the order were to be
placed immediately.
:return: The quantity missing from available stock to fulfill the line, in
the unit of measure matching self.product_uom.
"""
self.ensure_one()
is_order = self.order_id.state in ('sale', 'done')
if is_order and self.qty_to_deliver == 0:
return 0
elif is_order and self.qty_to_deliver > 0:
reserved = sum([m.reserved_availability for m in self.move_ids])
missing = self.qty_to_deliver - reserved
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
# Not enough reserved, check stock
missing = missing - self.product_id.qty_available
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
# Missing some stock to meet demand, return the quantity
return missing
else:
# Enough stock available to meet this line's demand
return 0
else:
# Already have stock reserved
return 0
else:
# This is a quotation, don't bother with stock reservations
missing = self.product_uom_qty - self.product_id.qty_available
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
return missing
else:
return 0
@api.depends('product_id', 'product_id.seller_ids',
'product_id.seller_ids.price')
def _compute_purchase_price_vendor(self):
for line in self:
product = line.product_id
suppinfos = product.seller_ids.sorted('sequence')
if not suppinfos:
line.purchase_price_vendor = 0.0
continue
suppinfo = suppinfos[0]
purch_currency = suppinfo.currency_id
to_cur = line.currency_id or line.order_id.currency_id
line.purchase_price_vendor = purch_currency._convert(
from_amount=suppinfo.price,
to_currency=to_cur,
company=line.company_id or self.env.company,
date=line.order_id.date_order or fields.Date.today(),
round=False,
) if to_cur and suppinfo.price else suppinfo.price
@api.depends('purchase_price_vendor')
def _compute_margin_vendor(self):
for line in self:
if not line.price_unit or float_is_zero(line.price_unit):
line.margin_vendor = 0
line.margin_percent_vendor = 0
continue
unit_margin = line.price_unit - line.purchase_price_vendor
line.margin_percent_vendor = unit_margin / line.price_unit
line.margin_vendor = unit_margin * line.product_uom_qty

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record model="ir.ui.view" id="sale_margin_sale_order_inherit">
<field name="name">sale.order.margin.view.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order"/>
<field name="arch" type="xml">
<field name="margin" position="replace">
<field name="margin_actual" class="oe_inline"/>
</field>
<label for="margin" position="replace">
<label for="margin_actual" groups="base.group_user"/>
</label>
<field name="margin_percent" position="replace">
<field name="margin_percent_actual" nolabel="1" class="oe_inline"
widget="percentage"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="sale_margin_sale_order_line_inherit">
<field name="name">sale.order.line.margin.view.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order_line"/>
<field name="arch" type="xml">
<field name="purchase_price" position="replace">
<field name="purchase_price_actual" groups="base.group_user"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="sale_margin_sale_order_line_form_inherit">
<field name="name">sale.order.view.form</field>
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order_line_form"/>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<field name="purchase_price" position="replace">
<field name="purchase_price_actual" optional="hide"/>
</field>
<field name="margin" position="replace">
<field name="margin_actual" optional="hide"/>
</field>
<field name="margin_percent" position="replace">
<field name="margin_percent_actual" optional="hide"/>
</field>
</field>
</record>
<!-- For now, we don't override the pivot and graph views because our fields
are not stored and this will cause performance issues. -->
</data>
</odoo>