uniform model
This commit is contained in:
parent
0c46514bc5
commit
8aa1dfdc4b
10 changed files with 209 additions and 190 deletions
|
|
@ -34,6 +34,7 @@
|
|||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
# BV: Commented out the following lines to avoid errors when installing the module.
|
||||
# "bemade_mailcow_integration/static/src/js/mailcow.js",
|
||||
# "bemade_mailcow_integration/static/src/xml/mailcow_templates.xml",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
|
|
@ -21,157 +21,3 @@ class SaleOrder(models.Model):
|
|||
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="Our Margin"
|
||||
)
|
||||
|
||||
margin_percent_actual = fields.Float(
|
||||
compute="_compute_actual_margins",
|
||||
groups="base.group_user",
|
||||
string="Our 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
|
||||
|
|
|
|||
158
bemade_margin_vendor_pricelist/models/sale_order_line.py
Normal file
158
bemade_margin_vendor_pricelist/models/sale_order_line.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.tools.float_utils import float_is_zero, float_compare
|
||||
|
||||
|
||||
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="Our Margin"
|
||||
)
|
||||
|
||||
margin_percent_actual = fields.Float(
|
||||
compute="_compute_actual_margins",
|
||||
groups="base.group_user",
|
||||
string="Our 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
|
||||
|
|
@ -17,22 +17,27 @@
|
|||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'bemade_multiple_billing_contacts',
|
||||
'name': 'Multiple Billing Contacts',
|
||||
'version': '17.0.1.0.1',
|
||||
'summary': 'Send invoices to multiple contacts by default.',
|
||||
'description': """By default, newly created invoices add all invoice addresses for the given partner as
|
||||
followers on the invoice. If billing contacts are set manually on the sales order, those billing
|
||||
contacts are added as followers on the invoice instead.""",
|
||||
'description': """
|
||||
By default, newly created invoices add all invoice addresses for the given partner as
|
||||
followers on the invoice. If billing contacts are set manually on the sales order, those billing
|
||||
contacts are added as followers on the invoice instead.
|
||||
""",
|
||||
'category': 'Invoicing Management',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['sale',
|
||||
'account',
|
||||
'bemade_partner_root_ancestor',
|
||||
],
|
||||
'data': ['views/account_move_views.xml',
|
||||
'views/res_partner_views.xml'],
|
||||
'depends': [
|
||||
'sale',
|
||||
'account',
|
||||
'bemade_partner_root_ancestor',
|
||||
],
|
||||
'data': [
|
||||
'views/account_move_views.xml',
|
||||
'views/res_partner_views.xml'
|
||||
],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ from odoo import models, fields, api
|
|||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
billing_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
string="Billing Contacts",
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts',
|
||||
store=True,)
|
||||
billing_contacts = fields.Many2many(
|
||||
comodel_name='res.partner',
|
||||
string="Billing Contacts",
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts',
|
||||
store=True
|
||||
)
|
||||
|
||||
@api.depends('line_ids.sale_line_ids.order_id', 'partner_id')
|
||||
def _compute_billing_contacts(self):
|
||||
|
|
|
|||
|
|
@ -4,12 +4,17 @@ from odoo import models, fields, api, _, Command
|
|||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
billing_contacts = fields.Many2many(string='Default Billing Contacts',
|
||||
comodel_name='res.partner',
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts')
|
||||
potential_billing_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
compute='_compute_billing_contacts')
|
||||
billing_contacts = fields.Many2many(
|
||||
string='Default Billing Contacts',
|
||||
comodel_name='res.partner',
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts'
|
||||
)
|
||||
|
||||
potential_billing_contacts = fields.Many2many(
|
||||
comodel_name='res.partner',
|
||||
compute='_compute_billing_contacts'
|
||||
)
|
||||
|
||||
@api.depends('child_ids.type')
|
||||
def _compute_billing_contacts(self):
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ from odoo import models, fields, api, _, Command
|
|||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
billing_contacts = fields.Many2many(comodel_name='res.partner',
|
||||
string='Billing Contacts',
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts',
|
||||
store=True)
|
||||
billing_contacts = fields.Many2many(
|
||||
comodel_name='res.partner',
|
||||
string='Billing Contacts',
|
||||
compute='_compute_billing_contacts',
|
||||
inverse='_inverse_billing_contacts',
|
||||
store=True
|
||||
)
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_billing_contacts(self):
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
<field name="partner_to">{{object.id}}</field>
|
||||
<field name="subject">Select Your Division at {{object.company_id.name}}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial, sans-serif; color: #454748; width: 100%; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial, sans-serif; color: #454748; width: 100%; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top" style="font-size: 13px;">
|
||||
|
|
@ -55,8 +55,8 @@
|
|||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="middle" align="left">
|
||||
|
|
@ -104,4 +104,3 @@
|
|||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@
|
|||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
Loading…
Reference in a new issue