[IMP] *: product back2basics 15.0
Rework the whole view, generally. task-2605931 Part-of: odoo/odoo#75862
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', '<=', 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', '>', 1), ('is_product_variant', '=', False)]}"/>
|
||||
<div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 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 & 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', '>', 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', '>', 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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user