Начальное наполнение

This commit is contained in:
parent 08a104bda6
commit eec08269ad
12 changed files with 533 additions and 0 deletions

4
__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

16
__manifest__.py Normal file
View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'MTO Sale <-> Purchase',
'version': '1.0',
'category': 'Hidden',
'summary': 'SO/PO relation in case of MTO',
'description': """
Add relation information between Sale Orders and Purchase Orders if Make to Order (MTO) is activated on one sold product.
""",
'depends': ['sale_stock', 'purchase_stock', 'sale_purchase'],
'installable': True,
'auto_install': True,
'license': 'LGPL-3',
}

28
i18n/ru.po Normal file
View File

@ -0,0 +1,28 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_purchase_stock
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-26 21:55+0000\n"
"PO-Revision-Date: 2024-01-30 15:14+0400\n"
"Last-Translator: \n"
"Language-Team: Russian (https://app.transifex.com/odoo/teams/41243/ru/)\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#. module: sale_purchase_stock
#: model:ir.model,name:sale_purchase_stock.model_purchase_order
msgid "Purchase Order"
msgstr "Заказа на покупку"
#. module: sale_purchase_stock
#: model:ir.model,name:sale_purchase_stock.model_sale_order
msgid "Sales Order"
msgstr "Заказ на продажу"

View File

@ -0,0 +1,26 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_purchase_stock
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-26 21:55+0000\n"
"PO-Revision-Date: 2023-10-26 21:55+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sale_purchase_stock
#: model:ir.model,name:sale_purchase_stock.model_purchase_order
msgid "Purchase Order"
msgstr ""
#. module: sale_purchase_stock
#: model:ir.model,name:sale_purchase_stock.model_sale_order
msgid "Sales Order"
msgstr ""

5
models/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import purchase_order
from . import sale_order

15
models/purchase_order.py Normal file
View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
@api.depends('order_line.move_dest_ids.group_id.sale_id', 'order_line.move_ids.move_dest_ids.group_id.sale_id')
def _compute_sale_order_count(self):
super(PurchaseOrder, self)._compute_sale_order_count()
def _get_sale_orders(self):
return super(PurchaseOrder, self)._get_sale_orders() | self.order_line.move_dest_ids.group_id.sale_id | self.order_line.move_ids.move_dest_ids.group_id.sale_id

15
models/sale_order.py Normal file
View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.depends('procurement_group_id.stock_move_ids.created_purchase_line_ids.order_id', 'procurement_group_id.stock_move_ids.move_orig_ids.purchase_line_id.order_id')
def _compute_purchase_order_count(self):
super(SaleOrder, self)._compute_purchase_order_count()
def _get_purchase_orders(self):
return super(SaleOrder, self)._get_purchase_orders() | self.procurement_group_id.stock_move_ids.created_purchase_line_ids.order_id | self.procurement_group_id.stock_move_ids.move_orig_ids.purchase_line_id.order_id

6
tests/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sale_purchase_stock_flow
from . import test_access_rights
from . import test_unwanted_replenish_flow

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.sale_purchase.tests.common import TestCommonSalePurchaseNoChart
@tagged('post_install', '-at_install')
class TestAccessRights(TestCommonSalePurchaseNoChart):
@classmethod
def setUpClass(cls):
super(TestAccessRights, cls).setUpClass()
group_sale_user = cls.env.ref('sales_team.group_sale_salesman')
cls.user_salesperson = cls.env['res.users'].with_context(no_reset_password=True).create({
'name': 'Le Grand Jojo User',
'login': 'grand.jojo',
'email': 'grand.jojo@chansonbelge.com',
'groups_id': [(6, 0, [group_sale_user.id])]
})
def test_access_saleperson_decreases_qty(self):
"""
Suppose a user who has no right on PO
Suppose a PO linked to a SO
The user decreases the qty on the SO
This test ensures that an activity (warning) is added to the PO
"""
mto_route = self.env.ref('stock.route_warehouse0_mto')
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy')
mto_route.active = True
vendor = self.env['res.partner'].create({'name': 'vendor'})
seller = self.env['product.supplierinfo'].create({
'partner_id': vendor.id,
'price': 8,
})
product = self.env['product.product'].create({
'name': 'SuperProduct',
'type': 'product',
'seller_ids': [(6, 0, seller.ids)],
'route_ids': [(6, 0, (mto_route + buy_route).ids)]
})
so = self.env['sale.order'].with_user(self.user_salesperson).create({
'partner_id': self.partner_a.id,
'user_id': self.user_salesperson.id,
})
so_line, _ = self.env['sale.order.line'].create([{
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
'tax_id': False,
'order_id': so.id,
}, {
'name': 'Super Section',
'display_type': 'line_section',
'order_id': so.id,
}])
so.action_confirm()
po = self.env['purchase.order'].search([('partner_id', '=', vendor.id)])
po.button_confirm()
# salesperson writes on the SO
so.write({
'order_line': [(1, so_line.id, {'product_uom_qty': 0.9})]
})
self.assertIn(so.name, po.activity_ids.note)

