Merge pull request '6.8' (#5) from 6.8 into master

Reviewed-on: https://gitea.onecluster.org/OneTeam/trytondo-purchase_shop/pulls/5
This commit is contained in:
Rodia 2024-01-04 22:15:06 -05:00
commit 6c5d0388fc
14 changed files with 194 additions and 221 deletions

View File

@ -201,3 +201,7 @@ msgstr "Tiendas"
msgctxt "view:purchase.shop:"
msgid "Users"
msgstr "Usuarios"
msgctxt "field:purchase.shop,invoice_subtype:"
msgid "Subtype"
msgstr "Tipo de Documento"

View File

@ -2,7 +2,6 @@
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from trytond.model import fields
from trytond.pyson import Eval
from trytond.pool import PoolMeta
from trytond.transaction import Transaction
@ -17,7 +16,6 @@ class Party(metaclass=PoolMeta):
@staticmethod
def default_shops():
if Transaction().context.get('shops'):
return Transaction().context.get('shops')
return Transaction().context.get('shops')
else:
return []
return []

View File

@ -3,11 +3,6 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="party_view_tree">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_tree"/>
<field name="name">party_tree</field>
</record>
<record model="ir.ui.view" id="party_view_form">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_form"/>

View File

@ -7,7 +7,6 @@ from trytond.model import fields
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval
from trytond.exceptions import UserError
__all__ = ['Purchase', 'PurchaseLine']
@ -15,13 +14,14 @@ __all__ = ['Purchase', 'PurchaseLine']
class Purchase(metaclass=PoolMeta):
__name__ = 'purchase.purchase'
shop = fields.Many2One('purchase.shop', 'Shop', required=True, domain=[
('id', 'in', Eval('context', {}).get('shops', [])),
],
('id', 'in', Eval('context', {}).get('shops', [])),
],
states={
'readonly': (Eval('state') != 'draft') | Bool(Eval('number')),
}, depends=['number', 'state'])
}, depends=['number', 'state'])
shop_address = fields.Function(fields.Many2One('party.address',
'Shop Address'), 'on_change_with_shop_address')
'Shop Address'),
'on_change_with_shop_address')
@classmethod
def __setup__(cls):
@ -77,7 +77,6 @@ class Purchase(metaclass=PoolMeta):
@classmethod
def default_price_list(cls):
raise UserError(str(cls))
shop = cls.current_shop()
if not shop or not shop.price_list:
return
@ -89,7 +88,7 @@ class Purchase(metaclass=PoolMeta):
if not shop or not shop.address:
return
return shop.address.id
@fields.depends('shop', 'party')
def on_change_shop(self):
if not self.shop:
@ -105,7 +104,7 @@ class Purchase(metaclass=PoolMeta):
@fields.depends('shop')
def on_change_with_shop_address(self, name=None):
return (self.shop and self.shop.address and
self.shop.address.id or None)
self.shop.address.id or None)
@fields.depends('shop')
def on_change_party(self):
@ -117,7 +116,8 @@ class Purchase(metaclass=PoolMeta):
@classmethod
def set_number(cls, purchases):
'''
Fill the reference field with the purchase shop or purchase config sequence
Fill the reference field with the purchase
shop or purchase config sequence
'''
for purchase in purchases:
if purchase.number:
@ -128,15 +128,14 @@ class Purchase(metaclass=PoolMeta):
super().set_number(purchases)
class PurchaseLine(metaclass=PoolMeta):
__name__ = 'purchase.line'
@fields.depends('product', 'unit', 'quantity', 'purchase',
'_parent_purchase.party', 'product_supplier',
'_parent_purchase.shop',
methods=['_get_tax_rule_pattern', '_get_context_purchase_price'])
methods=['_get_tax_rule_pattern',
'_get_context_purchase_price'])
def on_change_product(self):
pool = Pool()
Product = pool.get('product.product')
@ -172,16 +171,17 @@ class PurchaseLine(metaclass=PoolMeta):
if self.purchase and self.purchase.party:
product_suppliers = [ps for ps in self.product.product_suppliers
if ps.party == self.purchase.party]
if ps.party == self.purchase.party]
if len(product_suppliers) == 1:
self.product_supplier, = product_suppliers
if (self.product_supplier
and (self.product_supplier
not in self.product.product_suppliers)):
not in self.product.product_suppliers)):
self.product_supplier = None
with Transaction().set_context(self._get_context_purchase_price()):
self.unit_price = Product.get_purchase_price([self.product],
self.unit_price = Product.get_purchase_price(
[self.product],
abs(self.quantity or 0))[self.product.id]
if self.unit_price:
self.unit_price = self.unit_price.quantize(

View File

@ -26,9 +26,10 @@ def get_require_version(name):
else:
require = '%s >= %s.%s, < %s.%s'
require %= (name, major_version, minor_version,
major_version, minor_version + 1)
major_version, minor_version + 1)
return require
config = ConfigParser()
config.readfp(open('tryton.cfg'))
info = dict(config.items('tryton'))
@ -54,26 +55,30 @@ if minor_version % 2:
dependency_links.append('https://trydevpi.tryton.org/')
setup(name='%s_%s' % (PREFIX, MODULE),
version=version,
description='Tryton Purchase Shop Module',
long_description=read('README'),
author='Alnus Tmp',
author_email='alnus@disroot.org',
url='https://git.disroot.org/Etrivial/',
download_url='https://git.disroot.org/Etrivial/trytond-%s' % MODULE,
keywords='',
package_dir={'trytond.modules.%s' % MODULE: '.'},
packages=[
'trytond.modules.%s' % MODULE,
'trytond.modules.%s.tests' % MODULE,
],
version=version,
description='Tryton Purchase Shop Module',
long_description=read('README'),
author='Alnus Tmp',
author_email='alnus@disroot.org',
url='https://git.disroot.org/Etrivial/',
download_url='https://git.disroot.org/Etrivial/trytond-%s' % MODULE,
keywords='',
package_dir={'trytond.modules.%s' % MODULE: '.'},
packages=[
'trytond.modules.%s' % MODULE,
'trytond.modules.%s.tests' % MODULE,
],
package_data={
'trytond.modules.%s' % MODULE: (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po', '*.odt',
'icons/*.svg', 'tests/*.rst']),
},
'trytond.modules.%s' % MODULE: (info.get('xml', [])
+ ['tryton.cfg',
'view/*.xml',
'locale/*.po',
'*.odt',
'icons/*.svg',
'tests/*.rst']),
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
'Framework :: Tryton',
'Intended Audience :: Developers',
@ -101,7 +106,7 @@ setup(name='%s_%s' % (PREFIX, MODULE),
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Office/Business',
],
],
license='GPL-3',
install_requires=requires,
dependency_links=dependency_links,
@ -115,6 +120,6 @@ setup(name='%s_%s' % (PREFIX, MODULE),
tests_require=tests_require,
use_2to3=True,
convert_2to3_doctests=[
'tests/scenario_purchase_shop.rst',
],
)
'tests/scenario_purchase_shop.rst',
],
)

