summaryrefslogtreecommitdiff
path: root/addons/sale_management/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/sale_management/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/sale_management/models')
-rw-r--r--addons/sale_management/models/__init__.py7
-rw-r--r--addons/sale_management/models/digest.py29
-rw-r--r--addons/sale_management/models/res_company.py15
-rw-r--r--addons/sale_management/models/res_config_settings.py28
-rw-r--r--addons/sale_management/models/sale_order.py277
-rw-r--r--addons/sale_management/models/sale_order_template.py173
6 files changed, 529 insertions, 0 deletions
diff --git a/addons/sale_management/models/__init__.py b/addons/sale_management/models/__init__.py
new file mode 100644
index 00000000..7671a85a
--- /dev/null
+++ b/addons/sale_management/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import digest
+from . import res_company
+from . import res_config_settings
+from . import sale_order
+from . import sale_order_template
diff --git a/addons/sale_management/models/digest.py b/addons/sale_management/models/digest.py
new file mode 100644
index 00000000..8828ad98
--- /dev/null
+++ b/addons/sale_management/models/digest.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, _
+from odoo.exceptions import AccessError
+
+
+class Digest(models.Model):
+ _inherit = 'digest.digest'
+
+ kpi_all_sale_total = fields.Boolean('All Sales')
+ kpi_all_sale_total_value = fields.Monetary(compute='_compute_kpi_sale_total_value')
+
+ def _compute_kpi_sale_total_value(self):
+ if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
+ raise AccessError(_("Do not have access, skip this data for user's digest email"))
+ for record in self:
+ start, end, company = record._get_kpi_compute_parameters()
+ all_channels_sales = self.env['sale.report'].read_group([
+ ('date', '>=', start),
+ ('date', '<', end),
+ ('state', 'not in', ['draft', 'cancel', 'sent']),
+ ('company_id', '=', company.id)], ['price_total'], ['price_total'])
+ record.kpi_all_sale_total_value = sum([channel_sale['price_total'] for channel_sale in all_channels_sales])
+
+ def _compute_kpis_actions(self, company, user):
+ res = super(Digest, self)._compute_kpis_actions(company, user)
+ res['kpi_all_sale_total'] = 'sale.report_all_channels_sales_action&menu_id=%s' % self.env.ref('sale.sale_menu_root').id
+ return res
diff --git a/addons/sale_management/models/res_company.py b/addons/sale_management/models/res_company.py
new file mode 100644
index 00000000..ea9e139e
--- /dev/null
+++ b/addons/sale_management/models/res_company.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+ _check_company_auto = True
+
+ sale_order_template_id = fields.Many2one(
+ "sale.order.template", string="Default Sale Template",
+ domain="['|', ('company_id', '=', False), ('company_id', '=', id)]",
+ check_company=True,
+ )
diff --git a/addons/sale_management/models/res_config_settings.py b/addons/sale_management/models/res_config_settings.py
new file mode 100644
index 00000000..bdfc0e0d
--- /dev/null
+++ b/addons/sale_management/models/res_config_settings.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, api
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ group_sale_order_template = fields.Boolean(
+ "Quotation Templates", implied_group='sale_management.group_sale_order_template')
+ company_so_template_id = fields.Many2one(
+ related="company_id.sale_order_template_id", string="Default Template", readonly=False,
+ domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+ module_sale_quotation_builder = fields.Boolean("Quotation Builder")
+
+ @api.onchange('group_sale_order_template')
+ def _onchange_group_sale_order_template(self):
+ if not self.group_sale_order_template:
+ self.module_sale_quotation_builder = False
+
+ def set_values(self):
+ if not self.group_sale_order_template:
+ self.company_so_template_id = None
+ self.env['res.company'].sudo().search([]).write({
+ 'sale_order_template_id': False,
+ })
+ return super(ResConfigSettings, self).set_values()
diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py
new file mode 100644
index 00000000..165d5acf
--- /dev/null
+++ b/addons/sale_management/models/sale_order.py
@@ -0,0 +1,277 @@
+# -*- 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.exceptions import UserError, ValidationError
+
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ @api.model
+ def default_get(self, fields_list):
+ default_vals = super(SaleOrder, self).default_get(fields_list)
+ if "sale_order_template_id" in fields_list and not default_vals.get("sale_order_template_id"):
+ company_id = default_vals.get('company_id', False)
+ company = self.env["res.company"].browse(company_id) if company_id else self.env.company
+ default_vals['sale_order_template_id'] = company.sale_order_template_id.id
+ return default_vals
+
+ sale_order_template_id = fields.Many2one(
+ 'sale.order.template', 'Quotation Template',
+ readonly=True, check_company=True,
+ states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
+ domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+ sale_order_option_ids = fields.One2many(
+ 'sale.order.option', 'order_id', 'Optional Products Lines',
+ copy=True, readonly=True,
+ states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
+
+ @api.constrains('company_id', 'sale_order_option_ids')
+ def _check_optional_product_company_id(self):
+ for order in self:
+ companies = order.sale_order_option_ids.product_id.company_id
+ if companies and companies != order.company_id:
+ bad_products = order.sale_order_option_ids.product_id.filtered(lambda p: p.company_id and p.company_id != order.company_id)
+ raise ValidationError(_(
+ "Your quotation contains products from company %(product_company)s whereas your quotation belongs to company %(quote_company)s. \n Please change the company of your quotation or remove the products from other companies (%(bad_products)s).",
+ product_company=', '.join(companies.mapped('display_name')),
+ quote_company=order.company_id.display_name,
+ bad_products=', '.join(bad_products.mapped('display_name')),
+ ))
+
+ @api.returns('self', lambda value: value.id)
+ def copy(self, default=None):
+ if self.sale_order_template_id and self.sale_order_template_id.number_of_days > 0:
+ default = dict(default or {})
+ default['validity_date'] = fields.Date.context_today(self) + timedelta(self.sale_order_template_id.number_of_days)
+ return super(SaleOrder, self).copy(default=default)
+
+ @api.onchange('partner_id')
+ def onchange_partner_id(self):
+ super(SaleOrder, self).onchange_partner_id()
+ template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
+ self.note = template.note or self.note
+
+ def _compute_line_data_for_template_change(self, line):
+ return {
+ 'display_type': line.display_type,
+ 'name': line.name,
+ 'state': 'draft',
+ }
+
+ def _compute_option_data_for_template_change(self, option):
+ price = option.product_id.lst_price
+ discount = 0
+
+ if self.pricelist_id:
+ pricelist_price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
+
+ if self.pricelist_id.discount_policy == 'without_discount' and price:
+ discount = max(0, (price - pricelist_price) * 100 / price)
+ else:
+ price = pricelist_price
+
+ return {
+ 'product_id': option.product_id.id,
+ 'name': option.name,
+ 'quantity': option.quantity,
+ 'uom_id': option.uom_id.id,
+ 'price_unit': price,
+ 'discount': discount
+ }
+
+ def update_prices(self):
+ self.ensure_one()
+ res = super().update_prices()
+ for line in self.sale_order_option_ids:
+ line.price_unit = self.pricelist_id.get_product_price(line.product_id, line.quantity, self.partner_id, uom_id=line.uom_id.id)
+ return res
+
+ @api.onchange('sale_order_template_id')
+ def onchange_sale_order_template_id(self):
+
+ if not self.sale_order_template_id:
+ self.require_signature = self._get_default_require_signature()
+ self.require_payment = self._get_default_require_payment()
+ return
+
+ template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
+
+ # --- first, process the list of products from the template
+ order_lines = [(5, 0, 0)]
+ for line in template.sale_order_template_line_ids:
+ data = self._compute_line_data_for_template_change(line)
+
+ if line.product_id:
+ price = line.product_id.lst_price
+ discount = 0
+
+ if self.pricelist_id:
+ pricelist_price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
+
+ if self.pricelist_id.discount_policy == 'without_discount' and price:
+ discount = max(0, (price - pricelist_price) * 100 / price)
+ else:
+ price = pricelist_price
+
+ data.update({
+ 'price_unit': price,
+ 'discount': discount,
+ 'product_uom_qty': line.product_uom_qty,
+ 'product_id': line.product_id.id,
+ 'product_uom': line.product_uom_id.id,
+ 'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id),
+ })
+
+ order_lines.append((0, 0, data))
+
+ self.order_line = order_lines
+ self.order_line._compute_tax_id()
+
+ # then, process the list of optional products from the template
+ option_lines = [(5, 0, 0)]
+ for option in template.sale_order_template_option_ids:
+ data = self._compute_option_data_for_template_change(option)
+ option_lines.append((0, 0, data))
+
+ self.sale_order_option_ids = option_lines
+
+ if template.number_of_days > 0:
+ self.validity_date = fields.Date.context_today(self) + timedelta(template.number_of_days)
+
+ self.require_signature = template.require_signature
+ self.require_payment = template.require_payment
+
+ if template.note:
+ self.note = template.note
+
+ def action_confirm(self):
+ res = super(SaleOrder, self).action_confirm()
+ for order in self:
+ if order.sale_order_template_id and order.sale_order_template_id.mail_template_id:
+ self.sale_order_template_id.mail_template_id.send_mail(order.id)
+ return res
+
+ def get_access_action(self, access_uid=None):
+ """ Instead of the classic form view, redirect to the online quote if it exists. """
+ self.ensure_one()
+ user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
+
+ if not self.sale_order_template_id or (not user.share and not self.env.context.get('force_website')):
+ return super(SaleOrder, self).get_access_action(access_uid)
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': self.get_portal_url(),
+ 'target': 'self',
+ 'res_id': self.id,
+ }
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+ _description = "Sales Order Line"
+
+ sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
+
+ # Take the description on the order template if the product is present in it
+ @api.onchange('product_id')
+ def product_id_change(self):
+ domain = super(SaleOrderLine, self).product_id_change()
+ if self.product_id and self.order_id.sale_order_template_id:
+ for line in self.order_id.sale_order_template_id.sale_order_template_line_ids:
+ if line.product_id == self.product_id:
+ self.name = line.with_context(lang=self.order_id.partner_id.lang).name + self._get_sale_order_line_multiline_description_variants()
+ break
+ return domain
+
+
+class SaleOrderOption(models.Model):
+ _name = "sale.order.option"
+ _description = "Sale Options"
+ _order = 'sequence, id'
+
+ is_present = fields.Boolean(string="Present on Quotation",
+ help="This field will be checked if the option line's product is "
+ "already present in the quotation.",
+ compute="_compute_is_present", search="_search_is_present")
+ order_id = fields.Many2one('sale.order', 'Sales Order Reference', ondelete='cascade', index=True)
+ line_id = fields.Many2one('sale.order.line', ondelete="set null", copy=False)
+ name = fields.Text('Description', required=True)
+ product_id = fields.Many2one('product.product', 'Product', required=True, domain=[('sale_ok', '=', True)])
+ price_unit = fields.Float('Unit Price', required=True, digits='Product Price')
+ discount = fields.Float('Discount (%)', digits='Discount')
+ uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True, domain="[('category_id', '=', product_uom_category_id)]")
+ product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
+ quantity = fields.Float('Quantity', required=True, digits='Product Unit of Measure', default=1)
+ sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of optional products.")
+
+ @api.depends('line_id', 'order_id.order_line', 'product_id')
+ def _compute_is_present(self):
+ # NOTE: this field cannot be stored as the line_id is usually removed
+ # through cascade deletion, which means the compute would be false
+ for option in self:
+ option.is_present = bool(option.order_id.order_line.filtered(lambda l: l.product_id == option.product_id))
+
+ def _search_is_present(self, operator, value):
+ if (operator, value) in [('=', True), ('!=', False)]:
+ return [('line_id', '=', False)]
+ return [('line_id', '!=', False)]
+
+ @api.onchange('product_id', 'uom_id', 'quantity')
+ def _onchange_product_id(self):
+ if not self.product_id:
+ return
+ product = self.product_id.with_context(
+ lang=self.order_id.partner_id.lang,
+ partner=self.order_id.partner_id,
+ quantity=self.quantity,
+ date=self.order_id.date_order,
+ pricelist=self.order_id.pricelist_id.id,
+ uom=self.uom_id.id,
+ fiscal_position=self.env.context.get('fiscal_position')
+ )
+ self.name = product.get_product_multiline_description_sale()
+ self.uom_id = self.uom_id or product.uom_id
+ # To compute the discount a so line is created in cache
+ values = self._get_values_to_add_to_order()
+ new_sol = self.env['sale.order.line'].new(values)
+ new_sol._onchange_discount()
+ self.discount = new_sol.discount
+ if self.order_id.pricelist_id and self.order_id.partner_id:
+ self.price_unit = new_sol._get_display_price(product)
+
+ def button_add_to_order(self):
+ self.add_option_to_order()
+
+ def add_option_to_order(self):
+ self.ensure_one()
+
+ sale_order = self.order_id
+
+ if sale_order.state not in ['draft', 'sent']:
+ raise UserError(_('You cannot add options to a confirmed order.'))
+
+ values = self._get_values_to_add_to_order()
+ order_line = self.env['sale.order.line'].create(values)
+ order_line._compute_tax_id()
+
+ self.write({'line_id': order_line.id})
+ if sale_order:
+ sale_order.add_option_to_order_with_taxcloud()
+
+
+ def _get_values_to_add_to_order(self):
+ self.ensure_one()
+ return {
+ 'order_id': self.order_id.id,
+ 'price_unit': self.price_unit,
+ 'name': self.name,
+ 'product_id': self.product_id.id,
+ 'product_uom_qty': self.quantity,
+ 'product_uom': self.uom_id.id,
+ 'discount': self.discount,
+ 'company_id': self.order_id.company_id.id,
+ }
diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py
new file mode 100644
index 00000000..f1cdeb1b
--- /dev/null
+++ b/addons/sale_management/models/sale_order_template.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError, ValidationError
+
+
+class SaleOrderTemplate(models.Model):
+ _name = "sale.order.template"
+ _description = "Quotation Template"
+
+ def _get_default_require_signature(self):
+ return self.env.company.portal_confirmation_sign
+
+ def _get_default_require_payment(self):
+ return self.env.company.portal_confirmation_pay
+
+ name = fields.Char('Quotation Template', required=True)
+ sale_order_template_line_ids = fields.One2many('sale.order.template.line', 'sale_order_template_id', 'Lines', copy=True)
+ note = fields.Text('Terms and conditions', translate=True)
+ sale_order_template_option_ids = fields.One2many('sale.order.template.option', 'sale_order_template_id', 'Optional Products', copy=True)
+ number_of_days = fields.Integer('Quotation Duration',
+ help='Number of days for the validity date computation of the quotation')
+ require_signature = fields.Boolean('Online Signature', default=_get_default_require_signature, help='Request a online signature to the customer in order to confirm orders automatically.')
+ require_payment = fields.Boolean('Online Payment', default=_get_default_require_payment, help='Request an online payment to the customer in order to confirm orders automatically.')
+ mail_template_id = fields.Many2one(
+ 'mail.template', 'Confirmation Mail',
+ domain=[('model', '=', 'sale.order')],
+ help="This e-mail template will be sent on confirmation. Leave empty to send nothing.")
+ active = fields.Boolean(default=True, help="If unchecked, it will allow you to hide the quotation template without removing it.")
+ company_id = fields.Many2one('res.company', string='Company')
+
+ @api.constrains('company_id', 'sale_order_template_line_ids', 'sale_order_template_option_ids')
+ def _check_company_id(self):
+ for template in self:
+ companies = template.mapped('sale_order_template_line_ids.product_id.company_id') | template.mapped('sale_order_template_option_ids.product_id.company_id')
+ if len(companies) > 1:
+ raise ValidationError(_("Your template cannot contain products from multiple companies."))
+ elif companies and companies != template.company_id:
+ raise ValidationError(_(
+ "Your template contains products from company %(product_company)s whereas your template belongs to company %(template_company)s. \n Please change the company of your template or remove the products from other companies.",
+ product_company=', '.join(companies.mapped('display_name')),
+ template_company=template.company_id.display_name,
+ ))
+
+ @api.onchange('sale_order_template_line_ids', 'sale_order_template_option_ids')
+ def _onchange_template_line_ids(self):
+ companies = self.mapped('sale_order_template_option_ids.product_id.company_id') | self.mapped('sale_order_template_line_ids.product_id.company_id')
+ if companies and self.company_id not in companies:
+ self.company_id = companies[0]
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super(SaleOrderTemplate, self).create(vals_list)
+ records._update_product_translations()
+ return records
+
+ def write(self, vals):
+ if 'active' in vals and not vals.get('active'):
+ companies = self.env['res.company'].sudo().search([('sale_order_template_id', 'in', self.ids)])
+ companies.sale_order_template_id = None
+ result = super(SaleOrderTemplate, self).write(vals)
+ self._update_product_translations()
+ return result
+
+ def _update_product_translations(self):
+ languages = self.env['res.lang'].search([('active', '=', 'true')])
+ for lang in languages:
+ for line in self.sale_order_template_line_ids:
+ if line.name == line.product_id.get_product_multiline_description_sale():
+ self.create_or_update_translations(model_name='sale.order.template.line,name', lang_code=lang.code,
+ res_id=line.id,src=line.name,
+ value=line.product_id.with_context(lang=lang.code).get_product_multiline_description_sale())
+ for option in self.sale_order_template_option_ids:
+ if option.name == option.product_id.get_product_multiline_description_sale():
+ self.create_or_update_translations(model_name='sale.order.template.option,name', lang_code=lang.code,
+ res_id=option.id,src=option.name,
+ value=option.product_id.with_context(lang=lang.code).get_product_multiline_description_sale())
+
+ def create_or_update_translations(self, model_name, lang_code, res_id, src, value):
+ data = {
+ 'type': 'model',
+ 'name': model_name,
+ 'lang': lang_code,
+ 'res_id': res_id,
+ 'src': src,
+ 'value': value,
+ 'state': 'inprogress',
+ }
+ existing_trans = self.env['ir.translation'].search([('name', '=', model_name),
+ ('res_id', '=', res_id),
+ ('lang', '=', lang_code)])
+ if not existing_trans:
+ self.env['ir.translation'].create(data)
+ else:
+ existing_trans.write(data)
+
+
+
+class SaleOrderTemplateLine(models.Model):
+ _name = "sale.order.template.line"
+ _description = "Quotation Template Line"
+ _order = 'sale_order_template_id, sequence, id'
+
+ sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of sale quote lines.",
+ default=10)
+ sale_order_template_id = fields.Many2one(
+ 'sale.order.template', 'Quotation Template Reference',
+ required=True, ondelete='cascade', index=True)
+ company_id = fields.Many2one('res.company', related='sale_order_template_id.company_id', store=True, index=True)
+ name = fields.Text('Description', required=True, translate=True)
+ product_id = fields.Many2one(
+ 'product.product', 'Product', check_company=True,
+ domain=[('sale_ok', '=', True)])
+ product_uom_qty = fields.Float('Quantity', required=True, digits='Product Unit of Measure', default=1)
+ product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
+ product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
+
+ display_type = fields.Selection([
+ ('line_section', "Section"),
+ ('line_note', "Note")], default=False, help="Technical field for UX purpose.")
+
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ self.ensure_one()
+ if self.product_id:
+ self.product_uom_id = self.product_id.uom_id.id
+ self.name = self.product_id.get_product_multiline_description_sale()
+
+ @api.model
+ def create(self, values):
+ if values.get('display_type', self.default_get(['display_type'])['display_type']):
+ values.update(product_id=False, product_uom_qty=0, product_uom_id=False)
+ return super(SaleOrderTemplateLine, self).create(values)
+
+ def write(self, values):
+ if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
+ raise UserError(_("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type."))
+ return super(SaleOrderTemplateLine, self).write(values)
+
+ _sql_constraints = [
+ ('accountable_product_id_required',
+ "CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL))",
+ "Missing required product and UoM on accountable sale quote line."),
+
+ ('non_accountable_fields_null',
+ "CHECK(display_type IS NULL OR (product_id IS NULL AND product_uom_qty = 0 AND product_uom_id IS NULL))",
+ "Forbidden product, unit price, quantity, and UoM on non-accountable sale quote line"),
+ ]
+
+
+class SaleOrderTemplateOption(models.Model):
+ _name = "sale.order.template.option"
+ _description = "Quotation Template Option"
+ _check_company_auto = True
+
+ sale_order_template_id = fields.Many2one('sale.order.template', 'Quotation Template Reference', ondelete='cascade',
+ index=True, required=True)
+ company_id = fields.Many2one('res.company', related='sale_order_template_id.company_id', store=True, index=True)
+ name = fields.Text('Description', required=True, translate=True)
+ product_id = fields.Many2one(
+ 'product.product', 'Product', domain=[('sale_ok', '=', True)],
+ required=True, check_company=True)
+ uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True, domain="[('category_id', '=', product_uom_category_id)]")
+ product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
+ quantity = fields.Float('Quantity', required=True, digits='Product Unit of Measure', default=1)
+
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ if not self.product_id:
+ return
+ self.uom_id = self.product_id.uom_id
+ self.name = self.product_id.get_product_multiline_description_sale()