changeset c7e49e91e271 in modules/account_stock_landed_cost:default
details: 
https://hg.tryton.org/modules/account_stock_landed_cost?cmd=changeset;node=c7e49e91e271
description:
        Add stock move filter per category and product

        We allow to filter the stock moves updated by landed cost.

        issue9560
        review302071002
diffstat:

 CHANGELOG                                    |    1 +
 __init__.py                                  |    2 +
 account.py                                   |  118 +++++++++++++++++++++++++-
 exceptions.py                                |   11 ++
 message.xml                                  |   17 +++
 tests/scenario_account_stock_landed_cost.rst |   32 +++++--
 tryton.cfg                                   |    1 +
 view/landed_cost_form.xml                    |    4 +
 8 files changed, 171 insertions(+), 15 deletions(-)

diffs (354 lines):

diff -r c819aa53a503 -r c7e49e91e271 CHANGELOG
--- a/CHANGELOG Sun Oct 11 13:37:32 2020 +0200
+++ b/CHANGELOG Sun Oct 11 13:46:31 2020 +0200
@@ -1,3 +1,4 @@
+* Add stock move filter per category and product
 * Allow posted landed cost to be cancelled
 * Add landed cost relate from supplier shipment
 * Rename landed cost state from cancel to cancelled
diff -r c819aa53a503 -r c7e49e91e271 __init__.py
--- a/__init__.py       Sun Oct 11 13:37:32 2020 +0200
+++ b/__init__.py       Sun Oct 11 13:46:31 2020 +0200
@@ -15,6 +15,8 @@
         account.ConfigurationLandedCostSequence,
         account.LandedCost,
         account.LandedCost_Shipment,
+        account.LandedCost_ProductCategory,
+        account.LandedCost_Product,
         account.InvoiceLine,
         stock.Move,
         module='account_stock_landed_cost', type_='model')
diff -r c819aa53a503 -r c7e49e91e271 account.py
--- a/account.py        Sun Oct 11 13:37:32 2020 +0200
+++ b/account.py        Sun Oct 11 13:46:31 2020 +0200
@@ -3,7 +3,8 @@
 from decimal import Decimal, ROUND_DOWN, ROUND_HALF_EVEN
 from operator import itemgetter
 
-from trytond.model import ModelSQL, ModelView, Workflow, fields
+from trytond.i18n import gettext
+from trytond.model import ModelSQL, ModelView, Workflow, MatchMixin, fields
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval
 from trytond.transaction import Transaction
@@ -12,6 +13,15 @@
 from trytond.modules.company.model import CompanyValueMixin
 from trytond.modules.product import round_price
 
+from .exceptions import NoMoveWarning, FilterUnusedWarning
+
+
+def _parents(records):
+    for record in records:
+        while record:
+            yield record
+            record = record.parent
+
 
 class Configuration(metaclass=PoolMeta):
     __name__ = 'account.configuration'
@@ -69,7 +79,7 @@
             return None
 
 
-class LandedCost(Workflow, ModelSQL, ModelView):
+class LandedCost(Workflow, ModelSQL, ModelView, MatchMixin):
     'Landed Cost'
     __name__ = 'account.landed_cost'
     _rec_name = 'number'
@@ -111,6 +121,24 @@
             'readonly': Eval('state') != 'draft',
             },
         depends=['state'])
