mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch '12.0-test' into 12.0
This commit is contained in:
1
delivery_hibou/__init__.py
Normal file
1
delivery_hibou/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
24
delivery_hibou/__manifest__.py
Normal file
24
delivery_hibou/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
2
delivery_hibou/models/__init__.py
Normal file
2
delivery_hibou/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import delivery
|
||||||
|
from . import stock
|
||||||
152
delivery_hibou/models/delivery.py
Normal file
152
delivery_hibou/models/delivery.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
50
delivery_hibou/models/stock.py
Normal file
50
delivery_hibou/models/stock.py
Normal 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
|
||||||
1
delivery_hibou/tests/__init__.py
Normal file
1
delivery_hibou/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_delivery_hibou
|
||||||
161
delivery_hibou/tests/test_delivery_hibou.py
Normal file
161
delivery_hibou/tests/test_delivery_hibou.py
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
14
delivery_hibou/views/delivery_views.xml
Normal file
14
delivery_hibou/views/delivery_views.xml
Normal 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>
|
||||||
16
delivery_hibou/views/stock_views.xml
Normal file
16
delivery_hibou/views/stock_views.xml
Normal 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>
|
||||||
29
delivery_partner/README.rst
Normal file
29
delivery_partner/README.rst
Normal 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
|
||||||
1
delivery_partner/__init__.py
Normal file
1
delivery_partner/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
24
delivery_partner/__manifest__.py
Executable file
24
delivery_partner/__manifest__.py
Executable 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,
|
||||||
|
}
|
||||||
1
delivery_partner/models/__init__.py
Normal file
1
delivery_partner/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import delivery
|
||||||
48
delivery_partner/models/delivery.py
Normal file
48
delivery_partner/models/delivery.py
Normal 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()
|
||||||
2
delivery_partner/security/ir.model.access.csv
Normal file
2
delivery_partner/security/ir.model.access.csv
Normal 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
|
||||||
|
94
delivery_partner/views/delivery_views.xml
Normal file
94
delivery_partner/views/delivery_views.xml
Normal 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>
|
||||||
2
sale_line_reconfigure/__init__.py
Executable file
2
sale_line_reconfigure/__init__.py
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
29
sale_line_reconfigure/__manifest__.py
Executable file
29
sale_line_reconfigure/__manifest__.py
Executable 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',
|
||||||
|
],
|
||||||
|
}
|
||||||
1
sale_line_reconfigure/controllers/__init__.py
Normal file
1
sale_line_reconfigure/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import product_configurator
|
||||||
32
sale_line_reconfigure/controllers/product_configurator.py
Normal file
32
sale_line_reconfigure/controllers/product_configurator.py
Normal 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)
|
||||||
1
sale_line_reconfigure/models/__init__.py
Normal file
1
sale_line_reconfigure/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import product
|
||||||
47
sale_line_reconfigure/models/product.py
Normal file
47
sale_line_reconfigure/models/product.py
Normal 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)
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
});
|
||||||
11
sale_line_reconfigure/views/assets.xml
Normal file
11
sale_line_reconfigure/views/assets.xml
Normal 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>
|
||||||
24
sale_line_reconfigure/views/product_views.xml
Normal file
24
sale_line_reconfigure/views/product_views.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user