Merge branch '12.0-test' into 12.0

This commit is contained in:
Jared Kipe
2019-03-08 07:59:10 -08:00
29 changed files with 1294 additions and 0 deletions

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,24 @@
{
'name': 'Delivery Hibou',
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
'version': '11.0.1.0.0',
'author': "Hibou Corp.",
'category': 'Stock',
'license': 'AGPL-3',
'images': [],
'website': "https://hibou.io",
'description': """
This is a collection of "typical" carrier needs, and a bridge into Hibou modules like `delivery_partner` and `sale_planner`.
""",
'depends': [
'delivery',
'delivery_partner',
],
'demo': [],
'data': [
'views/delivery_views.xml',
'views/stock_views.xml',
],
'auto_install': False,
'installable': True,
}

View File

@@ -0,0 +1,2 @@
from . import delivery
from . import stock

View File

@@ -0,0 +1,152 @@
from odoo import fields, models
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
class DeliveryCarrier(models.Model):
_inherit = 'delivery.carrier'
automatic_insurance_value = fields.Float(string='Automatic Insurance Value',
help='Will be used during shipping to determine if the '
'picking\'s value warrants insurance being added.')
procurement_priority = fields.Selection(PROCUREMENT_PRIORITIES,
string='Procurement Priority',
help='Priority for this carrier. Will affect pickings '
'and procurements related to this carrier.')
def get_insurance_value(self, order=None, picking=None):
value = 0.0
if order:
if order.order_line:
value = sum(order.order_line.filtered(lambda l: l.type != 'service').mapped('price_subtotal'))
else:
return value
if picking:
value = picking.declared_value()
if picking.require_insurance == 'no':
value = 0.0
elif picking.require_insurance == 'auto' and self.automatic_insurance_value and self.automatic_insurance_value > value:
value = 0.0
return value
def get_third_party_account(self, order=None, picking=None):
if order and order.shipping_account_id:
return order.shipping_account_id
if picking and picking.shipping_account_id:
return picking.shipping_account_id
return None
def get_order_name(self, order=None, picking=None):
if order:
return order.name
if picking:
if picking.sale_id:
return picking.sale_id.name # + ' - ' + picking.name
return picking.name
return ''
def get_attn(self, order=None, picking=None):
if order:
return order.client_order_ref
if picking and picking.sale_id:
return picking.sale_id.client_order_ref
# If Picking has a reference, decide what it is.
return False
def _classify_picking(self, picking):
if picking.picking_type_id.code == 'incoming' and picking.location_id.usage == 'supplier' and picking.location_dest_id.usage == 'customer':
return 'dropship'
elif picking.picking_type_id.code == 'incoming' and picking.location_id.usage == 'customer' and picking.location_dest_id.usage == 'supplier':
return 'dropship_in'
elif picking.picking_type_id.code == 'incoming':
return 'in'
return 'out'
# Shipper Company
def get_shipper_company(self, order=None, picking=None):
"""
Shipper Company: The `res.partner` that provides the name of where the shipment is coming from.
"""
if order:
return order.company_id.partner_id
if picking:
return getattr(self, ('_get_shipper_company_%s' % (self._classify_picking(picking),)),
self._get_shipper_company_out)(picking)
return None
def _get_shipper_company_dropship(self, picking):
return picking.company_id.partner_id
def _get_shipper_company_dropship_in(self, picking):
return picking.company_id.partner_id
def _get_shipper_company_in(self, picking):
return picking.company_id.partner_id
def _get_shipper_company_out(self, picking):
return picking.company_id.partner_id
# Shipper Warehouse
def get_shipper_warehouse(self, order=None, picking=None):
"""
Shipper Warehouse: The `res.partner` that is basically the physical address a shipment is coming from.
"""
if order:
return order.warehouse_id.partner_id
if picking:
return getattr(self, ('_get_shipper_warehouse_%s' % (self._classify_picking(picking),)),
self._get_shipper_warehouse_out)(picking)
return None
def _get_shipper_warehouse_dropship(self, picking):
return picking.partner_id
def _get_shipper_warehouse_dropship_in(self, picking):
if picking.sale_id:
picking.sale_id.partner_shipping_id
return self._get_shipper_warehouse_dropship_in_no_sale(picking)
def _get_shipper_warehouse_dropship_in_no_sale(self, picking):
return picking.company_id.partner_id
def _get_shipper_warehouse_in(self, picking):
return picking.partner_id
def _get_shipper_warehouse_out(self, picking):
return picking.picking_type_id.warehouse_id.partner_id
# Recipient
def get_recipient(self, order=None, picking=None):
"""
Recipient: The `res.partner` receiving the shipment.
"""
if order:
return order.partner_shipping_id
if picking:
return getattr(self, ('_get_recipient_%s' % (self._classify_picking(picking),)),
self._get_recipient_out)(picking)
return None
def _get_recipient_dropship(self, picking):
if picking.sale_id:
return picking.sale_id.partner_shipping_id
return picking.sale_id.partner_shipping_id
def _get_recipient_dropship_no_sale(self, picking):
return picking.company_id.partner_id
def _get_recipient_dropship_in(self, picking):
return picking.picking_type_id.warehouse_id.partner_id
def _get_recipient_in(self, picking):
return picking.picking_type_id.warehouse_id.partner_id
def _get_recipient_out(self, picking):
return picking.partner_id

