[IMP] *: product back2basics 15.0

Rework the whole view, generally.

task-2605931

Part-of: odoo/odoo#75862
This commit is contained in:
wan
2021-09-07 15:50:00 +00:00
committed by william-andre
parent a0f38c6d97
commit fa034b2a64
61 changed files with 524 additions and 365 deletions
+19
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.tools import format_amount
ACCOUNT_DOMAIN = "['&', '&', '&', ('deprecated', '=', False), ('internal_type','=','other'), ('company_id', '=', current_company_id), ('is_off_balance', '=', False)]"
@@ -24,6 +25,7 @@ class ProductTemplate(models.Model):
taxes_id = fields.Many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id', help="Default taxes used when selling the product.", string='Customer Taxes',
domain=[('type_tax_use', '=', 'sale')], default=lambda self: self.env.company.account_sale_tax_id)
tax_string = fields.Char(compute='_compute_tax_string')
supplier_taxes_id = fields.Many2many('account.tax', 'product_supplier_taxes_rel', 'prod_id', 'tax_id', string='Vendor Taxes', help='Default taxes used when buying the product.',
domain=[('type_tax_use', '=', 'purchase')], default=lambda self: self.env.company.account_purchase_tax_id)
property_account_income_id = fields.Many2one('account.account', company_dependent=True,
@@ -58,6 +60,23 @@ class ProductTemplate(models.Model):
fiscal_pos = self.env['account.fiscal.position']
return fiscal_pos.map_accounts(accounts)
@api.depends('taxes_id', 'list_price')
def _compute_tax_string(self):
for record in self:
currency = record.currency_id
res = record.taxes_id.compute_all(record.list_price)
joined = []
included = res['total_included']
if currency.compare_amounts(included, record.list_price):
joined.append(_('%s Incl. Taxes', format_amount(self.env, included, currency)))
excluded = res['total_excluded']
if currency.compare_amounts(excluded, record.list_price):
joined.append(_('%s Excl. Taxes', format_amount(self.env, excluded, currency)))
if joined:
record.tax_string = f"(= {', '.join(joined)})"
else:
record.tax_string = " "
class ProductProduct(models.Model):
_inherit = "product.product"
+4 -1
View File
@@ -9,7 +9,7 @@
<tree string="Products" sample="1">
<field name="default_code"/>
<field name="name"/>
<field name="lst_price"/>
<field name="list_price"/>
<field name="taxes_id" widget="many2many_tags"/>
<field name="supplier_taxes_id" widget="many2many_tags"/>
<field name="activity_exception_decoration" widget="activity_exception"/>
@@ -72,6 +72,9 @@
<xpath expr="//div[@name='pricing']" position="after">
<field name="taxes_id" widget="many2many_tags" context="{'default_type_tax_use':'sale', 'search_default_sale': 1, 'search_default_service': type == 'service', 'search_default_goods': type == 'consu'}"/>
</xpath>
<xpath expr="//div[@name='pricing']" position="inside">
<span class="ml-2"/><field name="tax_string"/>
</xpath>
<group name="bill" position="inside">
<field name="supplier_taxes_id" widget="many2many_tags" context="{'default_type_tax_use':'purchase', 'search_default_purchase': 1, 'search_default_service': type == 'service', 'search_default_goods': type == 'consu'}"/>
</group>
+5
View File
@@ -15,6 +15,11 @@ class ProductTemplate(models.Model):
self.type = 'service'
self.invoice_policy = 'order'
def _compute_show_service_fields(self):
super()._compute_show_service_fields()
for record in self:
record.show_service_fields |= record.type == 'event'
class Product(models.Model):
_inherit = 'product.product'
+2 -19
View File
@@ -153,12 +153,12 @@
<label for="total_amount" attrs="{'invisible': [('product_has_cost', '=', True)]}"/>
<div class="o_row" attrs="{'invisible': [('product_has_cost', '=', True)]}">
<field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="currency_id" groups="base.group_multi_currency"/>
</div>
<field name="amount_residual" widget='monetary' options="{'currency_field': 'currency_id'}"/>
<div class="o_td_label" attrs="{'invisible': [('same_currency', '=', True), ('product_has_cost', '=', False)]}">
<field name="label_total_amount_company" nolabel="1" class="o_form_label" />
</div>
</div>
<div class="o_row" attrs="{'invisible': [('same_currency', '=', True), ('product_has_cost', '=', False)]}">
<field name="total_amount_company" widget='monetary' options="{'currency_field': 'company_currency_id'}" />
<field name="label_convert_rate"/>
@@ -474,23 +474,6 @@
<field name="act_window_id" ref="hr_expense_actions_my_all"/>
</record>
<record id="view_product_hr_expense_form" model="ir.ui.view">
<field name="name">product.template.expense.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<div name="options" position="inside">
<div>
<field name="can_be_expensed"/>
<label for="can_be_expensed"/>
</div>
</div>
<xpath expr="//page[@name='general_information']//field[@name='taxes_id']" position="after">
<field name="supplier_taxes_id" widget="many2many_tags" attrs="{'invisible':['|',('can_be_expensed','=',False),('purchase_ok','=',True)]}" context="{'default_type_tax_use':'purchase', 'search_default_purchase': 1, 'search_default_service': type == 'service', 'search_default_goods': type == 'consu'}"/>
</xpath>
</field>
</record>
<record id="product_template_search_view_inherit_hr_expense" model="ir.ui.view">
<field name="name">product.template.search.view.inherit.hr_expense</field>
<field name="model">product.template</field>
+8 -8
View File
@@ -3,7 +3,7 @@
<record id="product_product_quote_despacho" model="product.product">
<field name="name">Import Clearance</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="default_code">AFIP_DESPACHO</field>
<field name="sale_ok" eval="False"/>
<field name="uom_id" ref="uom.product_uom_unit"/>
@@ -11,7 +11,7 @@
<record id="product_product_tasa_estadistica" model="product.product">
<field name="name">Statistics Rate</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="default_code">AFIP_TASA_EST</field>
<field name="sale_ok" eval="False"/>
<field name="uom_id" ref="uom.product_uom_unit"/>
@@ -19,7 +19,7 @@
<record id="product_product_arancel" model="product.product">
<field name="name">Tariff</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="default_code">AFIP_ARANCEL</field>
<field name="sale_ok" eval="False"/>
<field name="uom_id" ref="uom.product_uom_unit"/>
@@ -28,7 +28,7 @@
<record id="product_product_servicio_de_guarda" model="product.product">
<field name="name">Guard Service</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="default_code">AFIP_SERV_GUARDA</field>
<field name="sale_ok" eval="False"/>
<field name="uom_id" ref="uom.product_uom_unit"/>
@@ -40,7 +40,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">100.0</field>
<field name="list_price">80.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">EXENTO</field>
@@ -53,7 +53,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">200.0</field>
<field name="list_price">160.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">CERO</field>
@@ -65,7 +65,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">50.0</field>
<field name="list_price">40.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">NOGRAVADO</field>
@@ -77,7 +77,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">250.0</field>
<field name="list_price">130.0</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">TELEFONIA</field>
+1 -1
View File
@@ -3,7 +3,7 @@
<record id="product_product_ad_valorem" model="product.product">
<field name="name">Ad-Valorem</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="default_code">AD_VALOREM</field>
<field name="description">Cargo para calculo de Ad-Valorem en DIN</field>
<field name="sale_ok" eval="False"/>
+1 -1
View File
@@ -10,7 +10,7 @@
<record id="route_warehouse0_manufacture" model='stock.location.route'>
<field name="name">Manufacture</field>
<field name="company_id"></field>
<field name="sequence">5</field>
<field name="sequence">10</field>
</record>
<!-- Enable the manufacturing in warehouse0 -->
+14 -14
View File
@@ -92,7 +92,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">290</field>
<field name="list_price">520</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -116,7 +116,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">240</field>
<field name="list_price">380</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -130,7 +130,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">10</field>
<field name="list_price">50</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -144,7 +144,7 @@
<field name="categ_id" ref="product.product_category_consumable"/>
<field name="standard_price">0.5</field>
<field name="list_price">0.5</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -157,7 +157,7 @@
<field name="categ_id" ref="product.product_category_consumable"/>
<field name="standard_price">0.1</field>
<field name="list_price">0.2</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -171,7 +171,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">10</field>
<field name="list_price">10</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -184,7 +184,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">10</field>
<field name="list_price">10</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -197,7 +197,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">10</field>
<field name="list_price">10</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -210,7 +210,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">80</field>
<field name="list_price">100</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -222,7 +222,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">3000</field>
<field name="list_price">1000</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -417,7 +417,7 @@
<field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">600.0</field>
<field name="list_price">147.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -460,7 +460,7 @@
<field name="tracking">lot</field>
<field name="standard_price">20.0</field>
<field name="list_price">24.0</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -476,7 +476,7 @@
<field name="tracking">lot</field>
<field name="standard_price">10</field>
<field name="list_price">20</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -640,7 +640,7 @@
</record>
<record id="product.product_product_27" model="product.product">
<field name="type">product</field>
<field name="detailed_type">product</field>
</record>
<record id="mrp_production_drawer" model="mrp.production">
<field name="product_id" ref="product.product_product_27"/>
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.tests import Form, tagged
from odoo.addons.mrp.tests.common import TestMrpCommon
@tagged('post_install', '-at_install')
class TestMultistepManufacturingWarehouse(TestMrpCommon):
def setUp(self):
@@ -23,7 +24,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
product_form.name = 'Stick'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
@@ -32,7 +33,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
# Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Stick'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.raw_product = product_form.save()
@@ -401,7 +402,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
# Create an additional BoM for component
product_form = Form(self.env['product.product'])
product_form.name = 'Wood'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.wood_product = product_form.save()
+1 -1
View File
@@ -8,7 +8,7 @@
<field name="inherit_id" ref="stock.view_template_property_form"/>
<field name="arch" type="xml">
<xpath expr="//label[@for='sale_delay']" position="before">
<label for="produce_delay" attrs="{'invisible':[('type','=','service')]}"/>
<label for="produce_delay" string="Manuf. Lead Time" attrs="{'invisible':[('type','=','service')]}"/>
<div attrs="{'invisible':[('type','=','service')]}">
<field name="produce_delay" class="oe_inline"/> days
</div>
@@ -4,7 +4,7 @@
<record id="route_resupply_subcontractor_mto" model='stock.location.route'>
<field name="name">Resupply Subcontractor on Order</field>
<field name="company_id"></field>
<field name="sequence">5</field>
<field name="sequence">15</field>
</record>
<function model="res.company" name="create_missing_subcontracting_location" />
<function model="stock.warehouse" name="write">
@@ -13,4 +13,3 @@
</function>
</data>
</odoo>
+1 -1
View File
@@ -754,7 +754,7 @@ class PosConfig(models.Model):
WHERE p.active
AND t.available_in_pos
ORDER BY t.priority DESC,
t.type DESC,
t.detailed_type DESC,
COALESCE(pm.date,p.write_date) DESC limit %s
""", [str(self.limited_products_amount)])
return self.env.cr.fetchall()
+6 -10
View File
@@ -67,16 +67,12 @@
<xpath expr="//page[@name='sales']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//page[@name='sales']" position="after">
<page name="pos" string="Point of Sale" attrs="{'invisible': [('sale_ok','=',False)]}">
<group name="pos">
<group>
<field name="available_in_pos"/>
<field name="pos_categ_id" attrs="{'invisible': [('available_in_pos', '=', False)]}" string="Category"/>
<field name="to_weight" attrs="{'invisible': [('available_in_pos', '=', False)]}"/>
</group>
</group>
</page>
<xpath expr="//page[@name='sales']/group[@name='sale']" position="inside">
<group name="pos" string="Point of Sale" attrs="{'invisible': [('sale_ok','=',False)]}">
<field name="available_in_pos"/>
<field name="to_weight" attrs="{'invisible': [('available_in_pos', '=', False)]}"/>
<field name="pos_categ_id" attrs="{'invisible': [('available_in_pos', '=', False)]}" string="Category"/>
</group>
</xpath>
</field>
</record>
+27 -27
View File
@@ -36,7 +36,7 @@
<field name="name">Restaurant Expenses</field>
<field name="list_price">14.0</field>
<field name="standard_price">8.0</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="categ_id" ref="product.cat_expense"/>
</record>
@@ -44,7 +44,7 @@
<field name="name">Hotel Accommodation</field>
<field name="list_price">400.0</field>
<field name="standard_price">400.0</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="uom_id" ref="uom.product_uom_day"/>
<field name="uom_po_id" ref="uom.product_uom_day"/>
<field name="categ_id" ref="cat_expense"/>
@@ -56,7 +56,7 @@
<field name="categ_id" ref="product_category_3"/>
<field name="standard_price">20.5</field>
<field name="list_price">30.75</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="uom_id" ref="uom.product_uom_hour"/>
<field name="uom_po_id" ref="uom.product_uom_hour"/>
</record>
@@ -66,7 +66,7 @@
<field name="categ_id" ref="product_category_3"/>
<field name="standard_price">25.5</field>
<field name="list_price">38.25</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="uom_id" ref="uom.product_uom_hour"/>
<field name="uom_po_id" ref="uom.product_uom_hour"/>
</record>
@@ -78,7 +78,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">55.0</field>
<field name="list_price">70.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -91,7 +91,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">35.0</field>
<field name="list_price">40.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -104,7 +104,7 @@
<field name="categ_id" ref="product_category_4"/>
<field name="standard_price">235.0</field>
<field name="list_price">280.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -117,7 +117,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="list_price">450.0</field>
<field name="standard_price">300.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -176,7 +176,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">500.0</field>
<field name="list_price">750.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -271,7 +271,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">600.0</field>
<field name="list_price">147.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -284,7 +284,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">800.0</field>
<field name="list_price">320.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -298,7 +298,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">14.0</field>
<field name="list_price">15.8</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -311,7 +311,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">1299.0</field>
<field name="list_price">1799.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -325,7 +325,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">10.0</field>
<field name="list_price">47.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -338,7 +338,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">120.50</field>
<field name="list_price">140</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -351,7 +351,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">28</field>
<field name="list_price">33</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="image_1920" type="base64" file="product/static/img/product_product_11-image.png"/>
@@ -412,7 +412,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">180</field>
<field name="list_price">120.50</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -425,7 +425,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">78.0</field>
<field name="list_price">85.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -438,7 +438,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">20.0</field>
<field name="list_price">25.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -451,7 +451,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">1700.0</field>
<field name="list_price">1950.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -463,7 +463,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">2010.0</field>
<field name="list_price">2100.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -476,7 +476,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">876.0</field>
<field name="list_price">885.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -489,7 +489,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">287.0</field>
<field name="list_price">295.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -502,7 +502,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">100.0</field>
<field name="list_price">110.50</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -516,7 +516,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">2500.0</field>
<field name="list_price">2350.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -530,7 +530,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">4500.0</field>
<field name="list_price">4000.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
@@ -544,7 +544,7 @@
<field name="categ_id" ref="product_category_5"/>
<field name="standard_price">1000</field>
<field name="list_price">1500</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="weight">0.01</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
+5 -1
View File
@@ -90,7 +90,7 @@ class ProductProduct(models.Model):
help="This is the sum of the extra price of all attributes")
# lst_price: catalog value + extra, context dependent (uom)
lst_price = fields.Float(
'Public Price', compute='_compute_product_lst_price',
'Sales Price', compute='_compute_product_lst_price',
digits='Product Price', inverse='_set_product_lst_price',
help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.")
@@ -274,6 +274,7 @@ class ProductProduct(models.Model):
value -= product.price_extra
product.write({'list_price': value})
@api.onchange('lst_price')
def _set_product_lst_price(self):
for product in self:
if self._context.get('uom'):
@@ -356,12 +357,15 @@ class ProductProduct(models.Model):
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self.product_tmpl_id._sanitize_vals(vals)
products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list)
# `_get_variant_id_for_combination` depends on existing variants
self.clear_caches()
return products
def write(self, values):
self.product_tmpl_id._sanitize_vals(values)
res = super(ProductProduct, self).write(values)
if 'product_template_attribute_value_ids' in values:
# `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids`
+30 -5
View File
@@ -30,7 +30,7 @@ class ProductAttribute(models.Model):
- Never: Variants are never created for the attribute.
Note: the variants creation mode cannot be changed once the attribute is used on at least one product.""",
required=True)
is_used_on_products = fields.Boolean('Used on Products', compute='_compute_is_used_on_products')
number_related_products = fields.Integer(compute='_compute_number_related_products')
product_tmpl_ids = fields.Many2many('product.template', string="Related Products", compute='_compute_products', store=True)
display_type = fields.Selection([
('radio', 'Radio'),
@@ -39,9 +39,9 @@ class ProductAttribute(models.Model):
('color', 'Color')], default='radio', required=True, help="The display type used in the Product Configurator.")
@api.depends('product_tmpl_ids')
def _compute_is_used_on_products(self):
def _compute_number_related_products(self):
for pa in self:
pa.is_used_on_products = bool(pa.product_tmpl_ids)
pa.number_related_products = len(pa.product_tmpl_ids)
@api.depends('attribute_line_ids.active', 'attribute_line_ids.product_tmpl_id')
def _compute_products(self):
@@ -61,7 +61,7 @@ class ProductAttribute(models.Model):
the user knowing about it."""
if 'create_variant' in vals:
for pa in self:
if vals['create_variant'] != pa.create_variant and pa.is_used_on_products:
if vals['create_variant'] != pa.create_variant and pa.number_related_products:
raise UserError(
_("You cannot change the Variants Creation Mode of the attribute %s because it is used on the following products:\n%s") %
(pa.display_name, ", ".join(pa.product_tmpl_ids.mapped('display_name')))
@@ -78,12 +78,21 @@ class ProductAttribute(models.Model):
@api.ondelete(at_uninstall=False)
def _unlink_except_used_on_product(self):
for pa in self:
if pa.is_used_on_products:
if pa.number_related_products:
raise UserError(
_("You cannot delete the attribute %s because it is used on the following products:\n%s") %
(pa.display_name, ", ".join(pa.product_tmpl_ids.mapped('display_name')))
)
def action_open_related_products(self):
return {
'type': 'ir.actions.act_window',
'name': _("Related Products"),
'res_model': 'product.template',
'view_mode': 'tree,form',
'domain': [('id', 'in', self.product_tmpl_ids.ids)],
}
class ProductAttributeValue(models.Model):
_name = "product.attribute.value"
@@ -390,6 +399,22 @@ class ProductTemplateAttributeLine(models.Model):
def _without_no_variant_attributes(self):
return self.filtered(lambda ptal: ptal.attribute_id.create_variant != 'no_variant')
def action_open_attribute_values(self):
return {
'type': 'ir.actions.act_window',
'name': _("Product Variant Values"),
'res_model': 'product.template.attribute.value',
'view_mode': 'tree,form',
'domain': [('id', 'in', self.product_template_value_ids.ids)],
'views': [
(self.env.ref('product.product_template_attribute_value_view_tree').id, 'list'),
(self.env.ref('product.product_template_attribute_value_view_form').id, 'form'),
],
'context': {
'search_default_active': 1,
},
}
class ProductTemplateAttributeValue(models.Model):
"""Materialized relationship between attribute values
+54 -7
View File
@@ -44,12 +44,22 @@ class ProductTemplate(models.Model):
'Sales Description', translate=True,
help="A description of the Product that you want to communicate to your customers. "
"This description will be copied to every Sales Order, Delivery Order and Customer Invoice/Credit Note")
type = fields.Selection([
detailed_type = fields.Selection([
('consu', 'Consumable'),
('service', 'Service')], string='Product Type', default='consu', required=True,
help='A storable product is a product for which you manage stock. The Inventory app has to be installed.\n'
'A consumable product is a product for which stock is not managed.\n'
'A service is a non-material product you provide.')
type = fields.Selection(
selection=[
('consu', 'Consumable'),
('service', 'Service')
],
default='consu',
compute='_compute_type',
store=True,
readonly=False,
)
categ_id = fields.Many2one(
'product.category', 'Product Category',
change_default=True, default=_get_default_category_id, group_expand='_read_group_categ_id',
@@ -69,11 +79,8 @@ class ProductTemplate(models.Model):
list_price = fields.Float(
'Sales Price', default=1.0,
digits='Product Price',
help="Price at which the product is sold to customers.")
# lst_price: catalog price for template, but including extra for variants
lst_price = fields.Float(
'Public Price', related='list_price', readonly=False,
digits='Product Price')
help="Price at which the product is sold to customers.",
)
standard_price = fields.Float(
'Cost', compute='_compute_standard_price',
inverse='_set_standard_price', search='_search_standard_price',
@@ -102,7 +109,7 @@ class ProductTemplate(models.Model):
help="Default unit of measure used for all stock operations.")
uom_name = fields.Char(string='Unit of Measure Name', related='uom_id.name', readonly=True)
uom_po_id = fields.Many2one(
'uom.uom', 'Purchase Unit of Measure',
'uom.uom', 'Purchase UoM',
default=_get_default_uom_id, required=True,
help="Default unit of measure used for purchase orders. It must be in the same category as the default unit of measure.")
company_id = fields.Many2one(
@@ -140,6 +147,8 @@ class ProductTemplate(models.Model):
can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed', store=True)
has_configurable_attributes = fields.Boolean("Is a configurable product", compute='_compute_has_configurable_attributes', store=True)
product_tooltip = fields.Char(compute='_compute_product_tooltip')
priority = fields.Selection([
('0', 'Normal'),
('1', 'Favorite'),
@@ -388,6 +397,33 @@ class ProductTemplate(models.Model):
if len(p.product_variant_ids) == 1:
p.product_variant_ids.packaging_ids = p.packaging_ids
@api.depends('type')
def _compute_product_tooltip(self):
for record in self:
if record.type == 'consu':
record.product_tooltip = _(
"Consumables are physical products for which you don't manage the inventory "
"level: they are always available."
)
else:
record.product_tooltip = ""
def _detailed_type_mapping(self):
return {}
@api.depends('detailed_type')
def _compute_type(self):
type_mapping = self._detailed_type_mapping()
for record in self:
record.type = type_mapping.get(record.detailed_type, record.detailed_type)
# @api.constrains('type', 'detailed_type')
# def _constrains_detailed_type(self):
# type_mapping = self._detailed_type_mapping()
# for record in self:
# if record.type != type_mapping.get(record.detailed_type, record.detailed_type):
# raise ValidationError(_("The Type of this product doesn't match the Detailed Type"))
@api.constrains('uom_id', 'uom_po_id')
def _check_uom(self):
if any(template.uom_id and template.uom_po_id and template.uom_id.category_id != template.uom_po_id.category_id for template in self):
@@ -408,9 +444,19 @@ class ProductTemplate(models.Model):
# Do nothing but needed for inheritance
return {}
def _sanitize_vals(self, vals):
if 'type' in vals and 'detailed_type' not in vals:
if vals['type'] not in self.mapped('type'):
vals['detailed_type'] = vals['type']
if 'detailed_type' in vals and 'type' not in vals:
type_mapping = self._detailed_type_mapping()
vals['type'] = type_mapping.get(vals['detailed_type'], vals['detailed_type'])
@api.model_create_multi
def create(self, vals_list):
''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
for vals in vals_list:
self._sanitize_vals(vals)
templates = super(ProductTemplate, self).create(vals_list)
if "create_product_product" not in self._context:
templates._create_variant_ids()
@@ -437,6 +483,7 @@ class ProductTemplate(models.Model):
return templates
def write(self, vals):
self._sanitize_vals(vals)
if 'uom_id' in vals or 'uom_po_id' in vals:
uom_id = self.env['uom.uom'].browse(vals.get('uom_id')) or self.uom_id
uom_po_id = self.env['uom.uom'].browse(vals.get('uom_po_id')) or self.uom_po_id
+1 -1
View File
@@ -65,7 +65,7 @@ class ProductProduct(models.Model):
("active", populate.randomize([True, False], [0.8, 0.2])),
("type", populate.randomize(types, types_distribution)),
("categ_id", populate.randomize(category_ids)),
("lst_price", populate.compute(get_rand_float)),
("list_price", populate.compute(get_rand_float)),
("standard_price", populate.compute(get_rand_float)),
]
@@ -18,13 +18,23 @@
<field name="model">product.attribute</field>
<field name="arch" type="xml">
<form string="Product Attribute">
<field name="is_used_on_products" invisible="1"/>
<field name="number_related_products" invisible="1"/>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="action_open_related_products"
type="object" icon="fa-bars"
attrs="{'invisible': [('number_related_products', '=', [])]}">
<div class="o_stat_info">
<span class="o_stat_value"><field name="number_related_products"/></span>
<span class="o_stat_text">Related Products</span>
</div>
</button>
</div>
<group name="main_fields" class="o_label_nowrap">
<label for="name" string="Attribute Name"/>
<field name="name" nolabel="1"/>
<field name="display_type" widget="radio"/>
<field name="create_variant" widget="radio" attrs="{'readonly': [('is_used_on_products', '=', True)]}"/>
<field name="create_variant" widget="radio" attrs="{'readonly': [('number_related_products', '!=', 0)]}"/>
</group>
<notebook>
<page string="Attribute Values" name="attribute_values">
@@ -38,13 +48,6 @@
</tree>
</field>
</page>
<page string="Related Products" name="related_products" attrs="{'invisible': [('is_used_on_products', '=', False)]}">
<field name="product_tmpl_ids">
<tree>
<field name="name"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
@@ -88,9 +91,9 @@
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Attributes" create="0" delete="0">
<field name="attribute_id"/>
<field name="attribute_id" optional="hide"/>
<field name="name"/>
<field name="display_type"/>
<field name="display_type" optional="hide"/>
<field name="html_color" attrs="{'invisible': [('display_type', '!=', 'color')]}" widget="color"/>
<field name="ptav_active" optional="hide"/>
<field name="price_extra" widget="monetary"/>
@@ -136,19 +139,4 @@
</field>
</record>
<record id="product_attribute_value_action" model="ir.actions.act_window">
<field name="name">Product Variant Values</field>
<field name="res_model">product.template.attribute.value</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('product_tmpl_id', '=', active_id)]</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('product.product_template_attribute_value_view_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('product.product_template_attribute_value_view_form')})]" />
<field name="context">{
'default_product_tmpl_id': active_id,
'search_default_active': 1,
}</field>
</record>
</odoo>
@@ -21,7 +21,8 @@
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id'}" optional="show" decoration-muted="not sale_ok"/>
<field name="standard_price" widget='monetary' options="{'currency_field': 'cost_currency_id'}" optional="show" readonly="1"/>
<field name="categ_id" optional="hide"/>
<field name="type" optional="hide" readonly="1"/>
<field name="detailed_type" optional="hide" readonly="1"/>
<field name="type" invisible="1"/>
<field name="uom_id" readonly="1" optional="show" groups="uom.group_uom"/>
<field name="active" invisible="1"/>
<field name="activity_exception_decoration" widget="activity_exception"/>
@@ -60,6 +61,9 @@
<field name="value_count" invisible="1"/>
<field name="attribute_id" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="value_ids" widget="many2many_tags" options="{'no_create_edit': True, 'color_field': 'color'}" context="{'default_attribute_id': attribute_id, 'show_attribute': False}"/>
<button string="Configure" class="float-right btn-secondary"
type="object" name="action_open_attribute_values"
groups="product.group_product_variant"/>
</tree>
</field>
<p class="oe_grey oe_edit_only">
@@ -104,7 +108,7 @@
</strong>
</div>
<div name="product_lst_price" class="mt-1">
Price: <field name="lst_price" widget="monetary" options="{'currency_field': 'currency_id', 'field_digits': True}"></field>
Price: <field name="list_price" widget="monetary" options="{'currency_field': 'currency_id', 'field_digits': True}"></field>
</div>
</div>
</div>
+51 -40
View File
@@ -9,17 +9,32 @@
<field name="arch" type="xml">
<form string="Product">
<header>
<button string="Configure Variants" type="action"
name="%(product_attribute_value_action)d"
attrs="{'invisible': ['|', ('attribute_line_ids', '&lt;=', 0), ('is_product_variant', '=', True)]}"
groups="product.group_product_variant"/>
<button string="Print Labels" type="object" name="action_open_label_layout"/>
</header>
<sheet>
<sheet name="product_form">
<field name='product_variant_count' invisible='1'/>
<field name='is_product_variant' invisible='1'/>
<field name='attribute_line_ids' invisible='1'/>
<div class="oe_button_box" name="button_box"/>
<field name="type" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button"
name="open_pricelist_rules"
icon="fa-list-ul"
groups="product.group_product_pricelist"
type="object">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="pricelist_item_count"/>
</span>
<span attrs="{'invisible': [('pricelist_item_count', '=', 1)]}">
Extra Prices
</span>
<span attrs="{'invisible': [('pricelist_item_count', '!=', 1)]}">
Extra Price
</span>
</div>
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="id" invisible="True"/>
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_128'}"/>
@@ -31,40 +46,38 @@
<field class="o_text_overflow" name="name" placeholder="e.g. Cheese Burger"/>
</div>
</h1>
<div name="options" groups="base.group_user">
<div>
<field name="sale_ok"/>
<label for="sale_ok"/>
</div>
<div>
<field name="purchase_ok"/>
<label for="purchase_ok"/>
</div>
</div>
</div>
<style>
div[name="options"] .o_field_boolean {
margin-left: 10px;
margin-right: 0px;
}
</style>
<div name="options" groups="base.group_user">
<span class="d-inline-block">
<field name="sale_ok"/>
<label for="sale_ok"/>
</span>
<span class="d-inline-block">
<field name="purchase_ok"/>
<label for="purchase_ok"/>
</span>
</div>
<notebook>
<page string="General Information" name="general_information">
<group>
<group name="group_general">
<field name="categ_id" string="Product Category"/>
<field name="active" invisible="1"/>
<field name="type"/>
<field name="detailed_type"/>
<field name="product_tooltip" string="" class="font-italic text-muted"/>
<field name="uom_id" groups="uom.group_uom" options="{'no_create': True}"/>
<field name="uom_po_id" groups="uom.group_uom" options="{'no_create': True}"/>
</group>
<group name="group_standard_price">
<label for="list_price" class="mt-1"/>
<div name="pricing">
<field name="list_price" class="oe_inline" widget='monetary'
options="{'currency_field': 'currency_id', 'field_digits': True}"/>
<button name="open_pricelist_rules" icon="fa-arrow-right" type="object"
groups="product.group_product_pricelist" class="oe_inline btn-link pt-1">
<field name="pricelist_item_count" attrs="{'invisible': [('pricelist_item_count', '=', 0)]}"/>
<span attrs="{'invisible': [('pricelist_item_count', '=', 1)]}">
Extra Prices
</span>
<span attrs="{'invisible': [('pricelist_item_count', '!=', 1)]}">
Extra Price
</span>
</button>
</div>
<label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1), ('is_product_variant', '=', False)]}"/>
<div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1), ('is_product_variant', '=', False)]}" class="o_row">
@@ -73,10 +86,9 @@
<field name="uom_name"/>
</span>
</div>
<field name="categ_id" string="Product Category"/>
<field name="company_id" groups="base.group_multi_company"
options="{'no_create': True}"/>
<field name="uom_id" groups="uom.group_uom" options="{'no_create': True}"/>
<field name="uom_po_id" groups="uom.group_uom" options="{'no_create': True}"/>
<field name="currency_id" invisible="1"/>
<field name="cost_currency_id" invisible="1"/>
<field name="product_variant_id" invisible="1"/>
@@ -88,9 +100,12 @@
</page>
<page string="Sales" attrs="{'invisible':[('sale_ok','=',False)]}" name="sales" invisible="1">
<group name="sale">
<group string="Upsell &amp; Cross-Sell" name="upsell"/>
</group>
<group string="Sales Description" name="description">
<field name="description_sale" nolabel="1" placeholder="This note is added to sales orders and invoices."/>
<group>
<group string="Sales Description" name="description">
<field name="description_sale" nolabel="1" placeholder="This note is added to sales orders and invoices."/>
</group>
</group>
</page>
<page string="Purchase" name="purchase" attrs="{'invisible': [('purchase_ok','=',False)]}" invisible="1">
@@ -248,7 +263,7 @@
</group>
<group name="pricing" string="Pricing">
<field name="product_variant_count" invisible="1"/>
<label for="lst_price"/>
<label for="lst_price" string="Sales Price"/>
<div class="o_row col-5 pl-0">
<field name="lst_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('product_variant_count', '&gt;', 1)]}"/>
</div>
@@ -341,27 +356,23 @@
<attribute name="string">Product Variant</attribute>
<attribute name="duplicate">false</attribute>
</form>
<field name="type" position="after">
<xpath expr="//div[@name='standard_price_uom']" position="after">
<field name="default_code"/>
<field name="barcode"/>
</field>
</xpath>
<xpath expr="//field[@name='priority']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
<field name="list_price" position="attributes">
<attribute name="name">lst_price</attribute>
<attribute name="attrs">{'readonly': [('product_variant_count', '&gt;', 1)]}</attribute>
</field>
<xpath expr="//label[@for='list_price']" position="replace">
<label for="lst_price"/>
</xpath>
<group name="packaging" position="attributes">
<attribute name="attrs">{'invisible': 0}</attribute>
</group>
<field name="name" position="after">
<field name="product_tmpl_id" class="oe_inline" readonly="1" invisible="1" attrs="{'required': [('id', '!=', False)]}"/>
</field>
<xpath expr="//div[hasclass('oe_title')]//div[@name='options']" position="before">
<xpath expr="//div[hasclass('oe_title')]" position="inside">
<field name="product_template_variant_value_ids" widget="many2many_tags" readonly="1" groups="product.group_product_variant"/>
</xpath>
</field>
@@ -5,7 +5,7 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']//group[@name='sale']" position="inside">
<xpath expr="//page[@name='invoicing']//group[@name='accounting']" position="inside">
<group name="email_template" string="Automatic Email at Invoice">
<field name="email_template_id" string="Email Template" help="Send a product-specific email once the invoice is validated"
domain="[('model','=','account.move')]"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<!-- Size -->
<record id="product_attribute_size" model="product.attribute">
<field name="name">Size</field>
@@ -31,7 +31,7 @@
<field name="attribute_id" ref="product_attribute_size"/>
<field name="sequence">5</field>
</record>
<!-- Color -->
<record id="product_attribute_value_color_1" model="product.attribute.value">
<field name="name">Blue</field>
@@ -53,7 +53,7 @@
<field name="attribute_id" ref="product.product_attribute_2"/>
<field name="sequence">6</field>
</record>
<!-- Gender -->
<record id="product_attribute_gender" model="product.attribute">
<field name="name">Gender</field>
@@ -75,7 +75,7 @@
<field name="categ_id" ref="product.product_category_6"/>
<field name="standard_price">7.0</field>
<field name="list_price">15.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="description_sale">Show your company love around you =).</field>
+1 -1
View File
@@ -868,7 +868,7 @@ class PurchaseOrderLine(models.Model):
product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True)
product_type = fields.Selection(related='product_id.type', readonly=True)
product_type = fields.Selection(related='product_id.detailed_type', readonly=True)
price_unit = fields.Float(string='Unit Price', required=True, digits='Product Price')
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
+9 -7
View File
@@ -50,14 +50,16 @@
<field name="purchase_method" widget="radio"/>
</group>
<page name="purchase" position="inside">
<group string="Purchase Description">
<field name="description_purchase" nolabel="1"
placeholder="This note is added to purchase orders."/>
</group>
<group string="Warning when Purchasing this Product" groups="purchase.group_warning_purchase">
<field name="purchase_line_warn" nolabel="1"/>
<field name="purchase_line_warn_msg" colspan="3" nolabel="1"
<group>
<group string="Purchase Description">
<field name="description_purchase" nolabel="1"
placeholder="This note is added to purchase orders."/>
</group>
<group string="Warning when Purchasing this Product" groups="purchase.group_warning_purchase">
<field name="purchase_line_warn" nolabel="1"/>
<field name="purchase_line_warn_msg" colspan="3" nolabel="1"
attrs="{'required':[('purchase_line_warn','!=','no-message')],'readonly':[('purchase_line_warn','=','no-message')], 'invisible':[('purchase_line_warn','=','no-message')]}"/>
</group>
</group>
</page>
</field>
@@ -2,9 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import Form, TransactionCase
from odoo.tests import Form
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestSaleMrpFlow(TransactionCase):
def setUp(self):
@@ -5,11 +5,12 @@ from datetime import datetime as dt
from datetime import timedelta as td
from odoo import SUPERUSER_ID
from odoo.tests import Form
from odoo.tests import Form, tagged
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
@tagged('post_install', '-at_install')
class TestReorderingRule(TransactionCase):
@classmethod
def setUpClass(cls):
@@ -21,7 +22,7 @@ class TestReorderingRule(TransactionCase):
# create product and set the vendor
product_form = Form(cls.env['product.product'])
product_form.name = 'Product A'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.description = 'Internal Notes'
with product_form.seller_ids.new() as seller:
seller.name = cls.partner
@@ -179,14 +180,14 @@ class TestReorderingRule(TransactionCase):
product_form = Form(self.env['product.product'])
product_form.name = 'Simple Product'
product_form.type = 'product'
product_form.detailed_type = 'product'
with product_form.seller_ids.new() as s:
s.name = partner
product = product_form.save()
product_form = Form(self.env['product.product'])
product_form.name = 'Product BUY + MTO'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.route_ids.add(route_buy)
product_form.route_ids.add(route_mto)
with product_form.seller_ids.new() as s:
@@ -277,14 +278,14 @@ class TestReorderingRule(TransactionCase):
product_form = Form(self.env['product.product'])
product_form.name = 'Simple Product'
product_form.type = 'product'
product_form.detailed_type = 'product'
with product_form.seller_ids.new() as s:
s.name = partner
product = product_form.save()
product_form = Form(self.env['product.product'])
product_form.name = 'Product BUY + MTO'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.route_ids.add(route_buy)
product_form.route_ids.add(route_mto)
with product_form.seller_ids.new() as s:
+1 -4
View File
@@ -15,9 +15,6 @@ _logger = logging.getLogger(__name__)
class ProductTemplate(models.Model):
_inherit = 'product.template'
def _default_visible_expense_policy(self):
return self.user_has_groups('analytic.group_analytic_accounting')
service_type = fields.Selection([('manual', 'Manually set quantities on order')], string='Track Service',
help="Manually set quantities on order: Invoice based on the manually entered quantity, without creating an analytic account.\n"
"Timesheets on contract: Invoice based on the tracked hours on the related timesheet.\n"
@@ -31,7 +28,7 @@ class ProductTemplate(models.Model):
default='no',
help="Expenses and vendor bills can be re-invoiced to a customer."
"With this option, a validated expense can be re-invoice to a customer at its cost or sales price.")
visible_expense_policy = fields.Boolean("Re-Invoice Policy visible", compute='_compute_visible_expense_policy', default=lambda self: self._default_visible_expense_policy())
visible_expense_policy = fields.Boolean("Re-Invoice Policy visible", compute='_compute_visible_expense_policy')
sales_count = fields.Float(compute='_compute_sales_count', string='Sold')
visible_qty_configurator = fields.Boolean("Quantity visible in configurator", compute='_compute_visible_qty_configurator')
invoice_policy = fields.Selection([
+10 -3
View File
@@ -9,9 +9,16 @@
<xpath expr="//group[@name='sale']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='invoicing']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<field name="product_tooltip" position="after">
<label for="product_tooltip" string="" attrs="{'invisible': ['|', ('type', 'not in', ('product', 'consu')), ('invoice_policy', '!=', 'order')]}"/>
<div attrs="{'invisible': ['|', ('type', 'not in', ('product', 'consu')), ('invoice_policy', '!=', 'order')]}" class="font-italic text-muted">
You can invoice them before they are delivered.
</div>
<label for="product_tooltip" string="" attrs="{'invisible': ['|', ('type', 'not in', ('product', 'consu')), ('invoice_policy', '!=', 'delivery')]}"/>
<div attrs="{'invisible': ['|', ('type', 'not in', ('product', 'consu')), ('invoice_policy', '!=', 'delivery')]}" class="font-italic text-muted">
Invoice after delivery, based on quantities delivered, not ordered.
</div>
</field>
</field>
</record>
</odoo>
+8 -8
View File
@@ -1182,14 +1182,14 @@
<field name="priority">15</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']//group[@name='sale']" position="inside">
<group string="Invoicing" name="invoicing" invisible="1">
<field name="invoice_policy" widget="radio"/>
<field name="service_type" widget="radio" invisible="True"/>
<field name="visible_expense_policy" invisible="1"/>
<field name="expense_policy" widget="radio" attrs="{'invisible': [('visible_expense_policy', '=', False)]}"/>
</group>
</xpath>
<field name="product_variant_count" position="after">
<field name="service_type" widget="radio" invisible="True"/>
<field name="visible_expense_policy" invisible="1"/>
</field>
<field name="detailed_type" position="after">
<field name="invoice_policy" required="1"/>
<field name="expense_policy" widget="radio" attrs="{'invisible': [('visible_expense_policy', '=', False)]}"/>
</field>
</field>
</record>
@@ -7,10 +7,6 @@ from odoo import api, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
def _default_visible_expense_policy(self):
visibility = self.user_has_groups('hr_expense.group_hr_expense_user')
return visibility or super(ProductTemplate, self)._default_visible_expense_policy()
@api.depends('can_be_expensed')
def _compute_visible_expense_policy(self):
expense_products = self.filtered(lambda p: p.can_be_expensed)
+2 -2
View File
@@ -154,7 +154,7 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
def _cls_create_product(cls, name, uom_id, routes=()):
p = Form(cls.env['product.product'])
p.name = name
p.type = 'product'
p.detailed_type = 'product'
p.uom_id = uom_id
p.uom_po_id = uom_id
p.route_ids.clear()
@@ -165,7 +165,7 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
def _create_product(self, name, uom_id, routes=()):
p = Form(self.env['product.product'])
p.name = name
p.type = 'product'
p.detailed_type = 'product'
p.uom_id = uom_id
p.uom_po_id = uom_id
p.route_ids.clear()
@@ -87,7 +87,7 @@ class TestSaleMrpProcurement(TransactionCase):
# Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Stick'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.raw_product = product_form.save()
@@ -97,7 +97,7 @@ class TestSaleMrpProcurement(TransactionCase):
product_form.name = 'Stick'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
@@ -106,7 +106,7 @@ class TestSaleMrpProcurement(TransactionCase):
# Create manifactured product which uses another manifactured
product_form = Form(self.env['product.product'])
product_form.name = 'Arrow'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
@@ -115,7 +115,7 @@ class TestSaleMrpProcurement(TransactionCase):
## Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Iron'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.raw_product_2 = product_form.save()
@@ -5,13 +5,14 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='sale']" position="after">
<group name="options" groups="product.group_product_variant">
<group string="Options">
<field name="optional_product_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('id', '!=', active_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]" />
</group>
</group>
</xpath>
<group name="upsell" position="inside">
<field name="optional_product_ids"
widget="many2many_tags"
options="{'color_field': 'color'}"
domain="[('id', '!=', active_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"
placeholder="Recommend when 'Adding to Cart' or quotation"
groups="product.group_product_variant" />
</group>
</field>
</record>
@@ -21,16 +21,12 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="sale_product_configurator.product_template_view_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='options']" position="inside">
<field name="optional_product_ids" position="before">
<field name="product_add_mode" widget="radio" invisible="1"/>
<!--
Field is only needed as invisible on product variants view
-->
</xpath>
<xpath expr="//group[@name='options']" position="attributes">
<!-- HIDE optional products many2many when product is added through the grid to SOLines -->
</field>
<field name="optional_product_ids" position="attributes">
<attribute name="attrs">{'invisible': [('product_add_mode', '=', 'grid')]}</attribute>
</xpath>
</field>
</field>
</record>
</odoo>
+33 -6
View File
@@ -8,12 +8,14 @@ from odoo.exceptions import ValidationError
class ProductTemplate(models.Model):
_inherit = 'product.template'
service_tracking = fields.Selection([
('no', 'Don\'t create a task'),
('task_global_project', 'Create a task in an existing project'),
('task_in_project', 'Create a task in sales order\'s project'),
('project_only', 'Create a new project but no task')],
string="Service Tracking", default="no",
service_tracking = fields.Selection(
selection=[
('no', 'Nothing'),
('task_global_project', 'Task'),
('task_in_project', 'Project & Task'),
('project_only', 'Project'),
],
string="Create on Order", default="no",
help="On Sales order confirmation, this product can generate a project and/or task. \
From those, you can track the service you are selling.\n \
'In sale order\'s project': Will use the sale order\'s configured project if defined or fallback to \
@@ -27,6 +29,31 @@ class ProductTemplate(models.Model):
domain="[('company_id', '=', current_company_id)]",
help='Select a billable project to be the skeleton of the new created project when selling the current product. Its stages and tasks will be duplicated.')
@api.depends('service_tracking', 'type')
def _compute_product_tooltip(self):
super()._compute_product_tooltip()
for record in self.filtered(lambda record: record.type == 'service'):
if record.service_tracking == 'no':
record.product_tooltip = _(
"Invoice ordered quantities as soon as this service is sold."
)
elif record.service_tracking == 'task_global_project':
record.product_tooltip = _(
"Invoice as soon as this service is sold, and create a task in an existing "
"project to track the time spent."
)
elif record.service_tracking == 'task_in_project':
record.product_tooltip = _(
"Invoice ordered quantities as soon as this service is sold, and create a "
"project for the order with a task for each sales order line to track the time"
" spent."
)
elif record.service_tracking == 'project_only':
record.product_tooltip = _(
"Invoice ordered quantities as soon as this service is sold, and create an "
"empty project for the order to track the time spent."
)
@api.constrains('project_id', 'project_template_id')
def _check_project_and_template(self):
""" NOTE 'service_tracking' should be in decorator parameters but since ORM check constraints twice (one after setting
+5 -3
View File
@@ -6,11 +6,13 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="sale.product_template_form_view_invoice_policy"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='service_type']" position="after">
<field name="service_tracking" widget="radio" attrs="{'invisible': [('type','!=','service')]}"/>
<field name="invoice_policy" position="after">
<field name="service_tracking" required="1" attrs="{'invisible': [('type', '!=', 'service')]}"/>
</field>
<field name="product_tooltip" position="after">
<field name="project_id" context="{'default_allow_billable': True}" attrs="{'invisible':[('service_tracking','!=','task_global_project')], 'required':[('service_tracking','==','task_global_project')]}"/>
<field name="project_template_id" context="{'active_test': False, 'default_allow_billable': True}" attrs="{'invisible':[('service_tracking','not in',['task_in_project', 'project_only'])]}"/>
</xpath>
</field>
</field>
</record>
@@ -7,7 +7,7 @@ from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
service_to_purchase = fields.Boolean("Purchase Automatically", help="If ticked, each time you sell this product through a SO, a RfQ is automatically created to buy the product. Tip: don't forget to set a vendor on the product.")
service_to_purchase = fields.Boolean("Subcontract Service", help="If ticked, each time you sell this product through a SO, a RfQ is automatically created to buy the product. Tip: don't forget to set a vendor on the product.")
_sql_constraints = [
('service_to_purchase', "CHECK((type != 'service' AND service_to_purchase != true) or (type = 'service'))", 'Product that is not a service can not create RFQ.'),
+1 -1
View File
@@ -268,7 +268,7 @@ class SaleOrderLine(models.Model):
qty_delivered_method = fields.Selection(selection_add=[('stock_move', 'Stock Moves')])
route_id = fields.Many2one('stock.location.route', string='Route', domain=[('sale_selectable', '=', True)], ondelete='restrict', check_company=True)
move_ids = fields.One2many('stock.move', 'sale_line_id', string='Stock Moves')
product_type = fields.Selection(related='product_id.type')
product_type = fields.Selection(related='product_id.detailed_type')
virtual_available_at_date = fields.Float(compute='_compute_qty_at_date', digits='Product Unit of Measure')
scheduled_date = fields.Datetime(compute='_compute_qty_at_date')
forecast_expected_date = fields.Datetime(compute='_compute_qty_at_date')
+78 -36
View File
@@ -6,14 +6,21 @@ import threading
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
SERVICE_POLICY = [
# (service_policy, (invoice_policy, service_type), string)
('ordered_timesheet', ('order', 'timesheet'), 'Prepaid/Fixed Price'),
('delivered_timesheet', ('delivery', 'timesheet'), 'Based on Timesheets'),
('delivered_manual', ('delivery', 'manual'), 'Based on Milestones'),
]
SERVICE_TO_GENERAL = {policy[0]: policy[1] for policy in SERVICE_POLICY}
GENERAL_TO_SERVICE = {policy[1]: policy[0] for policy in SERVICE_POLICY}
class ProductTemplate(models.Model):
_inherit = 'product.template'
service_policy = fields.Selection([
('ordered_timesheet', 'Prepaid'),
('delivered_timesheet', 'Timesheets on tasks'),
('delivered_manual', 'Milestones (manually set quantities on order)')
(policy[0], policy[2]) for policy in SERVICE_POLICY
], string="Service Invoicing Policy", compute='_compute_service_policy', inverse='_inverse_service_policy')
service_type = fields.Selection(selection_add=[
('timesheet', 'Timesheets on project (one fare per SO/Project)'),
@@ -21,12 +28,14 @@ class ProductTemplate(models.Model):
# override domain
project_id = fields.Many2one(domain="[('company_id', '=', current_company_id), ('allow_billable', '=', True), ('pricing_type', '=', 'task_rate'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet', True])]")
project_template_id = fields.Many2one(domain="[('company_id', '=', current_company_id), ('allow_billable', '=', True), ('pricing_type', '!=', 'task_rate'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet', True])]")
service_upsell_warning = fields.Boolean('Upsell Warning', help="The salesperson in charge will be assigned an activity informing him of an upselling opportunity once the selected threshold is reached.")
service_upsell_threshold = fields.Float('Threshold', help="Percentage of time delivered compared to the prepaid amount that must be reached for the upselling opportunity activity to be triggered.")
service_upsell_threshold = fields.Float('Threshold', default=1, help="Percentage of time delivered compared to the prepaid amount that must be reached for the upselling opportunity activity to be triggered.")
service_upsell_threshold_ratio = fields.Char(compute='_compute_service_upsell_threshold_ratio')
def _default_visible_expense_policy(self):
visibility = self.user_has_groups('project.group_project_user')
return visibility or super(ProductTemplate, self)._default_visible_expense_policy()
@api.depends('uom_id')
def _compute_service_upsell_threshold_ratio(self):
product_uom_hour = self.env.ref('uom.product_uom_hour')
for record in self:
record.service_upsell_threshold_ratio = f"1 {record.uom_id.name} = {product_uom_hour.factor / record.uom_id.factor:.2f} Hours"
def _compute_visible_expense_policy(self):
visibility = self.user_has_groups('project.group_project_user')
@@ -35,40 +44,71 @@ class ProductTemplate(models.Model):
product_template.visible_expense_policy = visibility
return super(ProductTemplate, self)._compute_visible_expense_policy()
@api.depends('invoice_policy', 'service_type')
@api.depends('invoice_policy', 'service_type', 'type')
def _compute_service_policy(self):
for product in self:
policy = None
if product.invoice_policy == 'delivery':
policy = 'delivered_manual' if product.service_type == 'manual' else 'delivered_timesheet'
elif product.invoice_policy == 'order' and (product.service_type == 'timesheet' or product.type == 'service'):
policy = 'ordered_timesheet'
product.service_policy = policy
product.service_policy = GENERAL_TO_SERVICE.get((product.invoice_policy, product.service_type), False)
if not product.service_policy and product.type == 'service':
product.service_policy = 'ordered_timesheet'
@api.onchange('service_policy')
def _inverse_service_policy(self):
for product in self:
policy = product.service_policy
if not policy and not product.invoice_policy =='delivery':
product.invoice_policy = 'order'
product.service_type = 'manual'
elif policy == 'ordered_timesheet':
product.invoice_policy = 'order'
product.service_type = 'timesheet'
else:
product.invoice_policy = 'delivery'
product.service_type = 'manual' if policy == 'delivered_manual' else 'timesheet'
if product.service_policy:
product.invoice_policy, product.service_type = SERVICE_TO_GENERAL.get(product.service_policy, (False, False))
@api.onchange('type')
def _onchange_type(self):
res = super(ProductTemplate, self)._onchange_type()
if self.type == 'service' and not self.invoice_policy:
self.invoice_policy = 'order'
self.service_type = 'timesheet'
elif self.type == 'service' and self.invoice_policy == 'order':
self.service_policy = 'ordered_timesheet'
elif self.type == 'consu' and not self.invoice_policy and self.service_policy == 'ordered_timesheet':
self.invoice_policy = 'order'
return res
@api.depends('service_tracking', 'service_policy')
def _compute_product_tooltip(self):
super()._compute_product_tooltip()
for record in self.filtered(lambda record: record.type == 'service'):
if record.service_policy == 'ordered_timesheet':
pass
elif record.service_policy == 'delivered_timesheet':
if record.service_tracking == 'no':
record.product_tooltip = _(
"Invoice based on timesheets (delivered quantity) on projects or tasks "
"you'll create later on."
)
elif record.service_tracking == 'task_global_project':
record.product_tooltip = _(
"Invoice based on timesheets (delivered quantity), and create a task in "
"an existing project to track the time spent."
)
elif record.service_tracking == 'task_in_project':
record.product_tooltip = _(
"Invoice based on timesheets (delivered quantity), and create a project "
"for the order with a task for each sales order line to track the time "
"spent."
)
elif record.service_tracking == 'project_only':
record.product_tooltip = _(
"Invoice based on timesheets (delivered quantity), and create an empty "
"project for the order to track the time spent."
)
elif record.service_policy == 'delivered_manual':
if record.service_tracking == 'no':
record.product_tooltip = _(
"Sales order lines define milestones of the project to invoice by setting "
"the delivered quantity."
)
elif record.service_tracking == 'task_global_project':
record.product_tooltip = _(
"Sales order lines define milestones of the project to invoice by setting "
"the delivered quantity. Create a task in an existing project to track the"
" time spent."
)
elif record.service_tracking == 'task_in_project':
record.product_tooltip = _(
"Sales order lines define milestones of the project to invoice by setting "
"the delivered quantity. Create an empty project for the order to track "
"the time spent."
)
elif record.service_tracking == 'project_only':
record.product_tooltip = _(
"Sales order lines define milestones of the project to invoice by setting "
"the delivered quantity. Create a project for the order with a task for "
"each sales order line to track the time spent."
)
@api.model
def _get_onchange_service_policy_updates(self, service_tracking, service_policy, project_id, project_template_id):
@@ -82,6 +122,7 @@ class ProductTemplate(models.Model):
@api.onchange('service_policy')
def _onchange_service_policy(self):
self._inverse_service_policy()
vals = self._get_onchange_service_policy_updates(self.service_tracking,
self.service_policy,
self.project_id,
@@ -115,6 +156,7 @@ class ProductProduct(models.Model):
@api.onchange('service_policy')
def _onchange_service_policy(self):
self.product_tmpl_id._inverse_service_policy()
vals = self.product_tmpl_id._get_onchange_service_policy_updates(self.service_tracking,
self.service_policy,
self.project_id,
@@ -78,7 +78,6 @@ class SaleOrder(models.Model):
These SOLs should contain a product which has:
- type="service",
- service_policy="ordered_timesheet",
- service_upsell_warning=True.
"""
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
@@ -86,7 +85,6 @@ class SaleOrder(models.Model):
sol.is_service
and not sol.has_displayed_warning_upsell # we don't want to display many times the warning each time we timesheet on the SOL
and sol.product_id.service_policy == 'ordered_timesheet'
and sol.product_id.service_upsell_warning
and float_compare(
sol.qty_delivered,
sol.product_uom_qty * (sol.product_id.service_upsell_threshold or 1.0),
@@ -25,7 +25,6 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
"""
# 1) Configure the upsell warning in prepaid service product
self.product_order_timesheet1.write({
'service_upsell_warning': True,
'service_upsell_threshold': 0.6,
})
+11 -5
View File
@@ -7,13 +7,19 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_policy']" position="attributes">
<attribute name="invisible">False</attribute>
<attribute name="attrs">{'invisible': [('type','==','service')]}</attribute>
<attribute name="attrs">{'invisible': [('type', '=', 'service')]}</attribute>
</xpath>
<xpath expr="//field[@name='service_type']" position="after">
<field name="service_policy" widget="radio" attrs="{'invisible': [('type','!=','service')]}"/>
<field name="service_upsell_warning" attrs="{'invisible': ['|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_timesheet')]}" />
<field name="service_upsell_threshold" attrs="{'invisible': ['|', '|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_timesheet'), ('service_upsell_warning', '=', False)]}" widget="percentage"/>
<xpath expr="//field[@name='invoice_policy']" position="after">
<field name="service_policy" string="Invoicing Policy" attrs="{'invisible': [('type', '!=', 'service')], 'required': [('type', '=', 'service')]}"/>
</xpath>
<field name="product_tooltip" position="after">
<label for="product_tooltip" string="" attrs="{'invisible': ['|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_timesheet')]}"/>
<div attrs="{'invisible': ['|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_timesheet')]}" class="font-italic text-muted">
Warn the salesperson for an upsell when work done exceeds
<field name="service_upsell_threshold" widget="percentage" class="oe_inline"/>
of hours sold. (<field name="service_upsell_threshold_ratio" class="oe_inline"/>)
</div>
</field>
</field>
</record>
+1 -1
View File
@@ -27,7 +27,7 @@
<record id="product_cable_management_box" model="product.product">
<field name="default_code">FURN_5555</field>
<field name="name">Cable Management Box</field>
<field name="type">product</field>
<field name="detailed_type">product</field>
<field name="weight">0.01</field>
<field name="categ_id" ref="product.product_category_5"/>
<field name="lst_price">100.0</field>
+12
View File
@@ -615,6 +615,9 @@ class ProductTemplate(models.Model):
responsible_id = fields.Many2one(
'res.users', string='Responsible', default=lambda self: self.env.uid, company_dependent=True, check_company=True,
help="This user will be responsible of the next activities related to logistic operations for this product.")
detailed_type = fields.Selection(selection_add=[
('product', 'Storable Product')
], tracking=True, ondelete={'product': 'set default'})
type = fields.Selection(selection_add=[
('product', 'Storable Product')
], tracking=True, ondelete={'product': 'set default'})
@@ -795,6 +798,14 @@ class ProductTemplate(models.Model):
template.reordering_min_qty = res[template.id]['reordering_min_qty']
template.reordering_max_qty = res[template.id]['reordering_max_qty']
def _compute_product_tooltip(self):
super()._compute_product_tooltip()
for record in self:
if record.type == 'product':
record.product_tooltip += _(
"Storable products are physical items for which you manage the inventory level."
)
@api.onchange('tracking')
def onchange_tracking(self):
return self.mapped('product_variant_ids').onchange_tracking()
@@ -820,6 +831,7 @@ class ProductTemplate(models.Model):
return res
def write(self, vals):
self._sanitize_vals(vals)
if 'uom_id' in vals:
new_uom = self.env['uom.uom'].browse(vals['uom_id'])
updated = self.filtered(lambda template: template.uom_id != new_uom)
+2 -2
View File
@@ -155,7 +155,7 @@ class StockMove(models.Model):
picking_code = fields.Selection(related='picking_id.picking_type_id.code', readonly=True)
show_details_visible = fields.Boolean('Details Visible', compute='_compute_show_details_visible')
show_reserved_availability = fields.Boolean('From Supplier', compute='_compute_show_reserved_availability')
product_type = fields.Selection(related='product_id.type', readonly=True)
product_type = fields.Selection(related='product_id.detailed_type', readonly=True)
additional = fields.Boolean("Whether the move was added after the picking's confirmation", default=False)
is_locked = fields.Boolean(compute='_compute_is_locked', readonly=True)
is_initial_demand_editable = fields.Boolean('Is initial demand editable', compute='_compute_is_initial_demand_editable')
@@ -991,7 +991,7 @@ class StockMove(models.Model):
def _key_assign_picking(self):
self.ensure_one()
return self.group_id, self.location_id, self.location_dest_id, self.picking_type_id
def _search_picking_for_assignation_domain(self):
return [('group_id', '=', self.group_id.id),
('location_id', '=', self.location_id.id),
+6 -6
View File
@@ -4990,14 +4990,14 @@ class StockMove(TransactionCase):
# Check raise UserError(_("You can not change the type of a product that is currently reserved on a stock
with self.assertRaises(UserError):
self.product.type = 'consu'
self.product.detailed_type = 'consu'
move_in._action_cancel()
self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10)
# Check raise UserError(_("Available quantity should be set to zero before changing type"))
# Check raise UserError(_("Available quantity should be set to zero before changing detailed_type"))
with self.assertRaises(UserError):
self.product.type = 'consu'
self.product.detailed_type = 'consu'
move_out = self.env['stock.move'].create({
'name': 'test_customer',
@@ -5012,7 +5012,7 @@ class StockMove(TransactionCase):
move_out._action_assign()
move_out.quantity_done = self.product.qty_available
move_out._action_done()
self.product.type = 'consu'
self.product.detailed_type = 'consu'
move2 = self.env['stock.move'].create({
'name': 'test_customer',
@@ -5028,9 +5028,9 @@ class StockMove(TransactionCase):
move2._action_assign()
with self.assertRaises(UserError):
self.product.type = 'product'
self.product.detailed_type = 'product'
move2._action_cancel()
self.product.type = 'product'
self.product.detailed_type = 'product'
def test_edit_done_picking_1(self):
""" Add a new move line in a done picking should generate an
+4 -4
View File
@@ -18,7 +18,7 @@ class TestReportsCommon(TransactionCase):
cls.stock_location = cls.env['stock.location'].browse(cls.ModelDataObj._xmlid_to_res_id('stock.stock_location_stock'))
product_form = Form(cls.env['product.product'])
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.name = 'Product'
cls.product = product_form.save()
cls.product_template = cls.product.product_tmpl_id
@@ -62,7 +62,7 @@ class TestReports(TestReportsCommon):
def test_report_quantity_1(self):
product_form = Form(self.env['product.product'])
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.name = 'Product'
product = product_form.save()
@@ -181,7 +181,7 @@ class TestReports(TestReportsCommon):
""" Not supported case.
"""
product_form = Form(self.env['product.product'])
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.name = 'Product'
product = product_form.save()
@@ -241,7 +241,7 @@ class TestReports(TestReportsCommon):
def test_report_quantity_3(self):
product_form = Form(self.env['product.product'])
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.name = 'Product'
product = product_form.save()
+11 -9
View File
@@ -132,14 +132,16 @@
</group>
</xpath>
<page name="inventory" position="inside">
<group string="Description for Delivery Orders">
<field name="description_pickingout" nolabel="1" placeholder="This note is added to delivery orders."/>
</group>
<group string="Description for Receipts">
<field name="description_pickingin" nolabel="1" placeholder="This note is added to receipt orders (e.g. where to store the product in the warehouse)."/>
</group>
<group string="Description for Internal Transfers" groups="stock.group_stock_multi_locations">
<field name="description_picking" placeholder="This note is added to internal transfer orders (e.g. where to pick the product in the warehouse)." nolabel="1"/>
<group>
<group string="Description for Receipts">
<field name="description_pickingin" nolabel="1" placeholder="This note is added to receipt orders (e.g. where to store the product in the warehouse)."/>
</group>
<group string="Description for Delivery Orders">
<field name="description_pickingout" nolabel="1" placeholder="This note is added to delivery orders."/>
</group>
<group string="Description for Internal Transfers" groups="stock.group_stock_multi_locations">
<field name="description_picking" placeholder="This note is added to internal transfer orders (e.g. where to pick the product in the warehouse)." nolabel="1"/>
</group>
</group>
</page>
<page name="inventory" position="attributes">
@@ -443,7 +445,7 @@
</xpath>
<xpath expr="//label[@for='weight']" position="before">
<field name="responsible_id" domain="[('share', '=', False)]"/>
<field name="responsible_id" domain="[('share', '=', False)]" widget="many2one_avatar_user"/>
</xpath>
</data>
</field>
@@ -6,7 +6,7 @@
<record id="route_drop_shipping" model='stock.location.route'>
<field name="name">Dropship</field>
<field name="sequence">3</field>
<field name="sequence">20</field>
<field name="company_id"></field>
<field name="sale_selectable" eval="True"/>
<field name="product_selectable" eval="True"/>
@@ -25,9 +25,9 @@ class TestLifoPrice(ValuationReconciliationTestCommon):
product_form = Form(self.env['product.product'])
product_form.default_code = 'LIFO'
product_form.name = 'LIFO Ice Cream'
product_form.type = 'product'
product_form.detailed_type = 'product'
product_form.categ_id = product_category_001
product_form.lst_price = 100.0
product_form.list_price = 100.0
product_form.uom_id = self.env.ref('uom.product_uom_kgm')
product_form.uom_po_id = self.env.ref('uom.product_uom_kgm')
# these are not available (visible) in either product or variant
@@ -17,7 +17,7 @@ class TestProcurementException(common.TransactionCase):
# I create a product with no supplier define for it.
product_form = Form(self.env['product.product'])
product_form.name = 'product with no seller'
product_form.lst_price = 20.00
product_form.list_price = 20.00
product_form.categ_id = self.env.ref('product.product_category_1')
product_with_no_seller = product_form.save()
@@ -50,7 +50,7 @@ class AccountMove(models.Model):
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
product_type = fields.Selection(related='product_id.type', readonly=True)
product_type = fields.Selection(related='product_id.detailed_type', readonly=True)
is_landed_costs_line = fields.Boolean()
@api.onchange('is_landed_costs_line')
@@ -6,13 +6,9 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="account.product_template_form_view"/>
<field name="arch" type="xml">
<group name="bill" position="after">
<div attrs="{'invisible':[('type', '!=', 'service')]}">
<group string="Landed Costs" name="landedcosts">
<field name="landed_cost_ok"/>
<field name="split_method_landed_cost" attrs="{'invisible': [('landed_cost_ok', '=', False)]}" class="col-4"/>
</group>
</div>
<group name="bill" position="inside">
<field name="landed_cost_ok" attrs="{'invisible':[('type', '!=', 'service')]}"/>
<field name="split_method_landed_cost" attrs="{'invisible': ['|', ('landed_cost_ok', '=', False), ('type', '!=', 'service')]}"/>
</group>
</field>
</record>
@@ -4,7 +4,7 @@
<field name="name">Water can</field>
<field name="standard_price">12.0</field>
<field name="list_price">12.0</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/water_can.jpg"/>
</record>
@@ -13,7 +13,7 @@
<field name="name">Flower pot</field>
<field name="standard_price">4.5</field>
<field name="list_price">4.5</field>
<field name="type">consu</field>
<field name="detailed_type">consu</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/flower_pot.jpg"/>
</record>
@@ -71,10 +71,10 @@ class ProductTemplate(models.Model):
if len(template.product_variant_ids) == 1:
template.product_variant_ids.base_unit_id = template.base_unit_id
@api.depends('price', 'lst_price', 'base_unit_count')
@api.depends('price', 'list_price', 'base_unit_count')
def _compute_base_unit_price(self):
for template in self:
template.base_unit_price = template.base_unit_count and (template.price or template.lst_price) / template.base_unit_count
template.base_unit_price = template.base_unit_count and (template.price or template.list_price) / template.base_unit_count
@api.depends('uom_name', 'base_unit_id.name')
def _compute_base_unit_name(self):
+19 -28
View File
@@ -87,17 +87,6 @@
</field>
</record>
<record model="ir.ui.view" id="product_template_form_view_invoice_policy">
<field name="name">product.template.invoice.policy</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="sale.product_template_form_view_invoice_policy"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='invoicing']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="product_template_only_website_form_view">
<field name="name">product.template.product.only.website.form</field>
<field name="model">product.template</field>
@@ -144,23 +133,25 @@
<div name="button_box" position="inside">
<field name="is_published" widget="website_redirect_button" attrs="{'invisible': [('sale_ok','=',False)]}"/>
</div>
<xpath expr="//page[@name='sales']" position="after">
<page name="shop" string="eCommerce" attrs="{'invisible': [('sale_ok','=',False)]}">
<group name="shop">
<group string="Shop">
<field name="website_url" invisible="1"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="website_sequence" groups="base.group_no_one"/>
<field name="public_categ_ids" widget="many2many_tags" string="Categories"/>
<field name="alternative_product_ids" widget="many2many_tags" domain="[('id', '!=', active_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"/>
<field name="accessory_product_ids" widget="many2many_tags"/>
<field name="website_ribbon_id" groups="base.group_no_one"/>
</group>
</group>
<group name="product_template_images" string="Extra Product Media">
<field name="product_template_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" add-label="Add a Media" nolabel="1"/>
</group>
</page>
<xpath expr="//group[@name='upsell']" position="inside">
<field name="accessory_product_ids" widget="many2many_tags" attrs="{'invisible': [('sale_ok','=',False)]}"
placeholder="Suggested accessories in the eCommerce cart"/>
<field name="alternative_product_ids" widget="many2many_tags"
domain="[('id', '!=', active_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"
attrs="{'invisible': [('sale_ok','=',False)]}"
placeholder="Displayed in bottom of product pages"/>
</xpath>
<xpath expr="//page[@name='sales']/group[@name='sale']" position="inside">
<group string="eCommerce Shop" name="shop" attrs="{'invisible': [('sale_ok','=',False)]}">
<field name="website_url" invisible="1"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="website_sequence" groups="base.group_no_one"/>
<field name="public_categ_ids" widget="many2many_tags" string="Categories"/>
<field name="website_ribbon_id" groups="base.group_no_one"/>
</group>
<group name="product_template_images" string="Extra Product Media" attrs="{'invisible': [('sale_ok','=',False)]}">
<field name="product_template_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" add-label="Add a Media" nolabel="1"/>
</group>
</xpath>
</field>
</record>
@@ -5,7 +5,7 @@
<field name="name">eBook: Office Renovation for Dummies</field>
<field name="standard_price">2</field>
<field name="list_price">4.50</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_digital/static/digital_product_1.jpg"/>
<field name="categ_id" ref="product.product_category_6"/>
@@ -6,10 +6,10 @@
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='options']" position="inside">
<div>
<span class="d-inline-block">
<field name="is_gift_card"/>
<label for="is_gift_card"/>
</div>
</span>
</xpath>
</field>
</record>
@@ -6,7 +6,7 @@
<field name="name">Taking care of Trees Course</field>
<field name="standard_price">150.0</field>
<field name="list_price">150.0</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="invoice_policy">order</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/channel_demo_tree_1.jpg"/>
@@ -18,7 +18,7 @@
<field name="name">DIY Furniture Course</field>
<field name="standard_price">100.0</field>
<field name="list_price">100.0</field>
<field name="type">service</field>
<field name="detailed_type">service</field>
<field name="invoice_policy">order</field>
<field name="is_published" eval="True"/>
<field name="image_1920" type="base64" file="website_sale_slides/static/img/product_course.png"/>
@@ -33,7 +33,7 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"
position: 'left',
run: 'text the_flow.product',
}, {
trigger: "select[name=type]",
trigger: "select[name=detailed_type]",
content: _t("Let's enter the product type"),
position: 'left',
run: 'text "product"',
@@ -358,7 +358,7 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
position: 'left',
run: 'text the_flow.service',
}, {
trigger: '.o_field_widget[name=type]',
trigger: '.o_field_widget[name=detailed_type]',
content: _t('Set to service'),
position: 'left',
run: 'text "service"',
@@ -376,17 +376,15 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
}
},
}, {
trigger: '.o_notebook a:contains("Sales")',
content: _t('Go to sales tab'),
position: 'bottom',
}, {
trigger: '.o_field_widget[name=service_policy] .o_radio_input[data-value="delivered_timesheet"]',
trigger: '.o_field_widget[name=service_policy]',
content: _t('Change service policy'),
position: 'left',
run: 'text "delivered_timesheet"',
}, {
trigger: '.o_field_widget[name=service_tracking] input[data-value="task_global_project"]',
trigger: '.o_field_widget[name=service_tracking]',
content: _t('Change track service'),
position: 'left',
run: 'text "task_global_project"',
}, {
mobile: false,
trigger: '.o_field_widget[name=project_id] input',