+
+    categories = fields.Many2Many(
+        'account.landed_cost-product.category', 'landed_cost', 'category',
+        "Categories",
+        states={
+            'readonly': Eval('state') != 'draft',
+            },
+        depends=['state'],
+        help="Apply only to products of these categories.")
+    products = fields.Many2Many(
+        'account.landed_cost-product.product', 'landed_cost', 'product',
+        "Products",
+        states={
+            'readonly': Eval('state') != 'draft',
+            },
+        depends=['state'],
+        help="Apply only to these products.")
+
     posted_date = fields.Date('Posted Date', readonly=True)
     state = fields.Selection([
             ('draft', 'Draft'),
@@ -209,6 +237,57 @@
                     line.invoice.currency, line.amount, currency, round=False)
         return cost
 
+    def stock_moves(self):
+        moves = []
+        for shipment in self.shipments:
+            for move in shipment.incoming_moves:
+                if move.state == 'cancelled':
+                    continue
+                if self._stock_move_filter(move):
+                    moves.append(move)
+        return moves
+
+    def _stock_move_filter(self, move):
+        if not self.categories and not self.products:
+            return True
+        result = False
+        if self.categories:
+            result |= bool(
+                set(self.categories)
+                & set(_parents(move.product.categories_all)))
+        if self.products:
+            result |= bool(move.product in self.products)
+        return result
+
+    def _stock_move_filter_unused(self, moves):
+        pool = Pool()
+        Warning = pool.get('res.user.warning')
+
+        categories = {
+            c for m in moves for c in _parents(m.product.categories_all)}
+        for category in self.categories:
+            if category not in categories:
+                key = '%s - %s' % (self, category)
+                if Warning.check(key):
+                    raise FilterUnusedWarning(
+                        key,
+                        gettext('account_stock_landed_cost'
+                            '.msg_landed_cost_unused_category',
+                            landed_cost=self.rec_name,
+                            category=category.rec_name))
+
+        products = {m.product for m in moves}
+        for product in self.products:
+            if product not in products:
+                key = '%s - %s' % (self, product)
+                if Warning.check(key):
+                    raise FilterUnusedWarning(
+                        key,
+                        gettext('account_stock_landed_cost'
+                            '.msg_landed_cost_unused_product',
+                            landed_cost=self.rec_name,
+                            product=product.rec_name))
+
     def allocate_cost_by_value(self):
         self._allocate_cost(self._get_value_factors())
 
@@ -221,8 +300,7 @@
         Currency = pool.get('currency.currency')
 
         currency = self.company.currency
-        moves = [m for s in self.shipments for m in s.incoming_moves
-            if m.state != 'cancelled']
+        moves = self.stock_moves()
 
         sum_value = 0
         unit_prices = {}
@@ -252,8 +330,7 @@
 
         cost = self.cost
         currency = self.company.currency
-        moves = [m for s in self.shipments for m in s.incoming_moves
-            if m.state != 'cancelled' and m.quantity]
+        moves = [m for m in self.stock_moves() if m.quantity]
 
         costs = []
         digit = Move.unit_price.digits[1]
@@ -300,8 +377,19 @@
     def post(cls, landed_costs):
         pool = Pool()
         Date = pool.get('ir.date')
+        Warning = pool.get('res.user.warning')
 
         for landed_cost in landed_costs:
+            stock_moves = landed_cost.stock_moves()
+            if not stock_moves:
+                key = '%s post no move' % landed_cost
+                if Warning.check(key):
+                    raise NoMoveWarning(
+                        key,
+                        gettext('account_stock_landed_cost'
+                            '.msg_landed_cost_post_no_stock_move',
+                            landed_cost=landed_cost.rec_name))
+            landed_cost._stock_move_filter_unused(stock_moves)
             getattr(landed_cost, 'allocate_cost_by_%s' %
                 landed_cost.allocation_method)()
         cls.write(landed_costs, {
@@ -332,6 +420,24 @@
         required=True)
 
 
+class LandedCost_ProductCategory(ModelSQL):
+    "Landed Cost - Product Category"
+    __name__ = 'account.landed_cost-product.category'
+    landed_cost = fields.Many2One(
+        'account.landed_cost', 'Landed Cost', required=True, select=True)
+    category = fields.Many2One(
+        'product.category', "Category", required=True)
+
+
+class LandedCost_Product(ModelSQL):
+    "Landed Cost - Product"
+    __name__ = 'account.landed_cost-product.product'
+    landed_cost = fields.Many2One(
+        'account.landed_cost', "Landed Cost", required=True, select=True)
+    product = fields.Many2One(
+        'product.product', "Product", required=True)
+
+
 class InvoiceLine(metaclass=PoolMeta):
     __name__ = 'account.invoice.line'
     landed_cost = fields.Many2One('account.landed_cost', 'Landed Cost',
diff -r c819aa53a503 -r c7e49e91e271 exceptions.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/exceptions.py     Sun Oct 11 13:46:31 2020 +0200
@@ -0,0 +1,11 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+from trytond.exceptions import UserWarning
+
+
+class NoMoveWarning(UserWarning):
+    pass
+
+
+class FilterUnusedWarning(UserWarning):
+    pass
diff -r c819aa53a503 -r c7e49e91e271 message.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/message.xml       Sun Oct 11 13:46:31 2020 +0200
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tryton>
+    <data grouped="1">
+        <record model="ir.message" id="msg_landed_cost_post_no_stock_move">
+            <field name="text">The posted landed cost "%(landed_cost)s" has no 
stock move.</field>
+        </record>
+        <record model="ir.message" id="msg_landed_cost_unused_category">
+            <field name="text">The category "%(category)s" on landed cost 
"%(landed_cost)s" is not used.</field>
+        </record>
+        <record model="ir.message" id="msg_landed_cost_unused_product">
+            <field name="text">The product "%(product)s" on landed cost 
"%(landed_cost)s" is not used.</field>
+        </record>
+    </data>
+</tryton>
+
diff -r c819aa53a503 -r c7e49e91e271 
tests/scenario_account_stock_landed_cost.rst
--- a/tests/scenario_account_stock_landed_cost.rst      Sun Oct 11 13:37:32 
2020 +0200
+++ b/tests/scenario_account_stock_landed_cost.rst      Sun Oct 11 13:46:31 
2020 +0200
@@ -55,11 +55,14 @@
     >>> account_category.account_expense = expense
     >>> account_category.account_revenue = revenue
     >>> account_category.save()
+    >>> category = ProductCategory(name="Category")
+    >>> category.save()
 
 Create products::
 
     >>> ProductUom = Model.get('product.uom')
     >>> ProductTemplate = Model.get('product.template')
+    >>> Product = Model.get('product.product')
     >>> unit, = ProductUom.find([('name', '=', 'Unit')])
     >>> template = ProductTemplate()
     >>> template.name = 'Product'
@@ -68,10 +71,13 @@
     >>> template.list_price = Decimal('300')
     >>> template.cost_price_method = 'average'
     >>> template.account_category = account_category
+    >>> template.categories.append(ProductCategory(category.id))
     >>> product, = template.products
     >>> product.cost_price = Decimal('80')
     >>> template.save()
     >>> product, = template.products
+    >>> template2, = template.duplicate(default={'categories': None})
+    >>> product2, = template2.products
 
     >>> template = ProductTemplate()
     >>> template.name = 'Landed Cost'
@@ -114,6 +120,13 @@
     >>> move.to_location = input_loc
     >>> move.unit_price = Decimal('100')
 
+    >>> move = shipment.incoming_moves.new()
+    >>> move.product = product2
+    >>> move.quantity = 1
+    >>> move.from_location = supplier_loc
+    >>> move.to_location = input_loc
+    >>> move.unit_price = Decimal('10')
+
     >>> move_empty = shipment.incoming_moves.new()
     >>> move_empty.product = product
     >>> move_empty.quantity = 0
@@ -122,9 +135,8 @@
     >>> move_empty.unit_price = Decimal('100')
 
     >>> shipment.click('receive')
-    >>> move, = [m for m in shipment.incoming_moves if m.quantity]
-    >>> move.unit_price
-    Decimal('100')
+    >>> sorted([m.unit_price for m in shipment.incoming_moves if m.quantity])
+    [Decimal('10'), Decimal('100')]
 
 Invoice landed cost::
 
@@ -149,6 +161,8 @@
     >>> invoice_line, = landed_cost.invoice_lines.find([])
     >>> landed_cost.invoice_lines.append(invoice_line)
     >>> landed_cost.allocation_method = 'value'
+    >>> landed_cost.categories.append(ProductCategory(category.id))
+    >>> landed_cost.products.append(Product(product.id))
     >>> landed_cost.save()
     >>> landed_cost.state
     'draft'
@@ -160,9 +174,9 @@
 
 Check move unit price is 101::
 
-    >>> move.reload()
-    >>> move.unit_price
-    Decimal('101.0000')
+    >>> shipment.reload()
+    >>> sorted([m.unit_price for m in shipment.incoming_moves if m.quantity])
+    [Decimal('10'), Decimal('101.0000')]
 
 Landed cost is cleared when duplicated invoice::
 
@@ -178,6 +192,6 @@
     'cancelled'
     >>> landed_cost.posted_date
 
-    >>> move.reload()
-    >>> move.unit_price
-    Decimal('100.0000')
+    >>> shipment.reload()
+    >>> sorted([m.unit_price for m in shipment.incoming_moves if m.quantity])
+    [Decimal('10'), Decimal('100.0000')]
diff -r c819aa53a503 -r c7e49e91e271 tryton.cfg
--- a/tryton.cfg        Sun Oct 11 13:37:32 2020 +0200
+++ b/tryton.cfg        Sun Oct 11 13:46:31 2020 +0200
@@ -12,3 +12,4 @@
 xml:
     account.xml
     product.xml
+    message.xml
diff -r c819aa53a503 -r c7e49e91e271 view/landed_cost_form.xml
--- a/view/landed_cost_form.xml Sun Oct 11 13:37:32 2020 +0200
+++ b/view/landed_cost_form.xml Sun Oct 11 13:46:31 2020 +0200
@@ -12,6 +12,10 @@
     <field name="company"/>
     <field name="invoice_lines" colspan="4"/>
     <field name="shipments" colspan="4"/>
+
+    <field name="categories" colspan="2"/>
+    <field name="products" colspan="2"/>
+
     <label name="state"/>
     <field name="state"/>
     <group col="-1" colspan="2" id="buttons">

Reply via email to