View File

@@ -0,0 +1,50 @@
from odoo import api, fields, models
class StockPicking(models.Model):
_inherit = 'stock.picking'
shipping_account_id = fields.Many2one('partner.shipping.account', string='Shipping Account')
require_insurance = fields.Selection([
('auto', 'Automatic'),
('yes', 'Yes'),
('no', 'No'),
], string='Require Insurance', default='auto',
help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.')
@api.one
@api.depends('move_lines.priority', 'carrier_id')
def _compute_priority(self):
if self.carrier_id.procurement_priority:
self.priority = self.carrier_id.procurement_priority
else:
super(StockPicking, self)._compute_priority()
@api.model
def create(self, values):
origin = values.get('origin')
if origin and not values.get('shipping_account_id'):
so = self.env['sale.order'].search([('name', '=', str(origin))], limit=1)
if so and so.shipping_account_id:
values['shipping_account_id'] = so.shipping_account_id.id
res = super(StockPicking, self).create(values)
return res
def declared_value(self):
self.ensure_one()
cost = sum([(l.product_id.standard_price * l.qty_done) for l in self.move_line_ids] or [0.0])
if not cost:
# Assume Full Value
cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0])
return cost
class StockMove(models.Model):
_inherit = 'stock.move'
def _prepare_procurement_values(self):
res = super(StockMove, self)._prepare_procurement_values()
res['priority'] = self.picking_id.priority or self.priority
return res

View File

@@ -0,0 +1 @@
from . import test_delivery_hibou

View File

