mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch '11.0-test' of https://github.com/hibou-io/hibou-odoo-suite into 11.0-test
This commit is contained in:
@@ -109,36 +109,17 @@ class ProviderStamps(models.Model):
|
|||||||
|
|
||||||
ret_val = service.create_shipping()
|
ret_val = service.create_shipping()
|
||||||
ret_val.ShipDate = date_planned.split()[0] if date_planned else date.today().isoformat()
|
ret_val.ShipDate = date_planned.split()[0] if date_planned else date.today().isoformat()
|
||||||
ret_val.FromZIPCode = order.warehouse_id.partner_id.zip
|
ret_val.FromZIPCode = self.get_shipper_warehouse(order=order).zip
|
||||||
ret_val.ToZIPCode = order.partner_shipping_id.zip
|
ret_val.ToZIPCode = order.partner_shipping_id.zip
|
||||||
ret_val.PackageType = self._stamps_package_type()
|
ret_val.PackageType = self._stamps_package_type()
|
||||||
ret_val.ServiceType = self.stamps_service_type
|
ret_val.ServiceType = self.stamps_service_type
|
||||||
ret_val.WeightLb = weight
|
ret_val.WeightLb = weight
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
def _get_order_for_picking(self, picking):
|
|
||||||
if picking.sale_id:
|
|
||||||
return picking.sale_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_company_for_order(self, order):
|
|
||||||
company = order.company_id
|
|
||||||
if order.team_id and order.team_id.subcompany_id:
|
|
||||||
company = order.team_id.subcompany_id.company_id
|
|
||||||
elif order.analytic_account_id and order.analytic_account_id.subcompany_id:
|
|
||||||
company = order.analytic_account_id.subcompany_id.company_id
|
|
||||||
return company
|
|
||||||
|
|
||||||
def _get_company_for_picking(self, picking):
|
|
||||||
order = self._get_order_for_picking(picking)
|
|
||||||
if order:
|
|
||||||
return self._get_company_for_order(order)
|
|
||||||
return picking.company_id
|
|
||||||
|
|
||||||
def _stamps_get_addresses_for_picking(self, picking):
|
def _stamps_get_addresses_for_picking(self, picking):
|
||||||
company = self._get_company_for_picking(picking)
|
company = self.get_shipper_company(picking=picking)
|
||||||
from_ = picking.picking_type_id.warehouse_id.partner_id
|
from_ = self.get_shipper_warehouse(picking=picking)
|
||||||
to = picking.partner_id
|
to = self.get_recipient(picking=picking)
|
||||||
return company, from_, to
|
return company, from_, to
|
||||||
|
|
||||||
def _stamps_get_shippings_for_picking(self, service, picking):
|
def _stamps_get_shippings_for_picking(self, service, picking):
|
||||||
@@ -251,7 +232,7 @@ class ProviderStamps(models.Model):
|
|||||||
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
|
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
|
||||||
|
|
||||||
from_address = service.create_address()
|
from_address = service.create_address()
|
||||||
from_address.FullName = company.partner_id.name
|
from_address.FullName = company.name
|
||||||
from_address.Address1 = from_partner.street
|
from_address.Address1 = from_partner.street
|
||||||
if from_partner.street2:
|
if from_partner.street2:
|
||||||
from_address.Address2 = from_partner.street2
|
from_address.Address2 = from_partner.street2
|
||||||
|
|||||||
2
external/hibou-oca/connector-magento
vendored
2
external/hibou-oca/connector-magento
vendored
Submodule external/hibou-oca/connector-magento updated: 8cce7538e6...802fe38dc1
@@ -25,10 +25,14 @@ class AccountInvoiceLine(models.Model):
|
|||||||
move_lines = self.sale_line_ids.mapped('move_ids.move_line_ids')
|
move_lines = self.sale_line_ids.mapped('move_ids.move_line_ids')
|
||||||
else:
|
else:
|
||||||
move_lines = self.purchase_line_id.mapped('move_ids.move_line_ids')
|
move_lines = self.purchase_line_id.mapped('move_ids.move_line_ids')
|
||||||
for move_line in move_lines:
|
for move_line in move_lines.filtered(lambda l: l.lot_id):
|
||||||
qty_done = move_line.qty_done
|
qty_done = move_line.qty_done
|
||||||
|
current_qty_done = qty_done + qty_done_total
|
||||||
r = move_line.lot_id.catch_weight_ratio
|
r = move_line.lot_id.catch_weight_ratio
|
||||||
ratio = ((ratio * qty_done_total) + (qty_done * r)) / (qty_done + qty_done_total)
|
if current_qty_done == 0:
|
||||||
|
ratio = 0
|
||||||
|
else:
|
||||||
|
ratio = ((ratio * qty_done_total) + (qty_done * r)) / current_qty_done
|
||||||
qty_done_total += qty_done
|
qty_done_total += qty_done
|
||||||
catch_weight += move_line.lot_id.catch_weight
|
catch_weight += move_line.lot_id.catch_weight
|
||||||
price = price * ratio
|
price = price * ratio
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ class StockProductionLot(models.Model):
|
|||||||
catch_weight = fields.Float(string='Catch Weight', digits=(10, 4))
|
catch_weight = fields.Float(string='Catch Weight', digits=(10, 4))
|
||||||
catch_weight_uom_id = fields.Many2one('product.uom', related='product_id.catch_weight_uom_id')
|
catch_weight_uom_id = fields.Many2one('product.uom', related='product_id.catch_weight_uom_id')
|
||||||
|
|
||||||
|
|
||||||
@api.depends('catch_weight')
|
@api.depends('catch_weight')
|
||||||
def _compute_catch_weight_ratio(self):
|
def _compute_catch_weight_ratio(self):
|
||||||
for lot in self:
|
for lot in self:
|
||||||
@@ -44,3 +43,14 @@ class StockMoveLine(models.Model):
|
|||||||
catch_weight_uom_id = fields.Many2one('product.uom', string='Catch Weight UOM')
|
catch_weight_uom_id = fields.Many2one('product.uom', string='Catch Weight UOM')
|
||||||
lot_catch_weight = fields.Float(related='lot_id.catch_weight')
|
lot_catch_weight = fields.Float(related='lot_id.catch_weight')
|
||||||
lot_catch_weight_uom_id = fields.Many2one('product.uom', related='product_id.catch_weight_uom_id')
|
lot_catch_weight_uom_id = fields.Many2one('product.uom', related='product_id.catch_weight_uom_id')
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = 'stock.picking'
|
||||||
|
|
||||||
|
has_catch_weight = fields.Boolean(string="Has Catch Weight", compute='_compute_has_catch_weight', store=True)
|
||||||
|
|
||||||
|
@api.depends('move_lines.product_catch_weight_uom_id')
|
||||||
|
def _compute_has_catch_weight(self):
|
||||||
|
for picking in self:
|
||||||
|
picking.has_catch_weight = any(picking.mapped('move_lines.product_catch_weight_uom_id'))
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TestPicking(TransactionCase):
|
|||||||
'uom_po_id': self.product_uom_id.id,
|
'uom_po_id': self.product_uom_id.id,
|
||||||
'catch_weight_uom_id': self.ref_uom_id.id,
|
'catch_weight_uom_id': self.ref_uom_id.id,
|
||||||
})
|
})
|
||||||
|
self.pricelist = self.env.ref('product.list0')
|
||||||
|
|
||||||
|
|
||||||
# def test_creation(self):
|
# def test_creation(self):
|
||||||
@@ -69,6 +70,7 @@ class TestPicking(TransactionCase):
|
|||||||
'partner_invoice_id': self.partner1.id,
|
'partner_invoice_id': self.partner1.id,
|
||||||
'partner_shipping_id': self.partner1.id,
|
'partner_shipping_id': self.partner1.id,
|
||||||
'order_line': [(0, 0, {'product_id': self.product1.id})],
|
'order_line': [(0, 0, {'product_id': self.product1.id})],
|
||||||
|
'pricelist_id': self.pricelist.id,
|
||||||
})
|
})
|
||||||
so.action_confirm()
|
so.action_confirm()
|
||||||
self.assertTrue(so.state in ('sale', 'done'))
|
self.assertTrue(so.state in ('sale', 'done'))
|
||||||
@@ -104,6 +106,7 @@ class TestPicking(TransactionCase):
|
|||||||
'partner_invoice_id': self.partner1.id,
|
'partner_invoice_id': self.partner1.id,
|
||||||
'partner_shipping_id': self.partner1.id,
|
'partner_shipping_id': self.partner1.id,
|
||||||
'order_line': [(0, 0, {'product_id': self.product1.id, 'product_uom_qty': 2.0})],
|
'order_line': [(0, 0, {'product_id': self.product1.id, 'product_uom_qty': 2.0})],
|
||||||
|
'pricelist_id': self.pricelist.id,
|
||||||
})
|
})
|
||||||
so.action_confirm()
|
so.action_confirm()
|
||||||
self.assertTrue(so.state in ('sale', 'done'))
|
self.assertTrue(so.state in ('sale', 'done'))
|
||||||
|
|||||||
@@ -13,16 +13,6 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!--<record id="view_move_line_form_inherit" model="ir.ui.view">-->
|
|
||||||
<!--<field name="name">stock.move.line.form.inherit</field>-->
|
|
||||||
<!--<field name="model">stock.move.line</field>-->
|
|
||||||
<!--<field name="inherit_id" ref="stock.view_move_line_form" />-->
|
|
||||||
<!--<field name="arch" type="xml">-->
|
|
||||||
<!--<xpath expr="//field[@name='lot_name']" position="after">-->
|
|
||||||
<!--<field name="lot_catch_weight_ratio" readonly="1"/>-->
|
|
||||||
<!--</xpath>-->
|
|
||||||
<!--</field>-->
|
|
||||||
<!--</record>-->
|
|
||||||
<record id="view_stock_move_operations_inherit" model="ir.ui.view">
|
<record id="view_stock_move_operations_inherit" model="ir.ui.view">
|
||||||
<field name="name">stock.move.operations.form.inherit</field>
|
<field name="name">stock.move.operations.form.inherit</field>
|
||||||
<field name="model">stock.move</field>
|
<field name="model">stock.move</field>
|
||||||
@@ -36,6 +26,7 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="view_stock_move_line_operation_tree_inherit" model="ir.ui.view">
|
<record id="view_stock_move_line_operation_tree_inherit" model="ir.ui.view">
|
||||||
<field name="name">stock.move.line.operations.tree.inherit</field>
|
<field name="name">stock.move.line.operations.tree.inherit</field>
|
||||||
<field name="model">stock.move.line</field>
|
<field name="model">stock.move.line</field>
|
||||||
@@ -49,6 +40,7 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="product_template_form_view_inherit" model="ir.ui.view">
|
<record id="product_template_form_view_inherit" model="ir.ui.view">
|
||||||
<field name="name">product.template.common.form.inherit</field>
|
<field name="name">product.template.common.form.inherit</field>
|
||||||
<field name="model">product.template</field>
|
<field name="model">product.template</field>
|
||||||
@@ -59,4 +51,18 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="stock_view_picking_internal_search_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">stock.view.picking.internal.search.inherit</field>
|
||||||
|
<field name="model">stock.picking</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_picking_internal_search" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[@name='done']" position="after">
|
||||||
|
<filter name="has_catch_weight" string="Has Catch Weight" domain="[('has_catch_weight','=',True)]" help="Pickings with Catch Weight"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//group/filter[@name='picking_type']" position="after">
|
||||||
|
<filter string="Has Catch Weight" domain="[]" context="{'group_by':'has_catch_weight'}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<field name="line_ids">
|
<field name="line_ids">
|
||||||
<tree editable="top">
|
<tree editable="top" create="false" delete="false">
|
||||||
<field name="product_id" readonly="1"/>
|
<field name="product_id" readonly="1"/>
|
||||||
<field name="qty_ordered" readonly="1"/>
|
<field name="qty_ordered" readonly="1"/>
|
||||||
<field name="qty_delivered" readonly="1"/>
|
<field name="qty_delivered" readonly="1"/>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<field name="line_ids">
|
<field name="line_ids">
|
||||||
<tree editable="top">
|
<tree editable="top" create="false" delete="false">
|
||||||
<field name="product_id" readonly="1"/>
|
<field name="product_id" readonly="1"/>
|
||||||
<field name="qty_ordered" readonly="1"/>
|
<field name="qty_ordered" readonly="1"/>
|
||||||
<field name="qty_delivered" readonly="1"/>
|
<field name="qty_delivered" readonly="1"/>
|
||||||
|
|||||||
1
sale_line_change/__init__.py
Normal file
1
sale_line_change/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import wizard
|
||||||
25
sale_line_change/__manifest__.py
Normal file
25
sale_line_change/__manifest__.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
'name': 'Sale Line Change',
|
||||||
|
'summary': 'Change Confirmed Sale Lines Routes or Warehouses.',
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'author': "Hibou Corp.",
|
||||||
|
'category': 'Sale',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'complexity': 'expert',
|
||||||
|
'images': [],
|
||||||
|
'website': "https://hibou.io",
|
||||||
|
'description': """
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'sale_sourced_by_line',
|
||||||
|
'sale_stock',
|
||||||
|
'stock_dropshipping',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'data': [
|
||||||
|
'wizard/sale_line_change_views.xml',
|
||||||
|
'views/sale_views.xml',
|
||||||
|
],
|
||||||
|
'auto_install': False,
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
1
sale_line_change/tests/__init__.py
Normal file
1
sale_line_change/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_sale_line_change
|
||||||
99
sale_line_change/tests/test_sale_line_change.py
Normal file
99
sale_line_change/tests/test_sale_line_change.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
from odoo.tests import common
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestSaleLineChange(common.TransactionCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSaleLineChange, self).setUp()
|
||||||
|
self.warehouse0 = self.env.ref('stock.warehouse0')
|
||||||
|
self.warehouse1 = self.env['stock.warehouse'].create({
|
||||||
|
'company_id': self.env.user.company_id.id,
|
||||||
|
# 'partner_id': self.env.user.company_id.partner_id.id,
|
||||||
|
'name': 'TWH1',
|
||||||
|
'code': 'TWH1',
|
||||||
|
})
|
||||||
|
self.product1 = self.env.ref('product.product_product_24')
|
||||||
|
self.partner1 = self.env.ref('base.res_partner_12')
|
||||||
|
self.so1 = self.env['sale.order'].create({
|
||||||
|
'partner_id': self.partner1.id,
|
||||||
|
'partner_invoice_id': self.partner1.id,
|
||||||
|
'partner_shipping_id': self.partner1.id,
|
||||||
|
'order_line': [(0, 0, {
|
||||||
|
'product_id': self.product1.id,
|
||||||
|
'name': 'N/A',
|
||||||
|
'product_uom_qty': 1.0,
|
||||||
|
'price_unit': 100.0,
|
||||||
|
})]
|
||||||
|
})
|
||||||
|
self.dropship_route = self.env.ref('stock_dropshipping.route_drop_shipping')
|
||||||
|
self.warehouse0_route = self.warehouse0.route_ids.filtered(lambda r: r.name.find('Ship') >= 0)
|
||||||
|
|
||||||
|
def test_00_sale_change_warehouse(self):
|
||||||
|
so = self.so1
|
||||||
|
|
||||||
|
so.action_confirm()
|
||||||
|
self.assertTrue(so.state in ('sale', 'done'))
|
||||||
|
self.assertTrue(so.picking_ids)
|
||||||
|
org_picking = so.picking_ids
|
||||||
|
self.assertEqual(org_picking.picking_type_id.warehouse_id, self.warehouse0)
|
||||||
|
|
||||||
|
wiz = self.env['sale.line.change.order'].with_context(default_order_id=so.id).create({})
|
||||||
|
self.assertTrue(wiz.line_ids)
|
||||||
|
wiz.line_ids.line_warehouse_id = self.warehouse1
|
||||||
|
wiz.line_ids.line_date_planned = '2018-01-01 00:00:00'
|
||||||
|
wiz.apply()
|
||||||
|
|
||||||
|
self.assertTrue(len(so.picking_ids) == 2)
|
||||||
|
self.assertTrue(org_picking.state == 'cancel')
|
||||||
|
new_picking = so.picking_ids - org_picking
|
||||||
|
self.assertTrue(new_picking)
|
||||||
|
self.assertEqual(new_picking.picking_type_id.warehouse_id, self.warehouse1)
|
||||||
|
self.assertEqual(new_picking.scheduled_date, '2018-01-01 00:00:00')
|
||||||
|
|
||||||
|
def test_01_sale_change_route(self):
|
||||||
|
so = self.so1
|
||||||
|
|
||||||
|
so.action_confirm()
|
||||||
|
self.assertTrue(so.state in ('sale', 'done'))
|
||||||
|
self.assertTrue(so.picking_ids)
|
||||||
|
org_picking = so.picking_ids
|
||||||
|
self.assertEqual(org_picking.picking_type_id.warehouse_id, self.warehouse0)
|
||||||
|
|
||||||
|
# Change route on wizard line
|
||||||
|
wiz = self.env['sale.line.change.order'].with_context(default_order_id=so.id).create({})
|
||||||
|
self.assertTrue(wiz.line_ids)
|
||||||
|
wiz.line_ids.line_route_id = self.dropship_route
|
||||||
|
wiz.apply()
|
||||||
|
|
||||||
|
# Check that RFQ/PO was created.
|
||||||
|
self.assertTrue(org_picking.state == 'cancel')
|
||||||
|
po_line = self.env['purchase.order.line'].search([('sale_line_id', '=', so.order_line.id)])
|
||||||
|
self.assertTrue(po_line)
|
||||||
|
|
||||||
|
def test_02_sale_dropshipping_to_warehouse(self):
|
||||||
|
self.assertTrue(self.warehouse0_route)
|
||||||
|
self.product1.route_ids += self.dropship_route
|
||||||
|
so = self.so1
|
||||||
|
|
||||||
|
so.action_confirm()
|
||||||
|
self.assertTrue(so.state in ('sale', 'done'))
|
||||||
|
self.assertFalse(so.picking_ids)
|
||||||
|
|
||||||
|
# Change route on wizard line
|
||||||
|
wiz = self.env['sale.line.change.order'].with_context(default_order_id=so.id).create({})
|
||||||
|
self.assertTrue(wiz.line_ids)
|
||||||
|
wiz.line_ids.line_route_id = self.warehouse0_route
|
||||||
|
wiz.line_ids.line_date_planned = '2018-01-01 00:00:00'
|
||||||
|
|
||||||
|
# Wizard cannot complete because of non-cancelled Purchase Order.
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
wiz.apply()
|
||||||
|
|
||||||
|
po_line = self.env['purchase.order.line'].search([('sale_line_id', '=', so.order_line.id)])
|
||||||
|
po_line.order_id.button_cancel()
|
||||||
|
wiz.apply()
|
||||||
|
|
||||||
|
# Check parameters on new picking
|
||||||
|
self.assertTrue(so.picking_ids)
|
||||||
|
self.assertEqual(so.picking_ids.picking_type_id.warehouse_id, self.warehouse0)
|
||||||
|
self.assertEqual(so.picking_ids.scheduled_date, '2018-01-01 00:00:00')
|
||||||
19
sale_line_change/views/sale_views.xml
Normal file
19
sale_line_change/views/sale_views.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_order_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.form.inherit</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="inherit_id" ref="sale.view_order_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//header/button[@name='action_confirm']" position="after">
|
||||||
|
<button name="%(action_sale_line_change_order)d"
|
||||||
|
type="action"
|
||||||
|
attrs="{'invisible': [('state', 'in', ('draft', 'sent', 'cancel'))]}"
|
||||||
|
string="Line Change"
|
||||||
|
class="btn-secondary"
|
||||||
|
context="{'default_order_id': active_id}"
|
||||||
|
groups="sales_team.group_sale_manager"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
sale_line_change/wizard/__init__.py
Normal file
1
sale_line_change/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import sale_line_change
|
||||||
89
sale_line_change/wizard/sale_line_change.py
Normal file
89
sale_line_change/wizard/sale_line_change.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class SaleLineChangeOrder(models.TransientModel):
|
||||||
|
_name = 'sale.line.change.order'
|
||||||
|
_description = 'Sale Line Change Order'
|
||||||
|
|
||||||
|
order_id = fields.Many2one('sale.order', string='Sale Order')
|
||||||
|
line_ids = fields.One2many('sale.line.change.order.line', 'change_order_id', string='Change Lines')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields):
|
||||||
|
rec = super(SaleLineChangeOrder, self).default_get(fields)
|
||||||
|
if 'order_id' in rec:
|
||||||
|
order = self.env['sale.order'].browse(rec['order_id'])
|
||||||
|
if not order:
|
||||||
|
return rec
|
||||||
|
|
||||||
|
line_model = self.env['sale.line.change.order.line']
|
||||||
|
rec['line_ids'] = [(0, 0, line_model.values_from_so_line(l)) for l in order.order_line]
|
||||||
|
return rec
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def apply(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self.line_ids.apply()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SaleLineChangeOrderLine(models.TransientModel):
|
||||||
|
_name = 'sale.line.change.order.line'
|
||||||
|
|
||||||
|
change_order_id = fields.Many2one('sale.line.change.order')
|
||||||
|
sale_line_id = fields.Many2one('sale.order.line', string='Sale Line')
|
||||||
|
line_ordered_qty = fields.Float(string='Ordered Qty')
|
||||||
|
line_delivered_qty = fields.Float(string='Delivered Qty')
|
||||||
|
line_reserved_qty = fields.Float(string='Reserved Qty')
|
||||||
|
line_date_planned = fields.Datetime(string='Planned Date')
|
||||||
|
line_warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
|
||||||
|
line_route_id = fields.Many2one('stock.location.route', string='Route')
|
||||||
|
|
||||||
|
def values_from_so_line(self, so_line):
|
||||||
|
move_ids = so_line.move_ids
|
||||||
|
reserved_qty = sum(move_ids.mapped('reserved_availability'))
|
||||||
|
return {
|
||||||
|
'sale_line_id': so_line.id,
|
||||||
|
'line_ordered_qty': so_line.product_uom_qty,
|
||||||
|
'line_delivered_qty': so_line.qty_delivered,
|
||||||
|
'line_reserved_qty': reserved_qty,
|
||||||
|
'line_date_planned': so_line.date_planned,
|
||||||
|
'line_warehouse_id': so_line.warehouse_id.id,
|
||||||
|
'line_route_id': so_line.route_id.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _apply(self):
|
||||||
|
self._apply_clean_dropship()
|
||||||
|
self._apply_clean_existing_moves()
|
||||||
|
self._apply_new_values()
|
||||||
|
self._apply_procurement()
|
||||||
|
|
||||||
|
def _apply_clean_dropship(self):
|
||||||
|
po_line_model = self.env['purchase.order.line'].sudo()
|
||||||
|
po_lines = po_line_model.search([('sale_line_id', 'in', self.mapped('sale_line_id.id'))])
|
||||||
|
|
||||||
|
if po_lines and po_lines.filtered(lambda l: l.order_id.state != 'cancel'):
|
||||||
|
names = ', '.join(po_lines.filtered(lambda l: l.order_id.state != 'cancel').mapped('order_id.name'))
|
||||||
|
raise ValidationError('One or more lines has existing non-cancelled Purchase Orders associated: ' + names)
|
||||||
|
|
||||||
|
def _apply_clean_existing_moves(self):
|
||||||
|
moves = self.mapped('sale_line_id.move_ids').filtered(lambda m: m.state != 'done')
|
||||||
|
moves._action_cancel()
|
||||||
|
|
||||||
|
def _apply_new_values(self):
|
||||||
|
for line in self:
|
||||||
|
line.sale_line_id.write({
|
||||||
|
'date_planned': line.line_date_planned,
|
||||||
|
'warehouse_id': line.line_warehouse_id.id,
|
||||||
|
'route_id': line.line_route_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _apply_procurement(self):
|
||||||
|
self.mapped('sale_line_id')._action_launch_procurement_rule()
|
||||||
|
|
||||||
|
def apply(self):
|
||||||
|
changed_lines = self.filtered(lambda l: (
|
||||||
|
l.sale_line_id.warehouse_id != l.line_warehouse_id
|
||||||
|
or l.sale_line_id.route_id != l.line_route_id))
|
||||||
|
changed_lines._apply()
|
||||||
40
sale_line_change/wizard/sale_line_change_views.xml
Normal file
40
sale_line_change/wizard/sale_line_change_views.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="sale_line_change_order_form" model="ir.ui.view">
|
||||||
|
<field name="name">sale.line.change.order.form</field>
|
||||||
|
<field name="model">sale.line.change.order</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<p>Changing Date Planned alone should be done on any existing Pickings or POs.</p>
|
||||||
|
<field name="order_id" invisible="1"/>
|
||||||
|
<field name="line_ids">
|
||||||
|
<tree editable="top" create="false" delete="false">
|
||||||
|
<field name="sale_line_id" string="Line" readonly="1" force_save="1"/>
|
||||||
|
<field name="line_ordered_qty" string="Ordered" readonly="1"/>
|
||||||
|
<field name="line_delivered_qty" string="Delivered" readonly="1"/>
|
||||||
|
<field name="line_reserved_qty" string="Reserved" readonly="1"/>
|
||||||
|
<field name="line_date_planned"/>
|
||||||
|
<field name="line_warehouse_id" options="{'no_create': True,}"/>
|
||||||
|
<field name="line_route_id" domain="[('sale_selectable', '=', True)]" options="{'no_create': True,}"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
<footer>
|
||||||
|
<button name="apply" type="object" string="Apply Changes" class="btn-primary"/>
|
||||||
|
<button class="oe_link"
|
||||||
|
special="cancel"
|
||||||
|
string="Cancel" />
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="action_sale_line_change_order" model="ir.actions.act_window">
|
||||||
|
<field name="name">Sale Line Change Order</field>
|
||||||
|
<field name="res_model">sale.line.change.order</field>
|
||||||
|
<field name="view_type">form</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="sale_line_change_order_form" />
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -15,8 +15,8 @@ class Partner(models.Model):
|
|||||||
for partner in self.with_context(lang='en_US'):
|
for partner in self.with_context(lang='en_US'):
|
||||||
if ZipcodeSearchEngine and partner.zip:
|
if ZipcodeSearchEngine and partner.zip:
|
||||||
with ZipcodeSearchEngine() as search:
|
with ZipcodeSearchEngine() as search:
|
||||||
zipcode = search.by_zipcode(partner.zip)
|
zipcode = search.by_zipcode(str(self.zip).split('-')[0])
|
||||||
if zipcode:
|
if zipcode and zipcode['Latitude']:
|
||||||
partner.write({
|
partner.write({
|
||||||
'partner_latitude': zipcode['Latitude'],
|
'partner_latitude': zipcode['Latitude'],
|
||||||
'partner_longitude': zipcode['Longitude'],
|
'partner_longitude': zipcode['Longitude'],
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class FakePartner():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def date_localization(self):
|
def date_localization(self):
|
||||||
if not hasattr(self, 'date_localization'):
|
if not hasattr(self, 'date_localization') and self.date_localization:
|
||||||
self.date_localization = 'TODAY!'
|
self.date_localization = 'TODAY!'
|
||||||
# The fast way.
|
# The fast way.
|
||||||
if ZipcodeSearchEngine and self.zip:
|
if ZipcodeSearchEngine and self.zip:
|
||||||
|
|||||||
1
stock_landed_costs_average/__init__.py
Normal file
1
stock_landed_costs_average/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
21
stock_landed_costs_average/__manifest__.py
Normal file
21
stock_landed_costs_average/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
'name': 'Landed Costs Average',
|
||||||
|
'summary': 'Use Landed Costs on Average Cost inventory.',
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'author': "Hibou Corp. <hello@hibou.io>",
|
||||||
|
'category': 'Warehouse',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'complexity': 'expert',
|
||||||
|
'images': [],
|
||||||
|
'website': "https://hibou.io",
|
||||||
|
'description': """
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'stock_landed_costs',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'data': [
|
||||||
|
],
|
||||||
|
'auto_install': False,
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
1
stock_landed_costs_average/models/__init__.py
Normal file
1
stock_landed_costs_average/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import stock_landed_cost
|
||||||
96
stock_landed_costs_average/models/stock_landed_cost.py
Normal file
96
stock_landed_costs_average/models/stock_landed_cost.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from odoo import api, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LandedCost(models.Model):
|
||||||
|
_inherit = 'stock.landed.cost'
|
||||||
|
|
||||||
|
def get_valuation_lines(self):
|
||||||
|
"""
|
||||||
|
Override for allowing Average value inventory.
|
||||||
|
:return: list of new line values
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for move in self.mapped('picking_ids').mapped('move_lines'):
|
||||||
|
# Only allow for real time valuated products with 'average' or 'fifo' cost
|
||||||
|
if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average'):
|
||||||
|
continue
|
||||||
|
vals = {
|
||||||
|
'product_id': move.product_id.id,
|
||||||
|
'move_id': move.id,
|
||||||
|
'quantity': move.product_qty,
|
||||||
|
'former_cost': move.value,
|
||||||
|
'weight': move.product_id.weight * move.product_qty,
|
||||||
|
'volume': move.product_id.volume * move.product_qty
|
||||||
|
}
|
||||||
|
lines.append(vals)
|
||||||
|
|
||||||
|
if not lines and self.mapped('picking_ids'):
|
||||||
|
raise UserError(_('The selected picking does not contain any move that would be impacted by landed costs. Landed costs are only possible for products configured in real time valuation with real price costing method. Please make sure it is the case, or you selected the correct picking'))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def button_validate(self):
|
||||||
|
"""
|
||||||
|
Override to directly set new standard_price on product if average costed.
|
||||||
|
:return: True
|
||||||
|
"""
|
||||||
|
if any(cost.state != 'draft' for cost in self):
|
||||||
|
raise UserError(_('Only draft landed costs can be validated'))
|
||||||
|
if any(not cost.valuation_adjustment_lines for cost in self):
|
||||||
|
raise UserError(_('No valuation adjustments lines. You should maybe recompute the landed costs.'))
|
||||||
|
if not self._check_sum():
|
||||||
|
raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))
|
||||||
|
|
||||||
|
for cost in self:
|
||||||
|
move = self.env['account.move']
|
||||||
|
move_vals = {
|
||||||
|
'journal_id': cost.account_journal_id.id,
|
||||||
|
'date': cost.date,
|
||||||
|
'ref': cost.name,
|
||||||
|
'line_ids': [],
|
||||||
|
}
|
||||||
|
for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
|
||||||
|
# Prorate the value at what's still in stock
|
||||||
|
_logger.warn('(line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost')
|
||||||
|
_logger.warn('(%s / %s) * %s' % (line.move_id.remaining_qty, line.move_id.product_qty, line.additional_landed_cost))
|
||||||
|
cost_to_add = (line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
|
||||||
|
_logger.warn('cost_to_add: ' + str(cost_to_add))
|
||||||
|
|
||||||
|
new_landed_cost_value = line.move_id.landed_cost_value + line.additional_landed_cost
|
||||||
|
line.move_id.write({
|
||||||
|
'landed_cost_value': new_landed_cost_value,
|
||||||
|
'value': line.move_id.value + cost_to_add,
|
||||||
|
'remaining_value': line.move_id.remaining_value + cost_to_add,
|
||||||
|
'price_unit': (line.move_id.value + new_landed_cost_value) / line.move_id.product_qty,
|
||||||
|
})
|
||||||
|
# `remaining_qty` is negative if the move is out and delivered products that were not
|
||||||
|
# in stock.
|
||||||
|
qty_out = 0
|
||||||
|
if line.move_id._is_in():
|
||||||
|
qty_out = line.move_id.product_qty - line.move_id.remaining_qty
|
||||||
|
elif line.move_id._is_out():
|
||||||
|
qty_out = line.move_id.product_qty
|
||||||
|
move_vals['line_ids'] += line._create_accounting_entries(move, qty_out)
|
||||||
|
|
||||||
|
# Need to set the standard price directly on the product.
|
||||||
|
if line.product_id.cost_method == 'average':
|
||||||
|
# From product.do_change_standard_price
|
||||||
|
quant_locs = self.env['stock.quant'].sudo().read_group([('product_id', '=', line.product_id.id)],
|
||||||
|
['location_id'], ['location_id'])
|
||||||
|
quant_loc_ids = [loc['location_id'][0] for loc in quant_locs]
|
||||||
|
locations = self.env['stock.location'].search(
|
||||||
|
[('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id),
|
||||||
|
('id', 'in', quant_loc_ids)])
|
||||||
|
qty_available = line.product_id.with_context(location=locations.ids).qty_available
|
||||||
|
total_cost = (qty_available * line.product_id.standard_price) + cost_to_add
|
||||||
|
line.product_id.write({'standard_price': total_cost / qty_available})
|
||||||
|
|
||||||
|
move = move.create(move_vals)
|
||||||
|
cost.write({'state': 'done', 'account_move_id': move.id})
|
||||||
|
move.post()
|
||||||
|
return True
|
||||||
1
stock_landed_costs_average/tests/__init__.py
Normal file
1
stock_landed_costs_average/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_stock_landed_cost
|
||||||
102
stock_landed_costs_average/tests/test_stock_landed_cost.py
Normal file
102
stock_landed_costs_average/tests/test_stock_landed_cost.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from odoo.addons.stock_landed_costs.tests.test_stock_landed_costs_purchase import TestLandedCosts
|
||||||
|
|
||||||
|
|
||||||
|
class TestLandedCostsAverage(TestLandedCosts):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestLandedCostsAverage, self).setUp()
|
||||||
|
self.product_refrigerator.cost_method = 'average'
|
||||||
|
self.product_oven.cost_method = 'average'
|
||||||
|
|
||||||
|
def test_00_landed_costs_on_incoming_shipment(self):
|
||||||
|
original_standard_price = self.product_refrigerator.standard_price
|
||||||
|
super(TestLandedCostsAverage, self).test_00_landed_costs_on_incoming_shipment()
|
||||||
|
self.assertTrue(original_standard_price != self.product_refrigerator.standard_price)
|
||||||
|
|
||||||
|
def test_01_landed_costs_simple_average(self):
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 0.0)
|
||||||
|
picking_in = self.Picking.create({
|
||||||
|
'partner_id': self.supplier_id,
|
||||||
|
'picking_type_id': self.picking_type_in_id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
self.Move.create({
|
||||||
|
'name': self.product_refrigerator.name,
|
||||||
|
'product_id': self.product_refrigerator.id,
|
||||||
|
'product_uom_qty': 5,
|
||||||
|
'product_uom': self.product_refrigerator.uom_id.id,
|
||||||
|
'picking_id': picking_in.id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
picking_in.action_confirm()
|
||||||
|
res_dict = picking_in.button_validate()
|
||||||
|
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||||
|
wizard.process()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 5.0)
|
||||||
|
|
||||||
|
stock_landed_cost = self._create_landed_costs({
|
||||||
|
'equal_price_unit': 50,
|
||||||
|
'quantity_price_unit': 0,
|
||||||
|
'weight_price_unit': 0,
|
||||||
|
'volume_price_unit': 0}, picking_in)
|
||||||
|
stock_landed_cost.compute_landed_cost()
|
||||||
|
stock_landed_cost.button_validate()
|
||||||
|
account_entry = self.env['account.move.line'].read_group(
|
||||||
|
[('move_id', '=', stock_landed_cost.account_move_id.id)], ['debit', 'credit', 'move_id'], ['move_id'])[0]
|
||||||
|
self.assertEqual(account_entry['debit'], 50.0, 'Wrong Account Entry')
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 11.0)
|
||||||
|
|
||||||
|
def test_02_landed_costs_average(self):
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 0.0)
|
||||||
|
picking_in = self.Picking.create({
|
||||||
|
'partner_id': self.supplier_id,
|
||||||
|
'picking_type_id': self.picking_type_in_id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
self.Move.create({
|
||||||
|
'name': self.product_refrigerator.name,
|
||||||
|
'product_id': self.product_refrigerator.id,
|
||||||
|
'product_uom_qty': 5,
|
||||||
|
'product_uom': self.product_refrigerator.uom_id.id,
|
||||||
|
'picking_id': picking_in.id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
picking_in.action_confirm()
|
||||||
|
res_dict = picking_in.button_validate()
|
||||||
|
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||||
|
wizard.process()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 5.0)
|
||||||
|
|
||||||
|
picking_out = self.Picking.create({
|
||||||
|
'partner_id': self.customer_id,
|
||||||
|
'picking_type_id': self.picking_type_out_id,
|
||||||
|
'location_id': self.stock_location_id,
|
||||||
|
'location_dest_id': self.customer_location_id})
|
||||||
|
self.Move.create({
|
||||||
|
'name': self.product_refrigerator.name,
|
||||||
|
'product_id': self.product_refrigerator.id,
|
||||||
|
'product_uom_qty': 2,
|
||||||
|
'product_uom': self.product_refrigerator.uom_id.id,
|
||||||
|
'picking_id': picking_out.id,
|
||||||
|
'location_id': self.stock_location_id,
|
||||||
|
'location_dest_id': self.customer_location_id})
|
||||||
|
picking_out.action_confirm()
|
||||||
|
picking_out.action_assign()
|
||||||
|
res_dict = picking_out.button_validate()
|
||||||
|
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||||
|
wizard.process()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 3.0)
|
||||||
|
|
||||||
|
stock_landed_cost = self._create_landed_costs({
|
||||||
|
'equal_price_unit': 50,
|
||||||
|
'quantity_price_unit': 0,
|
||||||
|
'weight_price_unit': 0,
|
||||||
|
'volume_price_unit': 0}, picking_in)
|
||||||
|
stock_landed_cost.compute_landed_cost()
|
||||||
|
stock_landed_cost.button_validate()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 11.0)
|
||||||
Reference in New Issue
Block a user