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 @@
+
+
+