@@ -0,0 +1,161 @@
from odoo.tests import common
class TestDeliveryHibou(common.TransactionCase):
def setUp(self):
super(TestDeliveryHibou, self).setUp()
self.partner = self.env.ref('base.res_partner_address_13')
self.product = self.env.ref('product.product_product_7')
# Create Shipping Account
self.shipping_account = self.env['partner.shipping.account'].create({
'name': '123123',
'delivery_type': 'other',
})
# Create Carrier
self.delivery_product = self.env['product.product'].create({
'name': 'Test Carrier1 Delivery',
'type': 'service',
})
self.carrier = self.env['delivery.carrier'].create({
'name': 'Test Carrier1',
'product_id': self.delivery_product.id,
})
def test_delivery_hibou(self):
# Assign a new shipping account
self.partner.shipping_account_id = self.shipping_account
# Assign values to new Carrier
test_insurance_value = 600
test_procurement_priority = '2'
self.carrier.automatic_insurance_value = test_insurance_value
self.carrier.procurement_priority = test_procurement_priority
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'partner_shipping_id': self.partner.id,
'partner_invoice_id': self.partner.id,
'carrier_id': self.carrier.id,
'shipping_account_id': self.shipping_account.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
})]
})
sale_order.get_delivery_price()
sale_order.set_delivery_line()
sale_order.action_confirm()
# Make sure 3rd party Shipping Account is set.
self.assertEqual(sale_order.shipping_account_id, self.shipping_account)
self.assertTrue(sale_order.picking_ids)
# Priority coming from Carrier procurement_priority
self.assertEqual(sale_order.picking_ids.priority, test_procurement_priority)
# 3rd party Shipping Account copied from Sale Order
self.assertEqual(sale_order.picking_ids.shipping_account_id, self.shipping_account)
self.assertEqual(sale_order.carrier_id.get_third_party_account(order=sale_order), self.shipping_account)
# Test attn
test_ref = 'TEST100'
self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), False)
sale_order.client_order_ref = test_ref
self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), test_ref)
# The picking should get this ref as well
self.assertEqual(sale_order.picking_ids.carrier_id.get_attn(picking=sale_order.picking_ids), test_ref)
# Test order_name
self.assertEqual(sale_order.carrier_id.get_order_name(order=sale_order), sale_order.name)
# The picking should get the same 'order_name'
self.assertEqual(sale_order.picking_ids.carrier_id.get_order_name(picking=sale_order.picking_ids), sale_order.name)
def test_carrier_hibou_out(self):
test_insurance_value = 4000
self.carrier.automatic_insurance_value = test_insurance_value
picking_out = self.env.ref('stock.outgoing_shipment_main_warehouse')
picking_out.action_assign()
self.assertEqual(picking_out.state, 'assigned')
picking_out.carrier_id = self.carrier
# This relies heavily on the 'stock' demo data.
# Should only have a single move_line_ids and it should not be done at all.
self.assertEqual(picking_out.move_line_ids.mapped('qty_done'), [0.0])
self.assertEqual(picking_out.move_line_ids.mapped('product_uom_qty'), [15.0])
self.assertEqual(picking_out.move_line_ids.mapped('product_id.standard_price'), [3300.0])
# The 'value' is assumed to be all of the product value from the initial demand.
self.assertEqual(picking_out.declared_value(), 15.0 * 3300.0)
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), picking_out.declared_value())
# Workflow where user explicitly opts out of insurance on the picking level.
picking_out.require_insurance = 'no'
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0)
picking_out.require_insurance = 'auto'
# Lets choose to only delivery one piece at the moment.
# This does not meet the minimum on the carrier to have insurance value.
picking_out.move_line_ids.qty_done = 1.0
self.assertEqual(picking_out.declared_value(), 3300.0)
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0)
# Workflow where user opts in to insurance.
picking_out.require_insurance = 'yes'
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 3300.0)
picking_out.require_insurance = 'auto'
# Test with picking having 3rd party account.
self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), None)
picking_out.shipping_account_id = self.shipping_account
self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), self.shipping_account)
# Shipment Time Methods!
self.assertEqual(picking_out.carrier_id._classify_picking(picking=picking_out), 'out')
self.assertEqual(picking_out.carrier_id.get_shipper_company(picking=picking_out),
picking_out.company_id.partner_id)
self.assertEqual(picking_out.carrier_id.get_shipper_warehouse(picking=picking_out),
picking_out.picking_type_id.warehouse_id.partner_id)
self.assertEqual(picking_out.carrier_id.get_recipient(picking=picking_out),
picking_out.partner_id)
# This picking has no `sale_id`
# Right now ATTN requires a sale_id, which this picking doesn't have (none of the stock ones do)
self.assertEqual(picking_out.carrier_id.get_attn(picking=picking_out), False)
self.assertEqual(picking_out.carrier_id.get_order_name(picking=picking_out), picking_out.name)
def test_carrier_hibou_in(self):
picking_in = self.env.ref('stock.incomming_shipment1')
self.assertEqual(picking_in.state, 'assigned')
picking_in.carrier_id = self.carrier
# This relies heavily on the 'stock' demo data.
# Should only have a single move_line_ids and it should not be done at all.
self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0])
self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0])
self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0])
self.assertEqual(picking_in.carrier_id._classify_picking(picking=picking_in), 'in')
self.assertEqual(picking_in.carrier_id.get_shipper_company(picking=picking_in),
picking_in.company_id.partner_id)
self.assertEqual(picking_in.carrier_id.get_shipper_warehouse(picking=picking_in),
picking_in.partner_id)
self.assertEqual(picking_in.carrier_id.get_recipient(picking=picking_in),
picking_in.picking_type_id.warehouse_id.partner_id)

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_delivery_carrier_form" model="ir.ui.view">
<field name="name">hibou.delivery.carrier.form</field>
<field name="model">delivery.carrier</field>
<field name="inherit_id" ref="delivery.view_delivery_carrier_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='integration_level']" position="after">
<field name="automatic_insurance_value"/>
<field name="procurement_priority"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
<field name="name">hibou.delivery.stock.picking_withcarrier.form.view</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='carrier_id']" position="before">
<field name="require_insurance" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,29 @@
*********************************
Hibou - Partner Shipping Accounts
*********************************
Records shipping account numbers on partners.
For more information and add-ons, visit `Hibou.io <https://hibou.io/>`_.
=============
Main Features
=============
* New model: Customer Shipping Account
* Includes manager-level access permissions.
.. image:: https://user-images.githubusercontent.com/15882954/41176601-e40f8558-6b15-11e8-998e-6a7ee5709c0f.png
:alt: 'Register Payment Detail'
:width: 988
:align: left
=======
License
=======
Please see `LICENSE <https://github.com/hibou-io/hibou-odoo-suite/blob/11.0/LICENSE>`_.
Copyright Hibou Corp. 2018

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,24 @@
{
'name': 'Partner Shipping Accounts',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '11.0.1.0.0',
'category': 'Stock',
'sequence': 95,
'summary': 'Record shipping account numbers on partners.',
'description': """
Record shipping account numbers on partners.
* Customer Shipping Account Model
""",
'website': 'https://hibou.io/',
'depends': [
'delivery',
'contacts',
],
'data': [
'security/ir.model.access.csv',
'views/delivery_views.xml',
],
'installable': True,
'application': False,
}

