diff --git a/.gitignore b/.gitignore index 63d810d..ad4a1f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,176 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python -# dependencies -/build -/dist -*egg-info +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +# C extensions +*.so -/node_modules +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -# testing -/coverage +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec -# production -/build +# Installer logs +pip-log.txt +pip-delete-this-directory.txt -# misc +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ -.DS_Store +# Translations +*.mo +*.pot -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal -package-lock* +# Flask stuff: +instance/ +.webassets-cache -/__pycache__ -/app/__pycache__ -/app/commons/__pycache__ +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/__init__.py b/__init__.py index 0ffc962..adfee55 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,7 @@ __all__ = ['register'] def register(): Pool.register( sale_order.SaleOrder, + sale_order.OrderLine, module='sale_order', type_='model') Pool.register( module='sale_order', type_='wizard') diff --git a/locale/es.po b/locale/es.po new file mode 100644 index 0000000..200bd06 --- /dev/null +++ b/locale/es.po @@ -0,0 +1,131 @@ +# +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgctxt "field:sale.order,company:" +msgid "Company" +msgstr "Empresa" + +msgctxt "field:sale.order,currency:" +msgid "Currency" +msgstr "Moneda" + +msgctxt "field:sale.order,number:" +msgid "Number" +msgstr "Número" + +msgctxt "field:sale.order,party:" +msgid "Party" +msgstr "Tercero" + +msgctxt "field:sale.order,order_address:" +msgid "Address" +msgstr "Dirección de Envío" + +msgctxt "field:sale.order,pickup_location:" +msgid "Pickup Location" +msgstr "Lugar de Recogida" + +msgctxt "selection:sale.order,pickup_location:" +msgid "On Site" +msgstr "En el sitio" + +msgctxt "selection:sale.order,pickup_location:" +msgid "At Home" +msgstr "A Domicilio" + +msgctxt "field:sale.order,order_mobile:" +msgid "Mobile" +msgstr "Teléfono de Contacto" + +msgctxt "field:sale.order,date:" +msgid "Date" +msgstr "Fecha de la Orden" + +msgctxt "field:sale.order,lines:" +msgid "Lines" +msgstr "Líneas" + +msgctxt "field:sale.order,total_order:" +msgid "Total" +msgstr "Total" + +msgctxt "field:sale.order,state:" +msgid "State" +msgstr "Estado" + +msgctxt "selection:sale.order,state:" +msgid "Draft" +msgstr "Borrador" + +msgctxt "selection:sale.order,state:" +msgid "Confirmed" +msgstr "Confirmado" + +msgctxt "selection:sale.order,state:" +msgid "Done" +msgstr "Finalizada" + +msgctxt "view:sale.order:" +msgid "Order" +msgstr "Pedido" + +msgctxt "field:order.line,order:" +msgid "Order" +msgstr "Orden" + +msgctxt "field:order.line,company:" +msgid "Company" +msgstr "Empresa" + +msgctxt "field:order.line,currency:" +msgid "Currency" +msgstr "Moneda" + +msgctxt "field:order.line,product:" +msgid "Product" +msgstr "Producto" + +msgctxt "field:order.line,unit:" +msgid "Unit" +msgstr "Unidad de Medida" + +msgctxt "field:order.line,product_uom_category:" +msgid "Product UOM Category" +msgstr "Categoría de la unidad de medida" + +msgctxt "field:order.line,quantity:" +msgid "Quantity" +msgstr "Cantidad" + +msgctxt "field:order.line,unitprice:" +msgid "Unit Price" +msgstr "Precio Unitario" + +msgctxt "field:order.line,total_amount:" +msgid "Total Amount" +msgstr "Valor Total" + +msgctxt "model:ir.action.act_window.domain,name:act_order_form_domain_draft" +msgid "Draft" +msgstr "Borrador" + +msgctxt "model:ir.action.act_window.domain,name:act_order_form_domain_confirm" +msgid "Confirm" +msgstr "Confirmada" + +msgctxt "model:ir.action.act_window.domain,name:act_order_form_domain_done" +msgid "Done" +msgstr "Finalizada" + +msgctxt "model:ir.model.button,string:confirm_order_button" +msgid "Confirm" +msgstr "Confirmar" + +msgctxt "model:ir.ui.menu,name:menu_order" +msgid "Order" +msgstr "Ordenes" + +msgctxt "model:ir.action,name:act_sale_order" +msgid "Order" +msgstr "Orden" diff --git a/sale_order.py b/sale_order.py index 0ce48ba..fe23fc4 100644 --- a/sale_order.py +++ b/sale_order.py @@ -1,14 +1,209 @@ -from trytond.model import ModelView, ModelSQL, fields +# 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 ModelView, ModelSQL, fields, Workflow +from trytond.pool import Pool +from trytond.transaction import Transaction +from trytond.modules.currency.fields import Monetary +from trytond.modules.product import price_digits +from trytond.pyson import Eval, Bool +from decimal import Decimal +from datetime import date -class SaleOrder(ModelView, ModelSQL): +class SaleOrder (Workflow, ModelView, ModelSQL): "Sale Order" __name__ = 'sale.order' + _states = { + 'readonly': Eval('state').in_(['confirmed', 'done']) + } + + company = fields.Many2One( + 'company.company', "Company", states={ + 'readonly': True, + 'required': True + }) + currency = fields.Many2One( + 'currency.currency', 'Currency', states={ + 'readonly': True, + 'required': True + }) + number = fields.Char( + "Number", readonly=True) party = fields.Many2One( - 'party.party', "Party", required=True - ) - pickup_location = fields.Selection( - [("on_site", "On Site"), - ("at_home", "At Home")], 'Pickup Location' + 'party.party', "Party", required=True, states=_states) + order_address = fields.Many2One( + 'party.address', 'Address', required=True, states=_states) + pickup_location = fields.Selection([ + ("on_site", "On Site"), + ("at_home", "At Home")], 'Pickup Location', states=_states) + order_mobile = fields.Char('Mobile', states=_states) + date = fields.Date("Date", required=True, states=_states) + lines = fields.One2Many( + 'order.line', 'order', 'Lines', states=_states) + total_order = fields.Function( + Monetary("Total", currency='currency', digits='currency'), + 'on_change_with_total_order') + state = fields.Selection([ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('done', "Done") + ], "State", readonly=True) + + @classmethod + def __setup__(cls): + super(SaleOrder, cls).__setup__() + cls._buttons.update({ + 'confirm': { + 'readonly': Eval('state').in_(['confirmed']), + 'invisible': Eval('state').in_(['confirmed']) | ~Bool( + Eval('lines', [0])) + } + }) + cls._transitions |= set(( + ('draft', 'confirmed'), + )) + + @classmethod + def set_code(cls, orders): + for order in orders: + order.number = cls.get_number()[0].get() + order.save() + + @classmethod + def get_number(cls): + pool = Pool() + Sequence = pool.get('ir.sequence') + order_sequence = Sequence.search([( + 'sequence_type.name', '=', "Order")]) + return order_sequence + + @staticmethod + def default_company(): + return Transaction().context.get('company') + + @classmethod + def default_currency(cls, **pattern): + pool = Pool() + Company = pool.get('company.company') + company = pattern.get('company') + if not company: + company = cls.default_company() + if company: + return Company(company).currency.id + + @staticmethod + def default_date(): + return date.today() + + @staticmethod + def default_state(): + return 'draft' + + @fields.depends('party') + def on_change_party(self): + if self.party: + self.order_address =\ + self.party.addresses[0].id if self.party.addresses else None + + @fields.depends('lines') + def on_change_with_total_order(self, name=None): + total = Decimal('0.0') + if self.lines: + for line in self.lines: + if line.total_amount: + total += Decimal(line.total_amount) + return total + + @fields.depends('party') + def on_change_with_order_mobile(self, name=None): + if self.party: + pool = Pool() + ContactMechanism = pool.get('party.contact_mechanism') + mechanisms = ContactMechanism.search([ + ('party', '=', self.party.id), + ('type', '=', 'mobile') + ]) + if mechanisms: + return mechanisms[0].value + return + + @classmethod + @ModelView.button + @Workflow.transition('confirmed') + def confirm(cls, orders): + for order in orders: + if order.number: + continue + else: + cls.set_code([order]) + + +class OrderLine(ModelView, ModelSQL): + "Order Line" + __name__ = 'order.line' + + _states = { + 'readonly': Eval('_parent_order.state').in_(['confirmed']) + } + order = fields.Many2One( + 'sale.order', "Order") + company = fields.Many2One( + 'company.company', "Company", states={ + 'readonly': True, + 'required': True + }) + currency = fields.Many2One( + 'currency.currency', 'Currency', states={ + 'readonly': True, + 'required': True + }) + product = fields.Many2One( + 'product.product', 'Product', required=True, states=_states) + unit = fields.Many2One( + 'product.uom', 'Unit', required=True, states=_states ) + product_uom_category = fields.Function( + fields.Many2One('product.uom.category', 'Product UOM Category'), + 'on_change_with_product_uom_category') + quantity = fields.Float( + "Quantity", digits=('unit'), required=True, states=_states) + unitprice = Monetary( + "Unit Price", + digits=price_digits, + currency='currency', required=True, states=_states) + total_amount = fields.Function( + Monetary("Total Amount", currency='currency', digits='currency'), + 'on_change_with_total_amount') + + @staticmethod + def default_company(): + return Transaction().context.get('company') + + @classmethod + def default_currency(cls, **pattern): + pool = Pool() + Company = pool.get('company.company') + company = pattern.get('company') + if not company: + company = cls.default_company() + if company: + return Company(company).currency.id + + @fields.depends('product') + def on_change_product(self): + if self.product: + self.unit = self.product.default_uom.id + + @fields.depends('product') + def on_change_with_product_uom_category(self, name=None): + if self.product: + return self.product.default_uom.category.id + return None + + @fields.depends('quantity', 'unitprice') + def on_change_with_total_amount(self, name=None): + if self.unitprice and self.quantity: + total_amount = self.unitprice * Decimal(self.quantity) + + return total_amount diff --git a/sale_order.xml b/sale_order.xml new file mode 100644 index 0000000..77e073d --- /dev/null +++ b/sale_order.xml @@ -0,0 +1,90 @@ + + + + + + sale.order + tree + order_tree + + + sale.order + form + order_form + + + order.line + tree + line_tree + + + order.line + form + line_form + + + Order + sale.order + + + + + + + + + + + + + confirm + Confirm + sale.order + + + Draft + + + + + + + Confirm + + + + + + + Done + + + + + + + + Order + + + Sale Order Number Sequence + + SO + 5 + + + diff --git a/tests/__pycache__/test_module.cpython-311.pyc b/tests/__pycache__/test_module.cpython-311.pyc deleted file mode 100644 index b04ed10..0000000 Binary files a/tests/__pycache__/test_module.cpython-311.pyc and /dev/null differ diff --git a/tests/__pycache__/test_scenario.cpython-311.pyc b/tests/__pycache__/test_scenario.cpython-311.pyc deleted file mode 100644 index aef1f1e..0000000 Binary files a/tests/__pycache__/test_scenario.cpython-311.pyc and /dev/null differ diff --git a/tests/scenario_sale_order.rst b/tests/scenario_sale_order.rst index 1d5e872..0f67a8d 100644 --- a/tests/scenario_sale_order.rst +++ b/tests/scenario_sale_order.rst @@ -34,11 +34,18 @@ Create order:: >>> order = SaleOrder() >>> order.party = party >>> order.pickup_location = "on_site" - >>> order.save() >>> line1 = order.lines.new() >>> line1.product = product - >>> line1.quantity = 4 - >>> line1.unitprice = 8400 - >>> line1.total_amount = 33600 + >>> line1.unit = unit + >>> line1.quantity = 4.0 + >>> line1.unitprice = Decimal('8400') + >>> line1.total_amount = Decimal('33600') + >>> order.total_order = Decimal('33600') >>> order.save() - \ No newline at end of file + >>> order.state + 'draft' + >>> order.click('confirm') + >>> order.state + 'confirmed' + >>> order.number + 'SO00001' \ No newline at end of file diff --git a/tryton.cfg b/tryton.cfg index e539420..cf9eb2d 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,7 +1,8 @@ [tryton] -version=7.4 +version=7.4.0 depends: ir party product xml: + sale_order.xml diff --git a/view/line_form.xml b/view/line_form.xml new file mode 100644 index 0000000..090603a --- /dev/null +++ b/view/line_form.xml @@ -0,0 +1,17 @@ + + +
+ + +
diff --git a/view/line_tree.xml b/view/line_tree.xml new file mode 100644 index 0000000..07a2a31 --- /dev/null +++ b/view/line_tree.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/view/order_form.xml b/view/order_form.xml new file mode 100644 index 0000000..eb31e3f --- /dev/null +++ b/view/order_form.xml @@ -0,0 +1,32 @@ + + +
+