diff --git a/locale/ca_ES.po b/locale/ca_ES.po index 74ca64b..cf8ee13 100644 --- a/locale/ca_ES.po +++ b/locale/ca_ES.po @@ -2,6 +2,18 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" +msgctxt "error:production:" +msgid "The production \"%s\" has no incoming shipment." +msgstr "La producció \"%s\" no té l'albarà d'entrada." + +msgctxt "error:production:" +msgid "The production \"%s\" has no the incoming shipment \"%s\" as done." +msgstr "La producció \"%s\" no té l'albarà d'entrada \"%s\" en estat realitzat." + +msgctxt "error:production:" +msgid "The warehouse \"%s\" has no production location." +msgstr "El magatzem \"%s\" no té una ubicació de producció." + msgctxt "field:party.party,production_warehouse:" msgid "Production Warehouse" msgstr "Magatzem producció" diff --git a/locale/es_ES.po b/locale/es_ES.po index f377974..09313ff 100644 --- a/locale/es_ES.po +++ b/locale/es_ES.po @@ -2,6 +2,20 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" +msgctxt "error:production:" +msgid "The production \"%s\" has no incoming shipment." +msgstr "La producción \"%s\" no tiene el albarán de entrada." + +msgctxt "error:production:" +msgid "The production \"%s\" has no the incoming shipment \"%s\" as done." +msgstr "" +"La producción \"%s\" no tiene el albarán de entrada \"%s\" en estado " +"realizado." + +msgctxt "error:production:" +msgid "The warehouse \"%s\" has no production location." +msgstr "El almacén \"%s\" no tiene una ubicació de producción." + msgctxt "field:party.party,production_warehouse:" msgid "Production Warehouse" msgstr "Almacén producción" diff --git a/production.py b/production.py index 22e645a..0b68583 100644 --- a/production.py +++ b/production.py @@ -1,7 +1,7 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from trytond.pool import Pool, PoolMeta -from trytond.model import ModelView, Workflow, fields +from trytond.model import ModelView, fields from trytond.pyson import Eval, Bool __all__ = ['Party', 'PurchaseRequest', 'BOM', 'Production', 'Purchase'] @@ -42,6 +42,7 @@ class BOM: ('type', '=', 'service'), ]) +# TODO: Subcontract cost must be added to the cost of the production class Production: __name__ = 'production' @@ -76,6 +77,16 @@ class Production: 'icon': 'tryton-go-home', } }) + cls._error_messages.update({ + 'no_subcontract_warehouse': 'The party "%s" has no production ' + 'location.', + 'no_warehouse_production_location': 'The warehouse "%s" has ' + 'no production location.', + 'no_incoming_shipment': 'The production "%s" has no incoming ' + 'shipment.', + 'no_incoming_shipment_done': ('The production "%s" has no the ' + 'incoming shipment "%s" as done.'), + }) def get_supplier(self, name): return (self.purchase_request.party.id if self.purchase_request and @@ -107,18 +118,16 @@ class Production: production.save() def on_change_product(self): - res = super(Production, self).on_change_product() + super(Production, self).on_change_product() if self.bom: - res['subcontract_product'] = (self.bom.subcontract_product.id if + self.subcontract_product = (self.bom.subcontract_product.id if self.bom.subcontract_product else None) - return res def on_change_bom(self): - res = super(Production, self).on_change_bom() + super(Production, self).on_change_bom() if self.bom: - res['subcontract_product'] = (self.bom.subcontract_product.id if + self.subcontract_product = (self.bom.subcontract_product.id if self.bom.subcontract_product else None) - return res def _get_purchase_request(self): PurchaseRequest = Pool().get('purchase.request') @@ -139,13 +148,21 @@ class Production: for production in productions: if not (production.purchase_request and production.purchase_request.purchase and - production.purchase_request.purchase.state == 'processing'): + production.purchase_request.purchase.state in + ('processing', 'done')): continue if production.destination_warehouse: continue subcontract_warehouse = production._get_subcontract_warehouse() + if not subcontract_warehouse: + cls.raise_user_error('no_subcontract_warehouse', ( + production.purchase_request.party.rec_name, )) production.destination_warehouse = production.warehouse production.warehouse = subcontract_warehouse + if not production.warehouse.production_location: + cls.raise_user_error('no_warehouse_production_location', ( + production.warehouse.rec_name, )) + production.location = production.warehouse.production_location from_location = production.warehouse.storage_location to_location = production.destination_warehouse.storage_location @@ -210,10 +227,24 @@ class Production: # ShipmentIn where there is no direct linke between stock moves but are # calculated by product and quantities. See _sync_inventory_to_outgoing in # stock/shipment.py. + @classmethod + def _sync_outputs_to_shipment(cls, productions): + pass + + @classmethod + def run(cls, productions): + for p in productions: + if p.purchase_request: + if not p.incoming_shipment: + cls.raise_user_error('no_incoming_shipment', ( + p.code,)) + if not p.incoming_shipment.state == 'done': + cls.raise_user_error('no_incoming_shipment_done', ( + p.code, p.incoming_shipment.rec_name,)) + + super(Production, cls).run(productions) @classmethod - @ModelView.button - @Workflow.transition('done') def done(cls, productions): InternalShipment = Pool().get('stock.shipment.internal') super(Production, cls).done(productions) diff --git a/production.xml b/production.xml index b19a03a..023e055 100644 --- a/production.xml +++ b/production.xml @@ -1,33 +1,28 @@ - + party.party - form party_form production - form production_form production - tree production_list production.bom - form bom_form production.bom - tree bom_list diff --git a/tests/scenario_production.rst b/tests/scenario_production_subcontract.rst similarity index 61% rename from tests/scenario_production.rst rename to tests/scenario_production_subcontract.rst index 29a9055..fa45009 100644 --- a/tests/scenario_production.rst +++ b/tests/scenario_production_subcontract.rst @@ -12,6 +12,12 @@ Imports:: >>> from dateutil.relativedelta import relativedelta >>> from decimal import Decimal >>> from proteus import config, Model, Wizard + >>> from trytond.modules.company.tests.tools import create_company, \ + ... get_company + >>> from trytond.modules.account.tests.tools import create_fiscalyear, \ + ... create_chart, get_accounts, create_tax + >>> from trytond.modules.account_invoice.tests.tools import \ + ... set_fiscalyear_invoice_sequences >>> today = datetime.date.today() >>> yesterday = today - relativedelta(days=1) @@ -22,42 +28,51 @@ Create database:: Install production Module:: - >>> Module = Model.get('ir.module.module') + >>> Module = Model.get('ir.module') >>> modules = Module.find([('name', '=', 'production_subcontract')]) >>> Module.install([x.id for x in modules], config.context) - >>> Wizard('ir.module.module.install_upgrade').execute('upgrade') + >>> Wizard('ir.module.install_upgrade').execute('upgrade') Create company:: - >>> Currency = Model.get('currency.currency') - >>> CurrencyRate = Model.get('currency.currency.rate') - >>> Company = Model.get('company.company') - >>> Party = Model.get('party.party') - >>> company_config = Wizard('company.company.config') - >>> company_config.execute('company') - >>> company = company_config.form - >>> party = Party(name='Dunder Mifflin') - >>> party.save() - >>> company.party = party - >>> currencies = Currency.find([('code', '=', 'USD')]) - >>> if not currencies: - ... currency = Currency(name='Euro', symbol=u'$', code='USD', - ... rounding=Decimal('0.01'), mon_grouping='[3, 3, 0]', - ... mon_decimal_point=',') - ... currency.save() - ... CurrencyRate(date=today + relativedelta(month=1, day=1), - ... rate=Decimal('1.0'), currency=currency).save() - ... else: - ... currency, = currencies - >>> company.currency = currency - >>> company_config.execute('add') - >>> company, = Company.find() + >>> _ = create_company() + >>> company = get_company() Reload the context:: >>> User = Model.get('res.user') >>> config._context = User.get_preferences(True, config.context) +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) + >>> revenue = accounts['revenue'] + >>> expense = accounts['expense'] + >>> cash = accounts['cash'] + + >>> Journal = Model.get('account.journal') + >>> cash_journal, = Journal.find([('type', '=', 'cash')]) + >>> cash_journal.credit_account = cash + >>> cash_journal.debit_account = cash + >>> cash_journal.save() + +Create payment term:: + + >>> PaymentTerm = Model.get('account.invoice.payment_term') + >>> payment_term = PaymentTerm(name='Term') + >>> line = payment_term.lines.new(type='percent', ratio=Decimal('.5')) + >>> delta = line.relativedeltas.new(days=20) + >>> line = payment_term.lines.new(type='remainder') + >>> delta = line.relativedeltas.new(days=40) + >>> payment_term.save() + Create supplier warehouse:: >>> Location = Model.get('stock.location') @@ -68,10 +83,10 @@ Create supplier warehouse:: >>> supplier_output = Location(name='Supplier Output', type='storage') >>> supplier_output.save() >>> supplier_production = Location(name='Supplier Production', - ... type='storage') + ... type='production') >>> supplier_production.save() >>> supplier_warehouse = Location() - >>> supplier_warehouse.type = 'warhouse' + >>> supplier_warehouse.type = 'warehouse' >>> supplier_warehouse.name = 'Supplier Warehouse' >>> supplier_warehouse.storage_location = supplier_storage >>> supplier_warehouse.input_location = supplier_input @@ -84,6 +99,7 @@ Create supplier:: >>> Party = Model.get('party.party') >>> party = Party(name='Supplier') >>> party.production_warehouse = supplier_warehouse + >>> party.save() Create product:: @@ -135,6 +151,9 @@ Create Subcontract Product:: >>> stemplate.name = 'Subcontract' >>> stemplate.default_uom = unit >>> stemplate.type = 'service' + >>> stemplate.purchasable = True + >>> stemplate.account_expense = expense + >>> stemplate.account_revenue = revenue >>> stemplate.list_price = Decimal(0) >>> stemplate.cost_price = Decimal(100) >>> stemplate.save() @@ -168,10 +187,11 @@ Create Bill of Material:: Create an Inventory:: + >>> warehouse, = Location.find(['code', '=', 'WH']) >>> Inventory = Model.get('stock.inventory') >>> InventoryLine = Model.get('stock.inventory.line') >>> Location = Model.get('stock.location') - >>> storage = supplier_warehouse.storage_location + >>> storage = warehouse.storage_location >>> inventory = Inventory() >>> inventory.location = storage >>> inventory_line1 = InventoryLine() @@ -187,9 +207,30 @@ Create an Inventory:: >>> inventory.state u'done' +Create a Supplier Inventory:: + + >>> storage = supplier_warehouse.storage_location + >>> inventory = Inventory() + >>> inventory.location = storage + >>> inventory_line1 = InventoryLine() + >>> inventory.lines.append(inventory_line1) + >>> inventory_line1.product = component1 + >>> inventory_line1.quantity = 20 + >>> inventory_line2 = InventoryLine() + >>> inventory.lines.append(inventory_line2) + >>> inventory_line2.product = component2 + >>> inventory_line2.quantity = 6 + >>> inventory_line3 = InventoryLine() + >>> inventory.lines.append(inventory_line3) + >>> inventory_line3.product = product + >>> inventory_line3.quantity = 2 + >>> inventory.save() + >>> Inventory.confirm([inventory.id], config.context) + >>> inventory.state + u'done' + Make a production:: - >>> warehouse = Location.find(['code', '=', 'WH']) >>> Production = Model.get('production') >>> production = Production() >>> production.warehouse = warehouse @@ -202,7 +243,7 @@ Make a production:: >>> output.quantity == 2 True >>> production.cost - Decimal('25.0') + Decimal('25.0000') >>> production.save() >>> Production.wait([production.id], config.context) >>> production.state @@ -225,43 +266,73 @@ Make a production:: u'done' >>> output.effective_date == production.effective_date True - >>> config._context['locations'] = [storage.id] + >>> config._context['locations'] = [warehouse.id] >>> product.reload() >>> product.quantity == 2 True -Make a production with effective date yesterday:: +Make a subcontract production:: - >>> Production = Model.get('production') + >>> Purchase = Model.get('purchase.purchase') + >>> Internal = Model.get('stock.shipment.internal') >>> production = Production() - >>> production.effective_date = yesterday + >>> production.warehouse = warehouse >>> production.product = product >>> production.bom = bom >>> production.quantity = 2 - >>> production.subcontract_product == subcontract - >>> production.click('wait') - >>> production.click('create_purchase_request') - -Process purchase request:: - + >>> sorted([i.quantity for i in production.inputs]) == [10, 300] + True + >>> output, = production.outputs + >>> output.quantity == 2 + True + >>> production.cost + Decimal('25.0000') + >>> production.subcontract_product = subcontract + >>> production.save() + >>> Production.wait([production.id], config.context) + >>> production.reload() + >>> production.state + u'waiting' + >>> Production.create_purchase_request([production.id], config.context) + >>> production.reload() >>> purchase_request = production.purchase_request >>> create_purchase = Wizard('purchase.request.create_purchase', ... [purchase_request]) + >>> create_purchase.form.party = party + >>> create_purchase.execute('start') >>> purchase_request.reload() >>> purchase = purchase_request.purchase - >>> purchase.click('quotation') - >>> purchase.click('confirm') - >>> purchase.click('process') + >>> purchase.payment_term = payment_term + >>> purchase.save() + >>> Purchase.quote([purchase.id], config.context) + >>> purchase.reload() + >>> purchase.state + u'quotation' + >>> Purchase.confirm([purchase.id], config.context) + >>> purchase.reload() + >>> purchase.state + u'confirmed' + >>> Purchase.process([purchase.id], config.context) + >>> purchase.reload() + >>> purchase.state + u'done' >>> production.reload() - >>> production.warehouse = supplier_warehouse - >>> production.destination_warehouse = warehouse - >>> shipment = production.incoming_shipment - -Process production:: - + >>> production.incoming_shipment.id + 1 + >>> internal = production.incoming_shipment + >>> Internal.wait([internal.id], config.context) + >>> internal.reload() + >>> internal.state + u'waiting' + >>> Internal.assign_try([internal.id], config.context) + True + >>> Internal.done([internal.id], config.context) + >>> internal.reload() + >>> internal.state + u'done' >>> Production.assign_try([production.id], config.context) True - >>> production.click('run') + >>> Production.run([production.id], config.context) >>> production.reload() - >>> shipment.reload() - >>> shipment.state = 'reserved' + >>> production.state + u'running' diff --git a/tests/test_production_subcontract.py b/tests/test_production_subcontract.py index d97cacc..75565c5 100644 --- a/tests/test_production_subcontract.py +++ b/tests/test_production_subcontract.py @@ -1,32 +1,24 @@ -#!/usr/bin/env python -# The COPYRIGHT file at the top level of this repository contains the full -# copyright notices and license terms. +# 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 unittest import doctest import trytond.tests.test_tryton -from trytond.tests.test_tryton import test_view, test_depends +from trytond.tests.test_tryton import ModuleTestCase from trytond.tests.test_tryton import doctest_setup, doctest_teardown +from trytond.tests.test_tryton import doctest_checker -class TestCase(unittest.TestCase): - 'Test module' - - def setUp(self): - trytond.tests.test_tryton.install_module('production_subcontract') - - def test0005views(self): - 'Test views' - test_view('production_subcontract') - - def test0006depends(self): - 'Test depends' - test_depends() +class ProductionSubcontractTestCase(ModuleTestCase): + 'Test Production Subcontract module' + module = 'production_subcontract' def suite(): suite = trytond.tests.test_tryton.suite() - suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCase)) - suite.addTests(doctest.DocFileSuite('scenario_production.rst', + suite.addTests(unittest.TestLoader().loadTestsFromTestCase( + ProductionSubcontractTestCase)) + suite.addTests(doctest.DocFileSuite('scenario_production_subcontract.rst', setUp=doctest_setup, tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite diff --git a/tryton.cfg b/tryton.cfg index 3104336..dbf495c 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=4.1.0 +version=3.4.0 depends: production stock_supply