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">