diff --git a/__init__.py b/__init__.py index f5f710c58a..810de6c295 100644 --- a/__init__.py +++ b/__init__.py @@ -3,9 +3,10 @@ from trytond.pool import Pool from .purchase import * +from .product import * +from .stock import * from .configuration import * from .invoice import * -from .stock import * def register(): diff --git a/product.py b/product.py new file mode 100644 index 0000000000..bb7ec93278 --- /dev/null +++ b/product.py @@ -0,0 +1,276 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +import datetime +from sql import Literal +from sql.aggregate import Count + +from trytond.model import ModelView, ModelSQL, fields +from trytond.pyson import Eval, If +from trytond.pool import Pool, PoolMeta +from trytond.transaction import Transaction +from trytond import backend + +__all__ = ['Template', 'Product', 'ProductSupplier', 'ProductSupplierPrice'] +__metaclass__ = PoolMeta + + +class Template: + __name__ = "product.template" + purchasable = fields.Boolean('Purchasable', states={ + 'readonly': ~Eval('active', True), + }, depends=['active']) + product_suppliers = fields.One2Many('purchase.product_supplier', + 'product', 'Suppliers', states={ + 'readonly': ~Eval('active', True), + 'invisible': (~Eval('purchasable', False) + | ~Eval('context', {}).get('company', 0)), + }, depends=['active', 'purchasable']) + purchase_uom = fields.Many2One('product.uom', 'Purchase UOM', states={ + 'readonly': ~Eval('active'), + 'invisible': ~Eval('purchasable'), + 'required': Eval('purchasable', False), + }, + domain=[('category', '=', Eval('default_uom_category'))], + on_change_with=['default_uom', 'purchase_uom', 'purchasable'], + depends=['active', 'purchasable', 'default_uom_category']) + + @classmethod + def __setup__(cls): + super(Template, cls).__setup__() + cls._error_messages.update({ + 'change_purchase_uom': ('Purchase prices are based ' + 'on the purchase uom, are you sure to change it?'), + }) + required = ~Eval('account_category') & Eval('purchasable', False) + if not cls.account_expense.states.get('required'): + cls.account_expense.states['required'] = required + else: + cls.account_expense.states['required'] = ( + cls.account_expense.states['required'] | required) + if 'account_category' not in cls.account_expense.depends: + cls.account_expense.depends.append('account_category') + if 'purchasable' not in cls.account_expense.depends: + cls.account_expense.depends.append('purchasable') + + def on_change_with_purchase_uom(self): + if self.default_uom: + if self.purchase_uom: + if self.default_uom.category == self.purchase_uom.category: + return self.purchase_uom.id + else: + return self.default_uom.id + else: + return self.default_uom.id + + @classmethod + def write(cls, templates, vals): + if vals.get("purchase_uom"): + for template in templates: + if not template.purchase_uom: + continue + if template.purchase_uom.id == vals["purchase_uom"]: + continue + for product in template.products: + if not product.product_suppliers: + continue + cls.raise_user_warning( + '%s@product_template' % template.id, + 'change_purchase_uom') + super(Template, cls).write(templates, vals) + + +class Product: + __name__ = 'product.product' + + @classmethod + def get_purchase_price(cls, products, quantity=0): + ''' + Return purchase price for product ids. + The context that can have as keys: + uom: the unit of measure + supplier: the supplier party id + currency: the currency id for the returned price + ''' + pool = Pool() + Uom = pool.get('product.uom') + User = pool.get('res.user') + Currency = pool.get('currency.currency') + Date = pool.get('ir.date') + + today = Date.today() + res = {} + + uom = None + if Transaction().context.get('uom'): + uom = Uom(Transaction().context['uom']) + + currency = None + if Transaction().context.get('currency'): + currency = Currency(Transaction().context['currency']) + + user = User(Transaction().user) + + for product in products: + res[product.id] = product.cost_price + default_uom = product.default_uom + default_currency = (user.company.currency if user.company + else None) + if not uom: + uom = default_uom + if (Transaction().context.get('supplier') + and product.product_suppliers): + supplier_id = Transaction().context['supplier'] + for product_supplier in product.product_suppliers: + if product_supplier.party.id == supplier_id: + for price in product_supplier.prices: + if Uom.compute_qty(product.purchase_uom, + price.quantity, uom) <= quantity: + res[product.id] = price.unit_price + default_uom = product.purchase_uom + default_currency = product_supplier.currency + break + res[product.id] = Uom.compute_price(default_uom, res[product.id], + uom) + if currency and default_currency: + date = Transaction().context.get('purchase_date') or today + with Transaction().set_context(date=date): + res[product.id] = Currency.compute(default_currency, + res[product.id], currency, round=False) + return res + + +class ProductSupplier(ModelSQL, ModelView): + 'Product Supplier' + __name__ = 'purchase.product_supplier' + product = fields.Many2One('product.template', 'Product', required=True, + ondelete='CASCADE', select=True) + party = fields.Many2One('party.party', 'Supplier', required=True, + ondelete='CASCADE', select=True, on_change=['party']) + name = fields.Char('Name', size=None, translate=True, select=True) + code = fields.Char('Code', size=None, select=True) + sequence = fields.Integer('Sequence') + prices = fields.One2Many('purchase.product_supplier.price', + 'product_supplier', 'Prices') + company = fields.Many2One('company.company', 'Company', required=True, + ondelete='CASCADE', select=True, + domain=[ + ('id', If(Eval('context', {}).contains('company'), '=', '!='), + Eval('context', {}).get('company', 0)), + ]) + delivery_time = fields.Integer('Delivery Time', help="In number of days") + currency = fields.Many2One('currency.currency', 'Currency', required=True, + ondelete='RESTRICT') + + @classmethod + def __setup__(cls): + super(ProductSupplier, cls).__setup__() + cls._order.insert(0, ('sequence', 'ASC')) + + @classmethod + def __register__(cls, module_name): + TableHandler = backend.get('TableHandler') + cursor = Transaction().cursor + table = TableHandler(cursor, cls, module_name) + sql_table = cls.__table__() + + # Migration from 2.2 new field currency + created_currency = table.column_exist('currency') + + super(ProductSupplier, cls).__register__(module_name) + + # Migration from 2.2 fill currency + if not created_currency: + Company = Pool().get('company.company') + company = Company.__table__() + limit = cursor.IN_MAX + cursor.execute(*sql_table.select(Count(sql_table.id))) + product_supplier_count, = cursor.fetchone() + for offset in range(0, product_supplier_count, limit): + cursor.execute(*sql_table.join(company, + condition=sql_table.company == company.id + ).select(sql_table.id, company.currency, + order_by=sql_table.id, + limit=limit, offset=offset)) + for product_supplier_id, currency_id in cursor.fetchall(): + cursor.execute(*sql_table.update( + columns=[sql_table.currency], + values=[currency_id], + where=sql_table.id == product_supplier_id)) + + # Migration from 2.4: drop required on sequence + table.not_null_action('sequence', action='remove') + + # Migration from 2.6: drop required on delivery_time + table.not_null_action('delivery_time', action='remove') + + @staticmethod + def order_sequence(tables): + table, _ = tables[None] + return [table.sequence == None, table.sequence] + + @staticmethod + def default_company(): + return Transaction().context.get('company') + + @staticmethod + def default_currency(): + Company = Pool().get('company.company') + if Transaction().context.get('company'): + company = Company(Transaction().context['company']) + return company.currency.id + + def on_change_party(self): + cursor = Transaction().cursor + changes = { + 'currency': self.default_currency(), + } + if self.party: + table = self.__table__() + cursor.execute(*table.select(table.currency, + where=table.party == self.party.id, + group_by=table.currency, + order_by=Count(Literal(1)).desc)) + row = cursor.fetchone() + if row: + changes['currency'], = row + return changes + + def compute_supply_date(self, date=None): + ''' + Compute the supply date for the Product Supplier at the given date + ''' + Date = Pool().get('ir.date') + + if not date: + date = Date.today() + if self.delivery_time is None: + return datetime.date.max + return date + datetime.timedelta(self.delivery_time) + + def compute_purchase_date(self, date): + ''' + Compute the purchase date for the Product Supplier at the given date + ''' + Date = Pool().get('ir.date') + + if self.delivery_time is None: + return Date.today() + return date - datetime.timedelta(self.delivery_time) + + +class ProductSupplierPrice(ModelSQL, ModelView): + 'Product Supplier Price' + __name__ = 'purchase.product_supplier.price' + product_supplier = fields.Many2One('purchase.product_supplier', + 'Supplier', required=True, ondelete='CASCADE') + quantity = fields.Float('Quantity', required=True, help='Minimal quantity') + unit_price = fields.Numeric('Unit Price', required=True, digits=(16, 4)) + + @classmethod + def __setup__(cls): + super(ProductSupplierPrice, cls).__setup__() + cls._order.insert(0, ('quantity', 'ASC')) + + @staticmethod + def default_quantity(): + return 0.0 diff --git a/purchase.py b/purchase.py index 416700bb6b..cddac168b9 100644 --- a/purchase.py +++ b/purchase.py @@ -6,7 +6,6 @@ from decimal import Decimal from sql import Table, Literal from sql.functions import Overlay, Position from sql.aggregate import Count -from sql.operators import Concat from trytond.model import Workflow, ModelView, ModelSQL, fields from trytond.modules.company import CompanyReport @@ -19,12 +18,9 @@ from trytond.pool import Pool, PoolMeta __all__ = ['Purchase', 'PurchaseInvoice', 'PurchaseIgnoredInvoice', 'PurchaseRecreadtedInvoice', 'PurchaseLine', 'PurchaseLineTax', - 'PurchaseLineIgnoredMove', - 'PurchaseLineRecreatedMove', 'PurchaseReport', 'Template', 'Product', - 'ProductSupplier', 'ProductSupplierPrice', 'ShipmentIn', - 'ShipmentInReturn', 'Move', 'OpenSupplier', 'HandleShipmentExceptionAsk', - 'HandleShipmentException', 'HandleInvoiceExceptionAsk', - 'HandleInvoiceException'] + 'PurchaseLineIgnoredMove', 'PurchaseLineRecreatedMove', 'PurchaseReport', + 'OpenSupplier', 'HandleShipmentExceptionAsk', 'HandleShipmentException', + 'HandleInvoiceExceptionAsk', 'HandleInvoiceException'] __metaclass__ = PoolMeta _STATES = { @@ -1320,543 +1316,6 @@ class PurchaseReport(CompanyReport): __name__ = 'purchase.purchase' -class Template: - __name__ = "product.template" - purchasable = fields.Boolean('Purchasable', states={ - 'readonly': ~Eval('active', True), - }, depends=['active']) - product_suppliers = fields.One2Many('purchase.product_supplier', - 'product', 'Suppliers', states={ - 'readonly': ~Eval('active', True), - 'invisible': (~Eval('purchasable', False) - | ~Eval('context', {}).get('company', 0)), - }, depends=['active', 'purchasable']) - purchase_uom = fields.Many2One('product.uom', 'Purchase UOM', states={ - 'readonly': ~Eval('active'), - 'invisible': ~Eval('purchasable'), - 'required': Eval('purchasable', False), - }, - domain=[('category', '=', Eval('default_uom_category'))], - on_change_with=['default_uom', 'purchase_uom', 'purchasable'], - depends=['active', 'purchasable', 'default_uom_category']) - - @classmethod - def __setup__(cls): - super(Template, cls).__setup__() - cls._error_messages.update({ - 'change_purchase_uom': ('Purchase prices are based ' - 'on the purchase uom, are you sure to change it?'), - }) - required = ~Eval('account_category') & Eval('purchasable', False) - if not cls.account_expense.states.get('required'): - cls.account_expense.states['required'] = required - else: - cls.account_expense.states['required'] = ( - cls.account_expense.states['required'] | required) - if 'account_category' not in cls.account_expense.depends: - cls.account_expense.depends.append('account_category') - if 'purchasable' not in cls.account_expense.depends: - cls.account_expense.depends.append('purchasable') - - def on_change_with_purchase_uom(self): - if self.default_uom: - if self.purchase_uom: - if self.default_uom.category == self.purchase_uom.category: - return self.purchase_uom.id - else: - return self.default_uom.id - else: - return self.default_uom.id - - @classmethod - def write(cls, templates, vals): - if vals.get("purchase_uom"): - for template in templates: - if not template.purchase_uom: - continue - if template.purchase_uom.id == vals["purchase_uom"]: - continue - for product in template.products: - if not product.product_suppliers: - continue - cls.raise_user_warning( - '%s@product_template' % template.id, - 'change_purchase_uom') - super(Template, cls).write(templates, vals) - - -class Product: - __name__ = 'product.product' - - @classmethod - def get_purchase_price(cls, products, quantity=0): - ''' - Return purchase price for product ids. - The context that can have as keys: - uom: the unit of measure - supplier: the supplier party id - currency: the currency id for the returned price - ''' - pool = Pool() - Uom = pool.get('product.uom') - User = pool.get('res.user') - Currency = pool.get('currency.currency') - Date = pool.get('ir.date') - - today = Date.today() - res = {} - - uom = None - if Transaction().context.get('uom'): - uom = Uom(Transaction().context['uom']) - - currency = None - if Transaction().context.get('currency'): - currency = Currency(Transaction().context['currency']) - - user = User(Transaction().user) - - for product in products: - res[product.id] = product.cost_price - default_uom = product.default_uom - default_currency = (user.company.currency if user.company - else None) - if not uom: - uom = default_uom - if (Transaction().context.get('supplier') - and product.product_suppliers): - supplier_id = Transaction().context['supplier'] - for product_supplier in product.product_suppliers: - if product_supplier.party.id == supplier_id: - for price in product_supplier.prices: - if Uom.compute_qty(product.purchase_uom, - price.quantity, uom) <= quantity: - res[product.id] = price.unit_price - default_uom = product.purchase_uom - default_currency = product_supplier.currency - break - res[product.id] = Uom.compute_price(default_uom, res[product.id], - uom) - if currency and default_currency: - date = Transaction().context.get('purchase_date') or today - with Transaction().set_context(date=date): - res[product.id] = Currency.compute(default_currency, - res[product.id], currency, round=False) - return res - - -class ProductSupplier(ModelSQL, ModelView): - 'Product Supplier' - __name__ = 'purchase.product_supplier' - product = fields.Many2One('product.template', 'Product', required=True, - ondelete='CASCADE', select=True) - party = fields.Many2One('party.party', 'Supplier', required=True, - ondelete='CASCADE', select=True, on_change=['party']) - name = fields.Char('Name', size=None, translate=True, select=True) - code = fields.Char('Code', size=None, select=True) - sequence = fields.Integer('Sequence') - prices = fields.One2Many('purchase.product_supplier.price', - 'product_supplier', 'Prices') - company = fields.Many2One('company.company', 'Company', required=True, - ondelete='CASCADE', select=True, - domain=[ - ('id', If(Eval('context', {}).contains('company'), '=', '!='), - Eval('context', {}).get('company', 0)), - ]) - delivery_time = fields.Integer('Delivery Time', help="In number of days") - currency = fields.Many2One('currency.currency', 'Currency', required=True, - ondelete='RESTRICT') - - @classmethod - def __setup__(cls): - super(ProductSupplier, cls).__setup__() - cls._order.insert(0, ('sequence', 'ASC')) - - @classmethod - def __register__(cls, module_name): - TableHandler = backend.get('TableHandler') - cursor = Transaction().cursor - table = TableHandler(cursor, cls, module_name) - sql_table = cls.__table__() - - # Migration from 2.2 new field currency - created_currency = table.column_exist('currency') - - super(ProductSupplier, cls).__register__(module_name) - - # Migration from 2.2 fill currency - if not created_currency: - Company = Pool().get('company.company') - company = Company.__table__() - limit = cursor.IN_MAX - cursor.execute(*sql_table.select(Count(sql_table.id))) - product_supplier_count, = cursor.fetchone() - for offset in range(0, product_supplier_count, limit): - cursor.execute(*sql_table.join(company, - condition=sql_table.company == company.id - ).select(sql_table.id, company.currency, - order_by=sql_table.id, - limit=limit, offset=offset)) - for product_supplier_id, currency_id in cursor.fetchall(): - cursor.execute(*sql_table.update( - columns=[sql_table.currency], - values=[currency_id], - where=sql_table.id == product_supplier_id)) - - # Migration from 2.4: drop required on sequence - table.not_null_action('sequence', action='remove') - - # Migration from 2.6: drop required on delivery_time - table.not_null_action('delivery_time', action='remove') - - @staticmethod - def order_sequence(tables): - table, _ = tables[None] - return [table.sequence == None, table.sequence] - - @staticmethod - def default_company(): - return Transaction().context.get('company') - - @staticmethod - def default_currency(): - Company = Pool().get('company.company') - if Transaction().context.get('company'): - company = Company(Transaction().context['company']) - return company.currency.id - - def on_change_party(self): - cursor = Transaction().cursor - changes = { - 'currency': self.default_currency(), - } - if self.party: - table = self.__table__() - cursor.execute(*table.select(table.currency, - where=table.party == self.party.id, - group_by=table.currency, - order_by=Count(Literal(1)).desc)) - row = cursor.fetchone() - if row: - changes['currency'], = row - return changes - - def compute_supply_date(self, date=None): - ''' - Compute the supply date for the Product Supplier at the given date - ''' - Date = Pool().get('ir.date') - - if not date: - date = Date.today() - if self.delivery_time is None: - return datetime.date.max - return date + datetime.timedelta(self.delivery_time) - - def compute_purchase_date(self, date): - ''' - Compute the purchase date for the Product Supplier at the given date - ''' - Date = Pool().get('ir.date') - - if self.delivery_time is None: - return Date.today() - return date - datetime.timedelta(self.delivery_time) - - -class ProductSupplierPrice(ModelSQL, ModelView): - 'Product Supplier Price' - __name__ = 'purchase.product_supplier.price' - product_supplier = fields.Many2One('purchase.product_supplier', - 'Supplier', required=True, ondelete='CASCADE') - quantity = fields.Float('Quantity', required=True, help='Minimal quantity') - unit_price = fields.Numeric('Unit Price', required=True, digits=(16, 4)) - - @classmethod - def __setup__(cls): - super(ProductSupplierPrice, cls).__setup__() - cls._order.insert(0, ('quantity', 'ASC')) - - @staticmethod - def default_quantity(): - return 0.0 - - -class ShipmentIn: - __name__ = 'stock.shipment.in' - - @classmethod - def __setup__(cls): - super(ShipmentIn, cls).__setup__() - add_remove = [ - ('supplier', '=', Eval('supplier')), - ] - if not cls.incoming_moves.add_remove: - cls.incoming_moves.add_remove = add_remove - else: - cls.incoming_moves.add_remove = [ - add_remove, - cls.incoming_moves.add_remove, - ] - if 'supplier' not in cls.incoming_moves.depends: - cls.incoming_moves.depends.append('supplier') - - cls._error_messages.update({ - 'reset_move': ('You cannot reset to draft move "%s" because ' - 'it was generated by a purchase.'), - }) - - @classmethod - def write(cls, shipments, vals): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - super(ShipmentIn, cls).write(shipments, vals) - - if 'state' in vals and vals['state'] in ('received', 'cancel'): - purchases = [] - move_ids = [] - for shipment in shipments: - move_ids.extend([x.id for x in shipment.incoming_moves]) - - purchase_lines = PurchaseLine.search([ - ('moves', 'in', move_ids), - ]) - if purchase_lines: - for purchase_line in purchase_lines: - if purchase_line.purchase not in purchases: - purchases.append(purchase_line.purchase) - - with Transaction().set_user(0, set_context=True): - purchases = Purchase.browse([p.id for p in purchases]) - Purchase.process(purchases) - - @classmethod - @ModelView.button - @Workflow.transition('draft') - def draft(cls, shipments): - PurchaseLine = Pool().get('purchase.line') - for shipment in shipments: - for move in shipment.incoming_moves: - if (move.state == 'cancel' - and isinstance(move.origin, PurchaseLine)): - cls.raise_user_error('reset_move', (move.rec_name,)) - - return super(ShipmentIn, cls).draft(shipments) - - -class ShipmentInReturn: - __name__ = 'stock.shipment.in.return' - - @classmethod - def __setup__(cls): - super(ShipmentInReturn, cls).__setup__() - cls._error_messages.update({ - 'reset_move': ('You cannot reset to draft a move generated ' - 'by a purchase.'), - }) - - @classmethod - def write(cls, shipments, vals): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - super(ShipmentInReturn, cls).write(shipments, vals) - - if 'state' in vals and vals['state'] == 'done': - move_ids = [] - for shipment in shipments: - move_ids.extend([x.id for x in shipment.moves]) - - purchase_lines = PurchaseLine.search([ - ('moves', 'in', move_ids), - ]) - purchases = set() - if purchase_lines: - for purchase_line in purchase_lines: - purchases.add(purchase_line.purchase) - - with Transaction().set_user(0, set_context=True): - purchases = Purchase.browse([p.id for p in purchases]) - Purchase.process(purchases) - - @classmethod - @ModelView.button - @Workflow.transition('draft') - def draft(cls, shipments): - PurchaseLine = Pool().get('purchase.line') - for shipment in shipments: - for move in shipment.moves: - if (move.state == 'cancel' - and isinstance(move.origin, PurchaseLine)): - cls.raise_user_error('reset_move') - - return super(ShipmentInReturn, cls).draft(shipments) - - -class Move(ModelSQL, ModelView): - __name__ = 'stock.move' - purchase = fields.Function(fields.Many2One('purchase.purchase', 'Purchase', - states={ - 'invisible': ~Eval('purchase_visible', False), - }, - depends=['purchase_visible']), 'get_purchase', - searcher='search_purchase') - purchase_quantity = fields.Function(fields.Float('Purchase Quantity', - digits=(16, Eval('unit_digits', 2)), - states={ - 'invisible': ~Eval('purchase_visible', False), - }, - depends=['purchase_visible', 'unit_digits']), - 'get_purchase_fields') - purchase_unit = fields.Function(fields.Many2One('product.uom', - 'Purchase Unit', states={ - 'invisible': ~Eval('purchase_visible', False), - }, depends=['purchase_visible']), 'get_purchase_fields') - purchase_unit_digits = fields.Function(fields.Integer( - 'Purchase Unit Digits'), 'get_purchase_fields') - purchase_unit_price = fields.Function(fields.Numeric('Purchase Unit Price', - digits=(16, 4), states={ - 'invisible': ~Eval('purchase_visible', False), - }, depends=['purchase_visible']), 'get_purchase_fields') - purchase_currency = fields.Function(fields.Many2One('currency.currency', - 'Purchase Currency', states={ - 'invisible': ~Eval('purchase_visible', False), - }, depends=['purchase_visible']), 'get_purchase_fields') - purchase_visible = fields.Function(fields.Boolean('Purchase Visible', - on_change_with=['from_location']), 'on_change_with_purchase_visible') - supplier = fields.Function(fields.Many2One('party.party', 'Supplier'), - 'get_supplier', searcher='search_supplier') - purchase_exception_state = fields.Function(fields.Selection([ - ('', ''), - ('ignored', 'Ignored'), - ('recreated', 'Recreated'), - ], 'Exception State'), 'get_purchase_exception_state') - - @classmethod - def __register__(cls, module_name): - TableHandler = backend.get('TableHandler') - cursor = Transaction().cursor - sql_table = cls.__table__() - - super(Move, cls).__register__(module_name) - - table = TableHandler(cursor, cls, module_name) - - # Migration from 2.6: remove purchase_line - if table.column_exist('purchase_line'): - cursor.execute(*sql_table.update( - columns=[sql_table.origin], - values=[Concat('purchase.line,', sql_table.purchase_line)], - where=sql_table.purchase_line != None)) - table.drop_column('purchase_line') - - @classmethod - def _get_origin(cls): - models = super(Move, cls)._get_origin() - models.append('purchase.line') - return models - - def get_purchase(self, name): - PurchaseLine = Pool().get('purchase.line') - if isinstance(self.origin, PurchaseLine): - return self.origin.purchase.id - - @classmethod - def search_purchase(cls, name, clause): - return [('origin.' + name,) + tuple(clause[1:]) + ('purchase.line',)] - - def get_purchase_exception_state(self, name): - PurchaseLine = Pool().get('purchase.line') - if not isinstance(self.origin, PurchaseLine): - return '' - if self in self.origin.moves_recreated: - return 'recreated' - if self in self.origin.moves_ignored: - return 'ignored' - - def get_purchase_fields(self, name): - PurchaseLine = Pool().get('purchase.line') - if isinstance(self.origin, PurchaseLine): - if name[9:] == 'currency': - return self.origin.purchase.currency.id - elif name[9:] in ('quantity', 'unit_digits', 'unit_price'): - return getattr(self.origin, name[9:]) - else: - return getattr(self.origin, name[9:]).id - else: - if name[9:] == 'quantity': - return 0.0 - elif name[9:] == 'unit_digits': - return 2 - - def on_change_with_purchase_visible(self, name=None): - if self.from_location: - if self.from_location.type == 'supplier': - return True - return False - - def get_supplier(self, name): - PurchaseLine = Pool().get('purchase.line') - if isinstance(self.origin, PurchaseLine): - return self.origin.purchase.party.id - - @property - def origin_name(self): - pool = Pool() - PurchaseLine = pool.get('purchase.line') - name = super(Move, self).origin_name - if isinstance(self.origin, PurchaseLine): - name = self.origin.purchase.rec_name - return name - - @classmethod - def search_supplier(cls, name, clause): - return [('origin.purchase.party',) + tuple(clause[1:]) + - ('purchase.line',)] - - @classmethod - @ModelView.button - @Workflow.transition('cancel') - def cancel(cls, moves): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - super(Move, cls).cancel(moves) - purchase_lines = PurchaseLine.search([ - ('moves', 'in', [m.id for m in moves]), - ]) - if purchase_lines: - purchase_ids = list(set(l.purchase.id for l in purchase_lines)) - with Transaction().set_user(0, set_context=True): - purchases = Purchase.browse(purchase_ids) - Purchase.process(purchases) - - @classmethod - def delete(cls, moves): - pool = Pool() - Purchase = pool.get('purchase.purchase') - PurchaseLine = pool.get('purchase.line') - - purchases = set() - purchase_lines = PurchaseLine.search([ - ('moves', 'in', [m.id for m in moves]), - ]) - - super(Move, cls).delete(moves) - - if purchase_lines: - for purchase_line in purchase_lines: - purchases.add(purchase_line.purchase) - if purchases: - with Transaction().set_user(0, set_context=True): - purchases = Purchase.browse([p.id for p in purchases]) - Purchase.process(purchases) - - class OpenSupplier(Wizard): 'Open Suppliers' __name__ = 'purchase.open_supplier' diff --git a/stock.py b/stock.py index 18ccf22b85..b01801a49c 100644 --- a/stock.py +++ b/stock.py @@ -1,13 +1,294 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import datetime +from sql.operators import Concat +from trytond.model import Workflow, ModelView, fields +from trytond.pyson import Eval from trytond.wizard import Wizard -from trytond.pool import Pool +from trytond.pool import Pool, PoolMeta from trytond.transaction import Transaction from trytond.pyson import PYSONDecoder, PYSONEncoder +from trytond import backend -__all__ = ['OpenProductQuantitiesByWarehouse'] +__all__ = ['ShipmentIn', 'ShipmentInReturn', 'Move', + 'OpenProductQuantitiesByWarehouse'] +__metaclass__ = PoolMeta + + +class ShipmentIn: + __name__ = 'stock.shipment.in' + + @classmethod + def __setup__(cls): + super(ShipmentIn, cls).__setup__() + add_remove = [ + ('supplier', '=', Eval('supplier')), + ] + if not cls.incoming_moves.add_remove: + cls.incoming_moves.add_remove = add_remove + else: + cls.incoming_moves.add_remove = [ + add_remove, + cls.incoming_moves.add_remove, + ] + if 'supplier' not in cls.incoming_moves.depends: + cls.incoming_moves.depends.append('supplier') + + cls._error_messages.update({ + 'reset_move': ('You cannot reset to draft move "%s" because ' + 'it was generated by a purchase.'), + }) + + @classmethod + def write(cls, shipments, vals): + pool = Pool() + Purchase = pool.get('purchase.purchase') + PurchaseLine = pool.get('purchase.line') + + super(ShipmentIn, cls).write(shipments, vals) + + if 'state' in vals and vals['state'] in ('received', 'cancel'): + purchases = [] + move_ids = [] + for shipment in shipments: + move_ids.extend([x.id for x in shipment.incoming_moves]) + + purchase_lines = PurchaseLine.search([ + ('moves', 'in', move_ids), + ]) + if purchase_lines: + for purchase_line in purchase_lines: + if purchase_line.purchase not in purchases: + purchases.append(purchase_line.purchase) + + with Transaction().set_user(0, set_context=True): + purchases = Purchase.browse([p.id for p in purchases]) + Purchase.process(purchases) + + @classmethod + @ModelView.button + @Workflow.transition('draft') + def draft(cls, shipments): + PurchaseLine = Pool().get('purchase.line') + for shipment in shipments: + for move in shipment.incoming_moves: + if (move.state == 'cancel' + and isinstance(move.origin, PurchaseLine)): + cls.raise_user_error('reset_move', (move.rec_name,)) + + return super(ShipmentIn, cls).draft(shipments) + + +class ShipmentInReturn: + __name__ = 'stock.shipment.in.return' + + @classmethod + def __setup__(cls): + super(ShipmentInReturn, cls).__setup__() + cls._error_messages.update({ + 'reset_move': ('You cannot reset to draft a move generated ' + 'by a purchase.'), + }) + + @classmethod + def write(cls, shipments, vals): + pool = Pool() + Purchase = pool.get('purchase.purchase') + PurchaseLine = pool.get('purchase.line') + + super(ShipmentInReturn, cls).write(shipments, vals) + + if 'state' in vals and vals['state'] == 'done': + move_ids = [] + for shipment in shipments: + move_ids.extend([x.id for x in shipment.moves]) + + purchase_lines = PurchaseLine.search([ + ('moves', 'in', move_ids), + ]) + purchases = set() + if purchase_lines: + for purchase_line in purchase_lines: + purchases.add(purchase_line.purchase) + + with Transaction().set_user(0, set_context=True): + purchases = Purchase.browse([p.id for p in purchases]) + Purchase.process(purchases) + + @classmethod + @ModelView.button + @Workflow.transition('draft') + def draft(cls, shipments): + PurchaseLine = Pool().get('purchase.line') + for shipment in shipments: + for move in shipment.moves: + if (move.state == 'cancel' + and isinstance(move.origin, PurchaseLine)): + cls.raise_user_error('reset_move') + + return super(ShipmentInReturn, cls).draft(shipments) + + +class Move: + __name__ = 'stock.move' + purchase = fields.Function(fields.Many2One('purchase.purchase', 'Purchase', + states={ + 'invisible': ~Eval('purchase_visible', False), + }, + depends=['purchase_visible']), 'get_purchase', + searcher='search_purchase') + purchase_quantity = fields.Function(fields.Float('Purchase Quantity', + digits=(16, Eval('unit_digits', 2)), + states={ + 'invisible': ~Eval('purchase_visible', False), + }, + depends=['purchase_visible', 'unit_digits']), + 'get_purchase_fields') + purchase_unit = fields.Function(fields.Many2One('product.uom', + 'Purchase Unit', states={ + 'invisible': ~Eval('purchase_visible', False), + }, depends=['purchase_visible']), 'get_purchase_fields') + purchase_unit_digits = fields.Function(fields.Integer( + 'Purchase Unit Digits'), 'get_purchase_fields') + purchase_unit_price = fields.Function(fields.Numeric('Purchase Unit Price', + digits=(16, 4), states={ + 'invisible': ~Eval('purchase_visible', False), + }, depends=['purchase_visible']), 'get_purchase_fields') + purchase_currency = fields.Function(fields.Many2One('currency.currency', + 'Purchase Currency', states={ + 'invisible': ~Eval('purchase_visible', False), + }, depends=['purchase_visible']), 'get_purchase_fields') + purchase_visible = fields.Function(fields.Boolean('Purchase Visible', + on_change_with=['from_location']), 'on_change_with_purchase_visible') + supplier = fields.Function(fields.Many2One('party.party', 'Supplier'), + 'get_supplier', searcher='search_supplier') + purchase_exception_state = fields.Function(fields.Selection([ + ('', ''), + ('ignored', 'Ignored'), + ('recreated', 'Recreated'), + ], 'Exception State'), 'get_purchase_exception_state') + + @classmethod + def __register__(cls, module_name): + TableHandler = backend.get('TableHandler') + cursor = Transaction().cursor + sql_table = cls.__table__() + + super(Move, cls).__register__(module_name) + + table = TableHandler(cursor, cls, module_name) + + # Migration from 2.6: remove purchase_line + if table.column_exist('purchase_line'): + cursor.execute(*sql_table.update( + columns=[sql_table.origin], + values=[Concat('purchase.line,', sql_table.purchase_line)], + where=sql_table.purchase_line != None)) + table.drop_column('purchase_line') + + @classmethod + def _get_origin(cls): + models = super(Move, cls)._get_origin() + models.append('purchase.line') + return models + + def get_purchase(self, name): + PurchaseLine = Pool().get('purchase.line') + if isinstance(self.origin, PurchaseLine): + return self.origin.purchase.id + + @classmethod + def search_purchase(cls, name, clause): + return [('origin.' + name,) + tuple(clause[1:]) + ('purchase.line',)] + + def get_purchase_exception_state(self, name): + PurchaseLine = Pool().get('purchase.line') + if not isinstance(self.origin, PurchaseLine): + return '' + if self in self.origin.moves_recreated: + return 'recreated' + if self in self.origin.moves_ignored: + return 'ignored' + + def get_purchase_fields(self, name): + PurchaseLine = Pool().get('purchase.line') + if isinstance(self.origin, PurchaseLine): + if name[9:] == 'currency': + return self.origin.purchase.currency.id + elif name[9:] in ('quantity', 'unit_digits', 'unit_price'): + return getattr(self.origin, name[9:]) + else: + return getattr(self.origin, name[9:]).id + else: + if name[9:] == 'quantity': + return 0.0 + elif name[9:] == 'unit_digits': + return 2 + + def on_change_with_purchase_visible(self, name=None): + if self.from_location: + if self.from_location.type == 'supplier': + return True + return False + + def get_supplier(self, name): + PurchaseLine = Pool().get('purchase.line') + if isinstance(self.origin, PurchaseLine): + return self.origin.purchase.party.id + + @property + def origin_name(self): + pool = Pool() + PurchaseLine = pool.get('purchase.line') + name = super(Move, self).origin_name + if isinstance(self.origin, PurchaseLine): + name = self.origin.purchase.rec_name + return name + + @classmethod + def search_supplier(cls, name, clause): + return [('origin.purchase.party',) + tuple(clause[1:]) + + ('purchase.line',)] + + @classmethod + @ModelView.button + @Workflow.transition('cancel') + def cancel(cls, moves): + pool = Pool() + Purchase = pool.get('purchase.purchase') + PurchaseLine = pool.get('purchase.line') + + super(Move, cls).cancel(moves) + purchase_lines = PurchaseLine.search([ + ('moves', 'in', [m.id for m in moves]), + ]) + if purchase_lines: + purchase_ids = list(set(l.purchase.id for l in purchase_lines)) + with Transaction().set_user(0, set_context=True): + purchases = Purchase.browse(purchase_ids) + Purchase.process(purchases) + + @classmethod + def delete(cls, moves): + pool = Pool() + Purchase = pool.get('purchase.purchase') + PurchaseLine = pool.get('purchase.line') + + purchases = set() + purchase_lines = PurchaseLine.search([ + ('moves', 'in', [m.id for m in moves]), + ]) + + super(Move, cls).delete(moves) + + if purchase_lines: + for purchase_line in purchase_lines: + purchases.add(purchase_line.purchase) + if purchases: + with Transaction().set_user(0, set_context=True): + purchases = Purchase.browse([p.id for p in purchases]) + Purchase.process(purchases) class OpenProductQuantitiesByWarehouse(Wizard):