diff --git a/__init__.py b/__init__.py
index 1a52ad9..c42df4b 100644
--- a/__init__.py
+++ b/__init__.py
@@ -4,6 +4,7 @@
from trytond.pool import Pool
from . import contract
from . import invoice
+from . import maquila
from . import history
from . import party
from . import plot
@@ -14,6 +15,7 @@ from . import production
from . import location
from . import move
from . import price_list
+from . import sale
def register():
Pool.register(
@@ -21,6 +23,15 @@ def register():
contract.AgronomicsContract,
contract.AgronomicsContractLine,
invoice.InvoiceLine,
+ maquila.Configuration,
+ maquila.ConfigurationSequence,
+ maquila.Contract,
+ maquila.ContractCrop,
+ maquila.ContractProductPercentage,
+ maquila.ProductYear,
+ maquila.Maquila,
+ maquila.MaquilaProductYearContractCrop,
+ maquila.MaquilaContractCrop,
history.WineAgingHistory,
history.ProductWineAgingHistory,
party.Party,
@@ -82,3 +93,10 @@ def register():
module='agronomics', type_='wizard')
Pool.register(
module='agronomics', type_='report')
+ Pool.register(
+ sale.Configuration,
+ sale.ConfigurationSequence,
+ sale.Sale,
+ sale.SaleLine,
+ module='agronomics', type_='model',
+ depends=['sale'])
diff --git a/contract.py b/contract.py
index 309ee3d..7ccb6ba 100644
--- a/contract.py
+++ b/contract.py
@@ -174,9 +174,8 @@ class AgronomicsContractLine(ModelSQL, ModelView):
@fields.depends('parcel')
def on_change_with_product(self, name=None):
- if self.parcel:
+ if self.parcel and self.parcel.product:
return self.parcel.product.id
- return None
@fields.depends('product', methods=['on_change_with_product'])
def on_change_with_unit(self, name=None):
diff --git a/contract.xml b/contract.xml
index fb3a8d7..a0aefbc 100644
--- a/contract.xml
+++ b/contract.xml
@@ -113,7 +113,7 @@ this repository contains the full copyright notices and license terms. -->
Done
-
+
diff --git a/doc/maquila.xml b/doc/maquila.xml
new file mode 100644
index 0000000..876560e
--- /dev/null
+++ b/doc/maquila.xml
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maquila.py b/maquila.py
new file mode 100644
index 0000000..6afefa8
--- /dev/null
+++ b/maquila.py
@@ -0,0 +1,719 @@
+from trytond.model import (Workflow, ModelSingleton, ModelView, ModelSQL,
+ fields, sequence_ordered)
+from trytond.pyson import Id, If, Eval, Bool
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+from trytond.modules.company.model import (
+ CompanyMultiValueMixin, CompanyValueMixin)
+from trytond.i18n import gettext
+from trytond.exceptions import UserError
+from trytond.model.modelstorage import AccessError
+
+
+def default_func(field_name):
+ @classmethod
+ def default(cls, **pattern):
+ return getattr(
+ cls.multivalue_model(field_name),
+ 'default_%s' % field_name, lambda: None)()
+ return default
+
+
+class Configuration(ModelSingleton, ModelSQL, ModelView, CompanyMultiValueMixin):
+ "Maquila Configuration"
+ __name__ = 'agronomics.maquila.configuration'
+ contract_sequence = fields.MultiValue(fields.Many2One(
+ 'ir.sequence', "Contract Sequence", required=True,
+ domain=[
+ ('company', 'in',
+ [Eval('context', {}).get('company', -1), None]),
+ ('sequence_type', '=', Id('agronomics', 'sequence_type_maquila_contract')),
+ ]))
+
+ @classmethod
+ def multivalue_model(cls, field):
+ pool = Pool()
+ if field == 'contract_sequence':
+ return pool.get('agronomics.maquila.configuration.sequence')
+ return super(Configuration, cls).multivalue_model(field)
+
+ default_contract_sequence = default_func('contract_sequence')
+
+
+class ConfigurationSequence(ModelSQL, CompanyValueMixin):
+ "Maquila Configuration Sequence"
+ __name__ = 'agronomics.maquila.configuration.sequence'
+ contract_sequence = fields.Many2One(
+ 'ir.sequence', "Contract Sequence", required=True,
+ domain=[
+ ('company', 'in', [Eval('company', -1), None]),
+ ('sequence_type', '=', Id('agronomics', 'sequence_type_maquila_contract')),
+ ],
+ depends=['company'])
+
+ @classmethod
+ def default_contract_sequence(cls):
+ pool = Pool()
+ ModelData = pool.get('ir.model.data')
+ try:
+ return ModelData.get_id('agronomics', 'sequence_maquila_contract')
+ except KeyError:
+ return None
+
+
+class Contract(sequence_ordered(), Workflow, ModelSQL, ModelView):
+ "Maquila Contract"
+ __name__ = 'agronomics.maquila.contract'
+ _rec_name = 'number'
+ company = fields.Many2One(
+ 'company.company', "Company", required=True, select=True,
+ states={
+ 'readonly': (
+ (Eval('state') != 'draft')
+ | Eval('product_crops', [0])
+ | Eval('product_percentages', [0])
+ | Eval('product_years', [0])
+ | Eval('party', True)),
+ },
+ depends=['state'])
+ number = fields.Char('Number', readonly=True, select=True)
+ reference = fields.Char('Reference')
+ party = fields.Many2One('party.party', "Party", required=True,
+ states={
+ 'readonly': Eval('state') != 'draft',
+ },
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['state', 'company'])
+ product = fields.Many2One('product.product', "Product", required=True,
+ domain=[
+ ('agronomic_type', '=', 'grape'),
+ ], states={
+ 'readonly': Eval('state') != 'draft',
+ },
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['state', 'company'])
+ quantity = fields.Float('Quantity', required=True,
+ digits=(16, Eval('unit_digits', 2)),
+ states={
+ 'readonly': Eval('state') != 'draft',
+ },
+ depends=['state', 'unit_digits'])
+ unit = fields.Many2One('product.uom', "Unit", required=True, ondelete='RESTRICT',
+ domain=[
+ If(Bool(Eval('product_uom_category')),
+ ('category', '=', Eval('product_uom_category')),
+ ('category', '!=', -1)),
+ ],
+ states={
+ 'readonly': Eval('state') != 'draft',
+ },
+ depends=['state', 'product_uom_category'])
+ unit_digits = fields.Function(fields.Integer("Unit Digits"),
+ 'on_change_with_unit_digits')
+ product_uom_category = fields.Function(
+ fields.Many2One('product.uom.category', "Product Uom Category"),
+ 'on_change_with_product_uom_category')
+ product_crops = fields.One2Many('agronomics.maquila.contract.crop',
+ 'contract', "Product Crops",
+ states={
+ 'readonly': Eval('state') != 'draft',
+ 'required': Eval('state') == 'active',
+ },
+ depends=['state'])
+ product_percentages = fields.One2Many('agronomics.maquila.contract.product_percentage',
+ 'contract', "Product Percentatges",
+ states={
+ 'readonly': Eval('state') != 'draft',
+ 'required': Eval('state') == 'active',
+ },
+ depends=['state'])
+ table = fields.Boolean("Table",
+ states={
+ 'readonly': Eval('state') != 'draft',
+ },
+ depends=['state'])
+ state = fields.Selection([
+ ('draft', "Draft"),
+ ('active', "Active"),
+ ('done', "Done"),
+ ('cancelled', "Cancelled"),
+ ], "State", readonly=True, required=True)
+
+ @classmethod
+ def __setup__(cls):
+ super(Contract, cls).__setup__()
+ cls._order = [
+ ('number', 'DESC NULLS FIRST'),
+ ('id', 'DESC'),
+ ]
+ cls._transitions |= set((
+ ('draft', 'active'),
+ ('draft', 'cancelled'),
+ ('active', 'done'),
+ ('cancelled', 'draft'),
+ ))
+ cls._buttons.update({
+ 'draft': {
+ 'invisible': Eval('state') != 'cancelled',
+ 'icon': If(Eval('state') == 'cancelled', 'tryton-undo',
+ 'tryton-back'),
+ },
+ 'active': {
+ 'invisible': Eval('state') != 'draft',
+ 'icon': 'tryton-forward',
+ },
+ 'cancel': {
+ 'invisible': Eval('state') != 'draft',
+ 'icon': 'tryton-cancel',
+ },
+ 'done': {
+ 'invisible': Eval('state') != 'active',
+ 'icon': 'tryton-ok',
+ },
+ })
+
+ @staticmethod
+ def default_state():
+ return 'draft'
+
+ @staticmethod
+ def default_company():
+ return Transaction().context.get('company')
+
+ def get_rec_name(self, name):
+ items = []
+ if self.number:
+ items.append(self.number)
+ if self.reference:
+ items.append('[%s]' % self.reference)
+ if not items:
+ items.append('(%s)' % self.id)
+ return ' '.join(items)
+
+ @classmethod
+ def search_rec_name(cls, name, clause):
+ _, operator, value = clause
+ if operator.startswith('!') or operator.startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ domain = [bool_op,
+ ('number', operator, value),
+ ('reference', operator, value),
+ ]
+ return domain
+
+ @classmethod
+ def copy(cls, contracts, default=None):
+ if default is None:
+ default = {}
+ else:
+ default = default.copy()
+ default.setdefault('number', None)
+ default.setdefault('product_years', None)
+ default.setdefault('maquilas', None)
+ return super(Contract, cls).copy(contracts, default=default)
+
+ @classmethod
+ def delete(cls, contracts):
+ # Cancel before delete
+ cls.cancel(contracts)
+ for contract in contracts:
+ if contract.state != 'cancelled':
+ raise AccessError(
+ gettext('agronomics.msg_contract_delete_cancel',
+ contract=contract.rec_name))
+ super(Contract, cls).delete(contracts)
+
+ @fields.depends('product')
+ def on_change_with_product_uom_category(self, name=None):
+ if self.product:
+ return self.product.default_uom_category.id
+
+ @fields.depends('unit')
+ def on_change_with_unit_digits(self, name=None):
+ if self.unit:
+ return self.unit.digits
+ return 2
+
+ @fields.depends('product', 'unit')
+ def on_change_product(self):
+ if not self.product:
+ return
+
+ category = self.product.default_uom.category
+ if not self.unit or self.unit.category != category:
+ self.unit = self.product.default_uom
+ self.unit_digits = self.product.default_uom.digits
+
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('cancelled')
+ def cancel(cls, contracts):
+ pass
+
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('draft')
+ def draft(cls, contracts):
+ pass
+
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('active')
+ def active(cls, contracts):
+ for contract in contracts:
+ contract.check_quantity()
+ contract.create_contract_product_year()
+ contract.create_maquila()
+ cls.set_number(contracts)
+
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('done')
+ def done(cls, contracts):
+ pass
+
+ @classmethod
+ def set_number(cls, contracts):
+ '''
+ Fill the number field with the contracts sequence
+ '''
+ pool = Pool()
+ Config = pool.get('agronomics.maquila.configuration')
+
+ config = Config(1)
+ for contract in contracts:
+ if contract.number:
+ continue
+ contract.number = config.get_multivalue(
+ 'contract_sequence', company=contract.company.id).get()
+ cls.save(contracts)
+
+ def check_quantity(self):
+ if sum(x.quantity for x in self.product_crops) != self.quantity:
+ raise UserError(gettext('agronomics.msg_maquila_contract_quantity',
+ contract=self.rec_name))
+
+ def create_contract_product_year(self):
+ MaquilaProductYear = Pool().get('agronomics.maquila.product_year')
+
+ crops = set()
+ products = set()
+ for crop in self.product_crops:
+ crops.add(crop.crop)
+ for ppercentatge in self.product_percentages:
+ products.add(ppercentatge.product)
+
+ records = MaquilaProductYear.search([
+ ('party', '=', self.party),
+ ('crop', 'in', crops),
+ ('product', 'in', products),
+ ])
+ product_years = dict(((x.party, x.crop, x.product), x) for x in records)
+
+ new_product_years = []
+ for crop in self.product_crops:
+ for ppercentatge in self.product_percentages:
+ key = (self.party, crop.crop, ppercentatge.product)
+ if key in product_years:
+ product_year = product_years.get(key)
+ product_year.contract_crops += (crop,)
+ product_year.save()
+ new_product_years.append(product_year)
+ else:
+ product_year = MaquilaProductYear()
+ product_year.company = self.company
+ product_year.party = self.party
+ product_year.crop = crop.crop
+ product_year.product = ppercentatge.product
+ product_year.unit = ppercentatge.product.default_uom
+ product_year.contract_crops = (crop,)
+ product_year.save()
+ new_product_years.append(product_year)
+ return new_product_years
+
+ def create_maquila(self):
+ Maquila = Pool().get('agronomics.maquila')
+
+ default_values = Maquila.default_get(Maquila._fields.keys(),
+ with_rec_name=False)
+
+ crops = set()
+ products = set()
+ for crop in self.product_crops:
+ crops.add(crop.crop)
+ products.add(self.product)
+
+ records = Maquila.search([
+ ('party', '=', self.party),
+ ('crop', 'in', crops),
+ ('product', 'in', products),
+ ])
+ maquilas = dict(((x.party, x.product, x.crop, x.table), x) for x in records)
+
+ new_maquilas = []
+ for crop in self.product_crops:
+ key = (self.party, self.product, crop.crop, self.table)
+ if key in maquilas:
+ maquila = maquilas.get(key)
+ maquila.contract_crops += (crop,)
+ maquila.save()
+ new_maquilas.append(maquila)
+ else:
+ maquila = Maquila(**default_values)
+ maquila.company = self.company
+ maquila.party = self.party
+ maquila.crop = crop.crop
+ maquila.party = self.party
+ maquila.product = self.product
+ maquila.unit = self.product.default_uom
+ maquila.table = self.table
+ maquila.contract_crops = (crop,)
+ maquila.save()
+ new_maquilas.append(maquila)
+ return new_maquilas
+
+
+class ContractCrop(ModelSQL, ModelView):
+ "Maquila Contract Crop"
+ __name__ = 'agronomics.maquila.contract.crop'
+ contract = fields.Many2One('agronomics.maquila.contract', "Contract",
+ ondelete='CASCADE', select=True, required=True)
+ crop = fields.Many2One('agronomics.crop', "Crop", required=True)
+ quantity = fields.Float("Quantity", digits=(16, 2), required=True)
+ penality = fields.Numeric("Penality", digits=(16, Eval('currency_digits', 2)),
+ depends=['currency_digits'], required=True)
+ currency_digits = fields.Function(fields.Integer('Currency Digits'),
+ 'on_change_with_currency_digits')
+ product_years = fields.Many2Many(
+ 'agronomics.maquila.product_year-agronomics.maquila.contract.crop',
+ 'contract_crop', 'product_year', "Product Years")
+ maquilas = fields.Many2Many(
+ 'agronomics.maquila-agronomics.maquila.contract.crop',
+ 'contract_crop', 'maquila', "Maquila", readonly=True)
+
+ def on_change_with_currency_digits(self, name=None):
+ Company = Pool().get('company.company')
+
+ company = Transaction().context.get('company')
+ if company:
+ return Company(company).currency.digits
+ return 2
+
+
+class ContractProductPercentage(ModelSQL, ModelView):
+ "Maquila Contract Product Percentage"
+ __name__ = 'agronomics.maquila.contract.product_percentage'
+ contract = fields.Many2One('agronomics.maquila.contract', "Contract",
+ ondelete='CASCADE', select=True, required=True)
+ product = fields.Many2One('product.product', "Product", required=True)
+ percentatge = fields.Float("Percentatge", digits=(16, 2), required=True)
+
+
+class ProductYear(ModelSQL, ModelView):
+ "Maquila Product Year"
+ __name__ = 'agronomics.maquila.product_year'
+ company = fields.Many2One(
+ 'company.company', "Company", required=True, select=True, readonly=True)
+ party = fields.Many2One('party.party', "Party", required=True, readonly=True,
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['company'])
+ crop = fields.Many2One('agronomics.crop', "Crop", required=True,
+ readonly=True)
+ product = fields.Many2One('product.product', "Product", required=True,
+ readonly=True, context={
+ 'company': Eval('company', None),
+ }, depends=['company'])
+ quantity = fields.Function(fields.Float("Quantity",
+ digits=(16, Eval('unit_digits', 2)),
+ depends=['unit_digits']), 'get_quantity')
+ delivered_quantity = fields.Function(fields.Float("Delivered Quantity",
+ digits=(16, Eval('unit_digits', 2)),
+ depends=['unit_digits']), 'get_delivered_quantity')
+ pending_delivered_quantity = fields.Function(fields.Float(
+ "Pending Delivered Quantity",
+ digits=(16, Eval('unit_digits', 2)),
+ depends=['unit_digits']), 'get_delivered_quantity')
+ unit = fields.Many2One('product.uom', "Unit", required=True, readonly=True,
+ ondelete='RESTRICT', domain=[
+ If(Bool(Eval('product_uom_category')),
+ ('category', '=', Eval('product_uom_category')),
+ ('category', '!=', -1)),
+ ],
+ depends=['product_uom_category'])
+ unit_digits = fields.Function(fields.Integer("Unit Digits"),
+ 'on_change_with_unit_digits')
+ product_uom_category = fields.Function(
+ fields.Many2One('product.uom.category', "Product Uom Category"),
+ 'on_change_with_product_uom_category')
+ contract_crops = fields.Many2Many(
+ 'agronomics.maquila.product_year-agronomics.maquila.contract.crop',
+ 'product_year', 'contract_crop', "Contract Crops", readonly=True)
+ contracts = fields.Function(fields.One2Many('agronomics.maquila.contract',
+ None, "Contracts"), 'get_contracts', searcher='search_contracts')
+
+ @classmethod
+ def __setup__(cls):
+ super(ProductYear, cls).__setup__()
+ cls._order = [
+ ('id', 'DESC'),
+ ]
+
+ def get_rec_name(self, name):
+ items = []
+ items.append(self.party.rec_name)
+ items.append('[%s]' % self.crop.rec_name)
+ return ' '.join(items)
+
+ @classmethod
+ def search_rec_name(cls, name, clause):
+ _, operator, value = clause
+ if operator.startswith('!') or operator.startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ domain = [bool_op,
+ ('party', operator, value),
+ ('crop', operator, value),
+ ]
+ return domain
+
+ @fields.depends('product')
+ def on_change_with_product_uom_category(self, name=None):
+ if self.product:
+ return self.product.default_uom_category.id
+
+ @fields.depends('unit')
+ def on_change_with_unit_digits(self, name=None):
+ if self.unit:
+ return self.unit.digits
+ return 2
+
+ @fields.depends('product', 'unit')
+ def on_change_product(self):
+ if not self.product:
+ return
+
+ category = self.product.default_uom.category
+ if not self.unit or self.unit.category != category:
+ self.unit = self.product.default_uom
+ self.unit_digits = self.product.default_uom.digits
+
+ @classmethod
+ def get_quantity(cls, product_years, name):
+ res = dict((x.id, 0) for x in product_years)
+ for product_year in product_years:
+ _sum = 0
+ for crop in product_year.contract_crops:
+ for ppercentatge in crop.contract.product_percentages:
+ if ppercentatge.product == product_year.product:
+ _sum += crop.quantity * ppercentatge.percentatge
+ res[product_year.id] = _sum
+ return res
+
+ @classmethod
+ def get_delivered_quantity(cls, product_years, names):
+ pool = Pool()
+ SaleLine = pool.get('sale.line')
+ Uom = pool.get('product.uom')
+
+ res = {n: {r.id: 0 for r in product_years} for n in names}
+
+ # get qty delivered from sales (moves)
+ product_years_delivered = {}
+ for product_year in product_years:
+ lines = SaleLine.search([
+ ('maquila', '=', product_year),
+ ('product', '=', product_year.product),
+ ('sale.state', 'not in', ['cancelled', 'draft', 'quotation']),
+ ])
+
+ _sum = 0
+ for line in lines:
+ for move in line.moves:
+ if not move.state == 'done':
+ continue
+ _sum += Uom.compute_qty(move.uom, move.quantity, product_year.unit, False)
+ product_years_delivered[product_year.id] = _sum
+
+ for name in names:
+ for product_year in product_years:
+ delivered_quantity = product_years_delivered.get(product_year.id, 0)
+ if name == 'delivered_quantity':
+ res[name][product_year.id] = delivered_quantity
+ elif name == 'pending_delivered_quantity':
+ # TODO total qty from?
+ res[name][product_year.id] = product_year.quantity - delivered_quantity
+ return res
+
+ @classmethod
+ def get_contracts(cls, product_years, name):
+ res = dict((x.id, None) for x in product_years)
+ for product_year in product_years:
+ contracts = [crop.contract.id for crop in product_year.contract_crops]
+ res[product_year.id] = contracts
+ return res
+
+ @classmethod
+ def search_contracts(cls, name, clause):
+ return [('contract_crops.contract',) + tuple(clause[1:])]
+
+
+class Maquila(ModelSQL, ModelView):
+ "Maquila"
+ __name__ = 'agronomics.maquila'
+ company = fields.Many2One(
+ 'company.company', "Company", required=True, select=True, readonly=True)
+ crop = fields.Many2One('agronomics.crop', "Crop", required=True, readonly=True)
+ party = fields.Many2One('party.party', "Party", required=True, readonly=True,
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['company'])
+ quantity = fields.Function(fields.Float("Quantity",
+ digits=(16, Eval('unit_digits', 2)),
+ depends=['unit_digits']), 'get_quantity')
+ pending_quantity = fields.Function(fields.Float("Pending Quantity",
+ digits=(16, Eval('unit_digits', 2)),
+ depends=['unit_digits']), 'get_pending_quantity')
+ product = fields.Many2One('product.product', "Product", required=True,
+ readonly=True,
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['company'])
+ unit = fields.Many2One('product.uom', "Unit", required=True, readonly=True,
+ ondelete='RESTRICT', domain=[
+ If(Bool(Eval('product_uom_category')),
+ ('category', '=', Eval('product_uom_category')),
+ ('category', '!=', -1)),
+ ],
+ depends=['product_uom_category'])
+ unit_digits = fields.Function(fields.Integer("Unit Digits"),
+ 'on_change_with_unit_digits')
+ product_uom_category = fields.Function(
+ fields.Many2One('product.uom.category', "Product Uom Category"),
+ 'on_change_with_product_uom_category')
+ weighings = fields.One2Many('agronomics.weighing', 'maquila',
+ "Weighings", readonly=True)
+ product_year = fields.Many2One('agronomics.maquila.product_year',
+ "Product Year", readonly=True)
+ table = fields.Boolean("Table", readonly=True)
+ contract_crops = fields.Many2Many(
+ 'agronomics.maquila-agronomics.maquila.contract.crop',
+ 'maquila', 'contract_crop', "Contract Crops", readonly=True)
+ contracts = fields.Function(fields.One2Many('agronomics.maquila.contract',
+ None, "Contracts"), 'get_contracts', searcher='search_contracts')
+
+ @staticmethod
+ def default_company():
+ return Transaction().context.get('company')
+
+ @classmethod
+ def __setup__(cls):
+ super(Maquila, cls).__setup__()
+ cls._order = [
+ ('id', 'DESC'),
+ ]
+
+ def get_rec_name(self, name):
+ items = []
+ items.append(self.party.rec_name)
+ items.append('[%s]' % self.crop.rec_name)
+ return ' '.join(items)
+
+ @classmethod
+ def search_rec_name(cls, name, clause):
+ _, operator, value = clause
+ if operator.startswith('!') or operator.startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ domain = [bool_op,
+ ('party', operator, value),
+ ('crop', operator, value),
+ ]
+ return domain
+
+ @fields.depends('product')
+ def on_change_with_product_uom_category(self, name=None):
+ if self.product:
+ return self.product.default_uom_category.id
+
+ @fields.depends('unit')
+ def on_change_with_unit_digits(self, name=None):
+ if self.unit:
+ return self.unit.digits
+ return 2
+
+ @classmethod
+ def get_quantity(cls, maquilas, name):
+ res = dict((x.id, 0) for x in maquilas)
+ for maquila in maquilas:
+ _sum = 0
+ for crop in maquila.contract_crops:
+ if crop.contract.product == maquila.product:
+ _sum += crop.quantity
+ res[maquila.id] = _sum
+ return res
+
+ @classmethod
+ def get_pending_quantity(cls, maquilas, name):
+ pool = Pool()
+ Weighing = pool.get('agronomics.weighing')
+ Uom = pool.get('product.uom')
+
+ res = dict((x.id, 0) for x in maquilas)
+ for maquila in maquilas:
+ weighings = Weighing.search([
+ ('maquila', '=', maquila),
+ ('product', '=', maquila.product),
+ ('state', '=', 'done'),
+ ])
+
+ _sum = 0
+ for weighing in weighings:
+ move = weighing.inventory_move
+ if move:
+ _sum += Uom.compute_qty(move.uom, move.quantity, maquila.unit, False)
+ res[maquila.id] = maquila.quantity - _sum
+ return res
+
+ @classmethod
+ def get_contracts(cls, maquilas, name):
+ res = dict((x.id, None) for x in maquilas)
+ for maquila in maquilas:
+ contracts = [crop.contract.id for crop in maquila.contract_crops]
+ res[maquila.id] = contracts
+ return res
+
+ @classmethod
+ def search_contracts(cls, name, clause):
+ return [('contract_crops.contract',) + tuple(clause[1:])]
+
+
+class MaquilaProductYearContractCrop(ModelSQL):
+ 'Party - Category'
+ __name__ = 'agronomics.maquila.product_year-agronomics.maquila.contract.crop'
+ _table = 'agronomics_maquila_product_year_contract_crop_rel'
+ product_year = fields.Many2One('agronomics.maquila.product_year', "Product Year", ondelete='CASCADE',
+ required=True, select=True)
+ contract_crop = fields.Many2One('agronomics.maquila.contract.crop', "Contract Crop",
+ ondelete='CASCADE', required=True, select=True)
+
+
+class MaquilaContractCrop(ModelSQL):
+ 'Party - Category'
+ __name__ = 'agronomics.maquila-agronomics.maquila.contract.crop'
+ _table = 'agronomics_maquila_contract_crop_rel'
+ maquila = fields.Many2One('agronomics.maquila', "Maquila", ondelete='CASCADE',
+ required=True, select=True)
+ contract_crop = fields.Many2One('agronomics.maquila.contract.crop', "Contract Crop",
+ ondelete='CASCADE', required=True, select=True)
diff --git a/maquila.xml b/maquila.xml
new file mode 100644
index 0000000..6176939
--- /dev/null
+++ b/maquila.xml
@@ -0,0 +1,427 @@
+
+
+
+
+
+
+
+
+ Maquila Contract
+
+
+
+
+
+
+
+
+
+
+
+ Maquila Contract
+
+
+
+
+
+ agronomics.maquila.configuration
+ form
+ maquila_configuration_form
+
+
+ Maquila Configuration
+ agronomics.maquila.configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ agronomics.maquila.contract
+ form
+ maquila_contract_form
+
+
+ agronomics.maquila.contract
+ tree
+
+ maquila_contract_list
+
+
+
+ Maquila Contracts
+ agronomics.maquila.contract
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Draft
+
+
+
+
+
+ Active
+
+
+
+
+
+ All
+
+
+
+
+
+
+
+
+ cancel
+ Cancel
+
+
+
+
+
+
+
+
+ active
+ Active
+
+
+
+
+
+
+
+
+ draft
+ Draft
+
+
+
+
+
+
+
+
+ done
+ Done
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User in companies
+
+
+
+
+
+
+
+
+
+ Product Years
+ agronomics.maquila.product_year
+
+
+
+ form_relate
+ agronomics.maquila.contract,-1
+
+
+
+
+ Maquilas
+ agronomics.maquila
+
+
+
+ form_relate
+ agronomics.maquila.contract,-1
+
+
+
+
+
+ agronomics.maquila.contract.crop
+ form
+ maquila_contract_crop_form
+
+
+ agronomics.maquila.contract.crop
+ tree
+
+ maquila_contract_crop_list
+
+
+
+ Maquila Contract Crops
+ agronomics.maquila.contract.crop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ agronomics.maquila.contract.product_percentage
+ form
+ maquila_contract_product_percentage_form
+
+
+ agronomics.maquila.contract.product_percentage
+ tree
+
+ maquila_contract_product_percentage_list
+
+
+
+ Maquila Contract Product Percentage
+ agronomics.maquila.contract.product_percentage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ agronomics.maquila.product_year
+ form
+ maquila_product_year_form
+
+
+ agronomics.maquila.product_year
+ tree
+
+ maquila_product_year_list
+
+
+
+ Maquila Product Year
+ agronomics.maquila.product_year
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User in companies
+
+
+
+
+
+
+
+
+
+
+ agronomics.maquila
+ form
+ maquila_form
+
+
+ agronomics.maquila
+ tree
+
+ maquila_list
+
+
+
+ Maquila
+ agronomics.maquila
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User in companies
+
+
+
+
+
+
+
+
+
+ Weighings
+ agronomics.weighing
+
+
+
+ form_relate
+ agronomics.maquila,-1
+
+
+
+
+
+
+
+
diff --git a/message.xml b/message.xml
index 46a2cf7..68fa0fa 100644
--- a/message.xml
+++ b/message.xml
@@ -54,7 +54,12 @@ this repository contains the full copyright notices and license terms. -->
The weighing "%(weighing)s" has the mark "Table" but has selected denomination of origin.
-
+
+ Total quantity is different from the quantity from the contract "%(contract)s".
+
+
+ To delete contract "%(contract)s" you must cancel it.
+
diff --git a/plot.py b/plot.py
index 89d2a3b..ae5ca8b 100644
--- a/plot.py
+++ b/plot.py
@@ -231,7 +231,8 @@ class Parcel(ModelSQL, ModelView):
)*self.surface, 2)
def get_purchased_quantity(self, name):
- return sum([(w.netweight or 0) for w in self.weighings if not w.table])
+ return sum([w.netweight for w in self.weighings
+ if w.netweight and not w.table])
def get_remaining_quantity(self, name):
return (self.max_production or 0) - (self.purchased_quantity or 0)
diff --git a/sale.py b/sale.py
new file mode 100644
index 0000000..4722de6
--- /dev/null
+++ b/sale.py
@@ -0,0 +1,125 @@
+# 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 fields
+from trytond.pool import Pool, PoolMeta
+from trytond.pyson import Bool, Eval, Id, If
+
+
+def default_func(field_name):
+ @classmethod
+ def default(cls, **pattern):
+ return getattr(
+ cls.multivalue_model(field_name),
+ 'default_%s' % field_name, lambda: None)()
+ return default
+
+
+class Configuration(metaclass=PoolMeta):
+ __name__ = 'sale.configuration'
+ maquila_sale_sequence = fields.MultiValue(fields.Many2One(
+ 'ir.sequence', "Maquila Sale Sequence", required=True,
+ domain=[
+ ('company', 'in',
+ [Eval('context', {}).get('company', -1), None]),
+ ('sequence_type', '=', Id('agronomics', 'sequence_type_maquila_sale')),
+ ]))
+
+ @classmethod
+ def multivalue_model(cls, field):
+ pool = Pool()
+ if field == 'maquila_sale_sequence':
+ return pool.get('sale.configuration.sequence')
+ return super(Configuration, cls).multivalue_model(field)
+
+ default_maquila_sale_sequence = default_func('maquila_sale_sequence')
+
+
+class ConfigurationSequence(metaclass=PoolMeta):
+ __name__ = 'sale.configuration.sequence'
+ maquila_sale_sequence = fields.Many2One(
+ 'ir.sequence', "Maquila Sale Sequence", required=True,
+ domain=[
+ ('company', 'in', [Eval('company', -1), None]),
+ ('sequence_type', '=', Id('agronomics', 'sequence_type_maquila_sale')),
+ ],
+ depends=['company'])
+
+ @classmethod
+ def default_maquila_sale_sequence(cls):
+ pool = Pool()
+ ModelData = pool.get('ir.model.data')
+ try:
+ return ModelData.get_id('agronomics', 'sequence_maquila_sale')
+ except KeyError:
+ return None
+
+
+class Sale(metaclass=PoolMeta):
+ __name__ = 'sale.sale'
+ is_maquila = fields.Boolean("Is Maquila",
+ states={
+ 'readonly': ((Eval('state') != 'draft') | (Eval('lines', [0]))),
+ },
+ depends=['state'])
+
+ @fields.depends('is_maquila')
+ def on_change_is_maquila(self):
+ self.invoice_method = ('manual' if self.is_maquila
+ else self.default_invoice_method())
+
+ @fields.depends('is_maquila')
+ def on_change_party(self):
+ super().on_change_party()
+ if self.is_maquila:
+ self.on_change_is_maquila()
+
+ @classmethod
+ def set_number(cls, sales):
+ pool = Pool()
+ Config = pool.get('sale.configuration')
+
+ config = Config(1)
+ for sale in sales:
+ if sale.number:
+ continue
+ if sale.is_maquila:
+ sale.number = config.get_multivalue(
+ 'maquila_sale_sequence', company=sale.company.id).get()
+ super().set_number(sales)
+
+
+class SaleLine(metaclass=PoolMeta):
+ __name__ = 'sale.line'
+ maquila = fields.Many2One('agronomics.maquila.product_year', "Maquila",
+ domain=[
+ ('product', '=', Eval('product')),
+ ('party', '=', Eval('_parent_sale', {}).get('party')),
+ ],
+ states={
+ 'invisible': ~Bool(Eval('_parent_sale', {}).get('is_maquila')),
+ 'required': Bool(Eval('_parent_sale', {}).get('is_maquila')),
+ 'readonly': Eval('sale_state') != 'draft',
+ })
+ liter_uom = fields.Function(fields.Many2One('product.uom', "Liter Uom"),
+ 'get_liter_uom')
+
+ @classmethod
+ def __setup__(cls):
+ super(SaleLine, cls).__setup__()
+ cls.product.domain += [If( Bool(Eval('_parent_sale', {}).get('is_maquila')),
+ ('sale_uom', '=', Eval('liter_uom')),
+ ())]
+ cls.product.depends |= {'liter_uom'}
+
+ @classmethod
+ def default_liter_uom(cls):
+ pool = Pool()
+ ModelData = pool.get('ir.model.data')
+
+ uom_liter_id = ModelData.get_id('product', 'uom_liter')
+ return uom_liter_id
+
+ @classmethod
+ def get_liter_uom(cls, lines, name):
+ uom_liter_id = cls.default_liter_uom()
+ return dict((x.id, uom_liter_id) for x in lines)
diff --git a/sale.xml b/sale.xml
new file mode 100644
index 0000000..e95ee4e
--- /dev/null
+++ b/sale.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ Maquila Sale
+
+
+
+
+
+
+
+
+
+
+
+ Maquila Sale
+
+
+
+
+ sale.configuration
+
+ sale_configuration_form
+
+
+
+ sale.sale
+
+ sale_tree
+
+
+ sale.sale
+
+ sale_form
+
+
+ sale.line
+
+ sale_line_tree_sequence
+
+
+ sale.line
+
+ sale_line_form
+
+
+
diff --git a/tests/test_module.py b/tests/test_module.py
index 6ae742c..9637338 100644
--- a/tests/test_module.py
+++ b/tests/test_module.py
@@ -9,6 +9,6 @@ from trytond.tests.test_tryton import ModuleTestCase
class AgronomicsTestCase(CompanyTestMixin, ModuleTestCase):
'Test Agronomics module'
module = 'agronomics'
-
+ extras = ['sale']
del ModuleTestCase
diff --git a/tryton.cfg b/tryton.cfg
index 5584f52..84cdd02 100644
--- a/tryton.cfg
+++ b/tryton.cfg
@@ -3,6 +3,7 @@ version=6.4.0
depends:
ir
res
+ company
party
product_classification
product_classification_taxonomic
@@ -13,6 +14,8 @@ depends:
stock
account_invoice
account_invoice_line_standalone
+extras_depend:
+ sale
xml:
plot.xml
party.xml
@@ -25,4 +28,6 @@ xml:
quality.xml
contract.xml
invoice.xml
+ maquila.xml
+ sale.xml
history.xml
diff --git a/view/maquila_configuration_form.xml b/view/maquila_configuration_form.xml
new file mode 100644
index 0000000..e9e5973
--- /dev/null
+++ b/view/maquila_configuration_form.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/view/maquila_contract_crop_form.xml b/view/maquila_contract_crop_form.xml
new file mode 100644
index 0000000..d828e51
--- /dev/null
+++ b/view/maquila_contract_crop_form.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/view/maquila_contract_crop_list.xml b/view/maquila_contract_crop_list.xml
new file mode 100644
index 0000000..a482958
--- /dev/null
+++ b/view/maquila_contract_crop_list.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/view/maquila_contract_form.xml b/view/maquila_contract_form.xml
new file mode 100644
index 0000000..ea33967
--- /dev/null
+++ b/view/maquila_contract_form.xml
@@ -0,0 +1,37 @@
+
+
diff --git a/view/maquila_contract_list.xml b/view/maquila_contract_list.xml
new file mode 100644
index 0000000..6f3dbc8
--- /dev/null
+++ b/view/maquila_contract_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/maquila_contract_product_percentage_form.xml b/view/maquila_contract_product_percentage_form.xml
new file mode 100644
index 0000000..f76746c
--- /dev/null
+++ b/view/maquila_contract_product_percentage_form.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/view/maquila_contract_product_percentage_list.xml b/view/maquila_contract_product_percentage_list.xml
new file mode 100644
index 0000000..612ed75
--- /dev/null
+++ b/view/maquila_contract_product_percentage_list.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/view/maquila_form.xml b/view/maquila_form.xml
new file mode 100644
index 0000000..0faa13e
--- /dev/null
+++ b/view/maquila_form.xml
@@ -0,0 +1,22 @@
+
+
diff --git a/view/maquila_list.xml b/view/maquila_list.xml
new file mode 100644
index 0000000..31f7992
--- /dev/null
+++ b/view/maquila_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/maquila_product_year_form.xml b/view/maquila_product_year_form.xml
new file mode 100644
index 0000000..f92d1d5
--- /dev/null
+++ b/view/maquila_product_year_form.xml
@@ -0,0 +1,22 @@
+
+
diff --git a/view/maquila_product_year_list.xml b/view/maquila_product_year_list.xml
new file mode 100644
index 0000000..260740c
--- /dev/null
+++ b/view/maquila_product_year_list.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/sale_configuration_form.xml b/view/sale_configuration_form.xml
new file mode 100644
index 0000000..88df58e
--- /dev/null
+++ b/view/sale_configuration_form.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/view/sale_form.xml b/view/sale_form.xml
new file mode 100644
index 0000000..9af60a2
--- /dev/null
+++ b/view/sale_form.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/view/sale_line_form.xml b/view/sale_line_form.xml
new file mode 100644
index 0000000..d8faa1d
--- /dev/null
+++ b/view/sale_line_form.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/view/sale_line_tree_sequence.xml b/view/sale_line_tree_sequence.xml
new file mode 100644
index 0000000..9130e19
--- /dev/null
+++ b/view/sale_line_tree_sequence.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/view/sale_tree.xml b/view/sale_tree.xml
new file mode 100644
index 0000000..31139e8
--- /dev/null
+++ b/view/sale_tree.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/view/weighing_form.xml b/view/weighing_form.xml
index 6459085..66909ad 100644
--- a/view/weighing_form.xml
+++ b/view/weighing_form.xml
@@ -15,6 +15,10 @@
+
+
+
+
@@ -31,7 +35,6 @@
-
diff --git a/weighing.py b/weighing.py
index d14a0f7..db0f572 100644
--- a/weighing.py
+++ b/weighing.py
@@ -25,6 +25,7 @@ class WeighingCenter(ModelSQL, ModelView):
READONLY = ['processing', 'distributed', 'in_analysis', 'done', 'cancelled']
READONLY2 = ['draft', 'distributed', 'in_analysis', 'done', 'cancelled']
+
class Weighing(Workflow, ModelSQL, ModelView):
""" Weighing """
__name__ = 'agronomics.weighing'
@@ -83,7 +84,8 @@ class Weighing(Workflow, ModelSQL, ModelView):
'Beneficiaries', states={
'readonly': Eval('state').in_(READONLY2),
'required': Eval('state') == 'in_analysis',
- })
+ 'invisible': Eval('is_maquila', True),
+ }, depends=['is_maquila', 'state'])
denomination_origin = fields.Many2Many('agronomics.weighing-agronomics.do',
'weighing', 'do', 'Denomination of Origin', states={
'readonly': Eval('state').in_(READONLY2) | Bool(Eval('table')),
@@ -91,7 +93,9 @@ class Weighing(Workflow, ModelSQL, ModelView):
})
beneficiaries_invoices_line = fields.Many2Many(
'agronomics.weighing-account.invoice.line', 'weighing', 'invoice_line',
- "Beneficiaries Invoices", readonly=True)
+ "Beneficiaries Invoices", readonly=True, states={
+ 'invisible': Eval('is_maquila', True),
+ }, depends=['is_maquila'])
plantations = fields.One2Many('agronomics.weighing-agronomics.plantation',
'weighing', 'plantations', states={
'readonly': Eval('state').in_(READONLY),
@@ -121,7 +125,20 @@ class Weighing(Workflow, ModelSQL, ModelView):
forced_analysis = fields.Boolean('Forced Analysis', readonly=True)
inventory_move = fields.Many2One('stock.move', "Inventory Move",
readonly=True)
-
+ is_maquila = fields.Boolean("Is Maquila", states={
+ 'readonly': Eval('state').in_(READONLY),
+ }, depends=['state'])
+ # TODO weighing table is readonly when is draft
+ maquila = fields.Many2One('agronomics.maquila', "Maquila",
+ domain=[
+ ('table', '=', Bool(Eval('table', False))),
+ ('product', '=', Eval('product')),
+ ],
+ states={
+ 'readonly': Eval('state').in_(['in_analysis', 'done', 'cancelled']),
+ 'invisible': ~Eval('is_maquila', False),
+ 'required': (Bool(Eval('is_maquila', False)) & (Eval('state') == 'in_analysis')),
+ }, depends=['is_maquila', 'table', 'product', 'state'])
@classmethod
def __setup__(cls):
@@ -398,8 +415,8 @@ class Weighing(Workflow, ModelSQL, ModelView):
cls.analysis(to_analysis)
def get_not_assigned_weight(self, name):
- return (self.netweight or 0) - sum([(p.netweight or 0)
- for p in self.parcels])
+ if self.netweight:
+ return self.netweight - sum([p.netweight or 0 for p in self.parcels])
@classmethod
@ModelView.button
@@ -448,7 +465,7 @@ class Weighing(Workflow, ModelSQL, ModelView):
InvoiceLine = pool.get('account.invoice.line')
Product = pool.get('product.product')
Company = pool.get('company.company')
- context = Transaction().context
+
ContractProductPriceListTypePriceList = pool.get(
'agronomics.contract-product.price_list.type-product.price_list')
WeighingInvoiceLine = pool.get(
@@ -457,6 +474,10 @@ class Weighing(Workflow, ModelSQL, ModelView):
type='wizard')
Move = pool.get('stock.move')
+ context = Transaction().context
+
+ company = Company(context['company'])
+
default_invoice_line_values = InvoiceLine.default_get(
InvoiceLine._fields.keys(), with_rec_name=False)
invoice_line = InvoiceLine(**default_invoice_line_values)
@@ -466,7 +487,8 @@ class Weighing(Workflow, ModelSQL, ModelView):
to_recompute_products = []
for weighing in weighings:
cost_price = Decimal(0)
- if weighing.beneficiaries:
+
+ if not weighing.is_maquila and weighing.beneficiaries:
for beneficiary in weighing.beneficiaries:
price_list = ContractProductPriceListTypePriceList.search([
('contract', '=', weighing.purchase_contract),
@@ -478,9 +500,8 @@ class Weighing(Workflow, ModelSQL, ModelView):
invoice_line.type = 'line'
invoice_line.invoice_type = 'in'
invoice_line.party = beneficiary.party
- invoice_line.currency = (
- Company(context['company']).currency)
- invoice_line.company = Company(context['company'])
+ invoice_line.currency = company.currency
+ invoice_line.company = company
invoice_line.description = ''
invoice_line.product = weighing.product_created
invoice_line.on_change_product()
@@ -524,6 +545,8 @@ class Weighing(Workflow, ModelSQL, ModelView):
recompute_cost_price.start.from_ = default_values['from_']
recompute_cost_price.transition_recompute()
+ cls.save(weighings)
+
WeighingInvoiceLine.save(to_save)
Move.save(to_save_moves)