175
shop.py
View File

@ -2,84 +2,81 @@
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from sql import Null, Table
from trytond.model import ModelView, ModelSQL, fields
from trytond.pyson import If, Eval, Id
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.pool import Pool
from trytond import backend
__all__ = ['PurchaseShop', 'PurchaseShopResUser', 'PurchaseShopParty']
_digits = (16, 2)
class PurchaseShop(ModelSQL, ModelView):
'Purchase Shop'
__name__ = 'purchase.shop'
name = fields.Char('Shop Name', required=True, select=True)
name = fields.Char('Shop Name', required=True, )
users = fields.Many2Many('purchase.shop-res.user', 'shop', 'user', 'Users')
address = fields.Many2One('party.address', 'Address', domain=[
('party', '=', Eval('company_party')),
], depends=['company_party'])
('party', '=', Eval('company_party')),
], depends=['company_party'])
warehouse = fields.Many2One('stock.location', "Warehouse", required=True,
domain=[('type', '=', 'warehouse')])
domain=[('type', '=', 'warehouse')])
currency = fields.Many2One('currency.currency', 'Currency',)
price_list = fields.Many2One('product.price_list', 'Price List')
payment_term = fields.Many2One('account.invoice.payment_term',
'Payment Term')
'Payment Term')
purchase_sequence = fields.Many2One(
'ir.sequence', 'Purchase Sequence', domain=[
('company', 'in', [Eval('company', -1), None]),
('sequence_type', '=', Id('purchase', 'sequence_type_purchase')),
],
],
depends=['company'])
purchase_invoice_method = fields.Selection([
(None, ''),
('manual', 'Manual'),
('order', 'On Order Processed'),
('shipment', 'On Shipment Sent')
], 'Purchase Invoice Method')
(None, ''),
('manual', 'Manual'),
('order', 'On Order Processed'),
('shipment', 'On Shipment Sent')
], 'Purchase Invoice Method')
purchase_shipment_method = fields.Selection([
(None, ''),
('manual', 'Manual'),
('order', 'On Order Processed'),
('invoice', 'On Invoice Paid'),
], 'Purchase Shipment Method')
(None, ''),
('manual', 'Manual'),
('order', 'On Order Processed'),
('invoice', 'On Invoice Paid'),
], 'Purchase Shipment Method')
company = fields.Many2One('company.company', 'Company', required=True,
domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', 0)),
], select=True)
domain=[
('id',
If(Eval('context', {}).contains('company'),
'=', '!='),
Eval('context', {}).get('company', 0)),
], )
company_party = fields.Function(fields.Many2One('party.party',
'Company Party'),
'on_change_with_company_party')
active = fields.Boolean('Active', select=True)
analytic_root = fields.Many2One('analytic_account.account','Analytic Root', required=True,
domain=[
('type', '=', 'root'),
],
depends=['type']
)
'Company Party'),
'on_change_with_company_party')
active = fields.Boolean('Active', )
analytic_root = fields.Many2One('analytic_account.account',
'Analytic Root', required=True,
domain=[
('type', '=', 'root'),
]
)
analytic_account = fields.Many2One('analytic_account.account',
'Analytic Account', required=True,
domain=[
('type', '=', 'normal')
],
depends=['analytic_root']
)
'Analytic Account', required=True,
domain=[
('type', '=', 'normal')
]
)
partys = fields.Many2Many('purchase.shop_party', 'shop', 'party',
'Partys')
'Partys')
withholding_tax = fields.Many2One('account.tax', 'WithholdingTax',
domain=[
('rate', '<', 0),
('group.kind', '=', 'purchase'),
],
depends=['rate', 'group'])
default_performance_rate = fields.Numeric('Default Performance Rate', _digits)
])
invoice_subtype = fields.Many2One('account.invoice.subtype', "Subtype")
@classmethod
def __register__(cls, module_name):
@ -101,7 +98,8 @@ class PurchaseShop(ModelSQL, ModelView):
if property_exist:
property_ = Table('ir_property')
purchase_sequence_exist = table_h.column_exist('purchase_sequence')
purchase_invoice_method_exist = table_h.column_exist('purchase_invoice_method')
purchase_invoice_method_exist = table_h.column_exist(
'purchase_invoice_method')
purchase_shipment_method_exist = table_h.column_exist(
'purchase_shipment_method')
@ -109,65 +107,66 @@ class PurchaseShop(ModelSQL, ModelView):
if backend.name != 'sqlite':
# SQLite doesn't support this query as it generates and update
# with an alias (AS) which is not valid on SQLite
query = shop_table.update(columns=[shop_table.currency],
query = shop_table.update(
columns=[shop_table.currency],
values=[company_table.currency],
from_=[company_table],
where=((shop_table.company == company_table.id)
& (shop_table.currency == Null)))
& (shop_table.currency == Null)))
cursor.execute(*query)
# Migration to remove Property
if not purchase_sequence_exist and property_exist:
cursor.execute(*property_
.join(field, condition=property_.field == field.id)
.join(model, condition=field.model == model.id)
.select(
property_.res,
property_.value,
where=property_.res.like(cls.__name__ + ',%')
& (field.name == 'purchase_sequence')
& (model.model == cls.__name__)))
.join(field, condition=property_.field == field.id)
.join(model, condition=field.model == model.id)
.select(
property_.res,
property_.value,
where=property_.res.like(cls.__name__ + ',%')
& (field.name == 'purchase_sequence')
& (model.model == cls.__name__)))
for res, value in cursor:
id_ = int(res.split(',')[1])
value = int(value.split(',')[1]) if value else None
update.execute(*table.update(
[table.purchase_sequence],
[value],
where=table.id == id_))
[table.purchase_sequence],
[value],
where=table.id == id_))
if not purchase_invoice_method_exist and property_exist:
cursor.execute(*property_
.join(field, condition=property_.field == field.id)
.join(model, condition=field.model == model.id)
.select(
property_.res,
property_.value,
where=property_.res.like(cls.__name__ + ',%')
& (field.name == 'purchase_invoice_method')
& (model.model == cls.__name__)))
.join(field, condition=property_.field == field.id)
.join(model, condition=field.model == model.id)
.select(
property_.res,
property_.value,
where=property_.res.like(cls.__name__ + ',%')
& (field.name == 'purchase_invoice_method')
& (model.model == cls.__name__)))
for res, value in cursor:
id_ = int(res.split(',')[1])
value = value.split(',')[1] if value else None
update.execute(*table.update(
[table.purchase_invoice_method],
[value],
where=table.id == id_))
[table.purchase_invoice_method],
[value],
where=table.id == id_))
if not purchase_shipment_method_exist and property_exist:
cursor.execute(*property_
.join(field, condition=property_.field == field.id)
.join(model, condition=field.model == model.id)
.select(
property_.res,
property_.value,
where=property_.res.like(cls.__name__ + ',%')
& (field.name == 'purchase_shipment_method')
& (model.model == cls.__name__)))
.join(field, condition=property_.field == field.id)
.join(model, condition=field.model == model.id)
.select(
property_.res,
property_.value,
where=property_.res.like(cls.__name__ + ',%')
& (field.name == 'purchase_shipment_method')
& (model.model == cls.__name__)))
for res, value in cursor:
id_ = int(res.split(',')[1])
value = value.split(',')[1] if value else None
update.execute(*table.update(
[table.purchase_shipment_method],
[value],
where=table.id == id_))
[table.purchase_shipment_method],
[value],
where=table.id == id_))
# Migration from 5.2: do not require price_list
table_h.not_null_action('price_list', action='remove')
@ -203,15 +202,6 @@ class PurchaseShop(ModelSQL, ModelView):
config = Config(1)
return config
@staticmethod
def default_performance_rate():
Config = Pool().get('purchase.configuration')
config = Config(1)
if config.default_performance_rate:
return config.default_performance_rate
@fields.depends('company')
def on_change_with_company_party(self, name=None):
if self.company and self.company.party:
@ -225,16 +215,15 @@ class PurchaseShopResUser(ModelSQL):
_table = 'purchase_shop_res_user'
shop = fields.Many2One('purchase.shop', 'Shop', ondelete='CASCADE',
select=True, required=True)
required=True)
user = fields.Many2One('res.user', 'User', ondelete='RESTRICT',
required=True)
required=True)
class PurchaseShopParty(ModelSQL):
'Purchase Schop Party'
__name__ = 'purchase.shop_party'
shop = fields.Many2One('purchase.shop', 'Shop', ondelete='CASCADE',
select=True, required=True)
required=True)
party = fields.Many2One('party.party', 'Party', ondelete='CASCADE',
required=True)

