Compare commits
4 commits
18.0
...
15.0-margi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcec6b5b4d | ||
|
|
f9e23e1853 | ||
|
|
005992c75b | ||
|
|
5196ebcf61 |
5 changed files with 236 additions and 0 deletions
1
bemade_margin_vendor_pricelist/__init__.py
Normal file
1
bemade_margin_vendor_pricelist/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
31
bemade_margin_vendor_pricelist/__manifest__.py
Normal file
31
bemade_margin_vendor_pricelist/__manifest__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#
|
||||||
|
# Bemade Inc.
|
||||||
|
#
|
||||||
|
# Copyright (C) September 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||||
|
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||||
|
#
|
||||||
|
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||||
|
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||||
|
# or modified copies of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
# DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
{
|
||||||
|
'name': 'Sales Margin on Vendor Price',
|
||||||
|
'version': '15.0.0.0.1',
|
||||||
|
'summary': 'Enables calculation of sales margins based on vendor pricelists.',
|
||||||
|
'description': """Adds a new method for calculating """,
|
||||||
|
'author': 'Bemade Inc.',
|
||||||
|
'website': 'https://www.bemade.org',
|
||||||
|
'license': 'OPL-1',
|
||||||
|
'depends': ['sale_stock_margin'],
|
||||||
|
'data': ['models/views/sale_order.xml'],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
1
bemade_margin_vendor_pricelist/models/__init__.py
Normal file
1
bemade_margin_vendor_pricelist/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import sale_order
|
||||||
153
bemade_margin_vendor_pricelist/models/sale_order.py
Normal file
153
bemade_margin_vendor_pricelist/models/sale_order.py
Normal 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
|
||||||
50
bemade_margin_vendor_pricelist/models/views/sale_order.xml
Normal file
50
bemade_margin_vendor_pricelist/models/views/sale_order.xml
Normal 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>
|
||||||
Loading…
Reference in a new issue