63
tests/test_lead_time.py Normal file
View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import fields
from odoo.tests import tagged
from odoo.addons.sale_purchase.tests.common import TestCommonSalePurchaseNoChart
@tagged('post_install', '-at_install')
class TestLeadTime(TestCommonSalePurchaseNoChart):
@classmethod
def setUpClass(cls):
super(TestLeadTime, cls).setUpClass()
cls.buy_route = cls.env.ref('purchase_stock.route_warehouse0_buy')
cls.mto_route = cls.env.ref('stock.route_warehouse0_mto')
cls.mto_route.active = True
cls.vendor = cls.env['res.partner'].create({'name': 'The Emperor'})
cls.user_salesperson = cls.env['res.users'].with_context(no_reset_password=True).create({
'name': 'Le Grand Horus',
'login': 'grand.horus',
'email': 'grand.horus@chansonbelge.dz',
})
def test_supplier_lead_time(self):
""" Basic stock configuration and a supplier with a minimum qty and a lead time """
self.env.user.company_id.po_lead = 7
seller = self.env['product.supplierinfo'].create({
'name': self.vendor.id,
'min_qty': 1,
'price': 10,
'date_start': fields.Date.today() - timedelta(days=1),
})
product = self.env['product.product'].create({
'name': 'corpse starch',
'type': 'product',
'seller_ids': [(6, 0, seller.ids)],
'route_ids': [(6, 0, (self.mto_route + self.buy_route).ids)],
})
so = self.env['sale.order'].with_user(self.user_salesperson).create({
'partner_id': self.partner_a.id,
'user_id': self.user_salesperson.id,
})
self.env['sale.order.line'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
'tax_id': False,
'order_id': so.id,
})
so.action_confirm()
po = self.env['purchase.order'].search([('partner_id', '=', self.vendor.id)])
self.assertEqual(po.order_line.price_unit, seller.price)

View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase, Form
from freezegun import freeze_time
from datetime import datetime, timedelta
class TestSalePurchaseStockFlow(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestSalePurchaseStockFlow, cls).setUpClass()
cls.mto_route = cls.env.ref('stock.route_warehouse0_mto')
cls.buy_route = cls.env.ref('purchase_stock.route_warehouse0_buy')
cls.mto_route.active = True
cls.customer_location = cls.env.ref('stock.stock_location_customers')
cls.vendor = cls.env['res.partner'].create({'name': 'Super Vendor'})
cls.customer = cls.env['res.partner'].create({'name': 'Super Customer'})
cls.mto_product = cls.env['product.product'].create({
'name': 'SuperProduct',
'type': 'product',
'route_ids': [(6, 0, (cls.mto_route + cls.buy_route).ids)],
'seller_ids': [(0, 0, {
'partner_id': cls.vendor.id,
})],
})
def test_cancel_so_with_draft_po(self):
"""
Sell a MTO+Buy product -> a PO is generated
Cancel the SO -> an activity should be added to the PO
"""
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.env.user.partner_id
with so_form.order_line.new() as line:
line.product_id = self.mto_product
so = so_form.save()
so.action_confirm()
po = self.env['purchase.order'].search([('partner_id', '=', self.vendor.id)])
so._action_cancel()
self.assertTrue(po.activity_ids)
self.assertIn(so.name, po.activity_ids.note)
def test_qty_delivered_with_mto_and_done_quantity_change(self):
"""
MTO product P
Sell 10 x P. On the delivery, set the done quantity to 12, validate and
then set the done quantity to 10: the delivered qty of the SOL should
be 10
"""
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [(0, 0, {
'name': self.mto_product.name,
'product_id': self.mto_product.id,
'product_uom_qty': 10,
'product_uom': self.mto_product.uom_id.id,
'price_unit': 1,
})],
})
so.action_confirm()
delivery = so.picking_ids.filtered(lambda p: p.location_dest_id == self.customer_location)
sm = delivery.move_ids
sm.move_line_ids = [(5, 0, 0), (0, 0, {
'location_id': sm.location_id.id,
'location_dest_id': sm.location_dest_id.id,
'product_id': sm.product_id.id,
'quantity': 12,
'company_id': sm.company_id.id,
'product_uom_id': sm.product_uom.id,
'picking_id': delivery.id,
})]
delivery.button_validate()
self.assertEqual(delivery.state, 'done')
self.assertEqual(delivery.move_ids.move_line_ids.quantity, 12)
self.assertEqual(so.order_line.qty_delivered, 12)
sm.move_line_ids.quantity = 10
self.assertEqual(so.order_line.qty_delivered, 10)
@freeze_time('2024-01-01')
def test_reordering_with_visibility_days(self):
"""
If reordering rules' visibility is set bigger than
DAYS_FROM_TODAY_TO_ORDER (plus lead time). Then the
order should be included in the calculation of quantity to order.
Today Scheduled Delivery
(2024-01-01) (2024-02-01)
aka commitment_date
time
DAYS_FROM_TODAY_TO_ORDER
lead_time
"""
N_ORDERED_QTY = 666
DAYS_FROM_TODAY_TO_ORDER = 30
MONTH_FROM_TODAY = (datetime.today() + timedelta(days=DAYS_FROM_TODAY_TO_ORDER)).strftime('%Y-%m-%d')
# Setup: Create a product with vendor
partner = self.env['res.partner'].create({'name': 'Azure Interior'})
seller = self.env['product.supplierinfo'].create({
'partner_id': partner.id,
'price': 1.0,
})
product = self.env['product.product'].create({
'name': 'Dummy Product',
'type': 'product',
'seller_ids': [seller.id],
})
# Setup: Create sale order scheduled in the future
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'commitment_date': MONTH_FROM_TODAY,
'order_line': [(0, 0, {
'name': product.name,
'product_id': product.id,
'price_unit': 1,
'product_uom_qty': N_ORDERED_QTY,
})],
})
so.action_confirm() # so.state: 'draft' -> 'sale'
# Create Reordering rule and trigger recalculation
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = product
orderpoint_form.visibility_days = DAYS_FROM_TODAY_TO_ORDER
orderpoint = orderpoint_form.save()
self.assertEqual(orderpoint.qty_to_order, N_ORDERED_QTY, f"Order from {DAYS_FROM_TODAY_TO_ORDER} days from today NOT included into the qty_to_order calculation, despite having visibility days set {DAYS_FROM_TODAY_TO_ORDER}!")