View File

@ -12,8 +12,8 @@ class ShipmentOut(metaclass=PoolMeta):
__name__ = 'stock.shipment.out'
shop_addresses = fields.Function(fields.Many2Many('party.address', None,
None, 'Shop Addresses'),
'on_change_with_shop_addresses')
None, 'Shop Addresses'),
'on_change_with_shop_addresses')
@classmethod
def __setup__(cls):
@ -24,13 +24,13 @@ class ShipmentOut(metaclass=PoolMeta):
'OR',
delivery_addr_domain,
[('id', 'in', Eval('shop_addresses'))],
]
]
else:
cls.delivery_address.domain = [
('id', 'in', Eval('shop_addresses')),
]
]
if 'shop_addresses' not in cls.delivery_address.depends:
cls.delivery_address.depends.append('shop_addresses')
cls.delivery_address.depends.add('shop_addresses')
@fields.depends('warehouse')
def on_change_with_shop_addresses(self, name=None):
@ -38,8 +38,8 @@ class ShipmentOut(metaclass=PoolMeta):
if not self.warehouse:
return []
warehouse_shops = Shop.search([
('warehouse', '=', self.warehouse.id),
])
('warehouse', '=', self.warehouse.id),
])
return [s.address.id for s in warehouse_shops if s.address]
@ -47,25 +47,25 @@ class ShipmentOutReturn(metaclass=PoolMeta):
__name__ = 'stock.shipment.out.return'
shop_addresses = fields.Function(fields.Many2Many('party.address', None,
None, 'Shop Addresses'),
'on_change_with_shop_addresses')
None, 'Shop Addresses'),
'on_change_with_shop_addresses')
@classmethod
def __setup__(cls):
super(ShipmentOutReturn, cls).__setup__()
delivery_addr_domain = cls.delivery_address.domain[:]
if delivery_addr_domain:
cls.delivery_address.domain = [
contact_addr_domain = cls.contact_address.domain[:]
if contact_addr_domain:
cls.contact_address.domain = [
'OR',
delivery_addr_domain,
contact_addr_domain,
[('id', 'in', Eval('shop_addresses'))],
]
]
else:
cls.delivery_address.domain = [
('id', 'in', Eval('shop_addresses')),
]
if 'shop_addresses' not in cls.delivery_address.depends:
cls.delivery_address.depends.append('shop_addresses')
]
if 'shop_addresses' not in cls.contact_address.depends:
cls.contact_address.depends.add('shop_addresses')
@fields.depends('warehouse')
def on_change_with_shop_addresses(self, name=None):
@ -73,6 +73,6 @@ class ShipmentOutReturn(metaclass=PoolMeta):
if not self.warehouse:
return []
warehouse_shops = Shop.search([
('warehouse', '=', self.warehouse.id),
])
('warehouse', '=', self.warehouse.id),
])
return [s.address.id for s in warehouse_shops if s.address]