View File

@@ -0,0 +1 @@
from . import delivery

View File

@@ -0,0 +1,48 @@
from odoo import api, fields, models
class Partner(models.Model):
_inherit = 'res.partner'
shipping_account_ids = fields.One2many('partner.shipping.account', 'partner_id', string='Shipping Accounts')
class SaleOrder(models.Model):
_inherit = 'sale.order'
shipping_account_id = fields.Many2one('partner.shipping.account', string='Shipping Account')
class PartnerShippingAccount(models.Model):
_name = 'partner.shipping.account'
name = fields.Char(string='Account Num.', required=True)
description = fields.Char(string='Description')
partner_id = fields.Many2one('res.partner', string='Partner', help='Leave blank to allow as a generic 3rd party shipper.')
delivery_type = fields.Selection([
('other', 'Other'),
], string='Carrier', required=True)
note = fields.Text(string='Internal Note')
@api.multi
def name_get(self):
delivery_types = self._fields['delivery_type']._description_selection(self.env)
def get_name(value):
name = [n for v, n in delivery_types if v == value]
return name[0] if name else 'Undefined'
res = []
for acc in self:
if acc.description:
res.append((acc.id, acc.description))
else:
res.append((acc.id, '%s: %s' % (get_name(acc.delivery_type), acc.name)))
return res
@api.constrains('name', 'delivery_type')
def _check_validity(self):
for acc in self:
check = getattr(acc, acc.delivery_type + '_check_validity', None)
if check:
return check()

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_partner_shipping_account,partner.shipping.account,model_partner_shipping_account,base.group_partner_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_partner_shipping_account partner.shipping.account model_partner_shipping_account base.group_partner_manager 1 1 1 1

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="partner_shipping_account_view_tree" model="ir.ui.view">
<field name="name">partner.shipping.account.tree</field>
<field name="model">partner.shipping.account</field>
<field name="arch" type="xml">
<tree string="Shipping Accounts">
<field name="description"/>
<field name="name"/>
<field name="delivery_type"/>
<field name="partner_id"/>
</tree>
</field>
</record>
<record id="partner_shipping_account_view_form" model="ir.ui.view">
<field name="name">partner.shipping.account.form</field>
<field name="model">partner.shipping.account</field>
<field name="arch" type="xml">
<form string="Shipping Account">
<sheet>
<group>
<group>
<field name="partner_id"/>
<field name="description"/>
<field name="name"/>
<field name="delivery_type"/>
</group>
<group name="carrier"/>
</group>
<field name="note" placeholder="Any additional notes..."/>
</sheet>
</form>
</field>
</record>
<record id="partner_shipping_account_view_search" model="ir.ui.view">
<field name="name">partner.shipping.account.search</field>
<field name="model">partner.shipping.account</field>
<field name="arch" type="xml">
<search string="Shipping Account Search">
<field name="description"/>
<field name="name"/>
<field name="partner_id"/>
<field name="delivery_type"/>
</search>
</field>
</record>
<record id="partner_shipping_account_action_main" model="ir.actions.act_window">
<field name="name">Shipping Accounts</field>
<field name="res_model">partner.shipping.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p>
No accounts
</p>
</field>
</record>
<menuitem id="partner_shipping_account_menu_main" name="Partner Shipping Accounts"
action="delivery_partner.partner_shipping_account_action_main"
sequence="20" parent="contacts.res_partner_menu_config"/>
<!-- Inherited -->
<record id="view_partner_property_form_inherit" model="ir.ui.view">
<field name="name">res.partner.carrier.property.form.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="delivery.view_partner_property_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='property_delivery_carrier_id']" position="after">
<field name="shipping_account_ids" context="{'default_partner_id': active_id}">
<tree>
<field name="description"/>
<field name="name"/>
<field name="delivery_type"/>
</tree>
</field>
</xpath>
</field>
</record>
<record id="view_order_form_with_carrier_inherit" model="ir.ui.view">
<field name="name">delivery.sale.order.form.view.with_carrier.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="delivery.view_order_form_with_carrier"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='id']" position="before">
<field name="shipping_account_id" options="{'no_create': True, 'no_open': True}" domain="['|', ('partner_id', '=', False), ('partner_id', '=', partner_id)]"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,2 @@
from . import controllers
from . import models

View File