View File

@ -0,0 +1,135 @@
from datetime import datetime, timedelta
from odoo import Command
from odoo.tests import common, Form, tagged
@tagged('post_install', '-at_install')
class TestWarnUnwantedReplenish(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.buy_route = cls.env.ref('purchase_stock.route_warehouse0_buy')
# Create a vendor (& suppliers) and a customer
cls.vendor = cls.env['res.partner'].create(dict(name='Vendor'))
cls.customer = cls.env['res.partner'].create(dict(name='Customer'))
cls.supplier_A = cls.env['product.supplierinfo'].create({
'partner_id' : cls.vendor.id,
'min_qty' : 0.0,
'price' : 10.0,
'delay' : 0
})
cls.supplier_B = cls.env['product.supplierinfo'].create({
'partner_id' : cls.vendor.id,
'min_qty' : 0.0,
'price' : 12.0,
'delay' : 0
})
# Create a "A" and a "B" Product :
# No Stock
# Partner/Customer Lead Time = 0
# Manual reordering 0 0
cls.product_A = cls.env['product.product'].create({
'name': 'Product A',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'purchase_method': 'purchase',
'invoice_policy': 'delivery',
'standard_price': 5.0,
'list_price': 10.0,
'seller_ids': [Command.link(cls.supplier_A.id)],
'route_ids': [Command.link(cls.buy_route.id)],
'sale_delay' : 0,
})
cls.product_B = cls.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'purchase_method': 'purchase',
'invoice_policy': 'delivery',
'standard_price': 6.0,
'list_price': 12.0,
'seller_ids': [Command.link(cls.supplier_B.id)],
'route_ids': [Command.link(cls.buy_route.id)],
'sale_delay': 0,
})
orderpoint_form = Form(cls.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = cls.product_A
orderpoint_form.product_min_qty = 0.0
orderpoint_form.product_max_qty = 0.0
cls.orderpoint_A = orderpoint_form.save()
cls.orderpoint_A.trigger = 'manual'
orderpoint_form = Form(cls.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = cls.product_B
orderpoint_form.product_min_qty = 0.0
orderpoint_form.product_max_qty = 0.0
cls.orderpoint_B = orderpoint_form.save()
cls.orderpoint_B.trigger = 'manual'
# Create Sales
# For A and for B
# Delivered today
# Confirm SO
cls.sale_order = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'order_line': [
Command.create({
'product_id': cls.product_A.id,
'product_uom_qty': 10,
}),
Command.create({
'product_id': cls.product_B.id,
'product_uom_qty': 10,
}),
],
})
cls.sale_order.action_confirm()
# Create PO for Product A
# Confirm PO with date planned : TODAY
# Incoming Picking : reschedule in one week
cls.po_A = cls.env['purchase.order'].create({
'partner_id': cls.vendor.id,
'order_line': [
Command.create({
'name': cls.product_A.name,
'product_id': cls.product_A.id,
'product_qty': 10.0,
'price_unit': 10.0,
'date_planned': datetime.today(),
})],
})
cls.po_A.button_confirm()
cls.picking_A = cls.po_A.picking_ids[0]
cls.picking_A.scheduled_date = (datetime.today() + timedelta(days=10))
def test_01_pre_updateA_post(self):
"""
TEST 1
Replenishment ->
Product A
unwanted_replenish SHALL be TRUE
Product B
unwanted_replenish SHALL be FALSE
Product A
Modify Visible Days past 1 Week -> unwanted_replenish SHALL be FALSE
"""
self.assertTrue(self.orderpoint_A.unwanted_replenish, 'Orderpoint A not set to unwanted_replenish')
self.assertFalse(self.orderpoint_B.unwanted_replenish, 'Orderpoint B is set to unwanted_replenish')
#Update Orderpoint A
self.orderpoint_A.visibility_days = 10
self.assertFalse(self.orderpoint_A.unwanted_replenish, 'Orderpoint A shall not be set to unwanted_replenish')