View File

@ -1,9 +1,3 @@
# This file is part sale_shop module for Tryton.
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
try:
from trytond.modules.sale_shop.tests.test_sale_shop import suite
except ImportError:
from .test_sale_shop import suite
__all__ = ['suite']

View File

@ -1,5 +1,5 @@
==================
Sale Shop Scenario
Purchase Shop Scenario
==================
Imports::
@ -14,12 +14,12 @@ Imports::
... create_chart, get_accounts, create_tax
>>> from trytond.modules.account_invoice.tests.tools import \
... set_fiscalyear_invoice_sequences, create_payment_term
>>> from trytond.modules.sale_shop.tests.tools import create_shop
>>> from trytond.modules.purchase_shop.tests.tools import create_shop
>>> today = datetime.date.today()
Install sale::
Install purchase::
>>> config = activate_modules('sale_shop')
>>> config = activate_modules('purchase_shop')
Create company::
@ -114,12 +114,12 @@ Create an Inventory::
>>> inventory.state == 'done'
True
Create Sale Shop::
Create Purchase Shop::
>>> shop = create_shop(payment_term, product_price_list)
>>> shop.save()
Save Sale Shop User::
Save Purchase Shop User::
>>> User = Model.get('res.user')
>>> user, = User.find([])
@ -128,22 +128,22 @@ Save Sale Shop User::
>>> user.save()
>>> set_user(user)
Sale 5 products::
Purchase 5 products::
>>> Sale = Model.get('sale.sale')
>>> SaleLine = Model.get('sale.line')
>>> sale = Sale()
>>> sale.party = customer
>>> sale.payment_term = payment_term
>>> sale.invoice_method = 'shipment'
>>> sale_line = SaleLine()
>>> sale.lines.append(sale_line)
>>> sale_line.product = product
>>> sale_line.quantity = 2.0
>>> sale_line = SaleLine()
>>> sale.lines.append(sale_line)
>>> sale_line.product = product
>>> sale_line.quantity = 3.0
>>> sale.save()
>>> sale.state == 'draft'
>>> Purchase = Model.get('purchase.purchase')
>>> PurchaseLine = Model.get('purchase.line')
>>> purchase = Purchase()
>>> purchase.party = customer
>>> purchase.payment_term = payment_term
>>> purchase.invoice_method = 'shipment'
>>> purchase_line = PurchaseLine()
>>> purchase.lines.append(purchase_line)
>>> purchase_line.product = product
>>> purchase_line.quantity = 2.0
>>> purchase_line = PurchaseLine()
>>> purchase.lines.append(purchase_line)
>>> purchase_line.product = product
>>> purchase_line.quantity = 3.0
>>> purchase.save()
>>> purchase.state == 'draft'
True

