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