@@ -0,0 +1,29 @@
{
'name': 'Sale Line Reconfigure',
'author': 'Hibou Corp. <hello@hibou.io>',
'category': 'Hidden',
'version': '12.0.1.0.0',
'description':
"""
Sale Line Reconfigure
=====================
The product configurator allows for truely complex sale order lines, with potentially
no-variant and custom attribute values.
This module allows you to create 'duplicate' sale order lines that start with all of
the attribute values from some other line. This lets you treat existing SO lines as
templates for the new additions. This lets you "re-configure" a line by a
workflow in which you add a new line, then remove old line (or reduce its ordered quantity).
""",
'depends': [
'sale',
'account',
],
'auto_install': False,
'data': [
'views/assets.xml',
'views/product_views.xml',
'views/sale_product_configurator_templates.xml',
],
}

View File

@@ -0,0 +1 @@
from . import product_configurator

View File

@@ -0,0 +1,32 @@
from odoo import http, fields
from odoo.addons.sale.controllers import product_configurator
from odoo.http import request
class ProductConfiguratorController(product_configurator.ProductConfiguratorController):
@http.route(['/product_configurator/configure'], type='json', auth="user", methods=['POST'])
def configure(self, product_id, pricelist_id, sale_line_id=None, **kw):
product_template = request.env['product.template'].browse(int(product_id))
to_currency = product_template.currency_id
pricelist = self._get_pricelist(pricelist_id)
if pricelist:
product_template = product_template.with_context(pricelist=pricelist.id, partner=request.env.user.partner_id)
to_currency = pricelist.currency_id
sale_line = None
if sale_line_id:
sale_line = request.env['sale.order.line'].browse(int(sale_line_id))
return request.env['ir.ui.view'].render_template("sale.product_configurator_configure", {
'product': product_template,
'to_currency': to_currency,
'pricelist': pricelist,
'sale_line': sale_line,
'get_attribute_exclusions': self._get_attribute_exclusions,
'get_attribute_value_defaults': self._get_attribute_value_defaults,
'sale_line': sale_line,
})
def _get_attribute_value_defaults(self, product, sale_line, **kw):
return product.get_default_attribute_values(sale_line)

View File

@@ -0,0 +1 @@
from . import product

View File

@@ -0,0 +1,47 @@
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
def get_default_attribute_values(self, so_line):
product = None
if so_line:
product = so_line.product_id
attribute_values = self.env['product.attribute.value'].browse()
for attribute_line in self.attribute_line_ids:
attribute = attribute_line.attribute_id
attribute_line_values = attribute_line.product_template_value_ids
# Product Values
if product:
product_values = product.attribute_value_ids.filtered(lambda v: v.attribute_id == attribute)
if product_values:
attribute_values += product_values
continue
so_line_values = so_line.product_no_variant_attribute_value_ids.filtered(
lambda v: v.attribute_id == attribute)
if so_line_values:
attribute_values += so_line_values.mapped('product_attribute_value_id')
continue
default_value = self.env['product.template.attribute.value'].search([
('product_tmpl_id', '=', self.id),
('attribute_id', '=', attribute.id),
('is_default', '=', True),
], limit=1)
if default_value:
attribute_values += default_value.mapped('product_attribute_value_id')
continue
# First value
attribute_values += attribute_line_values[0].product_attribute_value_id
return attribute_values
class ProductTemplateAttributeValue(models.Model):
_inherit = 'product.template.attribute.value'
is_default = fields.Boolean(string='Use as Default', copy=False)

View File

