367 lines
12 KiB
Diff
367 lines
12 KiB
Diff
--- a/trytond/trytond/modules/account_invoice_stock/__init__.py
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/__init__.py
|
|
@@ -8,6 +8,7 @@ from .stock import *
|
|
|
|
def register():
|
|
Pool.register(
|
|
+ Invoice,
|
|
InvoiceLineStockMove,
|
|
InvoiceLine,
|
|
StockMove,
|
|
--- a/trytond/trytond/modules/account_invoice_stock/account.py
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/account.py
|
|
@@ -1,13 +1,27 @@
|
|
# 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.model import ModelSQL, fields
|
|
+from trytond.model import ModelSQL, ModelView, Workflow, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval
|
|
|
|
-
|
|
-__all__ = ['InvoiceLineStockMove', 'InvoiceLine']
|
|
+__all__ = ['InvoiceLineStockMove', 'InvoiceLine', 'Invoice']
|
|
|
|
|
|
+class Invoice(metaclass=PoolMeta):
|
|
+ __name__ = 'account.invoice'
|
|
+
|
|
+ @classmethod
|
|
+ @ModelView.button
|
|
+ @Workflow.transition('posted')
|
|
+ def post(cls, invoices):
|
|
+ pool = Pool()
|
|
+ Move = pool.get('stock.move')
|
|
+ super().post(invoices)
|
|
+ moves = sum((l.stock_moves for i in invoices for l in i.lines), ())
|
|
+ if moves:
|
|
+ Move.__queue__.update_unit_price(moves)
|
|
+
|
|
+
|
|
class InvoiceLineStockMove(ModelSQL):
|
|
'Invoice Line - Stock Move'
|
|
__name__ = 'account.invoice.line-stock.move'
|
|
Index: /trytond/trytond/modules/account_invoice_stock/doc/index.rst
|
|
===================================================================
|
|
--- a/trytond/trytond/modules/account_invoice_stock/doc/index.rst
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/doc/index.rst
|
|
@@ -3,3 +3,5 @@
|
|
|
|
The account invoice stock module adds link between invoice lines and stock
|
|
moves.
|
|
+The unit price of the stock move is updated based on the average price of the
|
|
+posted invoice lines that are linked to it.
|
|
Index: /trytond/trytond/modules/account_invoice_stock/setup.py
|
|
===================================================================
|
|
--- a/trytond/trytond/modules/account_invoice_stock/setup.py
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/setup.py
|
|
@@ -57,6 +57,7 @@
|
|
requires.append(get_require_version('trytond_%s' % dep))
|
|
requires.append(get_require_version('trytond'))
|
|
|
|
+tests_require = [get_require_version('proteus')]
|
|
dependency_links = []
|
|
if minor_version % 2:
|
|
dependency_links.append('https://trydevpi.tryton.org/')
|
|
@@ -136,4 +137,5 @@
|
|
""",
|
|
test_suite='tests',
|
|
test_loader='trytond.test_loader:Loader',
|
|
+ tests_require=tests_require,
|
|
)
|
|
Index: /trytond/trytond/modules/account_invoice_stock/stock.py
|
|
===================================================================
|
|
--- a/trytond/trytond/modules/account_invoice_stock/stock.py
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/stock.py
|
|
@@ -1,8 +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.model import fields
|
|
+from decimal import Decimal
|
|
+from trytond.model import ModelView, Workflow, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.transaction import Transaction
|
|
+from trytond.pyson import Eval
|
|
+from trytond.modules.product import round_price
|
|
|
|
|
|
__all__ = ['StockMove', 'ShipmentOut']
|
|
@@ -10,8 +13,15 @@ __all__ = ['StockMove', 'ShipmentOut']
|
|
|
|
class StockMove(metaclass=PoolMeta):
|
|
__name__ = 'stock.move'
|
|
- invoice_lines = fields.Many2Many('account.invoice.line-stock.move',
|
|
- 'stock_move', 'invoice_line', 'Invoice Lines')
|
|
+ invoice_lines = fields.Many2Many(
|
|
+ 'account.invoice.line-stock.move', 'stock_move', 'invoice_line',
|
|
+ "Invoice Lines",
|
|
+ domain=[
|
|
+ ('product.default_uom_category',
|
|
+ '=', Eval('product_uom_category', -1)),
|
|
+ ('type', '=', 'line'),
|
|
+ ],
|
|
+ depends=['product_uom_category'])
|
|
|
|
@property
|
|
def invoiced_quantity(self):
|
|
@@ -34,6 +67,37 @@ class StockMove(metaclass=PoolMeta):
|
|
default.setdefault('invoice_lines', None)
|
|
return super(StockMove, cls).copy(moves, default=default)
|
|
|
|
+ @classmethod
|
|
+ @ModelView.button
|
|
+ @Workflow.transition('done')
|
|
+ def do(cls, moves):
|
|
+ super().do(moves)
|
|
+ cls.update_unit_price(moves)
|
|
+
|
|
+ @classmethod
|
|
+ def update_unit_price(cls, moves):
|
|
+ for move in moves:
|
|
+ if move.state == 'done':
|
|
+ unit_price = move._compute_unit_price()
|
|
+ if unit_price != move.unit_price:
|
|
+ move.unit_price = unit_price
|
|
+ cls.save(moves)
|
|
+
|
|
+ def _compute_unit_price(self):
|
|
+ pool = Pool()
|
|
+ UoM = pool.get('product.uom')
|
|
+ amount, quantity = 0, 0
|
|
+ for line in self.invoice_lines:
|
|
+ if line.invoice and line.invoice.state in {'posted', 'paid'}:
|
|
+ amount += line.amount
|
|
+ quantity += UoM.compute_qty(
|
|
+ line.unit, line.quantity, self.uom)
|
|
+ if not quantity:
|
|
+ unit_price = self.unit_price
|
|
+ else:
|
|
+ unit_price = round_price(amount / Decimal(str(quantity)))
|
|
+ return unit_price
|
|
+
|
|
|
|
class ShipmentOut(metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.out'
|
|
Index: /trytond/trytond/modules/account_invoice_stock/tests/scenario_account_invoice_stock.rst
|
|
===================================================================
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/tests/scenario_account_invoice_stock.rst
|
|
@@ -0,0 +1,140 @@
|
|
+========================
|
|
+Invoice - Stock Scenario
|
|
+========================
|
|
+
|
|
+Imports::
|
|
+
|
|
+ >>> from decimal import Decimal
|
|
+ >>> from proteus import Model, Wizard
|
|
+ >>> from trytond.tests.tools import activate_modules
|
|
+ >>> from trytond.modules.company.tests.tools import (
|
|
+ ... create_company, get_company)
|
|
+ >>> from trytond.modules.account.tests.tools import (
|
|
+ ... create_fiscalyear, create_chart, get_accounts)
|
|
+ >>> from trytond.modules.account_invoice.tests.tools import (
|
|
+ ... set_fiscalyear_invoice_sequences)
|
|
+
|
|
+Install account_invoice_stock::
|
|
+
|
|
+ >>> config = activate_modules('account_invoice_stock')
|
|
+
|
|
+Create company::
|
|
+
|
|
+ >>> _ = create_company()
|
|
+ >>> company = get_company()
|
|
+
|
|
+Create fiscal year::
|
|
+
|
|
+ >>> fiscalyear = set_fiscalyear_invoice_sequences(
|
|
+ ... create_fiscalyear(company))
|
|
+ >>> fiscalyear.click('create_period')
|
|
+
|
|
+Create chart of accounts::
|
|
+
|
|
+ >>> _ = create_chart(company)
|
|
+ >>> accounts = get_accounts(company)
|
|
+
|
|
+Create a party::
|
|
+
|
|
+ >>> Party = Model.get('party.party')
|
|
+ >>> party = Party(name="Party")
|
|
+ >>> party.save()
|
|
+
|
|
+Create an account category::
|
|
+
|
|
+ >>> ProductCategory = Model.get('product.category')
|
|
+ >>> account_category = ProductCategory(name="Account Category")
|
|
+ >>> account_category.accounting = True
|
|
+ >>> account_category.account_expense = accounts['expense']
|
|
+ >>> account_category.account_revenue = accounts['revenue']
|
|
+ >>> account_category.save()
|
|
+
|
|
+Create a product::
|
|
+
|
|
+ >>> ProductUom = Model.get('product.uom')
|
|
+ >>> unit, = ProductUom.find([('name', '=', 'Unit')])
|
|
+ >>> ProductTemplate = Model.get('product.template')
|
|
+ >>> template = ProductTemplate()
|
|
+ >>> template.name = 'product'
|
|
+ >>> template.default_uom = unit
|
|
+ >>> template.type = 'goods'
|
|
+ >>> template.list_price = Decimal('40')
|
|
+ >>> template.account_category = account_category
|
|
+ >>> template.save()
|
|
+ >>> product, = template.products
|
|
+
|
|
+Get stock locations::
|
|
+
|
|
+ >>> Location = Model.get('stock.location')
|
|
+ >>> output_loc, = Location.find([('code', '=', 'OUT')])
|
|
+ >>> customer_loc, = Location.find([('code', '=', 'CUS')])
|
|
+
|
|
+Create a shipment::
|
|
+
|
|
+ >>> Shipment = Model.get('stock.shipment.out')
|
|
+ >>> Move = Model.get('stock.move')
|
|
+ >>> shipment = Shipment()
|
|
+ >>> shipment.customer = party
|
|
+ >>> move = shipment.outgoing_moves.new()
|
|
+ >>> move.product = product
|
|
+ >>> move.quantity = 10
|
|
+ >>> move.from_location = output_loc
|
|
+ >>> move.to_location = customer_loc
|
|
+ >>> move.unit_price = Decimal('40.0000')
|
|
+ >>> shipment.click('wait')
|
|
+ >>> shipment.state
|
|
+ 'waiting'
|
|
+ >>> move, = shipment.outgoing_moves
|
|
+
|
|
+Create an invoice for half the quantity with higher price::
|
|
+
|
|
+ >>> Invoice = Model.get('account.invoice')
|
|
+ >>> invoice = Invoice(type='out')
|
|
+ >>> invoice.party = party
|
|
+ >>> line = invoice.lines.new()
|
|
+ >>> line.product = product
|
|
+ >>> line.quantity = 5
|
|
+ >>> line.unit_price = Decimal('50.0000')
|
|
+ >>> line.stock_moves.append(Move(move.id))
|
|
+ >>> invoice.click('post')
|
|
+ >>> invoice.state
|
|
+ 'posted'
|
|
+
|
|
+Check move unit price is not changed::
|
|
+
|
|
+ >>> move.reload()
|
|
+ >>> move.unit_price
|
|
+ Decimal('40.0000')
|
|
+
|
|
+Ship the products::
|
|
+
|
|
+ >>> shipment.click('assign_force')
|
|
+ >>> shipment.click('pack')
|
|
+ >>> shipment.click('done')
|
|
+ >>> shipment.state
|
|
+ 'done'
|
|
+
|
|
+Check move unit price has been updated::
|
|
+
|
|
+ >>> move.reload()
|
|
+ >>> move.unit_price
|
|
+ Decimal('50.0000')
|
|
+
|
|
+Create a second invoice for the remaining quantity cheaper::
|
|
+
|
|
+ >>> invoice = Invoice(type='out')
|
|
+ >>> invoice.party = party
|
|
+ >>> line = invoice.lines.new()
|
|
+ >>> line.product = product
|
|
+ >>> line.quantity = 5
|
|
+ >>> line.unit_price = Decimal('40.0000')
|
|
+ >>> line.stock_moves.append(Move(move.id))
|
|
+ >>> invoice.click('post')
|
|
+ >>> invoice.state
|
|
+ 'posted'
|
|
+
|
|
+Check move unit price has been updated again::
|
|
+
|
|
+ >>> move.reload()
|
|
+ >>> move.unit_price
|
|
+ Decimal('45.0000')
|
|
Index: /trytond/trytond/modules/account_invoice_stock/tests/test_account_invoice_stock.py
|
|
===================================================================
|
|
--- a/trytond/trytond/modules/account_invoice_stock/tests/test_account_invoice_stock.py
|
|
+++ b/trytond/trytond/modules/account_invoice_stock/tests/test_account_invoice_stock.py
|
|
@@ -1,8 +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.
|
|
+import doctest
|
|
import unittest
|
|
import trytond.tests.test_tryton
|
|
from trytond.tests.test_tryton import ModuleTestCase
|
|
+from trytond.tests.test_tryton import doctest_teardown
|
|
+from trytond.tests.test_tryton import doctest_checker
|
|
|
|
|
|
class AccountInvoiceStockTestCase(ModuleTestCase):
|
|
@@ -14,4 +17,9 @@
|
|
suite = trytond.tests.test_tryton.suite()
|
|
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
|
|
AccountInvoiceStockTestCase))
|
|
+ suite.addTests(doctest.DocFileSuite(
|
|
+ 'scenario_account_invoice_stock.rst',
|
|
+ tearDown=doctest_teardown, encoding='utf-8',
|
|
+ checker=doctest_checker,
|
|
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
|
return suite
|
|
Index: /trytond/trytond/modules/account_stock_landed_cost/stock.py
|
|
===================================================================
|
|
--- a/trytond/trytond/modules/account_stock_landed_cost/stock.py
|
|
+++ b/trytond/trytond/modules/account_stock_landed_cost/stock.py
|
|
@@ -9,3 +9,9 @@
|
|
__name__ = 'stock.move'
|
|
unit_landed_cost = fields.Numeric('Unit Landed Cost',
|
|
digits=price_digits, readonly=True)
|
|
+
|
|
+ def _compute_unit_price(self):
|
|
+ unit_price = super()._compute_unit_price()
|
|
+ if self.unit_landed_cost:
|
|
+ unit_price += self.unit_landed_cost
|
|
+ return unit_price
|
|
Index: /trytond/trytond/modules/account_stock_landed_cost/tryton.cfg
|
|
===================================================================
|
|
--- a/trytond/trytond/modules/account_stock_landed_cost/tryton.cfg
|
|
+++ b/trytond/trytond/modules/account_stock_landed_cost/tryton.cfg
|
|
@@ -7,6 +7,8 @@
|
|
product
|
|
res
|
|
stock
|
|
+extra_depends:
|
|
+ account_invoice_stock
|
|
xml:
|
|
account.xml
|
|
product.xml
|
|
--- a/trytond/trytond/modules/product/product.py
|
|
+++ b/trytond/trytond/modules/product/product.py
|
|
@@ -20,7 +20,7 @@ from trytond.modules.company.model import (
|
|
|
|
__all__ = ['Template', 'Product', 'price_digits', 'TemplateFunction',
|
|
'ProductListPrice', 'ProductCostPriceMethod', 'ProductCostPrice',
|
|
- 'TemplateCategory', 'TemplateCategoryAll']
|
|
+ 'TemplateCategory', 'TemplateCategoryAll', 'round_price']
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STATES = {
|
|
@@ -39,6 +39,11 @@ COST_PRICE_METHODS = [
|
|
|
|
price_digits = (16, config.getint('product', 'price_decimal', default=4))
|
|
|
|
+def round_price(value, rounding=None):
|
|
+ "Round price using the price digits"
|
|
+ return value.quantize(
|
|
+ Decimal(1) / 10 ** price_digits[1], rounding=rounding)
|
|
+
|
|
|
|
class Template(
|
|
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
|