[MERGE] various: improve product form usability

* product_product, product_template :
    - Add `uom_name` field to display stat buttons with UoM's name.

* delivery:
    - Move field 'HS Code' from `General Information` to `Accounting`.

* hr_expense:
    - Make checkbox `Can be Expensed` visible for all products.

* mrp:
    - Stat button update, we now count the uom quantity of product we have in our MOs
      limited to the last 365 days. Before that we were counting all the MOs.

    To achive this,
        - Add a stored compute field `product_uom_qty` on `mrp.production`.
        - Stat button now uses `action_view_mos`
        - The field was called `mo_count`, renaming it to `mrp_product_qty` as its
          function changed.

* point_of_sale:
    - Add a new tab (`Point of Sale`) and moved the `Point of Sale` section
      previously located in `Sales` to this newly created tab.

* product_email_template:
    - Renamed header 'Email' to 'Automatic Email at Invoice'.

* purchase:
    - Improved tooltip of field 'property_account_creditor_price_difference'.
    - Set `product_uom` to `uom.product_uom_hour` instead of
      `uom.product_uom_unit` in demo data.
    - Add a stored compute field `product_uom_qty` on `purchase.order.line`.

* report_intrastat:
    - Move field `Intrastat` from `General Information` to `Accounting`.
    - For product template's form view, inherit from
      `product.product_template_form_view` instead of 'stock.view_template_property_form'.

* sale:
    - Move `expense_policy` from `Purchase` to `Sales`.

* stock:
    - Add a new section in `Inventory` called `Logistics`, and put fields
    - `Weight`, `Volume` and `Responsible` in it.
    - `incoming_qty` is now only visible in debug mode.

* website_sale:
    - Add a new tab 'eCommerce' and move 'eCommerce' section from 'Sales' tab to this newly created tab.
    - Rename 'eCommerce Categories' to 'Categories'.