@@ -0,0 +1,198 @@
odoo.define('sale_line_reconfigure.ProductConfiguratorFormController', function (require) {
"use strict";
/*
Product Configurator is very powerful, but not so easy to extend.
*/
var core = require('web.core');
var _t = core._t;
var FormController = require('web.FormController');
var OptionalProductsModal = require('sale.OptionalProductsModal');
var ProductConfiguratorFormController = FormController.extend({
custom_events: _.extend({}, FormController.prototype.custom_events, {
field_changed: '_onFieldChanged'
}),
className: 'o_product_configurator',
/**
* @override
*/
init: function (){
this._super.apply(this, arguments);
},
/**
* We need to override the default click behavior for our "Add" button
* because there is a possibility that this product has optional products.
* If so, we need to display an extra modal to choose the options.
*
* @override
*/
_onButtonClicked: function (event) {
if (event.stopPropagation){
event.stopPropagation();
}
var attrs = event.data.attrs;
if (attrs.special === 'cancel') {
this._super.apply(this, arguments);
} else {
if (!this.$el
.parents('.modal')
.find('.o_sale_product_configurator_add')
.hasClass('disabled')){
this._handleAdd(this.$el);
}
}
},
/**
* This is overridden to allow catching the "select" event on our product template select field.
* This will not work anymore if more fields are added to the form.
* TODO awa: Find a better way to catch that event.
*
* @override
*/
_onFieldChanged: function (event) {
var self = this;
this.$el.parents('.modal').find('.o_sale_product_configurator_add').removeClass('disabled');
this._rpc({
route: '/product_configurator/configure',
params: {
product_id: event.data.changes.product_template_id.id,
pricelist_id: this.renderer.pricelistId,
sale_line_id: this.renderer.saleLineId,
}
}).then(function (configurator) {
self.renderer.renderConfigurator(configurator);
});
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* When the user adds a product that has optional products, we need to display
* a window to allow the user to choose these extra options.
*
* This will also create the product if it's in "dynamic" mode
* (see product_attribute.create_variant)
*
* @private
* @param {$.Element} $modal
*/
_handleAdd: function ($modal) {
var self = this;
var productSelector = [
'input[type="hidden"][name="product_id"]',
'input[type="radio"][name="product_id"]:checked'
];
var productId = parseInt($modal.find(productSelector.join(', ')).first().val(), 10);
var productReady = this.renderer.selectOrCreateProduct(
$modal,
productId,
$modal.find('.product_template_id').val(),
false
);
productReady.done(function (productId){
$modal.find(productSelector.join(', ')).val(productId);
var variantValues = self
.renderer
.getSelectedVariantValues($modal.find('.js_product'));
var productCustomVariantValues = self
.renderer
.getCustomVariantValues($modal.find('.js_product'));
var noVariantAttributeValues = self
.renderer
.getNoVariantAttributeValues($modal.find('.js_product'));
self.rootProduct = {
product_id: productId,
quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1),
variant_values: variantValues,
product_custom_attribute_values: productCustomVariantValues,
no_variant_attribute_values: noVariantAttributeValues
};
self.optionalProductsModal = new OptionalProductsModal($('body'), {
rootProduct: self.rootProduct,
pricelistId: self.renderer.pricelistId,
okButtonText: _t('Confirm'),
cancelButtonText: _t('Back'),
title: _t('Configure')
}).open();
self.optionalProductsModal.on('options_empty', null,
self._onModalOptionsEmpty.bind(self));
self.optionalProductsModal.on('update_quantity', null,
self._onOptionsUpdateQuantity.bind(self));
self.optionalProductsModal.on('confirm', null,
self._onModalConfirm.bind(self));
});
},
/**
* No optional products found for this product, only add the root product
*
* @private
*/
_onModalOptionsEmpty: function () {
this._addProducts([this.rootProduct]);
},
/**
* Add all selected products
*
* @private
*/
_onModalConfirm: function () {
this._addProducts(this.optionalProductsModal.getSelectedProducts());
},
/**
* Update product configurator form
* when quantity is updated in the optional products window
*
* @private
* @param {integer} quantity
*/
_onOptionsUpdateQuantity: function (quantity) {
this.$el
.find('input[name="add_qty"]')
.val(quantity)
.trigger('change');
},
/**
* This triggers the close action for the window and
* adds the product as the "infos" parameter.
* It will allow the caller (typically the SO line form) of this window
* to handle the added products.
*
* @private
* @param {Array} products the list of added products
* {integer} products.product_id: the id of the product
* {integer} products.quantity: the added quantity for this product
* {Array} products.product_custom_attribute_values:
* see product_configurator_mixin.getCustomVariantValues
* {Array} products.no_variant_attribute_values:
* see product_configurator_mixin.getNoVariantAttributeValues
*/
_addProducts: function (products) {
this.do_action({type: 'ir.actions.act_window_close', infos: products});
}
});
return ProductConfiguratorFormController;
});

View File

@@ -0,0 +1,55 @@
odoo.define('sale_line_reconfigure.ProductConfiguratorFormRenderer', function (require) {
"use strict";
var FormRenderer = require('web.FormRenderer');
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMixin ,{
/**
* @override
*/
init: function (){
this._super.apply(this, arguments);
this.pricelistId = this.state.context.default_pricelist_id || 0;
// Override
this.saleLineId = this.state.context.default_sale_line_id || 0;
// End Override
},
/**
* @override
*/
start: function () {
this._super.apply(this, arguments);
this.$el.append($('<div>', {class: 'configurator_container'}));
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Renders the product configurator within the form
*
* Will also:
* - add events handling for variant changes
* - trigger variant change to compute the price and other
* variant specific changes
*
* @param {string} configuratorHtml the evaluated template of
* the product configurator
*/
renderConfigurator: function (configuratorHtml) {
var $configuratorContainer = this.$('.configurator_container');
$configuratorContainer.empty();
var $configuratorHtml = $(configuratorHtml);
$configuratorHtml.appendTo($configuratorContainer);
this.triggerVariantChange($configuratorContainer);
}
});
return ProductConfiguratorFormRenderer;
});

View File