View File

@ -0,0 +1,12 @@
# This file is part of the sale_shop module for Tryton.
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from trytond.tests.test_tryton import ModuleTestCase
class PurchaseShopTestCase(ModuleTestCase):
'Test Sale Shop module'
module = 'purchase_shop'
del ModuleTestCase

View File

@ -1,26 +0,0 @@
# This file is part of the sale_shop module for 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 ModuleTestCase
from trytond.tests.test_tryton import doctest_teardown
from trytond.tests.test_tryton import doctest_checker
class SaleShopTestCase(ModuleTestCase):
'Test Sale Shop module'
module = 'sale_shop'
def suite():
suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
SaleShopTestCase))
suite.addTests(doctest.DocFileSuite(
'scenario_sale_shop.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite

View File

@ -7,7 +7,7 @@ __all__ = ['create_shop']
def create_shop(payment_term, product_price_list, name=None, warehouse=None,
sequence=None, config=None):
sequence=None, config=None):
"Create a sale shop"
Shop = Model.get('sale.shop', config=config)
Sequence = Model.get('ir.sequence', config=config)
@ -20,14 +20,14 @@ def create_shop(payment_term, product_price_list, name=None, warehouse=None,
if not warehouse:
warehouse, = Location.find([
('type', '=', 'warehouse'),
])
])
shop.warehouse = warehouse
shop.price_list = product_price_list
shop.payment_term = payment_term
if not sequence:
sequence, = Sequence.find([
('name', '=', 'Sale'),
])
])
shop.sale_sequence = sequence
shop.sale_invoice_method = 'shipment'
shop.sale_shipment_method = 'order'

View File

@ -1,11 +1,13 @@
[tryton]
version=6.0.0
version=6.8.0
depends:
ir
res
purchase_price_list
analytic_purchase
account
account_invoice_subtype
party
xml:
shop.xml
purchase.xml

View File

@ -18,8 +18,6 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
<field name="analytic_account"/>
<label name="withholding_tax"/>
<field name="withholding_tax"/>
<label name="default_performance_rate"/>
<field name="default_performance_rate"/>
<notebook colspan="4">
<page string="General" id="general">
<label name="purchase_sequence"/>
@ -35,6 +33,8 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
<field name="price_list"/>
<label name="payment_term"/>
<field name="payment_term"/>
<label name="invoice_subtype"/>
<field name="invoice_subtype"/>
</page>
<page string="Users" id="users">
<field name="users"/>