diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e75023fb5..ff3f3f18ee 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -339,4 +339,25 @@ test-sao: PACKAGE: sao TAG_PATTERN: '/^sao-.*/' +test-demo: + stage: test + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:3.8 + variables: + TRYTOND_DATABASE__PATH: /tmp + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + paths: + - .cache/pip/http + before_script: + - pip install packages/trytond* packages/proteus* + script: + - touch /tmp/demo.sqlite + - echo "admin" > /tmp/password + - TRYTONPASSFILE=/tmp/password trytond-admin --database demo --all --email admin@example.com + - python -m demo --database demo + rules: + - changes: + - demo/**/* + - modules/{account,account_invoice,account_payment,account_statement,company,country,currency,party,product,production,production_routing,production_work,project,purchase,sale,stock,timesheet}/**/* + include: '.gitlab-ci-modules.yml' diff --git a/demo/__main__.py b/demo/__main__.py new file mode 100755 index 0000000000..7ba25b1abf --- /dev/null +++ b/demo/__main__.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# 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 argparse import ArgumentDefaultsHelpFormatter, ArgumentParser + +from proteus import Model, Wizard +from proteus import config as pconfig + +from . import account, account_invoice, account_payment, account_statement +from . import company as company_ +from . import ( + country, currency, party, product, production, project, purchase, sale, + stock, timesheet) + + +def set_config(database, config_file): + return pconfig.set_trytond(database, config_file=config_file) + + +def activate_modules(config, modules): + Module = Model.get('ir.module') + modules = Module.find([ + ('name', 'in', modules), + ]) + for module in modules: + if module.state == 'activated': + module.click('upgrade') + else: + module.click('activate') + modules = [x.name for x in Module.find([('state', '=', 'to activate')])] + Wizard('ir.module.activate_upgrade').execute('upgrade') + + ConfigWizardItem = Model.get('ir.module.config_wizard.item') + for item in ConfigWizardItem.find([('state', '!=', 'done')]): + item.state = 'done' + item.save() + + activated_modules = [m.name + for m in Module.find([('state', '=', 'activated')])] + return modules, activated_modules + + +def setup_languages(config, demo_password, company=None): + Lang = Model.get('ir.lang') + User = Model.get('res.user') + Group = Model.get('res.group') + Action = Model.get('ir.action') + + langs = Lang.find() + Lang.click(langs, 'load_translations') + + admin = config.user + # Use root to skip password validation + config.user = 0 + + menu, = Action.find([('usage', '=', 'menu')]) + for lang in langs: + if lang.code == 'en': + name = 'Demo' + login = 'demo' + else: + if lang.code[:2] != lang.code[-2:].lower(): + continue + name = 'Demo %s' % lang.name + login = 'demo_%s' % lang.code[:2] + try: + user, = User.find([('login', '=', login)]) + except ValueError: + user = User() + user.name = name + user.login = login + user.password = demo_password + groups = Group.find([ + ('name', 'not ilike', '%Admin%'), + ]) + user.groups.extend(groups) + user.language = lang + user.menu = menu + if company: + Company = Model.get('company.company') + user.companies.append(Company(company.id)) + user.company = company + user.save() + + config.user = admin + + +def main(database, modules, demo_password, config_file=None): + config = set_config(database, config_file) + to_activate, activated = activate_modules(config, modules) + + if 'country' in to_activate: + country.do_import() + + if 'currency' in to_activate: + currency.do_import() + + if ('party' in to_activate + or 'sale' in to_activate + or 'purchase' in to_activate + or 'stock' in activated): + customers, suppliers = party.setup(config, modules) + + if 'company' in to_activate: + company = company_.setup(config, activated) + elif 'company' in activated: + company = company.get() + else: + company = None + + if 'account' in to_activate: + account.setup(config, activated, company) + + if 'company' in to_activate: + company_.setup_post(config, activated, company) + + if 'product' in to_activate: + product.setup(config, activated, company=company) + + if 'account_invoice' in to_activate: + account_invoice.setup(config, activated, company) + + if 'sale' in to_activate: + sale.setup(config, activated, company, customers) + + if 'purchase' in to_activate: + purchase.setup(config, activated, company, suppliers) + + if 'stock' in to_activate: + stock.setup(config, activated, company, suppliers) + + if 'account_invoice' in activated: + account_invoice.setup_post(config, activated, company) + + if 'account_payment' in activated: + account_payment.setup(config, activated, company) + + if 'account_statement' in activated: + account_statement.setup(config, activated, company) + + if 'project' in activated: + project.setup(config, activated, company, customers) + + if 'timesheet' in activated: + timesheet.setup(config, activated, company) + + if 'production' in to_activate: + production.setup(config, activated, company) + + setup_languages(config, demo_password, company=company) + + +if __name__ == '__main__': + parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument('-c', '--config', dest='config_file') + parser.add_argument('-m', '--module', dest='modules', nargs='+', + help='module to activate', default=[ + 'account', + 'account_invoice', + 'account_payment', + 'account_statement', + 'company', + 'party', + 'party_avatar', + 'product', + 'purchase', + 'sale', + 'stock', + 'project', + 'timesheet', + 'production', + 'production_routing', + 'production_work', + ]) + parser.add_argument('--demo_password', dest='demo_password', + default='demo', help='demo password') + parser.add_argument('-d', '--database', dest='database', + default='demo', help="database name") + options = parser.parse_args() + main(options.database, options.modules, options.demo_password, + config_file=options.config_file) diff --git a/demo/account.py b/demo/account.py new file mode 100644 index 0000000000..0d82ae5a28 --- /dev/null +++ b/demo/account.py @@ -0,0 +1,82 @@ +# 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 datetime as dt + +from dateutil.relativedelta import relativedelta + +from proteus import Model, Wizard + + +def setup(config, modules, company): + AccountTemplate = Model.get('account.account.template') + Account = Model.get('account.account') + FiscalYear = Model.get('account.fiscalyear') + Sequence = Model.get('ir.sequence') + SequenceStrict = Model.get('ir.sequence.strict') + SequenceType = Model.get('ir.sequence.type') + Party = Model.get('party.party') + + root_template, = AccountTemplate.find([ + ('parent', '=', None), + ('name', '=', 'Minimal Account Chart'), + ]) + create_chart_account = Wizard('account.create_chart') + create_chart_account.execute('account') + create_chart_account.form.account_template = root_template + create_chart_account.form.company = company + create_chart_account.execute('create_account') + + receivable, = Account.find([ + ('type.receivable', '=', True), + ('company', '=', company.id), + ]) + payable, = Account.find([ + ('type.payable', '=', True), + ('company', '=', company.id), + ]) + + create_chart_account.form.account_receivable = receivable + create_chart_account.form.account_payable = payable + create_chart_account.execute('create_properties') + + # Set account for parties created without company + parties = Party.find([]) + for party in parties: + party.account_receivable = receivable + party.account_payable = payable + Party.save(parties) + + move_sequence_type, = SequenceType.find( + [('name', '=', "Account Move")], limit=1) + invoice_sequence_type, = SequenceType.find([ + ('name', '=', "Invoice"), + ], limit=1) + today = dt.date.today() + for start_date in ( + today + relativedelta(month=1, day=1, years=-1), + today + relativedelta(month=1, day=1), + today + relativedelta(month=1, day=1, years=1)): + fiscalyear = FiscalYear(name='%s' % start_date.year) + fiscalyear.start_date = start_date + fiscalyear.end_date = start_date + relativedelta(month=12, day=31) + fiscalyear.company = company + post_move_sequence = Sequence(name='%s' % start_date.year, + sequence_type=move_sequence_type, + company=company) + post_move_sequence.save() + fiscalyear.post_move_sequence = post_move_sequence + invoice_sequence, = fiscalyear.invoice_sequences + if 'account_invoice' in modules: + for attr, name in (('out_invoice_sequence', 'Invoice'), + ('in_invoice_sequence', 'Supplier Invoice'), + ('out_credit_note_sequence', 'Credit Note'), + ('in_credit_note_sequence', 'Supplier Credit Note')): + sequence = SequenceStrict( + name='%s %s' % (name, start_date.year), + sequence_type=invoice_sequence_type, + company=company) + sequence.save() + setattr(invoice_sequence, attr, sequence) + fiscalyear.save() + fiscalyear.click('create_period') diff --git a/demo/account_invoice.py b/demo/account_invoice.py new file mode 100644 index 0000000000..24dce401fc --- /dev/null +++ b/demo/account_invoice.py @@ -0,0 +1,61 @@ +# 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 datetime as dt +import random +from itertools import chain + +from dateutil.relativedelta import relativedelta + +from proteus import Model + +from .tools import skip_warning + + +def setup(config, modules, company): + Party = Model.get('party.party') + PaymentTerm = Model.get('account.invoice.payment_term') + + payment_term = PaymentTerm(name='30 days') + line = payment_term.lines.new() + line.type = 'remainder' + delta = line.relativedeltas.new() + delta.days = 30 + payment_term.save() + + parties = Party.find() + Party.write([p.id for p in parties], { + 'customer_payment_term': payment_term.id, + 'supplier_payment_term': payment_term.id, + }, config.context) + + +def setup_post(config, modules, company): + Invoice = Model.get('account.invoice') + + invoices = Invoice.find([ + ('type', '=', 'out'), + ('state', 'in', ['draft', 'validated']), + ]) + invoices = random.sample(invoices, len(invoices) * 2 // 3) + invoices = list(chain(*list(zip(invoices, + Invoice.find([ + ('type', '=', 'in'), + ('state', 'in', ['draft', 'validated']), + ]))))) + + today = dt.date.today() + invoice_date = today + relativedelta(months=-1) + i = j = 0 + while invoice_date <= today: + j = random.randint(1, 5) + for invoice in invoices[i:i + j]: + invoice.invoice_date = invoice_date + invoice.save() + i += j + invoice_date += relativedelta(days=random.randint(1, 3)) + invoices = [inv for inv in invoices[0:i]] + for invoice in invoices: + skip_warning(config, 'invoice_payment_term', [invoice]) + skip_warning(config, 'invoice_date_future', invoices) + Invoice.click(invoices, 'post') diff --git a/demo/account_payment.py b/demo/account_payment.py new file mode 100644 index 0000000000..61f809b13f --- /dev/null +++ b/demo/account_payment.py @@ -0,0 +1,46 @@ +# 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 random + +from proteus import Model, Wizard + + +def setup(config, modules, company): + Currency = Model.get('currency.currency') + Journal = Model.get('account.payment.journal') + Line = Model.get('account.move.line') + Payment = Model.get('account.payment') + + usd, = Currency.find([('code', '=', 'USD')]) + journal = Journal(name='Manual', currency=usd, company=company, + process_method='manual') + journal.save() + + lines = Line.find([ + ('account.type.payable', '=', True), + ('party', '!=', None), + ('reconciliation', '=', None), + ('payment_amount', '!=', 0), + ]) + lines = random.sample(lines, len(lines) * 2 // 3) + if not lines: + return + + pay_line = Wizard('account.move.line.pay', lines) + pay_line.execute('next_') + pay_line.form.journal = journal + pay_line.execute('next_') + + payments = Payment.find([]) + payments = random.sample(payments, len(payments) * 2 // 3) + + for payment in payments: + payment.click('approve') + + payments = random.sample(payments, len(payments) * 2 // 3) + i = j = 0 + while i < len(payments): + j = random.randint(1, 5) + Wizard('account.payment.process', payments[i:i + j]) + i += j diff --git a/demo/account_statement.py b/demo/account_statement.py new file mode 100644 index 0000000000..414d824eed --- /dev/null +++ b/demo/account_statement.py @@ -0,0 +1,69 @@ +# 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 random +from decimal import Decimal + +from dateutil.relativedelta import relativedelta + +from proteus import Model + + +def setup(config, modules, company): + Journal = Model.get('account.statement.journal') + Statement = Model.get('account.statement') + AccountJournal = Model.get('account.journal') + Account = Model.get('account.account') + Sequence = Model.get('ir.sequence') + SequenceType = Model.get('ir.sequence.type') + Invoice = Model.get('account.invoice') + + sequence_type, = SequenceType.find([('name', '=', "Account Journal")]) + sequence = Sequence(name='Statement', + sequence_type=sequence_type, + company=company) + sequence.save() + + cash, = Account.find([ + ('name', '=', 'Main Cash'), + ('company', '=', company.id), + ]) + + account_journal = AccountJournal(name='Bank', + type='statement', + sequence=sequence) + account_journal.save() + + journal = Journal(name='Bank', + journal=account_journal, + account=cash, + validation='balance') + journal.save() + + invoices = Invoice.find([ + ('state', '=', 'posted'), + ]) + invoices = random.sample(invoices, len(invoices) * 2 // 3) + + total_amount = Decimal(0) + statement = Statement(name='001', + journal=journal, + start_balance=Decimal(0)) + for i, invoice in enumerate(invoices): + if not invoice.amount_to_pay: + continue + line = statement.lines.new() + line.number = str(i) + line.date = invoice.invoice_date + relativedelta( + days=random.randint(1, 20)) + amount = invoice.amount_to_pay + if invoice.type in ('in_invoice', 'out_credit_note'): + amount = - amount + line.amount = amount + line.party = invoice.party + if random.random() < 2. / 3.: + line.related_to = invoice + total_amount += line.amount + + statement.end_balance = total_amount + statement.click('validate_statement') diff --git a/demo/avatar.py b/demo/avatar.py new file mode 100644 index 0000000000..45dba7b8e2 --- /dev/null +++ b/demo/avatar.py @@ -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. + +import os + +_AVATAR_DIR = os.path.join(os.path.dirname(__file__), 'avatars') + + +def get(name): + with open(os.path.join(_AVATAR_DIR, name), 'rb') as fp: + return fp.read() diff --git a/demo/avatars/angela-martin.jpg b/demo/avatars/angela-martin.jpg new file mode 100644 index 0000000000..b21c8aab23 Binary files /dev/null and b/demo/avatars/angela-martin.jpg differ diff --git a/demo/avatars/charles-miner.jpg b/demo/avatars/charles-miner.jpg new file mode 100644 index 0000000000..677a1ee561 Binary files /dev/null and b/demo/avatars/charles-miner.jpg differ diff --git a/demo/avatars/dunder-mifflin.jpg b/demo/avatars/dunder-mifflin.jpg new file mode 100644 index 0000000000..e3582e9380 Binary files /dev/null and b/demo/avatars/dunder-mifflin.jpg differ diff --git a/demo/avatars/dwight-schrute.jpg b/demo/avatars/dwight-schrute.jpg new file mode 100644 index 0000000000..81533e8f56 Binary files /dev/null and b/demo/avatars/dwight-schrute.jpg differ diff --git a/demo/avatars/jim-halper.jpg b/demo/avatars/jim-halper.jpg new file mode 100644 index 0000000000..95aab4e036 Binary files /dev/null and b/demo/avatars/jim-halper.jpg differ diff --git a/demo/avatars/michael-scott-paper-inc.jpg b/demo/avatars/michael-scott-paper-inc.jpg new file mode 100644 index 0000000000..42dff6d1a7 Binary files /dev/null and b/demo/avatars/michael-scott-paper-inc.jpg differ diff --git a/demo/avatars/michael-scott.jpg b/demo/avatars/michael-scott.jpg new file mode 100644 index 0000000000..b7934828dd Binary files /dev/null and b/demo/avatars/michael-scott.jpg differ diff --git a/demo/avatars/pam-beesly.jpg b/demo/avatars/pam-beesly.jpg new file mode 100644 index 0000000000..7c4fc4e82f Binary files /dev/null and b/demo/avatars/pam-beesly.jpg differ diff --git a/demo/avatars/ryan-howard.jpg b/demo/avatars/ryan-howard.jpg new file mode 100644 index 0000000000..0b3d3354d0 Binary files /dev/null and b/demo/avatars/ryan-howard.jpg differ diff --git a/demo/avatars/saber.jpg b/demo/avatars/saber.jpg new file mode 100644 index 0000000000..8da84cebfd Binary files /dev/null and b/demo/avatars/saber.jpg differ diff --git a/demo/company.py b/demo/company.py new file mode 100644 index 0000000000..3f03cf4664 --- /dev/null +++ b/demo/company.py @@ -0,0 +1,134 @@ +# 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 datetime as dt +from decimal import Decimal + +from proteus import Model, Wizard + +from . import avatar + + +def setup(config, modules): + Party = Model.get('party.party') + Company = Model.get('company.company') + Currency = Model.get('currency.currency') + + usd, = Currency.find([('code', '=', 'USD')]) + rate = usd.rates.new() + rate.date = dt.date(dt.date.today().year, 1, 1) + rate.rate = Decimal('1') + usd.save() + + company_config = Wizard('company.company.config') + company_config.execute('company') + company = company_config.form + party = Party(name='Michael Scott Paper Company') + if 'party_avatar' in modules: + party.avatar = avatar.get('michael-scott-paper-inc.jpg') + party.save() + company.party = party + company.currency = usd + company_config.execute('add') + + # Reload context + User = Model.get('res.user') + config._context = User.get_preferences(True, {}) + + company, = Company.find() + return company + + +def setup_post(config, modules, company): + Party = Model.get('party.party') + Company = Model.get('company.company') + Currency = Model.get('currency.currency') + Address = Model.get('party.address') + Country = Model.get('country.country') + Subdivision = Model.get('country.subdivision') + Employee = Model.get('company.employee') + + party_avatar = 'party_avatar' in modules + + usd, = Currency.find([('code', '=', 'USD')]) + try: + us, = Country.find([('code', '=', 'US')]) + except ValueError: + us = None + + party_scott = Party(name='Michael Scott') + if party_avatar: + party_scott.avatar = avatar.get('michael-scott.jpg') + party_scott.save() + Employee(party=party_scott, company=company).save() + party_beesly = Party(name='Pam Beesly') + if party_avatar: + party_beesly.avatar = avatar.get('pam-beesly.jpg') + party_beesly.save() + Employee(party=party_beesly, company=company).save() + party_howard = Party(name='Ryan Howard') + if party_avatar: + party_howard.avatar = avatar.get('ryan-howard.jpg') + party_howard.save() + Employee(party=party_howard, company=company).save() + + dmi = Company() + party_dmi = Party(name='Dunder Mifflin inc') + address = Address() + party_dmi.addresses.append(address) + address.city = 'New York' + address.country = us + try: + address.subdivision, = Subdivision.find([('code', '=', 'US-NY')]) + except ValueError: + pass + if party_avatar: + party_dmi.avatar = avatar.get('dunder-mifflin.jpg') + party_dmi.save() + dmi.party = party_dmi + dmi.currency = usd + dmi.save() + + dms = Company() + party_dms = Party(name='Dunder Mifflin Scranton') + address = Address() + party_dms.addresses.append(address) + address.city = 'Scranton' + address.country = us + try: + address.subdivision, = Subdivision.find([('code', '=', 'US-PA')]) + except ValueError: + pass + party_dms.save() + dms.party = party_dms + dms.currency = usd + dms.save() + + party_halper = Party(name='Jim Halper') + if party_avatar: + party_halper.avatar = avatar.get('jim-halper.jpg') + party_halper.save() + Employee(party=party_halper, company=dms).save() + party_schrute = Party(name='Dwight Schrute') + if party_avatar: + party_schrute.avatar = avatar.get('dwight-schrute.jpg') + party_schrute.save() + Employee(party=party_schrute, company=dms).save() + party_martin = Party(name='Angela Martin') + if party_avatar: + party_martin.avatar = avatar.get('angela-martin.jpg') + party_martin.save() + Employee(party=party_martin, company=dms).save() + party_miner = Party(name='Charles Miner') + if party_avatar: + party_miner.avatar = avatar.get('charles-miner.jpg') + party_miner.save() + Employee(party=party_miner, company=dms).save() + + +def get(): + Company = Model.get('company.company') + company, = Company.find([ + ('party.name', '=', 'Michael Scott Paper Company'), + ]) + return company diff --git a/demo/country.py b/demo/country.py new file mode 100644 index 0000000000..035cfad36f --- /dev/null +++ b/demo/country.py @@ -0,0 +1,10 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +try: + from trytond.modules.country.scripts.import_countries import do_import +except ImportError: + def do_import(*args, **kwargs): + pass + +__all__ = [do_import] diff --git a/demo/currency.py b/demo/currency.py new file mode 100644 index 0000000000..e7045543e1 --- /dev/null +++ b/demo/currency.py @@ -0,0 +1,14 @@ +# 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 proteus import Model + +try: + from trytond.modules.currency.scripts.import_currencies import do_import +except ImportError: + def do_import(*args, **kwargs): + Currency = Model.get('currency.currency') + usd = Currency(name="USD", code='USD') + usd.save() + +__all__ = [do_import] diff --git a/demo/party.py b/demo/party.py new file mode 100644 index 0000000000..6e8fe96ece --- /dev/null +++ b/demo/party.py @@ -0,0 +1,83 @@ +# 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 proteus import Model + +from . import avatar + + +def setup(config, modules): + Party = Model.get('party.party') + Address = Model.get('party.address') + Country = Model.get('country.country') + Subdivision = Model.get('country.subdivision') + ContactMechanism = Model.get('party.contact_mechanism') + + customers, suppliers = [], [] + + try: + us, = Country.find([('code', '=', 'US')]) + except ValueError: + us = None + try: + pennsylvania, = Subdivision.find([('code', '=', 'US-PA')]) + except ValueError: + pennsylvania = None + + name = 'Electric City Trolley Museum Association' + try: + party, = Party.find([('name', '=', name)]) + except ValueError: + party = Party(name=name) + party.addresses.pop() + party.addresses.append(Address(street='300 Cliff Street', + postal_code='18503', city='Scranton', country=us, + subdivision=pennsylvania)) + party.contact_mechanisms.append(ContactMechanism(type='phone', + value='+1 570-963-6590')) + party.contact_mechanisms.append(ContactMechanism(type='website', + value='http://www.ectma.org/')) + party.save() + customers.append(party) + + name = 'Albright Memorial Library' + try: + party, = Party.find([('name', '=', name)]) + except ValueError: + party = Party(name=name) + party.addresses.pop() + party.addresses.append(Address(street='500 Vine Street', + postal_code='18509', city='Scranton', country=us, + subdivision=pennsylvania)) + party.contact_mechanisms.append(ContactMechanism(type='phone', + value='+1 570-348-3000')) + party.contact_mechanisms.append(ContactMechanism(type='website', + value='http://www.albright.org/')) + party.save() + customers.append(party) + + name = "Cooper's Seafood House" + try: + party, = Party.find([('name', '=', name)]) + except ValueError: + party = Party(name=name) + party.addresses.pop() + party.addresses.append(Address(street='701 North Washington Avenue', + postal_code='18509', city='Scranton', country=us, + subdivision=pennsylvania)) + party.contact_mechanisms.append(ContactMechanism(type='phone', + value='+1 570-346-6883')) + party.save() + customers.append(party) + + name = "Saber" + try: + party, = Party.find([('name', '=', name)]) + except ValueError: + party = Party(name=name) + if 'party_avatar' in modules: + party.avatar = avatar.get('saber.jpg') + party.save() + suppliers.append(party) + + return customers, suppliers diff --git a/demo/product.py b/demo/product.py new file mode 100644 index 0000000000..e841f3ef2c --- /dev/null +++ b/demo/product.py @@ -0,0 +1,62 @@ +# 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 decimal import Decimal + +from proteus import Model + + +def setup(config, modules, company=None): + ProductTemplate = Model.get('product.template') + Category = Model.get('product.category') + Uom = Model.get('product.uom') + + if 'account_product' in modules: + Account = Model.get('account.account') + expense, = Account.find([ + ('type.expense', '=', True), + ('company', '=', company.id), + ]) + revenue, = Account.find([ + ('type.revenue', '=', True), + ('company', '=', company.id), + ]) + account_category = Category(name="Papers", accounting=True) + account_category.account_expense = expense + account_category.account_revenue = revenue + account_category.save() + + papers = Category(name='Papers') + papers.save() + + sizes = {} + for format in ['A5', 'A4', 'A3', 'Letter', 'Legal', 'Ledger']: + size = Category(name=format, parent=papers) + if 'account_product' in modules: + size.account_expense = expense + size.account_revenue = revenue + size.save() + sizes[format] = size + + unit, = Uom.find([('name', '=', 'Unit')]) + + margin = Decimal('1.01') + for quantity in [250, 500]: + for format, category in sizes.items(): + paper_template = ProductTemplate(name='%s Paper %s' + % (format, quantity)) + paper_template.categories.append(Category(category.id)) + if 'account_product' in modules: + paper_template.account_category = account_category + paper_template.default_uom = unit + paper_template.type = 'goods' + paper_template.list_price = (Decimal('0.02') * quantity * margin + ).quantize(Decimal('0.0001')) + paper, = paper_template.products + paper.cost_price = Decimal('0.01') * quantity + if 'sale' in modules: + paper_template.salable = True + if 'purchase' in modules: + paper_template.purchasable = True + paper_template.save() + margin *= margin diff --git a/demo/production.py b/demo/production.py new file mode 100644 index 0000000000..14c96eddac --- /dev/null +++ b/demo/production.py @@ -0,0 +1,216 @@ +# 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 datetime as dt +import random +from decimal import Decimal + +from dateutil.relativedelta import relativedelta + +from proteus import Model + + +def setup(config, activated, company): + BOM = Model.get('production.bom') + Production = Model.get('production') + ProductTemplate = Model.get('product.template') + Category = Model.get('product.category') + Uom = Model.get('product.uom') + + unit, = Uom.find([('name', '=', 'Unit')]) + + if 'account_product' in activated: + Account = Model.get('account.account') + expense, = Account.find([ + ('type.expense', '=', True), + ('company', '=', company.id), + ]) + revenue, = Account.find([ + ('type.revenue', '=', True), + ('company', '=', company.id), + ]) + account_category = Category(name="Computers", accounting=True) + account_category.account_expense = expense + account_category.account_revenue = revenue + account_category.save() + + def create_product(name, list_price, cost_price): + template = ProductTemplate(name=name) + template.default_uom = unit + template.type = 'goods' + template.list_price = list_price + template.cost_price = cost_price + if 'account_product' in activated: + template.account_category = account_category + template.save() + return template.products[0] + + bom = BOM(name='Computer rev1') + + input_ = bom.inputs.new() + tower = create_product('Tower', Decimal(400), Decimal(250)) + input_.product = tower + input_.quantity = 1 + + input_ = bom.inputs.new() + input_.product = create_product('Keyboard', Decimal(30), Decimal(10)) + input_.quantity = 1 + + input_ = bom.inputs.new() + input_.product = create_product('Mouse', Decimal(10), Decimal(5)) + input_.quantity = 1 + + input_ = bom.inputs.new() + input_.product = create_product('Screen', Decimal(300), Decimal(200)) + input_.quantity = 1 + + output = bom.outputs.new() + computer = create_product('Computer', Decimal(750), Decimal(465)) + computer.template.producible = True + computer.template.save() + output.product = computer + output.quantity = 1 + + bom.save() + computer.boms.new(bom=bom) + computer.save() + + if 'production_routing' in activated: + setup_routing(config, activated, company) + + if 'production_work' in activated: + WorkCenter = Model.get('production.work.center') + WorkCycle = Model.get('production.work.cycle') + setup_production_work(config, activated, company) + work_centers = WorkCenter.find([('parent', '=', None)]) + + today = dt.date.today() + production_date = today + relativedelta(months=-1) + while production_date <= today + relativedelta(days=20): + for _ in range(random.randint(0, 3)): + production = Production() + production.effective_date = production_date + production.product = computer + production.quantity = random.randint(1, 40) + production.bom = computer.boms[0].bom + + if 'production_routing' in activated: + production.routing = computer.boms[0].routing + + if 'production_work' in activated: + production.work_center = random.choice(work_centers) + + production.save() + + if (production_date < today) or (random.random() <= 1. / 3.): + production.click('wait') + if production_date < today: + production.click('assign_force') + production.click('run') + if random.random() <= 2. / 3.: + if 'production_work' in activated: + for work in production.works: + for _ in range(0, random.randint(1, 2)): + cycle = WorkCycle( + work=work, + duration=dt.timedelta( + seconds=random.randint(60, 3600)), + ) + cycle.save() + cycle.click('run') + cycle.click('do') + output, = production.outputs + output.unit_price = (production.cost + / Decimal(production.quantity) + ).quantize(Decimal('0.0001')) + production.click('do') + production_date += relativedelta(days=random.randint(1, 3)) + + +def setup_routing(config, activated, company): + Routing = Model.get('production.routing') + Operation = Model.get('production.routing.operation') + Product = Model.get('product.product') + + routing = Routing(name='Computer routing rev1') + + operation1 = Operation(name='Assemble pieces') + operation1.save() + step1 = routing.steps.new() + step1.operation = operation1 + + operation2 = Operation(name='Install software') + operation2.save() + step2 = routing.steps.new() + step2.operation = operation2 + + operation3 = Operation(name='Test') + operation3.save() + step3 = routing.steps.new() + step3.operation = operation3 + + operation4 = Operation(name='Package') + operation4.save() + step4 = routing.steps.new() + step4.operation = operation4 + + routing.boms.extend(routing.boms.find([('name', '=', 'Computer rev1')])) + + routing.save() + + computer, = Product.find([('name', '=', 'Computer')]) + bom, = computer.boms + bom.routing = routing + bom.save() + + +def setup_production_work(config, activated, company): + WorkCenterCategory = Model.get('production.work.center.category') + WorkCenter = Model.get('production.work.center') + Operation = Model.get('production.routing.operation') + + assembly = WorkCenterCategory(name='Assembly') + assembly.save() + operation, = Operation.find([('name', '=', 'Assemble pieces')]) + operation.work_center_category = assembly + operation.save() + + installation = WorkCenterCategory(name='Installation') + installation.save() + operations = Operation.find([ + ('name', 'in', ['Install software', 'Test']), + ]) + for operation in operations: + operation.work_center_category = installation + Operation.save(operations) + + packaging = WorkCenterCategory(name='Packaging') + packaging.save() + operation, = Operation.find([('name', '=', 'Package')]) + operation.work_center_category = packaging + operation.save() + + lines = [] + for i in range(1, 4): + line = WorkCenter(name='Line %i' % i) + + assembly_line = line.children.new() + assembly_line.name = 'Assembly Line %i' % i + assembly_line.category = assembly + assembly_line.cost_method = 'cycle' + assembly_line.cost_price = Decimal(20) + + installation_line = line.children.new() + installation_line.name = 'Installation Line %i' % i + installation_line.category = installation + installation_line.cost_method = 'hour' + installation_line.cost_price = Decimal(15) + + packaging_line = line.children.new() + packaging_line.name = 'Packaging Line %i' % i + packaging_line.category = packaging + packaging_line.cost_method = 'hour' + packaging_line.cost_price = Decimal(10) + + lines.append(line) + WorkCenter.save(lines) diff --git a/demo/project.py b/demo/project.py new file mode 100644 index 0000000000..3915f46420 --- /dev/null +++ b/demo/project.py @@ -0,0 +1,29 @@ +# 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 datetime as dt +import random + +from proteus import Model + + +def setup(config, activated, company, customers): + Work = Model.get('project.work') + + customer_projects = { + 'Website': ['analysis', 'design', 'setup'], + 'Labels': ['design'], + 'Calendar': ['design'], + } + + for name, task_names in customer_projects.items(): + project = Work(name=name, type='project', timesheet_available=False) + project.party = random.choice(customers) + for task_name in task_names: + task = Work(name=task_name, type='task', timesheet_available=True) + task.effort_duration = dt.timedelta( + days=random.randint(10, 30), + hours=random.randint(1, 5)) + task.progress = random.randint(1, 100) // 5 * 5 / 100. + project.children.append(task) + project.save() diff --git a/demo/purchase.py b/demo/purchase.py new file mode 100644 index 0000000000..ae8295a559 --- /dev/null +++ b/demo/purchase.py @@ -0,0 +1,42 @@ +# 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 datetime as dt +import random + +from dateutil.relativedelta import relativedelta + +from proteus import Model + + +def setup(config, modules, company, suppliers): + Purchase = Model.get('purchase.purchase') + Product = Model.get('product.product') + + all_products = Product.find([ + ('purchasable', '=', True), + ]) + today = dt.date.today() + purchase_date = today - relativedelta(days=60) + while purchase_date <= today + relativedelta(days=20): + supplier = random.choice(suppliers) + purchase = Purchase() + purchase.party = supplier + purchase.purchase_date = purchase_date + products = random.sample( + all_products, random.randint(1, len(all_products))) + for product in products: + purchase_line = purchase.lines.new() + purchase_line.product = product + purchase_line.quantity = random.randint(20, 100) + purchase_line.unit_price = product.cost_price + purchase.save() + threshold = 2. / 3. + if random.random() <= threshold: + purchase.click('quote') + if random.random() <= threshold: + purchase.click('confirm') + purchase.click('process') + elif random.choice([True, False]): + purchase.click('cancel') + purchase_date += relativedelta(days=random.randint(5, 10)) diff --git a/demo/sale.py b/demo/sale.py new file mode 100644 index 0000000000..df2c1128e8 --- /dev/null +++ b/demo/sale.py @@ -0,0 +1,48 @@ +# 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 datetime as dt +import random + +from dateutil.relativedelta import relativedelta + +from proteus import Model + + +def setup(config, modules, company, customers): + Sale = Model.get('sale.sale') + Product = Model.get('product.product') + + all_products = Product.find([ + ('salable', '=', True), + ]) + today = dt.date.today() + sale_date = today + relativedelta(months=-2) + while sale_date <= today + relativedelta(days=10): + for _ in range(random.randint(1, 5)): + customer = random.choice(customers) + sale = Sale() + sale.party = customer + sale.sale_date = sale_date + products = random.sample( + all_products, random.randint(1, len(all_products))) + for product in products: + sale_line = sale.lines.new() + sale_line.product = product + sale_line.quantity = random.randint(1, 50) + sale.save() + if sale_date <= today: + threshold = 2. / 3. + else: + threshold = 1. / 3. + if random.random() <= threshold: + sale.click('quote') + if random.random() <= threshold: + sale.click('confirm') + if random.random() <= threshold: + sale.click('process') + elif random.random() >= threshold: + sale.click('cancel') + elif random.random() >= threshold: + sale.click('cancel') + sale_date += relativedelta(days=random.randint(1, 3)) diff --git a/demo/stock.py b/demo/stock.py new file mode 100644 index 0000000000..f89c5a7c89 --- /dev/null +++ b/demo/stock.py @@ -0,0 +1,32 @@ +# 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 random + +from proteus import Model + + +def setup(config, activated, company, suppliers): + ShipmentIn = Model.get('stock.shipment.in') + ShipmentOut = Model.get('stock.shipment.out') + + for supplier in suppliers: + shipment = ShipmentIn() + shipment.supplier = supplier + all_moves = shipment.incoming_moves.find() + moves = random.sample(all_moves, len(all_moves) * 2 // 3) + while moves: + shipment = ShipmentIn() + shipment.supplier = supplier + for _ in range(random.randint(1, len(moves))): + move = moves.pop() + shipment.incoming_moves.append(move) + shipment.click('receive') + shipment.click('do') + + shipments = ShipmentOut.find([('state', '=', 'waiting')]) + for shipment in shipments: + if shipment.click('assign_try'): + shipment.click('pick') + shipment.click('pack') + shipment.click('do') diff --git a/demo/timesheet.py b/demo/timesheet.py new file mode 100644 index 0000000000..0b5b88749c --- /dev/null +++ b/demo/timesheet.py @@ -0,0 +1,39 @@ +# 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 datetime as dt +import random + +from dateutil.relativedelta import relativedelta + +from proteus import Model + + +def setup(config, activated, company): + Work = Model.get('timesheet.work') + Employee = Model.get('company.employee') + Line = Model.get('timesheet.line') + + for name in ['Marketing', 'Accounting', 'Secretary']: + work = Work(name=name) + work.save() + + employees = Employee.find([('company', '=', company.id)]) + works = Work.find([]) + + today = dt.date.today() + date = today + relativedelta(months=-1) + day = dt.timedelta(hours=8) + while date <= today: + if date.weekday() < 5: + for employee in employees: + total = dt.timedelta() + while total < day: + if random.random() > 0.8: + break + line = Line(employee=employee, date=date) + line.work = random.choice(works) + duration = dt.timedelta(hours=random.randint(1, 8)) + line.duration = min(duration, day - total) + line.save() + date += dt.timedelta(days=1) diff --git a/demo/tools.py b/demo/tools.py new file mode 100644 index 0000000000..b8105a7326 --- /dev/null +++ b/demo/tools.py @@ -0,0 +1,16 @@ +# 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 hashlib + +from proteus import Model + + +def format_warning(name, records): + key = '|'.join(map(str, records)).encode('utf-8') + return '%s.%s' % (hashlib.md5(key).hexdigest(), name) + + +def skip_warning(config, name, records): + Warning_ = Model.get('res.user.warning') + Warning_(user=config.user, name=format_warning(name, records)).save()