Task: #51125
Closes: #23868
This commit is contained in:
RomainLibert
2018-08-08 14:51:15 +02:00
38 changed files with 480 additions and 283 deletions
+3
View File
@@ -7,6 +7,9 @@
<field name="priority">5</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='purchase']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<page name="inventory" position="after">
<page string="Accounting" name="invoicing" groups="account.group_account_user">
<group name="properties">
@@ -6,9 +6,11 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<field name="categ_id" position="after">
<field name="hs_code"/>
</field>
<xpath expr="//group[@name='accounting']" position="inside">
<group string="GST" name="gst">
<field name="hs_code"/>
</group>
</xpath>
</field>
</record>
+3 -1
View File
@@ -260,7 +260,7 @@
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<div name="options" position="inside">
<div attrs="{'invisible': [('type','not in', ['consu', 'service'])]}">
<div>
<field name="can_be_expensed"/>
<label for="can_be_expensed"/>
</div>
@@ -310,6 +310,7 @@
<group string="Accounting">
<field name="property_account_expense_id" groups="account.group_account_user"/>
<field name="supplier_taxes_id" widget="many2many_tags"/>
<field name="taxes_id" widget="many2many_tags"/>
</group>
</group>
</sheet>
@@ -322,6 +323,7 @@
<field name="res_model">product.product</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="search_view_id" ref="product.product_search_form_view"/>
<field name="context">{"default_can_be_expensed": 1, 'default_type': 'service'}</field>
<field name="domain">[('can_be_expensed', '=', True)]</field>
<field name="help" type="html">
+14 -5
View File
@@ -62,6 +62,7 @@ class MrpProduction(models.Model):
'uom.uom', 'Product Unit of Measure',
oldname='product_uom', readonly=True, required=True,
states={'confirmed': [('readonly', False)]})
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type',
default=_get_default_picking_type, required=True)
@@ -83,7 +84,7 @@ class MrpProduction(models.Model):
states={'confirmed': [('readonly', False)]}, oldname="date_planned")
date_planned_finished = fields.Datetime(
'Deadline End', copy=False, default=fields.Datetime.now,
index=True,
index=True,
states={'confirmed': [('readonly', False)]})
date_start = fields.Datetime('Start Date', copy=False, index=True, readonly=True)
date_finished = fields.Datetime('End Date', copy=False, index=True, readonly=True)
@@ -99,11 +100,11 @@ class MrpProduction(models.Model):
"work centers based on production planning.")
move_raw_ids = fields.One2many(
'stock.move', 'raw_material_production_id', 'Raw Materials', oldname='move_lines',
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
move_finished_ids = fields.One2many(
'stock.move', 'production_id', 'Finished Products',
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
finished_move_line_ids = fields.One2many(
'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
@@ -146,7 +147,7 @@ class MrpProduction(models.Model):
default=lambda self: self.env['res.company']._company_default_get('mrp.production'),
required=True)
check_to_done = fields.Boolean(compute="_get_produced_qty", string="Check Produced Qty",
check_to_done = fields.Boolean(compute="_get_produced_qty", string="Check Produced Qty",
help="Technical Field to see if we can show 'Mark as Done' button")
qty_produced = fields.Float(compute="_get_produced_qty", string="Quantity Produced")
procurement_group_id = fields.Many2one(
@@ -164,6 +165,14 @@ class MrpProduction(models.Model):
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production')
@api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
def _compute_product_uom_qty(self):
for production in self:
if production.product_id.uom_id != production.product_uom_id:
production.product_uom_qty = production.product_uom_id._compute_quantity(production.product_qty, production.product_id.uom_id)
else:
production.product_uom_qty = production.product_qty
@api.depends('product_id.tracking')
def _compute_show_lots(self):
for production in self:
@@ -648,7 +657,7 @@ class MrpProduction(models.Model):
action = self.env.ref('stock.action_stock_scrap').read()[0]
action['domain'] = [('production_id', '=', self.id)]
return action
@api.model
def get_empty_list_help(self, help):
self = self.with_context(
+31 -13
View File
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models
from odoo.tools.float_utils import float_round
class ProductTemplate(models.Model):
@@ -11,7 +13,7 @@ class ProductTemplate(models.Model):
bom_ids = fields.One2many('mrp.bom', 'product_tmpl_id', 'Bill of Materials')
bom_count = fields.Integer('# Bill of Material', compute='_compute_bom_count')
used_in_bom_count = fields.Integer('# of BoM Where is Used', compute='_compute_used_in_bom_count')
mo_count = fields.Integer('# Manufacturing Orders', compute='_compute_mo_count')
mrp_product_qty = fields.Float('Manufactured', compute='_compute_mrp_product_qty')
produce_delay = fields.Float(
'Manufacturing Lead Time', default=0.0,
help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added.")
@@ -34,16 +36,18 @@ class ProductTemplate(models.Model):
return action
@api.one
def _compute_mo_count(self):
# TDE FIXME: directly use a read_group
self.mo_count = sum(self.mapped('product_variant_ids').mapped('mo_count'))
def _compute_mrp_product_qty(self):
self.mrp_product_qty = float_round(sum(self.mapped('product_variant_ids').mapped('mrp_product_qty')), precision_rounding=self.uom_id.rounding)
@api.multi
def action_view_mos(self):
product_ids = self.mapped('product_variant_ids').ids
action = self.env.ref('mrp.act_product_mrp_production').read()[0]
action['domain'] = [('product_id', 'in', product_ids)]
action['context'] = {}
action = self.env.ref('mrp.mrp_production_report').read()[0]
action['domain'] = [('state', '=', 'done'), '&', ('product_tmpl_id', 'in', self.ids)]
action['context'] = {
'search_default_last_year_mo_order': 1,
'search_default_status': 1, 'search_default_scheduled_month': 1,
'graph_measure': 'product_uom_qty',
}
return action
@@ -54,7 +58,7 @@ class ProductProduct(models.Model):
bom_line_ids = fields.One2many('mrp.bom.line', 'product_id', 'BoM Components')
bom_count = fields.Integer('# Bill of Material', compute='_compute_bom_count')
used_in_bom_count = fields.Integer('# BoM Where Used', compute='_compute_used_in_bom_count')
mo_count = fields.Integer('# Manufacturing Orders', compute='_compute_mo_count')
mrp_product_qty = fields.Float('Manufactured', compute='_compute_mrp_product_qty')
def _compute_bom_count(self):
for product in self:
@@ -72,11 +76,14 @@ class ProductProduct(models.Model):
action['domain'] = [('bom_line_ids.product_id', '=', self.id)]
return action
def _compute_mo_count(self):
read_group_res = self.env['mrp.production'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id'])
mapped_data = dict([(data['product_id'][0], data['product_id_count']) for data in read_group_res])
def _compute_mrp_product_qty(self):
date_from = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=365))
#TODO: state = done?
domain = [('state', '=', 'done'), ('product_id', 'in', self.ids), ('date_planned_start', '>', date_from)]
read_group_res = self.env['mrp.production'].read_group(domain, ['product_id', 'product_uom_qty'], ['product_id'])
mapped_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in read_group_res])
for product in self:
product.mo_count = mapped_data.get(product.id, 0)
product.mrp_product_qty = float_round(mapped_data.get(product.id, 0), precision_rounding=product.uom_id.rounding)
@api.multi
def action_view_bom(self):
@@ -89,3 +96,14 @@ class ProductProduct(models.Model):
}
action['domain'] = ['|', ('product_id', 'in', self.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', template_ids)]
return action
@api.multi
def action_view_mos(self):
action = self.env.ref('mrp.mrp_production_report').read()[0]
action['domain'] = [('state', '=', 'done'), '&', ('product_id', 'in', self.ids)]
action['context'] = {
'search_default_last_year_mo_order': 1,
'search_default_status': 1, 'search_default_scheduled_month': 1,
'graph_measure': 'product_uom_qty',
}
return action
+6 -5
View File
@@ -260,6 +260,10 @@
<separator/>
<filter string="Late" domain="['&amp;', ('date_planned_start', '&lt;', current_date), ('state', '=', 'confirmed')]"
name="late" help="Production started late"/>
<filter string="Done Last 365 Days" name="last_year_mo_order"
domain="[('date_planned_start', '&gt;', (context_today() - relativedelta(days=365)).strftime('%Y-%m-%d'))]"/>
<filter string="My Activities" name="activities_my"
domain="[('activity_ids.user_id', '=', uid)]"/>
<separator/>
<filter string="No Routing" domain="[('routing_id','=', False)]" name="norouting" groups="mrp.group_mrp_routings"/>
<field name="product_id"/>
@@ -267,9 +271,6 @@
<field name="name" string="Work Center" filter_domain="[('routing_id.operation_ids.workcenter_id','ilike',self)]"/>
<field name="routing_id" groups="mrp.group_mrp_routings"/>
<separator/>
<filter string="My Activities" name="activities_my"
domain="[('activity_ids.user_id', '=', uid)]"/>
<separator/>
<filter string="Late Activities" name="activities_overdue"
domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
help="Show all records which has next action date is before today"/>
@@ -281,8 +282,8 @@
<group expand="0" string="Group By...">
<filter string="Product" name="product" domain="[]" context="{'group_by':'product_id'}"/>
<filter string="Routing" name="routing" domain="[]" context="{'group_by':'routing_id'}" groups="mrp.group_mrp_routings"/>
<filter string="Status" name="status" domain="[]" context="{'group_by':'state'}"/>
<filter string="Scheduled Date" name="scheduled_month" domain="[]" context="{'group_by':'date_planned_start'}"/>
<filter string="Scheduled Month" name="scheduled_month" domain="[]" context="{'group_by':'date_planned_start'}" help="Scheduled Date by Month"/>
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
+45 -16
View File
@@ -15,42 +15,59 @@
</xpath>
</field>
</record>
<record id="product_template_search_view_procurment" model="ir.ui.view">
<field name="name">product.template.search.bom</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<record id="mrp_product_template_search_view" model="ir.ui.view">
<field name="name">mrp.product.template.search</field>
<field name="model">product.template</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='consumable']" position="after">
<separator/>
<filter string="Manufactured Products" name="manufactured_products" domain="[('bom_ids', '!=', False)]"/>
<filter string="BoM Components" name="components" domain="[('bom_line_ids', '!=', False)]"/>
</xpath>
</field>
</field>
</record>
<record id="view_mrp_product_product_form_inherited_view" model="ir.ui.view">
<field name="name">product.form.mrp.inherited</field>
<record id="mrp_product_product_search_view" model="ir.ui.view">
<field name="name">mrp.product.product.search</field>
<field name="model">product.product</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="product.product_search_form_view"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='manufactured_products']" position="replace">
<filter string="Manufactured Products" name="manufactured_products" domain="['|',('bom_ids.product_id','=', False),('variant_bom_ids','!=', False)]"/>
<xpath expr="//filter[@name='consumable']" position="after">
<separator/>
<filter string="Manufactured Products" name="manufactured_products" domain="[('bom_ids', '!=', False)]"/>
<filter string="BoM Components" name="components" domain="[('bom_line_ids', '!=', False)]"/>
</xpath>
</field>
</record>
<record id="product_template_action" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product.template</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="mrp_product_template_search_view"/>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{"search_default_consumable": 1, 'default_type': 'product'}</field>
</record>
<menuitem id="menu_mrp_product_form"
name="Products"
action="product_template_action"
parent="menu_mrp_bom" sequence="1"/>
<record id="mrp_product_variant_action" model="ir.actions.act_window">
<field name="name">Product Variants</field>
<field name="res_model">product.product</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="mrp_product_product_search_view"/>
<field name="view_mode">kanban,tree,form</field>
</record>
<menuitem id="product_variant_mrp" name="Product Variants"
action="product.product_normal_action"
action="mrp_product_variant_action"
parent="menu_mrp_bom" groups="product.group_product_variant" sequence="2"/>
@@ -70,8 +87,14 @@
<field string="Used In" name="used_in_bom_count" widget="statinfo" />
</button>
<button class="oe_stat_button" name="action_view_mos" type="object"
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}" icon="fa-list-alt">
<field string="Manufacturing" name="mo_count" widget="statinfo" />
attrs="{'invisible': [('type', 'not in', ['product', 'consu'])]}" icon="fa-list-alt" help="Manufactured in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="mrp_product_qty" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Manufactured</span>
</div>
</button>
</button>
</field>
@@ -92,9 +115,15 @@
attrs="{'invisible':['|',('type', 'not in', ['product', 'consu']), ('used_in_bom_count', '=', 0)]}" icon="fa-level-up">
<field string="Used In" name="used_in_bom_count" widget="statinfo" />
</button>
<button class="oe_stat_button" name="%(act_product_mrp_production)d" type="action"
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}" icon="fa-list-alt">
<field string="Manufacturing" name="mo_count" widget="statinfo" />
<button class="oe_stat_button" name="action_view_mos" type="object"
attrs="{'invisible': [('type', 'not in', ['product', 'consu'])]}" icon="fa-list-alt" help="Manufactured in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="mrp_product_qty" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Manufactured</span>
</div>
</button>
</button>
</field>
+1 -1
View File
@@ -7,7 +7,7 @@ from odoo.exceptions import UserError
class ProductTemplate(models.Model):
_inherit = 'product.template'
available_in_pos = fields.Boolean(string='Available in Point of Sale', help='Check if you want this product to appear in the Point of Sale', default=False)
available_in_pos = fields.Boolean(string='Available in POS', help='Check if you want this product to appear in the Point of Sale', default=False)
to_weight = fields.Boolean(string='To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration")
pos_categ_id = fields.Many2one(
'pos.category', string='Point of Sale Category',
+11 -7
View File
@@ -52,13 +52,17 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.view_template_property_form"/>
<field name="arch" type="xml">
<group name="email_template_and_project" position="before">
<group name="pos" string="Point of Sale">
<field name="available_in_pos"/>
<field name="pos_categ_id" attrs="{'invisible': [('available_in_pos', '=', False)]}" string="PoS Category"/>
<field name="to_weight" attrs="{'invisible': [('available_in_pos', '=', False)]}"/>
</group>
</group>
<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>
</field>
</record>
@@ -103,6 +103,7 @@ class ProductTemplate(models.Model):
'uom.uom', 'Unit of Measure',
default=_get_default_uom_id, required=True,
help="Default Unit of Measure used for all stock operation.")
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',
default=_get_default_uom_id, required=True,
@@ -107,6 +107,7 @@
<field name="view_mode">kanban,tree,form</field>
<field name="view_type">form</field>
<field name="view_id" ref="product_template_kanban_view"/>
<field name="search_view_id" ref="product.product_template_search_view"/>
<field name="context">{"search_default_filter_to_sell":1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
+34 -35
View File
@@ -60,7 +60,40 @@
<field name="currency_id" invisible="1"/>
<field name="product_variant_id" invisible="1"/>
</group>
<group name="group_lots_and_weight" attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}">
</group>
<group string="Internal Notes">
<field name="description" nolabel="1" placeholder="This note is only for internal purposes."/>
</group>
</page>
<page string="Sales" attrs="{'invisible':[('sale_ok','=',False)]}" name="sales" invisible="1">
<group name="sale">
<div name="pricelist_item" groups="product.group_product_pricelist">
<separator string="Pricing"/>
<field name="item_ids" nolabel="1" context="{'default_base':'list_price', 'default_applied_on' :'1_product'}">
<tree string="Pricelist Items" editable="bottom">
<field name="pricelist_id" string="Pricelist" required='1'/>
<field name="fixed_price" string="Price" required='1'/>
<field name="min_quantity"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="applied_on" invisible="1"/>
</tree>
</field>
</div>
<group name="email_template_and_project" invisible="1"/>
</group>
<group string="Description for Customers" name="description">
<field name="description_sale" nolabel="1" placeholder="This note will show up on sales orders &amp; invoices."/>
</group>
</page>
<page string="Purchase" name="purchase" attrs="{'invisible': [('purchase_ok','=',False)]}" invisible="1">
<group name="purchase">
<group string="Vendor Bills" name="bill"/>
</group>
</page>
<page string="Inventory" name="inventory" groups="stock.group_stock_user,product.group_stock_packaging" attrs="{'invisible':[('type', '=', 'service')]}">
<group name="inventory">
<group name="group_lots_and_weight" string="Logistics" attrs="{'invisible': [('type', 'not in', ['product', 'consu'])]}">
<label for="weight"/>
<div class="o_row" name="weight">
<field name="weight"/>
@@ -72,41 +105,7 @@
<span>m³</span>
</div>
</group>
<group name="description_internal">
<group string="Description for Internal">
<field name="description" nolabel="1" placeholder="This note is only for internal purposes."/>
</group>
</group>
</group>
</page>
<page string="Sales" attrs="{'invisible':[('sale_ok','=',False)]}" name="sales">
<div name="pricelist_item" groups="product.group_product_pricelist">
<separator string="Pricing"/>
<field name="item_ids" nolabel="1" context="{'default_base':'list_price', 'default_applied_on' :'1_product'}">
<tree string="Pricelist Items" editable="bottom">
<field name="pricelist_id" string="Pricelist" required='1'/>
<field name="fixed_price" string="Price" required='1'/>
<field name="min_quantity"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="applied_on" invisible="1"/>
</tree>
</field>
</div>
<group name="sale">
<group name="email_template_and_project"
attrs="{'invisible':[('type', '!=', 'service')]}"/>
</group>
<group string="Description for Customers" name="description">
<field name="description_sale" nolabel="1" placeholder="This note will show up on sales orders &amp; invoices."/>
</group>
</page>
<page string="Purchase" name="purchase" attrs="{'invisible': [('purchase_ok','=',False)]}">
<group name="purchase">
<group string="Vendor Bills" name="bill"/>
</group>
</page>
<page string="Inventory" name="inventory" groups="stock.group_stock_user,product.group_stock_packaging" attrs="{'invisible':[('type', '=', 'service')]}">
<group name="packaging" string="Packaging"
colspan="4"
attrs="{'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]}"
@@ -5,11 +5,11 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<group name="email_template_and_project" position="attributes">
<attribute name="string">Email</attribute>
<attribute name="attrs"></attribute>
</group>
<group name="email_template_and_project" position="inside">
<xpath expr="//group[@name='email_template_and_project']" position="attributes">
<attribute name="string">Automatic Email at Invoice</attribute>
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='email_template_and_project']" position="inside">
<field name="email_template_id" string="Email Template" help="Send a product-specific email once the invoice is paid"
domain="[('model','=','product.template')]"
context="{
@@ -18,7 +18,7 @@
'default_subject': name,
'default_name': name,
}"/>
</group>
</xpath>
</field>
</record>
</odoo>
+1 -1
View File
@@ -66,7 +66,7 @@
'name': 'Support Services',
'price_unit': 25.50,
'product_qty': 10.0,
'product_uom': ref('uom.product_uom_unit'),
'product_uom': ref('uom.product_uom_hour'),
'date_planned': time.strftime('%Y-%m-%d')}),
]"/>
</record>
+41 -11
View File
@@ -1,24 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models, _
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.tools.float_utils import float_round
class ProductTemplate(models.Model):
_name = 'product.template'
_inherit = 'product.template'
@api.multi
def _purchase_count(self):
for template in self:
template.purchase_count = sum([p.purchase_count for p in template.product_variant_ids])
return True
property_account_creditor_price_difference = fields.Many2one(
'account.account', string="Price Difference Account", company_dependent=True,
help="This account will be used to value price difference between purchase price and cost price.")
purchase_count = fields.Integer(compute='_purchase_count', string='# Purchases')
help="This account is used in automated inventory valuation to "\
"record the price difference between a purchase order and its related vendor bill "\
"when validating this vendor bill.")
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased')
purchase_method = fields.Selection([
('purchase', 'On ordered quantities'),
('receive', 'On received quantities'),
@@ -27,6 +26,11 @@ class ProductTemplate(models.Model):
purchase_line_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Line', help=WARNING_HELP, required=True, default="no-message")
purchase_line_warn_msg = fields.Text('Message for Purchase Order Line')
@api.multi
def _compute_purchased_product_qty(self):
for template in self:
template.purchased_product_qty = float_round(sum([p.purchased_product_qty for p in template.product_variant_ids]), precision_rounding=template.uom_id.rounding)
@api.model
def get_import_templates(self):
res = super(ProductTemplate, self).get_import_templates()
@@ -37,22 +41,48 @@ class ProductTemplate(models.Model):
}]
return res
@api.multi
def action_view_po(self):
action = self.env.ref('purchase.action_purchase_order_report_all').read()[0]
action['domain'] = [('state', 'in', ['purchase', 'done']), '&', ('product_tmpl_id', 'in', self.ids)]
action['context'] = {
'search_default_last_year_purchase': 1,
'search_default_status': 1, 'search_default_order_month': 1,
'graph_measure': 'unit_quantity'
}
return action
class ProductProduct(models.Model):
_name = 'product.product'
_inherit = 'product.product'
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased')
@api.multi
def _purchase_count(self):
def _compute_purchased_product_qty(self):
date_from = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=365))
domain = [
('state', 'in', ['purchase', 'done']),
('product_id', 'in', self.mapped('id')),
('date_order', '>', date_from)
]
PurchaseOrderLines = self.env['purchase.order.line'].search(domain)
order_lines = self.env['purchase.order.line'].read_group(domain, ['product_id', 'product_uom_qty'], ['product_id'])
purchased_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in order_lines])
for product in self:
product.purchase_count = len(PurchaseOrderLines.filtered(lambda r: r.product_id == product).mapped('order_id'))
product.purchased_product_qty = float_round(purchased_data.get(product.id, 0), precision_rounding=product.uom_id.rounding)
purchase_count = fields.Integer(compute='_purchase_count', string='# Purchases')
@api.multi
def action_view_po(self):
action = self.env.ref('purchase.action_purchase_order_report_all').read()[0]
action['domain'] = [('state', 'in', ['purchase', 'done']), '&', ('product_id', 'in', self.ids)]
action['context'] = {
'search_default_last_year_purchase': 1,
'search_default_status': 1, 'search_default_order_month': 1,
'graph_measure': 'unit_quantity'
}
return action
class ProductCategory(models.Model):
+10
View File
@@ -405,6 +405,7 @@ class PurchaseOrderLine(models.Model):
name = fields.Text(string='Description', required=True)
sequence = fields.Integer(string='Sequence', default=10)
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True)
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
product_uom = fields.Many2one('uom.uom', string='Product Unit of Measure', required=True)
@@ -610,6 +611,15 @@ class PurchaseOrderLine(models.Model):
self.price_unit = price_unit
@api.multi
@api.depends('product_uom', 'product_qty', 'product_id.uom_id')
def _compute_product_uom_qty(self):
for line in self:
if line.product_id.uom_id != line.product_uom:
line.product_uom_qty = line.product_uom._compute_quantity(line.product_qty, line.product_id.uom_id)
else:
line.product_uom_qty = line.product_qty
def _suggest_quantity(self):
'''
Suggest a minimal quantity based on the seller
@@ -56,6 +56,8 @@
<search string="Purchase Orders">
<filter string="Quotations" name="quotes" domain="[('state','=','draft')]"/>
<filter string="Orders" name="orders" domain="[('state','!=','draft'),('state','!=','cancel')]"/>
<filter string="Purchased Last 365 Days" name="last_year_purchase"
domain="[('date_order', '&gt;', (context_today() - relativedelta(days=365)).strftime('%Y-%m-%d'))]"/>
<field name="partner_id"/>
<field name="product_id"/>
<group expand="0" string="Extended Filters">
@@ -70,10 +72,10 @@
<filter string="Partner's Country" name="country_id" context="{'group_by':'country_id'}"/>
<filter string="Purchase Representative" name="user_id" context="{'group_by':'user_id'}"/>
<filter string="Product Category" name="group_category_id" context="{'group_by':'category_id'}"/>
<filter string="Order Date" name="order_month" context="{'group_by': 'date_order:month'}"/>
<filter string="Status" name="status" context="{'group_by':'state'}"/>
<filter string="Company" name="company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<separator/>
<filter string="Order Date" name="order_month" context="{'group_by':'date_order:month'}"/>
<filter string="Fiscal Position" name="fiscal_position" context="{'group_by':'fiscal_position_id'}" help="Purchase Order Fiscal Position"/>
</group>
</search>
+34 -13
View File
@@ -8,6 +8,9 @@
<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='purchase']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<group name="purchase" position="before">
<group string="Vendors" name="vendors" attrs="{'invisible': [('product_variant_count','&gt;',1)]}">
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True}" nolabel="1"/>
@@ -47,15 +50,6 @@
</field>
</record>
<record model="ir.actions.act_window" id="action_purchase_line_product_tree">
<field name="context">{}</field>
<field name="domain">[('product_id.product_tmpl_id','in',active_ids), ('state', 'in', ['purchase', 'done'])]</field>
<field name="name">Purchase Order Lines</field>
<field name="res_model">purchase.order.line</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="purchase_order_line_tree"/>
</record>
<record id="view_product_account_purchase_ok_form" model="ir.ui.view">
<field name="name">product.template.account.purchase.ok.form.inherit</field>
<field name="model">product.template</field>
@@ -78,10 +72,16 @@
<field name="groups_id" eval="[(4, ref('purchase.group_purchase_user'))]"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="%(purchase.action_purchase_line_product_tree)d"
type="action" icon="fa-shopping-cart" attrs="{'invisible': [('purchase_ok', '=', False)]}">
<field string="Purchases" name="purchase_count" widget="statinfo"/>
</button>
<button class="oe_stat_button" name="action_view_po"
type="object" icon="fa-shopping-cart" attrs="{'invisible': [('purchase_ok', '=', False)]}" help="Purchased in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="purchased_product_qty" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Purchased</span>
</div>
</button>
</button>
</field>
</record>
@@ -97,4 +97,25 @@
</field>
</record>
<record id="product_normal_form_view_inherit_purchase" model="ir.ui.view">
<field name="name">product.product.purchase.order</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="groups_id" eval="[(4, ref('purchase.group_purchase_user'))]"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="action_view_po"
type="object" icon="fa-shopping-cart" attrs="{'invisible': [('purchase_ok', '=', False)]}" help="Purchased in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="purchased_product_qty" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Purchased</span>
</div>
</button>
</button>
</field>
</record>
</odoo>
+2 -1
View File
@@ -56,7 +56,7 @@
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{"search_default_filter_to_purchase":1, "purchase_product_template": 1}</field>
<field name="search_view_id" eval="False"/> <!-- Force empty -->
<field name="search_view_id" ref="product.product_template_search_view"/>
<field name="view_id" eval="False"/> <!-- Force empty -->
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -77,6 +77,7 @@
<field name="res_model">product.product</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="product.product_search_form_view"/>
<field name="context">{"search_default_filter_to_purchase": 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
+1
View File
@@ -17,6 +17,7 @@ This module contains all the common features of Sales Management and eCommerce.
'report/sale_report_views.xml',
'report/sale_report_templates.xml',
'report/invoice_report_templates.xml',
'report/report_all_channels_sales_views.xml',
'data/ir_sequence_data.xml',
'data/sale_data.xml',
'data/mail_data.xml',
+19 -3
View File
@@ -1,28 +1,44 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models
from odoo.tools.float_utils import float_round
class ProductProduct(models.Model):
_inherit = 'product.product'
sales_count = fields.Float(compute='_compute_sales_count', string='Sold')
@api.multi
def _sales_count(self):
def _compute_sales_count(self):
r = {}
if not self.user_has_groups('sales_team.group_sale_salesman'):
return r
date_from = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=365))
domain = [
('state', 'in', ['sale', 'done']),
('product_id', 'in', self.ids),
('date', '>', date_from)
]
for group in self.env['sale.report'].read_group(domain, ['product_id', 'product_uom_qty'], ['product_id']):
r[group['product_id'][0]] = group['product_uom_qty']
for product in self:
product.sales_count = r.get(product.id, 0)
product.sales_count = float_round(r.get(product.id, 0), precision_rounding=product.uom_id.rounding)
return r
sales_count = fields.Integer(compute='_sales_count', string='# Sales')
@api.multi
def action_view_sales(self):
action = self.env.ref('sale.report_all_channels_sales_action').read()[0]
action['domain'] = [('product_id', 'in', self.ids)]
action['context'] = {
'search_default_last_year': 1,
'pivot_measures': ['product_qty'],
'search_default_team_id': 1
}
return action
def _get_invoice_policy(self):
return self.invoice_policy
+33 -27
View File
@@ -3,6 +3,7 @@
from odoo import api, fields, models, _
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.tools.float_utils import float_round
class ProductTemplate(models.Model):
@@ -18,33 +19,15 @@ class ProductTemplate(models.Model):
expense_policy = fields.Selection(
[('no', 'No'), ('cost', 'At cost'), ('sales_price', 'Sales price')],
string='Re-Invoice Policy',
default='no')
@api.multi
@api.depends('product_variant_ids.sales_count')
def _sales_count(self):
for product in self:
product.sales_count = sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids])
@api.multi
def action_view_sales(self):
self.ensure_one()
action = self.env.ref('sale.action_product_sale_list')
product_ids = self.with_context(active_test=False).product_variant_ids.ids
return {
'name': action.name,
'help': action.help,
'type': action.type,
'view_type': action.view_type,
'view_mode': action.view_mode,
'target': action.target,
'context': "{'default_product_id': " + str(product_ids[0]) + "}",
'res_model': action.res_model,
'domain': [('state', 'in', ['sale', 'done']), ('product_id.product_tmpl_id', '=', self.id)],
}
sales_count = fields.Integer(compute='_sales_count', string='# Sales')
default='no',
help="Expenses registered in Expense app or in a vendor bill (with an analytic account set) "
"can be automatically re-invoiced to the customer. This is useful for customer projects. "
"With this option activated, the validated expense is added to the sales order linked to the "
"analytic account, in order to be invoiced. With Project Management, such an analytic account "
"can be automatically generated at the order confirmation, if set on the Service Tracking "
"option of one product item.")
sales_count = fields.Float(compute='_compute_sales_count', string='Sold')
hide_expense_policy = fields.Boolean(compute='_compute_hide_expense_policy')
invoice_policy = fields.Selection(
[('order', 'Ordered quantities'),
('delivery', 'Delivered quantities'),
@@ -53,6 +36,29 @@ class ProductTemplate(models.Model):
'Delivered Quantity: Invoiced based on the quantity the vendor delivered (time or deliveries).',
default='order')
@api.multi
def _compute_hide_expense_policy(self):
hide_expense_policy = self.user_has_groups('!analytic.group_analytic_accounting,!project.group_project_user,!hr_expense.group_hr_expense_user')
for template in self:
template.hide_expense_policy = hide_expense_policy
@api.multi
@api.depends('product_variant_ids.sales_count')
def _compute_sales_count(self):
for product in self:
product.sales_count = float_round(sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]), precision_rounding=product.uom_id.rounding)
@api.multi
def action_view_sales(self):
action = self.env.ref('sale.report_all_channels_sales_action').read()[0]
action['domain'] = [('product_tmpl_id', 'in', self.ids)]
action['context'] = {
'search_default_last_year': 1,
'pivot_measures': ['product_qty'],
'search_default_team_id': 1
}
return action
@api.onchange('type')
def _onchange_type(self):
""" Force values to stay consistent with integrity constraints """
+1
View File
@@ -3,3 +3,4 @@
from . import sale_report
from . import invoice_report
from . import report_all_channels_sales
@@ -17,7 +17,9 @@
<field name="arch" type="xml">
<search string="Sales by Channel Analysis">
<field name="date_order"/>
<field name="product_id"/>
<filter string="Date" name="current_year" date="date_order" default_period="this_year"/>
<filter string="Last 365 Days" name="last_year" domain="[('date_order', '&gt;=', (context_today() - datetime.timedelta(days=365)).strftime('%Y-%m-%d'))]"/>
<group expand="1" string="Group By">
<filter string="Sales Team" name="team_id" context="{'group_by':'team_id'}"/>
</group>
+1
View File
@@ -49,3 +49,4 @@ access_account_invoice_tax_sale_manager,account.invoice.tax sale manager,accoun
access_account_tax_group_sale_manager,account.tax.group sale manager,account.model_account_tax_group,sales_team.group_sale_salesman,1,0,0,0
access_account_account_sale_manager,account.account sale manager,account.model_account_account,sales_team.group_sale_manager,1,0,0,0
access_mail_activity_type_sale_manager,mail.activity.type.sale.manager,mail.model_mail_activity_type,sales_team.group_sale_manager,1,1,1,1
access_report_all_channels_sales,access_report_all_channels_sales,model_report_all_channels_sales,sales_team.group_sale_manager,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
49 access_account_tax_group_sale_manager account.tax.group sale manager account.model_account_tax_group sales_team.group_sale_salesman 1 0 0 0
50 access_account_account_sale_manager account.account sale manager account.model_account_account sales_team.group_sale_manager 1 0 0 0
51 access_mail_activity_type_sale_manager mail.activity.type.sale.manager mail.model_mail_activity_type sales_team.group_sale_manager 1 1 1 1
52 access_report_all_channels_sales access_report_all_channels_sales model_report_all_channels_sales sales_team.group_sale_manager 1 0 0 0
+22 -21
View File
@@ -62,6 +62,7 @@
<field name="view_mode">kanban,tree,form</field>
<field name="view_type">form</field>
<field name="view_id" ref="product.product_template_kanban_view"/>
<field name="search_view_id" ref="product.product_template_search_view"/>
<field name="context">{"search_default_filter_to_sell":1, "sale_multi_pricelist_product_template": 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -871,14 +872,6 @@
</field>
</record>
<record id="action_product_sale_list" model="ir.actions.act_window">
<field name="name">Sales Order Lines</field>
<field name="res_model">sale.order.line</field>
<field name="view_mode">tree</field>
<field name="context">{'search_default_product_id': [active_id], 'default_product_id': active_id}</field>
<field name="domain">[('state', 'in', ['sale', 'done'])]</field>
</record>
<record model="ir.ui.view" id="product_form_view_sale_order_button">
<field name="name">product.product.sale.order</field>
<field name="model">product.product</field>
@@ -886,9 +879,15 @@
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="%(action_product_sale_list)d"
type="action" icon="fa-usd" groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" />
<button class="oe_stat_button" name="action_view_sales"
type="object" icon="fa-signal" groups="sales_team.group_sale_salesman" help="Sold in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="sales_count" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Sold</span>
</div>
</button>
</button>
<group name="description" position="after">
@@ -909,9 +908,14 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="action_view_sales"
type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"
groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" />
type="object" icon="fa-signal" groups="sales_team.group_sale_salesman" help="Sold in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="sales_count" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Sold</span>
</div>
</button>
</button>
<group name="description" position="after">
@@ -929,17 +933,14 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<group name="sale" position="inside">
<xpath expr="//group[@name='email_template_and_project']" position="before">
<group string="Invoicing" name="invoicing">
<field name="invoice_policy" widget="radio"/>
<field name="service_type" widget="radio" invisible="True"/>
<field name="hide_expense_policy" invisible="1"/>
<field name="expense_policy" widget="radio" attrs="{'invisible': [('hide_expense_policy', '=', True)]}"/>
</group>
</group>
<group name="purchase" position="inside">
<group string="Invoicing" name="invoicing">
<field name="expense_policy" widget="radio"/>
</group>
</group>
</xpath>
</field>
</record>
@@ -12,6 +12,12 @@
<field name="expense_policy"/>
</group>
</xpath>
<xpath expr="//field[@name='list_price']" position="replace">
<field name="list_price" attrs="{'invisible':[('expense_policy', '!=', 'sales_price')]}"/>
</xpath>
<xpath expr="//field[@name='taxes_id']" position="replace">
<field name="taxes_id" widget="many2many_tags" attrs="{'invisible':[('expense_policy', '=', 'no')]}"/>
</xpath>
</field>
</record>
@@ -100,4 +100,15 @@
</field>
</record>
<record model="ir.ui.view" id="product_template_sale_form_view">
<field name="name">product.template.sales</field>
<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']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
</field>
</record>
</odoo>
-1
View File
@@ -33,7 +33,6 @@ You can choose flexible invoicing methods:
'views/res_config_settings_views.xml',
'views/account_invoice_views.xml',
'views/sale_stock_portal_template.xml',
'views/product_template_views.xml',
'views/res_users_views.xml',
'views/stock_production_lot_views.xml',
'report/sale_order_report_templates.xml',
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_view_form_view_inherit_invoice_policy" model="ir.ui.view">
<field name="name">product.template.form.expense.policy.inherit</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="//field[@name='expense_policy']" position="attributes">
<attribute name="attrs">{'invisible': [('type','=', 'product')]}</attribute>
</xpath>
</field>
</record>
</odoo>
+9 -3
View File
@@ -92,9 +92,15 @@
<field name="arch" type="xml">
<button name="action_view_orderpoints" position="after">
<button class="oe_stat_button" name="action_view_sales"
type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"
groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" />
type="object" icon="fa-signal" attrs="{'invisible': [('sale_ok', '=', False)]}"
groups="sales_team.group_sale_salesman" help="Sold in the last 365 days">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="sales_count" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">Sold</span>
</div>
</button>
</button>
</field>
+1 -1
View File
@@ -12,7 +12,7 @@ class ProductTemplate(models.Model):
('ordered_timesheet', 'Ordered quantities'),
('delivered_timesheet', 'Timesheets on tasks'),
('delivered_manual', 'Milestones (manually set quantities on order)')
], string="Invoice based on", compute='_compute_service_policy', inverse='_inverse_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)'),
])
@@ -18,20 +18,6 @@
</field>
</record>
<record id="product_template_search_view_sale_timesheet" model="ir.ui.view">
<field name="name">product.template.search.timesheet</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='consumable']" position="after">
<separator/>
<filter string="Time-based services" name="product_time_based" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')]"/>
<filter string="Fixed price services" name="product_service_fixed" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'order'), ('service_type', '=', 'timesheet')]"/>
<filter string="Milestone services" name="product_service_milestone" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'manual')]"/>
</xpath>
</field>
</record>
<record id="product_template_action_time_based" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product.template</field>
+67 -29
View File
@@ -62,17 +62,31 @@
<record id="product_template_search_form_view_stock" model="ir.ui.view">
<field name="name">product.template.search.stock.form</field>
<field name="model">product.template</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<field name="pricelist_id" position="after">
<xpath expr="//field[@name='pricelist_id']" position="after">
<separator/>
<field name="location_id" widget="selection" context="{'location': self}"/>
<field name="warehouse_id" widget="selection" context="{'warehouse': self}"/>
<separator/>
<filter name="real_stock_available" string="Available Products" domain="[('qty_available','&gt;',0)]"/>
<filter name="real_stock_exhausted" string="Exhausted Stock" domain="[('qty_available','&lt;=',0),('type','not in',('service', 'consu'))]"/>
<filter name="real_stock_negative" string="Negative Stock" domain="[('qty_available','&lt;',0)]"/>
</field>
<filter name="real_stock_negative" string="Negative Forecasted Quantity" domain="[('virtual_available','&lt;',0)]"/>
</xpath>
</field>
</record>
<record id="stock_product_search_form_view" model="ir.ui.view">
<field name="name">product.product.search.stock.form</field>
<field name="model">product.product</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="product.product_search_form_view"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='inactive']" position="after">
<separator/>
<filter name="real_stock_available" string="Available Products" domain="[('qty_available','&gt;',0)]"/>
<filter name="real_stock_negative" string="Negative Forecasted Quantity" domain="[('virtual_available','&lt;',0)]"/>
</xpath>
</field>
</record>
@@ -82,23 +96,21 @@
<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='packaging']" position="before">
<group name="inventory">
<group string="Operations" name="operations">
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>
<field name="route_from_categ_ids" widget="many2many_tags" readonly="1" attrs="{'invisible':[('route_from_categ_ids', '=', [])]}"/>
<label for="sale_delay"/>
<div>
<field name="sale_delay" attrs="{'readonly':[('sale_ok','=',False)]}" class="oe_inline" style="vertical-align:baseline"/> days
</div>
</group>
<group string="Traceability" name="traceability" groups="stock.group_production_lot">
<field name="tracking" widget="radio" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>
</group>
<group string="Counterpart Locations" name="stock_property" groups="base.group_no_one">
<field name="property_stock_production" domain="[('usage','=','production')]"/>
<field name="property_stock_inventory" domain="[('usage','=','inventory')]"/>
</group>
<xpath expr="//group[@name='group_lots_and_weight']" position="before">
<group string="Operations" name="operations">
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible': [('type', 'in', ['service', 'digital'])]}"/>
<field name="route_from_categ_ids" widget="many2many_tags" readonly="1" attrs="{'invisible': [('route_from_categ_ids', '=', [])]}"/>
<label for="sale_delay" attrs="{'invisible': [('sale_ok', '=', False)]}"/>
<div attrs="{'invisible': [('sale_ok', '=', False)]}">
<field name="sale_delay" class="oe_inline" style="vertical-align:baseline"/> days
</div>
</group>
<group string="Traceability" name="traceability" groups="stock.group_production_lot">
<field name="tracking" widget="radio" attrs="{'invisible': [('type', 'in', ['service', 'digital'])]}"/>
</group>
<group string="Counterpart Locations" name="stock_property" groups="base.group_no_one">
<field name="property_stock_production" domain="[('usage', '=', 'production')]"/>
<field name="property_stock_inventory" domain="[('usage', '=', 'inventory')]"/>
</group>
</xpath>
<page name="inventory" position="inside">
@@ -184,18 +196,25 @@
name="%(product_open_quants)d"
icon="fa-building-o"
type="action" attrs="{'invisible':[('type', '!=', 'product')]}">
<field name="qty_available" widget="statinfo" string="On Hand"/>
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="qty_available" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">On Hand</span>
</div>
</button>
<button type="action"
name="%(stock.action_stock_level_forecast_report_product)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-building-o">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="virtual_available" widget="statinfo" nolabel="1"/>
<span attrs="{'invisible':[('outgoing_qty', '=', 0),('incoming_qty','=',0)]}">
(-<field name="outgoing_qty" widget="statinfo" nolabel="1"/>
+<field name="incoming_qty" widget="statinfo" nolabel="1"/>)
<span class="o_stat_value">
<field name="virtual_available" widget="statinfo" nolabel="1"/>
<span attrs="{'invisible': [('outgoing_qty', '=', 0), ('incoming_qty', '=', 0)]}" groups="base.group_no_one">
(-<field name="outgoing_qty" widget="statinfo" nolabel="1"/>+<field name="incoming_qty" widget="statinfo" nolabel="1"/>)
</span>
<field name="uom_name" class="ml4"/>
</span>
<span class="o_stat_text">Forecasted</span>
</div>
@@ -257,17 +276,25 @@
name="action_open_quants"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-building-o">
<field name="qty_available" widget="statinfo" string="On Hand"/>
<div class="o_field_widget o_stat_info">
<span class="o_stat_value" widget="statinfo">
<field name="qty_available" widget="statinfo" nolabel="1" class="mr4"/>
<field name="uom_name"/>
</span>
<span class="o_stat_text">On Hand</span>
</div>
</button>
<button type="action"
name="%(stock.action_stock_level_forecast_report_template)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-building-o">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="virtual_available" widget="statinfo" nolabel="1"/>
<span class="o_stat_value">
<field name="virtual_available" widget="statinfo" nolabel="1"/>
<span attrs="{'invisible':[('outgoing_qty', '=', 0),('incoming_qty','=',0)]}" groups="base.group_no_one">
(-<field name="outgoing_qty" widget="statinfo" nolabel="1"/>+<field name="incoming_qty" widget="statinfo" nolabel="1"/>)
</span>
<field name="uom_name" class="ml4"/>
</span>
<span class="o_stat_text">Forecasted</span>
</div>
@@ -327,6 +354,7 @@
<field name="res_model">product.template</field>
<field name="view_mode">kanban,tree,form</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="product_template_search_form_view_stock"/>
<field name="context">{"search_default_consumable": 1, 'default_type': 'product'}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -334,9 +362,19 @@
</p>
</field>
</record>
<record id="stock_product_normal_action" model="ir.actions.act_window">
<field name="name">Product Variants</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form,kanban</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="stock_product_search_form_view"/>
</record>
<menuitem id="menu_product_variant_config_stock" name="Products" action="product_template_action_product"
parent="stock.menu_stock_inventory_control" sequence="1"/>
<menuitem id="product_product_menu" name="Product Variants" action="product.product_normal_action"
<menuitem id="product_product_menu" name="Product Variants" action="stock_product_normal_action"
parent="menu_stock_inventory_control" sequence="2" groups="product.group_product_variant"/>
<menuitem id="menu_product_packagings" name="Product Packagings" parent="stock.menu_product_in_config_stock" action="product.action_packaging_view" groups="product.group_stock_packaging"/>
@@ -596,6 +596,9 @@ var FormRenderer = BasicRenderer.extend({
extraClass: 'oe_stat_button',
});
$button.append(_.map(node.children, this._renderNode.bind(this)));
if (node.attrs.help) {
this._addButtonTooltip(node, $button);
}
this._addOnClickAction($button, node);
this._handleAttributes($button, node);
this._registerModifiers(node, this.state, $button);
+47 -45
View File
@@ -17,6 +17,7 @@
<field name="res_model">product.template</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="search_view_id" ref="product_template_search_view_website"/>
<field name="context">{'search_default_published': 1, 'default_website_published': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -73,58 +74,59 @@
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<!-- add state field in header -->
<group name="email_template_and_project" position="before">
<group name="website" string="eCommerce" >
<field name="website_url" invisible="1"/>
<field name="public_categ_ids" widget="many2many_tags" string="eCommerce Categories"/>
<field name="alternative_product_ids" widget="many2many_tags"/>
<field name="accessory_product_ids" widget="many2many_tags"/>
<field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/>
</group>
</group>
<button name="toggle_active" position="before">
<xpath expr="//button[@name='toggle_active']" position="before">
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe" attrs="{'invisible': [('sale_ok','=',False)]}">
<field name="website_published" widget="website_button"/>
</button>
</button>
<xpath expr="//notebook[last()]" position="inside">
<page string="Images">
<field name="product_image_ids" class="o_website_sale_image_list" context="{'default_name': name, 'default_product_tmpl_id': active_id}">
<kanban string="Product Images" class="o_attachments_previews">
<field name="name"/>
<field name="image"/>
<templates>
<t t-name="kanban-box">
<div class="o_attachment oe_kanban_global_click o_website_sale_image_card">
<div class="o_attachment_wrap">
<div class="o_image_box">
<div class="o_attachment_image" t-att-style="'background-image:url(data:image/png;base64,'+record.image.raw_value +')'"/>
<div t-attf-class="o_image_overlay" t-att-title="record.name.value"/>
<div class="o_website_sale_image_card_title" t-att-title="record.name.value">
<field name="name"/>
</xpath>
<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="public_categ_ids" widget="many2many_tags" string="Categories"/>
<field name="alternative_product_ids" widget="many2many_tags"/>
<field name="accessory_product_ids" widget="many2many_tags"/>
<field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/>
</group>
</group>
<group name="extra_images" string="Extra Images">
<field name="product_image_ids" class="o_website_sale_image_list" context="{'default_name': name, 'default_product_tmpl_id': active_id}" nolabel="1">
<kanban string="Product Images" class="o_attachments_previews">
<field name="name"/>
<field name="image"/>
<templates>
<t t-name="kanban-box">
<div class="o_attachment oe_kanban_global_click o_website_sale_image_card">
<div class="o_attachment_wrap">
<div class="o_image_box">
<div class="o_attachment_image" t-att-style="'background-image:url(data:image/png;base64,'+record.image.raw_value +')'"/>
<div t-attf-class="o_image_overlay" t-att-title="record.name.value"/>
<div class="o_website_sale_image_card_title" t-att-title="record.name.value">
<field name="name"/>
</div>
</div>
<t t-set="size" t-value="[record.image.value][0]"/>
<t t-set="size_status" t-value="'normal'" t-if="size.indexOf('Bytes') !== -1 or ( size.indexOf('Kb') !== -1 and size[2]=='.')"/>
<t t-set="size_status" t-value="'warning'" t-elif="( size.indexOf('Kb') !== -1 and size[3]=='.')"/>
<t t-set="size_status" t-value="'danger'" t-else=""/>
<t t-set="message" t-value="'Acceptable file size'" t-if="size_status == 'normal'"></t>
<t t-set="message" t-value="'Huge file size. The image should be optimized/reduced.'" t-elif="size_status == 'warning'"></t>
<t t-set="message" t-value="'Optimization required! Reduce the image size or increase your compression settings.'" t-else=""></t>
<span t-attf-class="o_website_sale_image_card_size badge badge-#{size_status}" t-att-title="message">
<t t-esc="record.image.value"/>
</span>
<a role="button" t-if="!read_only_mode" type="delete" class="btn btn-sm btn-secondary o_website_sale_image_card_remove" aria-label="Remove" title="Remove"><i class="fa fa-trash"/></a>
</div>
<t t-set="size" t-value="[record.image.value][0]"/>
<t t-set="size_status" t-value="'normal'" t-if="size.indexOf('Bytes') !== -1 or ( size.indexOf('Kb') !== -1 and size[2]=='.')"/>
<t t-set="size_status" t-value="'warning'" t-elif="( size.indexOf('Kb') !== -1 and size[3]=='.')"/>
<t t-set="size_status" t-value="'danger'" t-else=""/>
<t t-set="message" t-value="'Acceptable file size'" t-if="size_status == 'normal'"></t>
<t t-set="message" t-value="'Huge file size. The image should be optimized/reduced.'" t-elif="size_status == 'warning'"></t>
<t t-set="message" t-value="'Optimization required! Reduce the image size or increase your compression settings.'" t-else=""></t>
<span t-attf-class="o_website_sale_image_card_size badge badge-#{size_status}" t-att-title="message">
<t t-esc="record.image.value"/>
</span>
<a role="button" t-if="!read_only_mode" type="delete" class="btn btn-sm btn-secondary o_website_sale_image_card_remove" aria-label="Remove" title="Remove"><i class="fa fa-trash"/></a>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</t>
</templates>
</kanban>
</field>
</group>
</page>
</xpath>
</field>
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="action_open_attachments" type="object" icon="fa-file">
<field string="Portal Files" name="attachment_count" widget="statinfo" />
<field string="Digital Files" name="attachment_count" widget="statinfo" />
</button>
</button>
</field>
@@ -20,7 +20,7 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="action_open_attachments" type="object" icon="fa-file">
<field string="Portal Files" name="attachment_count" widget="statinfo" />
<field string="Digital Files" name="attachment_count" widget="statinfo" />
</button>
</button>
</field>
@@ -5,11 +5,11 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="website_sale.product_template_form_view" />
<field name="arch" type="xml">
<field name="website_style_ids" position="before">
<field name="inventory_availability" string="Inventory" widget="selection" attrs="{'invisible':[('type', 'in', ['service', 'consu'])]}"/>
<xpath expr="//field[@name='public_categ_ids']" position="after">
<field name="inventory_availability" string="Availability" widget="selection" attrs="{'invisible': [('type', 'in', ['service', 'consu'])]}"/>
<field name="available_threshold" attrs="{'invisible': ['|', ('type', 'in', ['service', 'consu']), ('inventory_availability', '!=', 'threshold')], 'required': [('inventory_availability', '=', 'threshold'), ('type', '=', 'product')]}"/>
<field name="custom_message" attrs="{'invisible': ['|', ('type', 'in', ['service', 'consu']), ('inventory_availability', '!=', 'custom')]}"/>
</field>
</xpath>
</field>
</record>
</odoo>