@@ -0,0 +1,20 @@
odoo.define('sale_line_reconfigure.ProductConfiguratorFormView', function (require) {
"use strict";
var ProductConfiguratorFormController = require('sale_line_reconfigure.ProductConfiguratorFormController');
var ProductConfiguratorFormRenderer = require('sale_line_reconfigure.ProductConfiguratorFormRenderer');
var FormView = require('web.FormView');
var viewRegistry = require('web.view_registry');
var ProductConfiguratorFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: ProductConfiguratorFormController,
Renderer: ProductConfiguratorFormRenderer,
}),
});
viewRegistry.add('product_configurator_form', ProductConfiguratorFormView);
return ProductConfiguratorFormView;
});

View File

@@ -0,0 +1,227 @@
odoo.define('sale_line_reconfigure.section_and_note_backend', function (require) {
// The goal of this file is to contain JS hacks related to allowing
// section and note on sale order and invoice.
// [UPDATED] now also allows configuring products on sale order.
// This is a copy job from `account` to support re-configuring a selected sale order line.
"use strict";
var pyUtils = require('web.py_utils');
var core = require('web.core');
var _t = core._t;
var FieldChar = require('web.basic_fields').FieldChar;
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
var fieldRegistry = require('web.field_registry');
var FieldText = require('web.basic_fields').FieldText;
var ListRenderer = require('web.ListRenderer');
var SectionAndNoteListRenderer = ListRenderer.extend({
/**
* We want section and note to take the whole line (except handle and trash)
* to look better and to hide the unnecessary fields.
*
* @override
*/
_renderBodyCell: function (record, node, index, options) {
var $cell = this._super.apply(this, arguments);
var isSection = record.data.display_type === 'line_section';
var isNote = record.data.display_type === 'line_note';
if (isSection || isNote) {
if (node.attrs.widget === "handle") {
return $cell;
} else if (node.attrs.name === "name") {
var nbrColumns = this._getNumberOfCols();
if (this.handleField) {
nbrColumns--;
}
if (this.addTrashIcon) {
nbrColumns--;
}
$cell.attr('colspan', nbrColumns);
} else {
return $cell.addClass('o_hidden');
}
}
return $cell;
},
/**
* We add the o_is_{display_type} class to allow custom behaviour both in JS and CSS.
*
* @override
*/
_renderRow: function (record, index) {
var $row = this._super.apply(this, arguments);
if (record.data.display_type) {
$row.addClass('o_is_' + record.data.display_type);
}
return $row;
},
/**
* We want to add .o_section_and_note_list_view on the table to have stronger CSS.
*
* @override
* @private
*/
_renderView: function () {
var def = this._super();
this.$el.find('> table').addClass('o_section_and_note_list_view');
return def;
},
/**
* Add support for product configurator
*
* @override
* @private
*/
_onAddRecord: function (ev) {
// we don't want the browser to navigate to a the # url
ev.preventDefault();
// we don't want the click to cause other effects, such as unselecting
// the row that we are creating, because it counts as a click on a tr
ev.stopPropagation();
var lineId = null;
if (this.currentRow !== null && this.state.data[this.currentRow].res_id) {
lineId = this.state.data[this.currentRow].res_id;
}
// but we do want to unselect current row
var self = this;
this.unselectRow().then(function () {
var context = ev.currentTarget.dataset.context;
var pricelistId = self._getPricelistId();
if (context && pyUtils.py_eval(context).open_product_configurator){
self._rpc({
model: 'ir.model.data',
method: 'xmlid_to_res_id',
kwargs: {xmlid: 'sale.sale_product_configurator_view_form'},
}).then(function (res_id) {
self.do_action({
name: _t('Configure a product'),
type: 'ir.actions.act_window',
res_model: 'sale.product.configurator',
views: [[res_id, 'form']],
target: 'new',
context: {
'default_pricelist_id': pricelistId,
'default_sale_line_id': lineId,
}
}, {
on_close: function (products) {
if (products && products !== 'special'){
self.trigger_up('add_record', {
context: self._productsToRecords(products),
forceEditable: "bottom" ,
allowWarning: true,
onSuccess: function (){
self.unselectRow();
}
});
}
}
});
});
} else {
self.trigger_up('add_record', {context: context && [context]}); // TODO write a test, the deferred was not considered
}
});
},
/**
* Will try to get the pricelist_id value from the parent sale_order form
*
* @private
* @returns {integer} pricelist_id's id
*/
_getPricelistId: function () {
var saleOrderForm = this.getParent() && this.getParent().getParent();
var stateData = saleOrderForm && saleOrderForm.state && saleOrderForm.state.data;
var pricelist_id = stateData.pricelist_id && stateData.pricelist_id.data && stateData.pricelist_id.data.id;
return pricelist_id;
},
/**
* Will map the products to appropriate record objects that are
* ready for the default_get
*
* @private
* @param {Array} products The products to transform into records
*/
_productsToRecords: function (products) {
var records = [];
_.each(products, function (product){
var record = {
default_product_id: product.product_id,
default_product_uom_qty: product.quantity
};
if (product.no_variant_attribute_values) {
var default_product_no_variant_attribute_values = [];
_.each(product.no_variant_attribute_values, function (attribute_value) {
default_product_no_variant_attribute_values.push(
[4, parseInt(attribute_value.value)]
);
});
record['default_product_no_variant_attribute_value_ids']
= default_product_no_variant_attribute_values;
}
if (product.product_custom_attribute_values) {
var default_custom_attribute_values = [];
_.each(product.product_custom_attribute_values, function (attribute_value) {
default_custom_attribute_values.push(
[0, 0, {
attribute_value_id: attribute_value.attribute_value_id,
custom_value: attribute_value.custom_value
}]
);
});
record['default_product_custom_attribute_value_ids']
= default_custom_attribute_values;
}
records.push(record);
});
return records;
}
});
// We create a custom widget because this is the cleanest way to do it:
// to be sure this custom code will only impact selected fields having the widget
// and not applied to any other existing ListRenderer.
var SectionAndNoteFieldOne2Many = FieldOne2Many.extend({
/**
* We want to use our custom renderer for the list.
*
* @override
*/
_getRenderer: function () {
if (this.view.arch.tag === 'tree') {
return SectionAndNoteListRenderer;
}
return this._super.apply(this, arguments);
},
});
// This is a merge between a FieldText and a FieldChar.
// We want a FieldChar for section,
// and a FieldText for the rest (product and note).
var SectionAndNoteFieldText = function (parent, name, record, options) {
var isSection = record.data.display_type === 'line_section';
var Constructor = isSection ? FieldChar : FieldText;
return new Constructor(parent, name, record, options);
};
fieldRegistry.add('section_and_note_one2many', SectionAndNoteFieldOne2Many);
fieldRegistry.add('section_and_note_text', SectionAndNoteFieldText);
});

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="assets_backend" name="sale line reconfigure assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/sale_line_reconfigure/static/src/js/product_configurator_controller.js"></script>
<script type="text/javascript" src="/sale_line_reconfigure/static/src/js/product_configurator_renderer.js"></script>
<script type="text/javascript" src="/sale_line_reconfigure/static/src/js/product_configurator_view.js"></script>
<script type="text/javascript" src="/sale_line_reconfigure/static/src/js/section_and_note_fields_backend.js"></script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="product_template_attribute_value_view_tree_inherit" model="ir.ui.view">
<field name="name">product.template.attribute.value.view.tree.inherit</field>
<field name="model">product.template.attribute.value</field>
<field name="inherit_id" ref="product.product_template_attribute_value_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="is_default"/>
</xpath>
</field>
</record>
<record id="product_template_attribute_value_view_form_inherit" model="ir.ui.view">
<field name="name">product.template.attribute.value.view.form.inherit</field>
<field name="model">product.template.attribute.value</field>
<field name="inherit_id" ref="product.product_template_attribute_value_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="is_default"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="product_configurator_configure_inherit" name="Configure" inherit_id="sale.product_configurator_configure">
<xpath expr="//input[@t-attf-name='product_id']" position="after">
<input type="hidden" class="start_product_id" t-attf-name="start_product_id" t-att-value="start_product_variant_id"/>
</xpath>
</template>
<template id="variants_inherit" inherit_id="sale.variants">
<xpath expr="//t[@t-set='attribute_exclusions']" position="after">
<t t-set="attribute_value_defaults" t-value="get_attribute_value_defaults(product, sale_line)"/>
</xpath>
<xpath expr="//t[@t-foreach='variant_id.product_template_value_ids']/option" position="attributes">
<attribute name="t-att-data-is_custom">value_id.is_custom</attribute>
<attribute name="t-att-selected">'selected' if value_id.product_attribute_value_id in attribute_value_defaults else None</attribute>
</xpath>
<xpath expr="//t[@t-foreach='variant_id.product_template_value_ids']/li/label/div/input" position="attributes">
<attribute name="t-att-checked">'checked' if value_id.product_attribute_value_id in attribute_value_defaults else None</attribute>
</xpath>
<xpath expr="//li[@t-foreach='variant_id.product_template_value_ids']/label" position="attributes">
<attribute name="t-attf-class">css_attribute_color #{'active' if value_id.product_attribute_value_id in attribute_value_defaults else ''} #{'custom_value' if value_id.is_custom else ''}</attribute>
</xpath>
<xpath expr="//li[@t-foreach='variant_id.product_template_value_ids']/label/input" position="attributes">
<attribute name="t-att-checked">'checked' if value_id.product_attribute_value_id in attribute_value_defaults else None</attribute>
</xpath>
</template>
</odoo>