class Property(ModelSQL, ModelView): _name = 'ir.property' company = fields.Many2One('company.company', 'Company', domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) def _set_values(self, name, model, res_id, val, field_id): user_obj = self.pool.get('res.user') user = user_obj.browse(Transaction().user) res = super(Property, self)._set_values(name, model, res_id, val, field_id) if user: res['company'] = user.company.id return res def search(self, domain, offset=0, limit=None, order=None, count=False): if Transaction().user == 0: domain = ['AND', domain[:], ('company', '=', False)] return super(Property, self).search(domain, offset=offset, limit=limit, order=order, count=count)
class ProductionLeadTime(sequence_ordered(), ModelSQL, ModelView, MatchMixin): 'Production Lead Time' __name__ = 'production.lead_time' product = fields.Many2One('product.product', 'Product', ondelete='CASCADE', select=True, required=True, domain=[ ('type', '!=', 'service'), ]) bom = fields.Many2One('production.bom', 'BOM', ondelete='CASCADE', domain=[ ('output_products', '=', If(Bool(Eval('product')), Eval('product', -1), Get(Eval('_parent_product', {}), 'id', 0))), ], depends=['product']) lead_time = fields.TimeDelta('Lead Time') @classmethod def __setup__(cls): super(ProductionLeadTime, cls).__setup__() cls._order.insert(0, ('product', 'ASC'))
class ProductBom(sequence_ordered(), ModelSQL, ModelView): 'Product - BOM' __name__ = 'product.product-production.bom' product = fields.Many2One('product.product', 'Product', ondelete='CASCADE', select=1, required=True, domain=[ ('producible', '=', True), ]) bom = fields.Many2One('production.bom', 'BOM', ondelete='CASCADE', select=1, required=True, domain=[ ('output_products', '=', If(Bool(Eval('product')), Eval('product', 0), Get(Eval('_parent_product', {}), 'id', 0))), ], depends=['product']) def get_rec_name(self, name): return self.bom.rec_name @classmethod def search_rec_name(cls, name, clause): return [('bom.rec_name', ) + tuple(clause[1:])]
def __setup__(cls): super().__setup__() cls._order.insert(0, ('rec_name', 'ASC')) types_cost_method = cls._cost_price_method_domain_per_type() cls.cost_price_method.domain = [ Get(types_cost_method, Eval('type'), []), ]
def view_attributes(cls): return super(SaleLine, cls).view_attributes() + [ ('//page[@id="gift_cards"]', 'states', { 'invisible': ~Bool(Eval('is_gift_card')) }), ('//separator[@id="recipient_details"]', 'states', { 'invisible': ~Bool(Eval('is_gift_card')) }), ( '//field[@name="message"]', 'spell', Get(Eval('_parent_sale', {}), 'party_lang') )]
class Configuration: __name__ = 'account.configuration' stock_journal = fields.Property(fields.Many2One( 'account.journal', 'Stock Journal', states={ 'required': Bool(Eval('context', {}).get('company')), })) cost_price_counterpart_account = fields.Property(fields.Many2One( 'account.account', 'Cost Price Counterpart Account', domain=[ ('company', 'in', [Get(Eval('context', {}), 'company'), None]), ]))
class InvoiceLine(ModelSQL, ModelView): """Invoice Line""" _name = 'account.invoice.line' work = fields.Many2One('timesheet.work', 'Work') timesheet_lines = fields.One2Many( 'timesheet.line', 'invoice_line', 'Timesheet Lines', add_remove=[ ('invoice_line', '=', False), ('work', '=', Eval('work')), ('billable', '=', True), ('hours', '>', 0), ], ) # add timesheet_lines to on_change_with list on parent quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), states={ 'invisible': Not(Equal(Eval('type'), 'line')), 'required': Equal(Eval('type'), 'line'), }, on_change_with=['timesheet_lines']) # add timesheet_lines to on_change_with list on parent amount = fields.Function( fields.Numeric('Amount', digits=(16, Get(Eval('_parent_invoice', {}), 'currency_digits', Eval('currency_digits', 2))), states={ 'invisible': Not(In(Eval('type'), ['line', 'subtotal'])), }, on_change_with=[ 'type', 'quantity', 'unit_price', '_parent_invoice.currency', 'currency', 'timesheet_lines', ]), 'get_amount') def on_change_with_quantity(self, vals): hours = 0.0 for line in vals.get('timesheet_lines'): hours = hours + line.get('hours') return hours
class Configuration: __metaclass__ = PoolMeta __name__ = 'account.configuration' stock_journal = fields.MultiValue(stock_journal) cost_price_counterpart_account = fields.MultiValue( fields.Many2One('account.account', "Cost Price Counterpart Account", domain=[ ('company', 'in', [Get(Eval('context', {}), 'company'), None]), ])) @classmethod def default_stock_journal(cls, **pattern): return cls.multivalue_model('stock_journal').default_stock_journal()
class Sequence(ModelSQL, ModelView): _name = 'ir.sequence' company = fields.Many2One('company.company', 'Company', domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) def __init__(self): super(Sequence, self).__init__() self._order.insert(0, ('company', 'ASC')) def default_company(self): return Transaction().context.get('company') or False
class ProductBom(ModelSQL, ModelView): 'Product - BOM' __name__ = 'product.product-production.bom' product = fields.Many2One('product.product', 'Product', ondelete='CASCADE', select=1, required=True, domain=[ ('type', '!=', 'service'), ]) bom = fields.Many2One('production.bom', 'BOM', ondelete='CASCADE', select=1, required=True, domain=[ ('output_products', '=', If(Bool(Eval('product')), Eval('product', 0), Get(Eval('_parent_product', {}), 'id', 0))), ], depends=['product']) sequence = fields.Integer('Sequence') @classmethod def __setup__(cls): super(ProductBom, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) def get_rec_name(self, name): return self.bom.rec_name @classmethod def search_rec_name(cls, name, clause): return [('bom.rec_name', ) + tuple(clause[1:])] @staticmethod def order_sequence(tables): table, _ = tables[None] return [Case((table.sequence == Null, 0), else_=1), table.sequence]
class DocumentRequest(ModelSQL, ModelView): "Documents of request" _name = 'ekd.document.head.request' _description = __doc__ _inherits = {'ekd.document': 'document'} document = fields.Many2One('ekd.document', 'Document', required=True, ondelete='CASCADE') template_request = fields.Function(fields.Many2One( 'ekd.document.template', 'Document', help="Template document", states=_REQUEST_STATES, depends=_REQUEST_DEPENDS, domain=[('type_account', '=', Eval('type_request', Get(Eval('context', {}), 'type_request')))]), 'get_fields', setter='set_fields') date_issued = fields.Function(fields.Date('Date of issue', states=_ISSUE_STATES, depends=_REQUEST_DEPENDS), 'get_fields', setter='set_fields') type_request = fields.Selection([ ('request_monies', 'Money'), ('request_product', 'Product'), ('request_service', 'Service'), ], required=True, string='Type request') document_ref = fields.Reference( 'Base', selection='documents_get', select=1, domain=[('state', '=', 'open')], context={'request_from': 'requestcash'}, states=_REQUEST_STATES, depends=_REQUEST_DEPENDS, on_change=['document_ref'], ) document_name = fields.Function(fields.Char('Document Name'), 'get_doc_name') invoice_ref = fields.Reference('Invoice', selection='invoice_get', select=1, states=_REQUEST_STATES, depends=_REQUEST_DEPENDS) manager = fields.Function(fields.Many2One('company.employee', 'Manager to confirm', states=_REQUEST_STATES, depends=_REQUEST_DEPENDS), 'get_fields', setter='set_fields') recipient = fields.Function(fields.Many2One('party.party', 'Recipient', states=_REQUEST_STATES, depends=_REQUEST_DEPENDS), 'get_fields', setter='set_fields') amount_request = fields.Function(fields.Numeric( 'Total Request', digits=(16, Eval('currency_digits', 2)), readonly=True, on_change_with=['lines']), 'get_fields', setter='set_fields') amount_received = fields.Function(fields.Numeric( 'Total Received', digits=(16, Eval('currency_digits', 2)), readonly=True, on_change_with=['lines']), 'get_paid_fields', setter='set_fields') amount_balance = fields.Function( fields.Numeric('Balance', digits=(16, Eval('currency_digits', 2)), readonly=True, on_change_with=['lines']), 'get_fields') lines = fields.One2Many('ekd.document.line.request', 'requestcash', 'Lines Request', states=_RECEIVED_STATES, depends=_RECEIVED_DEPENDS, context={ 'document_ref': Eval('document_ref'), 'currency_digits': Eval('currency_digits') }) # currency = fields.Many2One('currency.currency', 'Currency') currency_digits = fields.Function( fields.Integer('Currency Digits', on_change_with=['currency']), 'get_currency_digits') state_doc = fields.Function(fields.Selection(_STATE_REQUEST, required=True, readonly=True, string='State'), 'get_fields', setter='set_fields') # Инициализация класса def __init__(self): super(DocumentRequest, self).__init__() self._rpc.update({ 'button_add_number': True, 'button_draft': True, 'button_request': True, 'button_cancel': True, 'button_issue': True, 'button_restore': True, 'button_confirmed': True, 'button_payment': True, }) self._order.insert(0, ('date_document', 'ASC')) self._order.insert(1, ('number_our', 'ASC')) self._error_messages.update({ 'you_dont_confirm': 'You can not confirm this request on money!', 'manager_not_find': "Manager as User don't found", 'employee_not_find': "Employee as User don't found", }) # # Заполнение полей значениями по умолчанию # def default_state_doc(self): return 'empty' def default_type_request(self): return 'request_monies' def default_date_issued(self): return Transaction().context.get('current_date', datetime.datetime.now()) def default_employee(self): return Transaction().context.get('employee', False) def default_recipient(self): return Transaction().context.get('employee', False) # def default_currency(self): # company_obj = self.pool.get('company.company') # currency_obj = self.pool.get('currency.currency') # if context is None: # context = {} # if context.get('company'): # company = company_obj.browse(context['company'], # ) # return company.currency.id # return False # def default_currency_digits(self): # company_obj = self.pool.get('company.company') # if context is None: # context = {} # if context.get('company'): # company = company_obj.browse(context['company'], # ) # return company.currency.digits # return 2 ################################################################################ # # Процедуры заполнения функциональных полей # ################################################################################ def get_rec_name(self, ids, name): if not ids: return {} return self.pool.get('ekd.document').get_rec_name(ids, name) def get_template_select(self, ids, name): template_obj = self.pool.get('ekd.document.template') template_ids = template_obj.search( ['type_account', '=', 'request_monies']) res = [] for template in template_obj.browse(template_ids): res.append([template.id, template.name]) return res def get_currency_digits(self, ids, name): assert name in ('currency_digits'), 'Invalid name %s' % (name) res = {}.fromkeys(ids, 2) for document in self.browse(ids): if document.currency: res[document.id] = document.currency.digits return res def get_amount_line(self, ids, names): if not ids: return {} res = {} amount_request = Decimal('0.0') amount_received = Decimal('0.0') for request in self.browse(ids): for line in request.lines: amount_request += abs(line.amount_request) amount_received += abs(line.amount_received) for name in names: res.setdefault(name, {}) res[name].setdefault(request.id, False) if name == 'amount_request': res[name][request.id] = amount_request elif name == 'amount_received': res[name][request.id] = amount_received elif name == 'amount_balance': res[name][request.id] = amount_request - amount_received amount_request = Decimal('0.0') amount_received = Decimal('0.0') return res def get_doc_name(self, ids, name): if not ids: return res = {} for request in self.browse(ids): if request.document_ref: model, model_id = request.document_ref.split(',') if model and model_id: model_rec = self.pool.get(model).browse(int(model_id)) if model_rec.full_name: res[request.id] = model_rec.full_name elif model_rec.rec_name: res[request.id] = model_rec.rec_name elif model_rec.name: res[request.id] = model_rec.name else: res[request.id] = 'name null' return res def documents_get(self): dictions_obj = self.pool.get('ir.dictions') res = [] diction_ids = dictions_obj.search([ ('model', '=', 'ekd.document.head.request'), ('pole', '=', 'document_ref'), ]) for diction in dictions_obj.browse(diction_ids): res.append([diction.key, diction.value]) return res def get_documents_ref(ids, model, name, values=None): raise Exception('get_documents_ref') def invoice_get(self): dictions_obj = self.pool.get('ir.dictions') res = [] diction_ids = dictions_obj.search([ ('model', '=', 'ekd.document.head.request'), ('pole', '=', 'invoice_ref'), ]) for diction in dictions_obj.browse(diction_ids): res.append([diction.key, diction.value]) return res def get_fields(self, ids, names): if not ids: return {} res = {} for request in self.browse(ids): for name in names: res.setdefault(name, {}) res[name].setdefault(request.id, False) if name == 'manager': res[name][request.id] = request.from_party.id elif name == 'employee_': res[name][request.id] = request.employee.id elif name == 'recipient': res[name][request.id] = request.to_party.id elif name == 'date_issued': res[name][request.id] = request.date_account elif name == 'state_doc': res[name][request.id] = request.state elif name == 'template_request': res[name][request.id] = request.template.id elif name == 'amount_request': res[name][request.id] = request.amount # elif name == 'amount_received': # res[name][request.id] = request.amount_payment elif name == 'amount_balance': res[name][ request. id] = request.amount - request.amount_payment - request.amount_received return res def set_fields(self, id, name, value): # assert name in ('manager', 'employee', 'date_issued', 'state_doc'), 'Invalid name SET PARTY %s' % (name) if not value: return if name == 'manager': self.write(id, { 'from_party': value, }) elif name == 'employee': self.write(id, { 'employee': value, }) elif name == 'recipient': self.write(id, { 'to_party': value, }) elif name == 'date_issued': self.write(id, { 'date_account': value, }) elif name == 'state_doc': self.write(id, { 'state': value, }) elif name == 'amount_request': self.write(id, { 'amount': value, }) elif name == 'amount_received': self.write(id, { 'amount_pay': value, }) elif name == 'template_request': self.write(id, { 'template': value, }) elif name == 'amount_request': self.write(id, { 'amount': value, }) def get_paid_fields(self, ids, name): res = {}.fromkeys(ids, Decimal('0.0')) cr = Transaction().cursor cr.execute("SELECT doc_base, SUM(amount_payment) "\ "FROM ekd_document_line_payment "\ "WHERE doc_base in ("+','.join(map(str,ids))+") and state='done' "\ "GROUP BY doc_base") for id, sum in cr.fetchall(): # SQLite uses float for SUM if not isinstance(sum, Decimal): sum = Decimal(str(sum)) res[id] = sum return res ################################################################################ # # События изменения полей # ################################################################################ def on_change_with_amount_request(self, values): amount = Decimal('0.0') for line in values['lines']: amount += line['amount_request'] return amount def on_change_with_amount_received(self, values): amount = Decimal('0.0') for line in values['lines']: amount += line['amount_received'] return amount def on_change_with_amount_balance(self, values): amount = Decimal('0.0') for line in values['lines']: amount += abs(line['amount_request']) - abs( line['amount_received']) return amount def on_change_document_ref(self, value): res = {} if value.get('document_ref'): model, model_id = value.get('document_ref').split(',') if model_id == '0': return res if model == 'project.project': model_obj = self.pool.get(model) model_ids = model_obj.browse(int(model_id)) if model_ids.manager: res['manager'] = model_ids.manager.id if model_ids.employee: res['recepient'] = model_ids.employee.id if model_ids.company: res['company'] = model_ids.company.id return res # # Обработка клиентских вызовов процедур # def button_draft(self, ids): return self.draft(ids) def button_confirmed(self, ids): return self.confirmed(ids) def button_cancel(self, ids): return self.draft(ids) def button_issue(self, ids): return self.paid(ids) def button_restore(self, ids): return self.draft(ids) def button_request(self, ids): return self.request(ids) def button_payment(self, ids): return self.on_payment(ids) def button_add_number(self, ids): return self.new_number(ids) def new_number(self, ids): sequence_obj = self.pool.get('ir.sequence') for document in self.browse(ids): if document.template and document.template.sequence: sequence = sequence_obj.get_id(document.template.sequence.id) else: raise Exception('Error', 'Sequence for document not find!') self.write(document.id, { 'number_our': sequence, }) # Изменение состояния документа в Черновик def draft(self, ids): requestline_obj = self.pool.get('ekd.document.line.request') for line in self.browse(ids): requestline_obj.write([k.id for k in line.lines], { 'state': 'draft', }) self.write(ids, { 'state': 'draft', }) # Закрытие документа def done(self, ids): requestline_obj = self.pool.get('ekd.document.line.request') for line in self.browse(ids): requestline_obj.write([k.id for k in line.lines], { 'state': 'done', }) self.write(ids, { 'state': 'done', }) # Формирование запроса для подтверждения документа def request(self, ids): sequence_obj = self.pool.get('ir.sequence') date_obj = self.pool.get('ir.date') request_obj = self.pool.get('res.request') req_ref_obj = self.pool.get('res.request.reference') user_obj = self.pool.get('res.user') requestline_obj = self.pool.get('ekd.document.line.request') for requestcash in self.browse(ids): if requestcash.manager: reference = sequence_obj.get_id( requestcash.template.sequence.id) for line in self.browse(ids): requestline_obj.write([k.id for k in line.lines], { 'state': 'request', }) self.write( requestcash.id, { 'number_our': reference, 'state': 'request', 'post_date': date_obj.today(), }) manager_id = user_obj.search( [('employee', '=', requestcash.manager.id)], limit=1) employee_id = user_obj.search( [('employee', '=', requestcash.employee.id)], limit=1) if len(manager_id) == 0: self.raise_user_error(cursor, error="Warning", error_description="manager_not_find") if len(employee_id) == 0: self.raise_user_error( cursor, error="Warning", error_description="employee_not_find") if isinstance(manager_id, list): manager_id = manager_id[0] if isinstance(employee_id, list): employee_id = employee_id[0] vals = {} vals['name'] = u"Пожалуйста подтвердите"\ u" - %s с датой выдачи %s"%(requestcash.rec_name, requestcash.date_issued.strftime('%d.%m.%Y')) vals['active'] = True vals['priority'] = '1' vals['act_from'] = employee_id vals['act_to'] = manager_id vals['body'] = requestcash.note vals['state'] = 'waiting' request_id = request_obj.create(vals) req_ref_obj.create({ 'request': request_id, 'reference': '%s,%s' % ('ekd.document.head.request', ids[0]), }) # Подтверждение документа def confirmed(self, requestcash_id): sequence_obj = self.pool.get('ir.sequence') date_obj = self.pool.get('ir.date') requestline_obj = self.pool.get('ekd.document.line.request') for document in self.browse(requestcash_id): reference = sequence_obj.get_id(document.template.sequence.id) if document.from_party.id == Transaction().context.get( 'employee', False): requestline_obj.write([k.id for k in document.lines], { 'state': 'confirmed', }) self.write( document.id, { 'number_our': reference, 'state': 'confirmed', 'post_date': date_obj.today(), }) else: self.raise_user_error(error="Error", error_description="you_dont_confirm") return # Постановка на оплату def on_payment(self, ids): requestline_obj = self.pool.get('ekd.document.line.request') for line in self.browse(ids): requestline_obj.write([k.id for k in line.lines], { 'state': 'payment', }) self.write(ids, { 'state': 'payment', }) def paid(self, ids): self.write(ids, { 'state': 'paid', }) # Создание объекта def create(self, vals): later = {} vals = vals.copy() cr = Transaction().cursor for field in vals: if field in self._columns\ and hasattr(self._columns[field], 'set'): later[field] = vals[field] for field in later: del vals[field] if vals.get('invoice_ref') == ',': del vals['invoice_ref'] if vals.get('document_ref') == ',': del vals['invoice_ref'] if cr.nextid(self._table): cr.setnextid(self._table, cr.currid(self._table)) new_id = super(DocumentRequest, self).create(vals) request = self.browse(new_id) new_id = request.document.id cr.execute('UPDATE "' + self._table + '" SET id = %s '\ 'WHERE id = %s', (request.document.id, request.id)) ModelStorage.delete(self, request.id) self.write(new_id, later) res = self.browse(new_id) return res.id
class DocumentInvoiceGoods(ModelWorkflow, ModelSQL, ModelView): "Documents (Invoice) " _name='ekd.document.head.invoice_goods' _description=__doc__ _inherits = {'ekd.document': 'document'} document = fields.Many2One('ekd.document', 'Document', required=True, ondelete='CASCADE') direct_document = fields.Selection([ ('output_goods', 'For Customer'), ('move_goods', 'Internal Move'), ('input_goods', 'From Supplier'), ], 'Direct Document') type_product = fields.Selection([ ('fixed_assets', 'Fixed Assets'), ('intangible', 'Intangible assets'), ('material', 'Material'), ('goods', 'Goods'), ], 'Type Product') template_invoice = fields.Function(fields.Many2One('ekd.document.template', 'Name', domain=[('type_account', '=', Eval('direct_document', Get(Eval('context', {}), 'direct_document')))] ), 'get_fields', setter='set_fields') number_doc = fields.Function(fields.Char('Number', states={'invisible': And(Not(Bool(Eval('number_doc'))), Not(Equal(Eval('direct_document',''),'input_goods')))}), 'get_fields', setter='set_fields', searcher='template_search') from_to_party = fields.Function(fields.Many2One('party.party', 'Party', states={'invisible': Equal(Eval('direct_document'),'move_goods')} ), 'get_fields', setter='set_fields') from_party_fnc = fields.Function(fields.Many2One('party.party', 'From Party', states={'invisible': Not(Equal(Eval('direct_document'),'move_goods'))} ), 'get_fields', setter='set_fields') to_party_fnc = fields.Function(fields.Many2One('party.party', 'To Party', states={'invisible': Not(Equal(Eval('direct_document'),'move_goods'))} ), 'get_fields', setter='set_fields') from_to_stock = fields.Function(fields.Many2One('ekd.company.department.stock', 'Stock', states={'invisible': Equal(Eval('direct_document'),'move_goods')} ), 'get_fields', setter='set_fields') from_stock_fnc = fields.Function(fields.Many2One('ekd.company.department.stock', 'From Stock', states={'invisible': Not(Equal(Eval('direct_document'),'move_goods'))} ), 'get_fields', setter='set_fields') to_stock_fnc = fields.Function(fields.Many2One('ekd.company.department.stock', 'To Stock', states={'invisible': Not(Equal(Eval('direct_document'),'move_goods'))} ), 'get_fields', setter='set_fields') shipper = fields.Many2One('party.party', 'Shipper', states={'invisible': Eval('as_shipper', True)}) consignee = fields.Many2One('party.party', 'Consignee', states={'invisible': Eval('as_consignee', True)}) as_shipper = fields.Boolean('As Shipper') as_consignee = fields.Boolean('As Consignee') amount_doc = fields.Function(fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2))), 'get_fields', setter='set_fields') amount_tax = fields.Function(fields.Numeric('Amount Tax', digits=(16, Eval('currency_digits', 2))), 'get_fields') currency = fields.Many2One('currency.currency', 'Currency') currency_digits = fields.Function(fields.Integer('Currency Digits', on_change_with=['currency']), 'get_currency_digits') type_tax = fields.Selection([ ('not_vat','Not VAT'), ('including','Including'), ('over_amount','Over Amount') ], 'Type Compute Tax') lines = fields.One2Many('ekd.document.line.product', 'invoice', 'Lines', context={'type_product':Eval('type_product')}) state_doc = fields.Function(fields.Selection(_STATE_INVOICE_GOODS, required=True, readonly=True, string='State'), 'get_fields', setter='set_fields') deleting = fields.Boolean('Flag deleting', readonly=True) def __init__(self): super(DocumentInvoiceGoods, self).__init__() self._rpc.update({ 'button_add_number': True, 'button_draft': True, 'button_to_pay': True, 'button_part_paid': True, 'button_paid': True, 'button_received': True, 'button_cancel': True, 'button_restore': True, 'button_print': True, 'button_send': True, 'button_post': True, 'button_obtained': True, 'button_delivered': True, }) self._order.insert(0, ('date_document', 'ASC')) self._order.insert(1, ('template', 'ASC')) def default_state_doc(self): return Transaction().context.get('state') or 'draft' def default_as_shipper(self): return True def default_as_consignee(self): return True def default_type_product(self): return Transaction().context.get('type_product') or 'material' def default_from_to_party_doc(self): return Transaction().context.get('from_to_party') or False def default_direct_document(self): return Transaction().context.get('direct_document') or 'invoice_revenue' def default_template_invoice(self): context = Transaction().context if context.get('template_invoice', False): return context.get('template_invoice') else: template_obj = self.pool.get('ekd.document.template') template_ids = template_obj.search( [ ('type_account','=',context.get('direct_document')) ], order=[('sequence','ASC')]) if len(template_ids) > 0: return template_ids[0] def default_currency(self): company_obj = self.pool.get('company.company') currency_obj = self.pool.get('currency.currency') context = Transaction().context if context.get('company'): company = company_obj.browse( context['company']) return company.currency.id return False def default_currency_digits(self): company_obj = self.pool.get('company.company') context = Transaction().context if context.get('company'): company = company_obj.browse( context['company']) return company.currency.digits return 2 def default_state_doc(self): return Transaction().context.get('state_doc', 'draft') def get_currency_digits(self, ids, name): res = {} for line in self.browse( ids): res[line.id] = line.currency and line.currency.digits or 2 return res def default_amount(self): return Transaction().context.get('amount') or Decimal('0.0') def default_company(self): return Transaction().context.get('company') or False def documents_base_get(self): dictions_obj = self.pool.get('ir.dictions') res = [] diction_ids = dictions_obj.search([ ('model', '=', 'ekd.document.head.invoice_goods'), ('pole', '=', 'document_base'), ], order=[('sequence','ASC')]) for diction in dictions_obj.browse(diction_ids): res.append([diction.key, diction.value]) return res def get_fields(self, ids, names): if not ids: return {} res={} for line in self.browse( ids): for name in names: res.setdefault(name, {}) res[name].setdefault(line.id, False) if name == 'number_doc': if line.direct_document == 'input_goods' : res[name][line.id] = line.number_in else: res[name][line.id] = line.number_our elif name == 'state_doc': res[name][line.id] = line.state elif name == 'amount_doc': for line_spec in line.lines: if line_spec.type == 'line': res[name][line.id] += line_spec.amount elif name == 'amount_tax': for line_spec in line.lines: if line_spec.type == 'line': res[name][line.id] += line_spec.amount_tax elif name == 'from_to_party': if line.direct_document == 'input_goods' : res[name][line.id] = line.from_party.id else: res[name][line.id] = line.to_party.id elif name == 'from_party_fnc': res[name][line.id] = line.from_party.id elif name == 'to_party_fnc': res[name][line.id] = line.to_party.id elif name == 'template_invoice': res[name][line.id] = line.template.id elif name == 'note_cash': res[name][line.id] = line.note return res def set_fields(self, ids, name, value): if isinstance(ids, list): ids = ids[0] if not value: return document = self.browse( ids) if name == 'state_doc': self.write( ids, {'state':value, }) elif name == 'template_invoice': self.write( ids, { 'template': value, }) elif name == 'number_doc': if document.direct_document == 'input_goods': self.write( ids, { 'number_in': value, }) else: self.write( ids, { 'number_our': value, }) elif name == 'amount_doc': self.write( ids, { 'amount': value, }) elif name == 'from_party_doc': if document.direct_document == 'input_goods': self.write( ids, { 'from_party': value, }) else: self.write( ids, { 'to_party': value, }) elif name == 'from_party_fnc': self.write( ids, { 'from_party': value, }) elif name == 'to_party_fnc': self.write( ids, { 'to_party': value, }) def template_select_get(self): context = Transaction().context template_obj = self.pool.get('ekd.document.template') raise Exception(str(context)) template_ids = template_obj.search( ['type_account','=', context.get('direct_document', 'input_goods')]) res=[] for template in template_obj.browse( template_ids): res.append([template.id,template.name]) return res def template_search(self, name, domain=[]): if name == 'template_bank': for table, exp, value in domain: return [('template', 'ilike', value)] elif name == 'from_party_doc': document_obj = self.pool.get('ekd.document') table, exp, value = domain[0] find_ids = document_obj.search( [('from_party', 'ilike', value)]) return [('document', 'in', find_ids)] elif name == 'to_party_doc': document_obj = self.pool.get('ekd.document') table, exp, value = domain[0] find_ids = document_obj.search( [('to_party', 'ilike', value)]) return [('document', 'in', find_ids)] def get_rec_name(self, ids, name): if not ids: return {} return self.pool.get('ekd.document').get_rec_name( ids, name) def on_change_document_base(self, vals): if not vals.get('document_base'): return {} model, model_id = vals.get('document_base').split(',') model_obj = self.pool.get(model) if model_id == '0': return {} model_line = model_obj.browse(int(model_id)) lines_new = {} field_import = [ 'product_ref', 'type_tax', 'tax', 'type', 'currency', 'unit', 'currency_digits', 'unit_price', 'amount_tax', 'product', 'description', 'type_product', 'unit_digits', 'amount', 'quantity'] if hasattr(model_obj, 'lines'): for line_new in model_line.lines: if line_new.type == 'line': tmp_line = line_new.read(line_new.id, field_import) tmp_line['state'] = 'draft' lines_new.setdefault('add', []).append(tmp_line) return { 'from_to_party': model_line.from_to_party.id, 'parent': int(model_id), 'lines':lines_new} else: return { 'from_to_party': model_line.from_to_party.id, 'parent': int(model_id)} def create(self, vals): later = {} vals = vals.copy() cursor = Transaction().cursor for field in vals: if field in self._columns\ and hasattr(self._columns[field], 'set'): later[field] = vals[field] for field in later: del vals[field] if cursor.nextid(self._table): cursor.setnextid(self._table, cursor.currid(self._table)) new_id = super(DocumentInvoiceGoods, self).create( vals) invoice = self.browse( new_id) new_id = invoice.document.id cr = Transaction().cursor cr.execute('UPDATE "' + self._table + '" SET id = %s '\ 'WHERE id = %s', (invoice.document.id, invoice.id)) ModelStorage.delete(self, invoice.id) self.write( new_id, later) res = self.browse( new_id) return res.id def delete(self, ids): cr = Transaction().cursor doc_lines_obj = self.pool.get('ekd.document.line.product') for document in self.browse(ids): if document.state == 'deleted' and document.deleting: return super(DocumentInvoiceGoods, self).delete(ids) else: doc_lines_obj.write([x.id for x in document.lines], {'state': 'deleted', 'deleting':True}) self.write(document.id, {'state': 'deleted', 'deleting':True}) return True def button_add_number(self, ids): return self.new_number(ids) def button_post(self, ids): return self.post(ids) def button_obtained(self, ids): return self.obtained(ids) def button_delivered(self, ids): return self.delivered(ids) def button_paid(self, ids): return self.paid(ids) def button_part_paid(self, ids): return self.part_paid(ids) def button_to_pay(self, ids): return self.to_pay(ids) def button_send(self, ids): return self.send(ids) def button_print(self, ids): return self.print_document(ids) def button_cancel(self, ids): return self.cancel(ids) def button_draft(self, ids): return self.draft(ids) def button_restore(self, ids): return self.draft(ids) def new_number(self, ids): sequence_obj = self.pool.get('ir.sequence') for document in self.browse(ids): if document.template and document.template.sequence: sequence = sequence_obj.get_id(document.template.sequence.id) else: raise Exception('Error', 'Sequence for document not find!') self.write(document.id, { 'number_doc': sequence, }) def post(self, ids): return self.write(ids, { 'state': 'posted', }) def obtained(self, ids): return self.write(ids, { 'state': 'obtained', }) def delivered(self, ids): return self.write(ids, { 'state': 'delivered', }) def send(self, ids): return self.write(ids, { 'state': 'sended', }) def to_pay(self, ids): return self.write(ids, { 'state': 'to_pay', }) def paid(self, ids): return self.write(ids, { 'state': 'paid', }) def part_paid(self, ids): return self.write(ids, { 'state': 'part_paid', }) def print_document(self, ids): return self.write(ids, { 'state': 'printed', }) def draft(self, ids): return self.write(ids, { 'state': 'draft', 'deleting':False }) def cancel(self, ids): return self.write(ids, { 'state': 'canceled', })
class SaleOpportunity(Workflow, ModelSQL, ModelView, AttachmentCopyMixin, NoteCopyMixin): 'Sale Opportunity' __name__ = "sale.opportunity" _history = True _rec_name = 'number' _states_start = { 'readonly': Eval('state') != 'lead', } _states_stop = { 'readonly': Eval('state').in_(['converted', 'won', 'lost', 'cancelled']), } number = fields.Char('Number', readonly=True, required=True, select=True) reference = fields.Char('Reference', select=True) party = fields.Many2One( 'party.party', "Party", select=True, states={ 'readonly': Eval('state').in_(['converted', 'lost', 'cancelled']), 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']), }, context={ 'company': Eval('company', -1), }, depends={'company'}) contact = fields.Many2One('party.contact_mechanism', "Contact", context={ 'company': Eval('company', -1), }, search_context={ 'related_party': Eval('party'), }, depends=['party', 'company']) address = fields.Many2One('party.address', 'Address', domain=[('party', '=', Eval('party'))], select=True, states=_states_stop) company = fields.Many2One( 'company.company', 'Company', required=True, select=True, states={ 'readonly': _states_stop['readonly'] | Eval('party', True), }, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') amount = Monetary("Amount", currency='currency', digits='currency', states=_states_stop, help='Estimated revenue amount.') payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states={ 'readonly': In(Eval('state'), ['converted', 'lost', 'cancelled']), }) employee = fields.Many2One( 'company.employee', 'Employee', states={ 'readonly': _states_stop['readonly'], 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']), }, domain=[('company', '=', Eval('company'))]) start_date = fields.Date('Start Date', required=True, select=True, states=_states_start) end_date = fields.Date('End Date', select=True, states=_states_stop) description = fields.Char('Description', states=_states_stop) comment = fields.Text('Comment', states=_states_stop) lines = fields.One2Many('sale.opportunity.line', 'opportunity', 'Lines', states=_states_stop) conversion_probability = fields.Float( 'Conversion Probability', digits=(1, 4), required=True, domain=[ ('conversion_probability', '>=', 0), ('conversion_probability', '<=', 1), ], states={ 'readonly': ~Eval('state').in_(['opportunity', 'lead', 'converted']), }, help="Percentage between 0 and 100.") lost_reason = fields.Text('Reason for loss', states={ 'invisible': Eval('state') != 'lost', }) sales = fields.One2Many('sale.sale', 'origin', 'Sales') converted_by = employee_field( "Converted By", states=['converted', 'won', 'lost', 'cancelled']) state = fields.Selection([ ('lead', "Lead"), ('opportunity', "Opportunity"), ('converted', "Converted"), ('won', "Won"), ('lost', "Lost"), ('cancelled', "Cancelled"), ], "State", required=True, select=True, sort=False, readonly=True) del _states_start del _states_stop @classmethod def __register__(cls, module_name): pool = Pool() Sale = pool.get('sale.sale') transaction = Transaction() cursor = transaction.connection.cursor() update = transaction.connection.cursor() sql_table = cls.__table__() sale = Sale.__table__() table = cls.__table_handler__(module_name) number_exists = table.column_exist('number') # Migration from 3.8: rename reference into number if table.column_exist('reference') and not number_exists: table.column_rename('reference', 'number') number_exists = True super(SaleOpportunity, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.4: replace sale by origin if table.column_exist('sale'): cursor.execute(*sql_table.select( sql_table.id, sql_table.sale, where=sql_table.sale != Null)) for id_, sale_id in cursor: update.execute( *sale.update(columns=[sale.origin], values=['%s,%s' % (cls.__name__, id_)], where=sale.id == sale_id)) table.drop_column('sale') # Migration from 4.0: change probability into conversion probability if table.column_exist('probability'): cursor.execute( *sql_table.update([sql_table.conversion_probability], [sql_table.probability / 100.0])) table.drop_constraint('check_percentage') table.drop_column('probability') # Migration from 4.2: make employee not required table.not_null_action('employee', action='remove') # Migration from 5.0: drop required on description table.not_null_action('description', action='remove') @classmethod def __setup__(cls): super(SaleOpportunity, cls).__setup__() cls._order.insert(0, ('start_date', 'DESC')) cls._transitions |= set(( ('lead', 'opportunity'), ('lead', 'lost'), ('lead', 'cancelled'), ('lead', 'converted'), ('opportunity', 'converted'), ('opportunity', 'lead'), ('opportunity', 'lost'), ('opportunity', 'cancelled'), ('converted', 'won'), ('converted', 'lost'), ('won', 'converted'), ('lost', 'converted'), ('lost', 'lead'), ('cancelled', 'lead'), )) cls._buttons.update({ 'lead': { 'invisible': ~Eval('state').in_(['cancelled', 'lost', 'opportunity']), 'icon': If( Eval('state').in_(['cancelled', 'lost']), 'tryton-undo', 'tryton-back'), 'depends': ['state'], }, 'opportunity': { 'pre_validate': [ If(~Eval('party'), ('party', '!=', None), ()), If(~Eval('employee'), ('employee', '!=', None), ()), ], 'invisible': ~Eval('state').in_(['lead']), 'depends': ['state'], }, 'convert': { 'invisible': ~Eval('state').in_(['opportunity']), 'depends': ['state'], }, 'lost': { 'invisible': ~Eval('state').in_(['lead', 'opportunity']), 'depends': ['state'], }, 'cancel': { 'invisible': ~Eval('state').in_(['lead', 'opportunity']), 'depends': ['state'], }, }) @staticmethod def default_state(): return 'lead' @staticmethod def default_start_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_conversion_probability(): return 0.5 @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_employee(): return Transaction().context.get('employee') @classmethod def default_payment_term(cls): PaymentTerm = Pool().get('account.invoice.payment_term') payment_terms = PaymentTerm.search(cls.payment_term.domain) if len(payment_terms) == 1: return payment_terms[0].id @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] @classmethod def get_resources_to_copy(cls, name): return { 'sale.sale', } @classmethod def create(cls, vlist): pool = Pool() Config = pool.get('sale.configuration') config = Config(1) vlist = [x.copy() for x in vlist] default_company = cls.default_company() for vals in vlist: if vals.get('number') is None: vals['number'] = config.get_multivalue( 'sale_opportunity_sequence', company=vals.get('company', default_company)).get() return super(SaleOpportunity, cls).create(vlist) @classmethod def copy(cls, opportunities, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('sales', None) default.setdefault('converted_by') return super(SaleOpportunity, cls).copy(opportunities, default=default) @fields.depends('company') def on_change_with_currency(self, name=None): if self.company: return self.company.currency.id @fields.depends('party') def on_change_party(self): if self.party and self.party.customer_payment_term: self.payment_term = self.party.customer_payment_term else: self.payment_term = self.default_payment_term() def _get_sale_opportunity(self): ''' Return sale for an opportunity ''' Sale = Pool().get('sale.sale') return Sale( description=self.description, party=self.party, contact=self.contact, payment_term=self.payment_term, company=self.company, invoice_address=self.address, shipment_address=self.address, currency=self.company.currency, comment=self.comment, sale_date=None, origin=self, warehouse=Sale.default_warehouse(), ) def create_sale(self): ''' Create a sale for the opportunity and return the sale ''' sale = self._get_sale_opportunity() sale_lines = [] for line in self.lines: sale_lines.append(line.get_sale_line(sale)) sale.lines = sale_lines return sale @classmethod def delete(cls, opportunities): # Cancel before delete cls.cancel(opportunities) for opportunity in opportunities: if opportunity.state != 'cancelled': raise AccessError( gettext('sale_opportunity.msg_opportunity_delete_cancel', opportunity=opportunity.rec_name)) super(SaleOpportunity, cls).delete(opportunities) @classmethod @ModelView.button @Workflow.transition('lead') def lead(cls, opportunities): pass @classmethod @ModelView.button @Workflow.transition('opportunity') def opportunity(cls, opportunities): pass @classmethod @ModelView.button @Workflow.transition('converted') @set_employee('converted_by') def convert(cls, opportunities): pool = Pool() Sale = pool.get('sale.sale') sales = [o.create_sale() for o in opportunities if not o.sales] Sale.save(sales) for sale in sales: sale.origin.copy_resources_to(sale) @property def is_forecast(self): pool = Pool() Date = pool.get('ir.date') with Transaction().set_context(company=self.company.id): today = Date.today() return self.end_date or datetime.date.max > today @classmethod @Workflow.transition('won') def won(cls, opportunities): pool = Pool() Date = pool.get('ir.date') for company, c_opportunities in groupby(opportunities, key=lambda o: o.company): with Transaction().set_context(company=company.id): today = Date.today() cls.write([o for o in c_opportunities if o.is_forecast], { 'end_date': today, 'state': 'won', }) @classmethod @ModelView.button @Workflow.transition('lost') def lost(cls, opportunities): pool = Pool() Date = pool.get('ir.date') for company, c_opportunities in groupby(opportunities, key=lambda o: o.company): with Transaction().set_context(company=company.id): today = Date.today() cls.write([o for o in c_opportunities if o.is_forecast], { 'end_date': today, 'state': 'lost', }) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, opportunities): pool = Pool() Date = pool.get('ir.date') for company, c_opportunities in groupby(opportunities, key=lambda o: o.company): with Transaction().set_context(company=company.id): today = Date.today() cls.write([o for o in c_opportunities if o.is_forecast], { 'end_date': today, 'state': 'cancelled', }) @staticmethod def _sale_won_states(): return ['confirmed', 'processing', 'done'] @staticmethod def _sale_lost_states(): return ['cancelled'] def is_won(self): sale_won_states = self._sale_won_states() sale_lost_states = self._sale_lost_states() end_states = sale_won_states + sale_lost_states return (self.sales and all(s.state in end_states for s in self.sales) and any(s.state in sale_won_states for s in self.sales)) def is_lost(self): sale_lost_states = self._sale_lost_states() return (self.sales and all(s.state in sale_lost_states for s in self.sales)) @property def sale_amount(self): pool = Pool() Currency = pool.get('currency.currency') if not self.sales: return sale_lost_states = self._sale_lost_states() amount = 0 for sale in self.sales: if sale.state not in sale_lost_states: amount += Currency.compute(sale.currency, sale.untaxed_amount, self.currency) return amount @classmethod def process(cls, opportunities): won = [] lost = [] converted = [] for opportunity in opportunities: sale_amount = opportunity.sale_amount if opportunity.amount != sale_amount: opportunity.amount = sale_amount if opportunity.is_won(): won.append(opportunity) elif opportunity.is_lost(): lost.append(opportunity) elif (opportunity.state != 'converted' and opportunity.sales): converted.append(opportunity) cls.save(opportunities) if won: cls.won(won) if lost: cls.lost(lost) if converted: cls.convert(converted)
class Journal(ModelSQL, ModelView): 'Journal' _name = 'ekd.account.journal' _description = __doc__ name = fields.Char('Name', size=None, required=True, translate=True) code = fields.Char('Code', size=None) active = fields.Boolean('Active', select=2) type = fields.Selection('get_types', 'Type', required=True) view = fields.Many2One('ekd.account.journal.view', 'View') centralised = fields.Boolean('Centralised counterpart') update_posted = fields.Boolean('Allow cancelling moves') sequence = fields.Property(fields.Many2One('ir.sequence', 'Sequence', domain=[('code', '=', 'ekd.account.journal')], context={'code': 'ekd.account.journal'}, states={ 'required': Bool(Get(Eval('context', {}), 'company', 0)), })) credit_account = fields.Property(fields.Many2One('ekd.account', 'Default Credit Account', domain=[ ('kind', '!=', 'view'), ('company', '=', Get(Eval('context', {}), 'company', 0)), ], states={ 'required': And(Or(Bool(Eval('centralised')), Equal(Eval('type'), 'cash')), Bool(Get(Eval('context', {}), 'company', 0))), 'invisible': Not(Bool(Get(Eval('context', {}), 'company', 0))), }, depends=['type', 'centralised'])) debit_account = fields.Property(fields.Many2One('ekd.account', 'Default Debit Account', domain=[ ('kind', '!=', 'view'), ('company', '=', Get(Eval('context', {}), 'company', 0)), ], states={ 'required': And(Or(Bool(Eval('centralised')), Equal(Eval('type'), 'cash')), Bool(Get(Eval('context', {}), 'company', 0))), 'invisible': Not(Bool(Get(Eval('context', {}), 'company', 0))), }, depends=['type', 'centralised'])) def __init__(self): super(Journal, self).__init__() self._order.insert(0, ('name', 'ASC')) #def init(self, module_name): # super(Journal, self).init(module_name) # cursor = Transaction().cursor # table = TableHandler(cursor, self, module_name) def default_active(self): return True def default_centralised(self): return False def default_update_posted(self): return False def default_sequence(self): return False def get_types(self): type_obj = self.pool.get('ekd.account.journal.type') type_ids = type_obj.search([]) types = type_obj.browse(type_ids) return [(x.code, x.name) for x in types] def search_rec_name(self, name, clause): ids = self.search([ ('code',) + clause[1:], ], limit=1, order=[]) if ids: return [('code',) + clause[1:]] return [(self._rec_name,) + clause[1:]]
class SaleOpportunity(Workflow, ModelSQL, ModelView): 'Sale Opportunity' __name__ = "sale.opportunity" _history = True _rec_name = 'number' number = fields.Char('Number', readonly=True, required=True, select=True) reference = fields.Char('Reference', select=True) party = fields.Many2One( 'party.party', 'Party', select=True, states={ 'readonly': Eval('state').in_(['converted', 'lost', 'cancelled']), 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']), }, depends=['state']) address = fields.Many2One('party.address', 'Address', domain=[('party', '=', Eval('party'))], select=True, depends=['party', 'state'], states=_STATES_STOP) company = fields.Many2One('company.company', 'Company', required=True, select=True, states=_STATES_STOP, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ], depends=_DEPENDS_STOP) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'get_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), states=_STATES_STOP, depends=_DEPENDS_STOP + ['currency_digits'], help='Estimated revenue amount') payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states={ 'readonly': In(Eval('state'), ['converted', 'lost', 'cancelled']), }, depends=['state']) employee = fields.Many2One( 'company.employee', 'Employee', states={ 'readonly': _STATES_STOP['readonly'], 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']), }, depends=['state', 'company'], domain=[('company', '=', Eval('company'))]) start_date = fields.Date('Start Date', required=True, select=True, states=_STATES_START, depends=_DEPENDS_START) end_date = fields.Date('End Date', select=True, states=_STATES_STOP, depends=_DEPENDS_STOP) description = fields.Char('Description', required=True, states=_STATES_STOP, depends=_DEPENDS_STOP) comment = fields.Text('Comment', states=_STATES_STOP, depends=_DEPENDS_STOP) lines = fields.One2Many('sale.opportunity.line', 'opportunity', 'Lines', states=_STATES_STOP, depends=_DEPENDS_STOP) state = fields.Selection(STATES, 'State', required=True, select=True, sort=False, readonly=True) conversion_probability = fields.Float( 'Conversion Probability', digits=(1, 4), required=True, domain=[ ('conversion_probability', '>=', 0), ('conversion_probability', '<=', 1), ], states={ 'readonly': ~Eval('state').in_(['opportunity', 'lead', 'converted']), }, depends=['state'], help="Percentage between 0 and 100") lost_reason = fields.Text('Reason for loss', states={ 'invisible': Eval('state') != 'lost', }, depends=['state']) sales = fields.One2Many('sale.sale', 'origin', 'Sales') @classmethod def __register__(cls, module_name): pool = Pool() Sale = pool.get('sale.sale') cursor = Transaction().connection.cursor() sql_table = cls.__table__() sale = Sale.__table__() table = cls.__table_handler__(module_name) number_exists = table.column_exist('number') # Migration from 3.8: rename reference into number if table.column_exist('reference') and not number_exists: table.column_rename('reference', 'number') number_exists = True super(SaleOpportunity, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.4: replace sale by origin if table.column_exist('sale'): cursor.execute(*sql_table.select( sql_table.id, sql_table.sale, where=sql_table.sale != Null)) for id_, sale_id in cursor.fetchall(): cursor.execute( *sale.update(columns=[sale.origin], values=['%s,%s' % (cls.__name__, id_)], where=sale.id == sale_id)) table.drop_column('sale') # Migration from 4.0: change probability into conversion probability if table.column_exist('probability'): cursor.execute( *sql_table.update([sql_table.conversion_probability], [sql_table.probability / 100.0])) table.drop_constraint('check_percentage') table.drop_column('probability') # Migration from 4.2: make employee not required table.not_null_action('employee', action='remove') @classmethod def __setup__(cls): super(SaleOpportunity, cls).__setup__() cls._order.insert(0, ('start_date', 'DESC')) cls._error_messages.update({ 'delete_cancel': ('Sale Opportunity "%s" must be cancelled ' 'before deletion.'), }) cls._transitions |= set(( ('lead', 'opportunity'), ('lead', 'lost'), ('lead', 'cancelled'), ('lead', 'converted'), ('opportunity', 'converted'), ('opportunity', 'lead'), ('opportunity', 'lost'), ('opportunity', 'cancelled'), ('converted', 'won'), ('converted', 'lost'), ('won', 'converted'), ('lost', 'converted'), ('lost', 'lead'), ('cancelled', 'lead'), )) cls._buttons.update({ 'lead': { 'invisible': ~Eval('state').in_(['cancelled', 'lost', 'opportunity']), 'icon': If( Eval('state').in_(['cancelled', 'lost']), 'tryton-undo', 'tryton-back'), 'depends': ['state'], }, 'opportunity': { 'invisible': ~Eval('state').in_(['lead']), 'depends': ['state'], }, 'convert': { 'invisible': ~Eval('state').in_(['opportunity']), 'depends': ['state'], }, 'lost': { 'invisible': ~Eval('state').in_(['lead', 'opportunity']), 'depends': ['state'], }, 'cancel': { 'invisible': ~Eval('state').in_(['lead', 'opportunity']), 'depends': ['state'], }, }) @staticmethod def default_state(): return 'lead' @staticmethod def default_start_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_conversion_probability(): return 0.5 @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_employee(): return Transaction().context.get('employee') @classmethod def default_payment_term(cls): PaymentTerm = Pool().get('account.invoice.payment_term') payment_terms = PaymentTerm.search(cls.payment_term.domain) if len(payment_terms) == 1: return payment_terms[0].id @classmethod def create(cls, vlist): pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('sale.configuration') config = Config(1) vlist = [x.copy() for x in vlist] for vals in vlist: if vals.get('number') is None: vals['number'] = Sequence.get_id( config.sale_opportunity_sequence.id) return super(SaleOpportunity, cls).create(vlist) @classmethod def copy(cls, opportunities, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('sales', None) return super(SaleOpportunity, cls).copy(opportunities, default=default) def get_currency(self, name): return self.company.currency.id def get_currency_digits(self, name): return self.company.currency.digits @fields.depends('company') def on_change_company(self): if self.company: self.currency = self.company.currency self.currency_digits = self.company.currency.digits @fields.depends('party') def on_change_party(self): if self.party and self.party.customer_payment_term: self.payment_term = self.party.customer_payment_term else: self.payment_term = self.default_payment_term() def _get_sale_opportunity(self): ''' Return sale for an opportunity ''' Sale = Pool().get('sale.sale') return Sale( description=self.description, party=self.party, payment_term=self.payment_term, company=self.company, invoice_address=self.address, shipment_address=self.address, currency=self.company.currency, comment=self.comment, sale_date=None, origin=self, warehouse=Sale.default_warehouse(), ) def create_sale(self): ''' Create a sale for the opportunity and return the sale ''' sale = self._get_sale_opportunity() sale_lines = [] for line in self.lines: sale_lines.append(line.get_sale_line(sale)) sale.lines = sale_lines return sale @classmethod def delete(cls, opportunities): # Cancel before delete cls.cancel(opportunities) for opportunity in opportunities: if opportunity.state != 'cancelled': cls.raise_user_error('delete_cancel', opportunity.rec_name) super(SaleOpportunity, cls).delete(opportunities) @classmethod @ModelView.button @Workflow.transition('lead') def lead(cls, opportunities): pass @classmethod @ModelView.button @Workflow.transition('opportunity') def opportunity(cls, opportunities): pass @classmethod @ModelView.button @Workflow.transition('converted') def convert(cls, opportunities): pool = Pool() Sale = pool.get('sale.sale') sales = [o.create_sale() for o in opportunities if not o.sales] Sale.save(sales) @property def is_forecast(self): pool = Pool() Date = pool.get('ir.date') today = Date.today() return self.end_date or datetime.date.max > today @classmethod @Workflow.transition('won') def won(cls, opportunities): pool = Pool() Date = pool.get('ir.date') cls.write([o for o in opportunities if o.is_forecast], { 'end_date': Date.today(), 'state': 'won', }) @classmethod @ModelView.button @Workflow.transition('lost') def lost(cls, opportunities): Date = Pool().get('ir.date') cls.write([o for o in opportunities if o.is_forecast], { 'end_date': Date.today(), 'state': 'lost', }) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, opportunities): Date = Pool().get('ir.date') cls.write([o for o in opportunities if o.is_forecast], { 'end_date': Date.today(), }) @staticmethod def _sale_won_states(): return ['confirmed', 'processing', 'done'] @staticmethod def _sale_lost_states(): return ['cancel'] def is_won(self): sale_won_states = self._sale_won_states() sale_lost_states = self._sale_lost_states() end_states = sale_won_states + sale_lost_states return (self.sales and all(s.state in end_states for s in self.sales) and any(s.state in sale_won_states for s in self.sales)) def is_lost(self): sale_lost_states = self._sale_lost_states() return (self.sales and all(s.state in sale_lost_states for s in self.sales)) @property def sale_amount(self): pool = Pool() Currency = pool.get('currency.currency') if not self.sales: return sale_lost_states = self._sale_lost_states() amount = 0 for sale in self.sales: if sale.state not in sale_lost_states: amount += Currency.compute(sale.currency, sale.untaxed_amount, self.currency) return amount @classmethod def process(cls, opportunities): won = [] lost = [] converted = [] for opportunity in opportunities: sale_amount = opportunity.sale_amount if opportunity.amount != sale_amount: opportunity.amount = sale_amount if opportunity.is_won(): won.append(opportunity) elif opportunity.is_lost(): lost.append(opportunity) elif (opportunity.state != 'converted' and opportunity.sales): converted.append(opportunity) cls.save(opportunities) if won: cls.won(won) if lost: cls.lost(lost) if converted: cls.convert(converted)
class Contract(ModelWorkflow, ModelSQL, ModelView): """Contract Agreement""" _name = 'contract.contract' _description = __doc__ company = fields.Many2One('company.company', 'Company', required=True, states=STATES, select=1, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) journal = fields.Many2One('account.journal', 'Journal', required=True, states=STATES, domain=[('centralised', '=', False)]) name = fields.Char('Name', required=True, translate=True) description = fields.Char('Description', size=None, translate=True) reference = fields.Char('Reference', size=None, translate=True, states=STATES, help='Use for purchase orders') party = fields.Many2One('party.party', 'Party', required=True, states=STATES, on_change=['party']) product = fields.Many2One('product.product', 'Product', required=True, states=STATES) list_price = fields.Numeric('List Price', states=STATES, digits=(16, 4), help='''Fixed-price override. Leave at 0.0000 for no override. Use discount at 100% to set actual price to 0.0''') discount = fields.Numeric('Discount (%)', states=STATES, digits=(4,2), help='Discount percentage on the list_price') quantity = fields.Numeric('Quantity', digits=(16,2), states=STATES) payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', required=True, states=STATES) state = fields.Selection([ ('draft','Draft'), ('active','Active'), ('hold', 'Hold'), ('canceled','Canceled'), ], 'State', readonly=True) interval = fields.Selection([ ('day','Day'), ('week','Week'), ('month','Month'), ('year','Year'), ], 'Interval', required=True, states=STATES) interval_quant = fields.Integer('Interval count', states=STATES) next_invoice_date = fields.Date('Next Invoice', states=STATES) opt_invoice_date = fields.Date('Temporary Next Invoice', readonly=True) start_date = fields.Date('Since', states=STATES) stop_date = fields.Date('Until') lines = fields.One2Many('account.invoice.line', 'contract', 'Invoice Lines', readonly=True, domain=[('contract','=',Eval('id'))]) def __init__(self): super(Contract, self).__init__() self._rpc.update({ 'create_next_invoice': True, 'create_invoice_batch': True, 'cancel_with_credit': True, }) def default_state(self): return 'draft' def default_interval(self): return 'month' def default_quantity(self): return Decimal('1.0') def default_company(self): return Transaction().context.get('company') or False def default_start_date(self): return datetime.date.fromtimestamp(time.time()) def default_interval_quant(self): return Decimal("1.0") def default_payment_term(self): config_obj = self.pool.get('contract.configuration') config = config_obj.browse(1) if config.payment_term: return config.payment_term.id company_id = self.default_company() company_obj = self.pool.get('company.company') company = company_obj.browse(company_id) if company and company.payment_term: return company.payment_term.id return False def default_journal(self): journal_obj = self.pool.get('account.journal') journal_ids = journal_obj.search([('type','=','revenue')], limit=1) if journal_ids: return journal_ids[0] return False def on_change_party(self, vals): party_obj = self.pool.get('party.party') address_obj = self.pool.get('party.address') payment_term_obj = self.pool.get('account.invoice.payment_term') res = { 'invoice_address': False, } if vals.get('party'): party = party_obj.browse(vals['party']) payment_term = party.payment_term or False discount = party.discount or Decimal("0.0") if payment_term: res['payment_term'] = payment_term.id res['payment_term.rec_name'] = payment_term_obj.browse( res['payment_term']).rec_name res['discount'] = discount return res def write(self, ids, vals): if 'state' in vals: self.workflow_trigger_trigger(ids) return super(Contract, self).write(ids, vals) def _invoice_init(self, contract, invoice_date): invoice_obj = self.pool.get('account.invoice') invoice_address = contract.party.address_get(contract.party.id, type='invoice') config_obj = self.pool.get('contract.configuration') config = config_obj.browse(1) description = config.description invoice = invoice_obj.create(dict( company=contract.company.id, type='out_invoice', description=description, state='draft', currency=contract.company.currency.id, journal=contract.journal.id, account=contract.party.account_receivable.id or contract.company.account_receivable.id, payment_term=contract.party.payment_term.id or contract.payment_term.id, party=contract.party.id, invoice_address=invoice_address, invoice_date=invoice_date, )) return invoice_obj.browse([invoice])[0] def _contract_unit_price(self, contract): unit_price = contract.product.list_price if contract.list_price: unit_price = contract.list_price elif contract.discount: unit_price = contract.product.list_price - (contract.product.list_price * contract.discount / 100) return unit_price def _invoice_append(self, invoice, contract, period): (last_date, next_date, quant) = period line_obj = self.pool.get('account.invoice.line') quantity = Decimal("%f" % quant) unit_price = self._contract_unit_price(contract) if not unit_price: # skip this contract log.info("skip contract without unit_price: %s contract.product.list_price: %s contract.list_price: %s contract.discount: %s" %( unit_price, contract.product.list_price, contract.list_price, contract.discount)) return linedata = dict( type='line', product=contract.product.id, invoice=invoice.id, description="%s: %s (%s - %s)" % (contract.name, contract.product.name, last_date, next_date), quantity=quantity, unit=contract.product.default_uom.id, unit_price=unit_price, contract=contract.id, taxes=[], ) account = contract.product.get_account([contract.product.id],'account_revenue_used') if account: linedata['account'] = account.popitem()[1] tax_rule_obj = self.pool.get('account.tax.rule') taxes = contract.product.get_taxes([contract.product.id], 'customer_taxes_used') for tax in taxes[contract.product.id]: pattern = {} if contract.party.customer_tax_rule: tax_ids = tax_rule_obj.apply(contract.party.customer_tax_rule, tax, pattern) if tax_ids: for tax_id in tax_ids: linedata['taxes'].append(('add',tax_id)) continue linedata['taxes'].append(('add',tax)) if contract.reference and not invoice.reference: invoice_obj = self.pool.get('account.invoice') invoice_obj.write(invoice.id, {'reference': contract.reference}) return line_obj.create(linedata) def cancel_with_credit(self, ids): """ Contract is canceled. Open invoices are credited. i.e. in case of failure to provide proper and valid credentials such as an initial payment. """ contract_obj = self.pool.get('contract.contract') contract = self.browse(ids)[0] if not contract.state == 'active': return {} for id in ids: self.workflow_trigger_validate(id, 'cancel') invoice_obj = self.pool.get('account.invoice') invoice_line_obj = self.pool.get('account.invoice.line') line_ids = invoice_line_obj.search([('contract','in',ids)]) if not line_ids: return {} invoices = [] lines = invoice_line_obj.browse(line_ids) for l in lines: invoices.append(l.invoice.id) invoice_obj.credit(invoices, refund=True) return {} def create_next_invoice(self, ids, data=None): if data.get('form') and data['form'].get('invoice_date'): invoice_date = data['form']['invoice_date'] else: invoice_date = datetime.date.today() contract_obj = self.pool.get('contract.contract') contract = self.browse(ids)[0] period = self._check_contract(contract, invoice_date) if not period: return {} log.debug("invoice_date: %s period: %s" %(invoice_date, period)) ## create a new invoice invoice = self._invoice_init(contract, invoice_date) ## create invoice lines line = self._invoice_append(invoice, contract, period) self.write(contract.id, {'opt_invoice_date': period[1]}) return invoice.id def _check_contract(self, contract, invoice_date): """ returns tuple 'period' or False False is returned if this contract is not up for billing at this moment. period is defined as (last_date, next_date, quantity) where the first two are datetime values indicating the billing period this contract is valid for, and the last indicates the quantity on the invoice line (months, weeks, years) """ if not contract.state == 'active': return {} # don't invoice contracts unless they are due within 30 days # after the invoice_date end = invoice_date + datetime.timedelta(NOTICE_DAYS) if contract.next_invoice_date and end < contract.next_invoice_date: log.info('too early to invoice: %s + %d days < %s' %(invoice_date, NOTICE_DAYS, end)) return False last_date = contract.next_invoice_date or contract.start_date or invoice_date next_date = last_date quant = 0 while next_date < end: if contract.interval == 'year': next_date = next_date + relativedelta(years=contract.interval_quant) quant = relativedelta(next_date, last_date).years elif contract.interval == 'month': next_date = next_date + relativedelta(months=contract.interval_quant) delta = relativedelta(next_date, last_date) quant = delta.years * 12 + delta.months elif contract.interval == 'week': next_date = next_date + relativedelta(weeks=contract.interval_quant) quant = (next_date - last_date).days/7 elif contract.interval == 'day': next_date = next_date + relativedelta(days=contract.interval_quant) quant = (next_date - last_date).days if next_date and contract.stop_date and next_date > contract.stop_date: log.info('contract stopped: %s > %s' % (next_date, contract.stop_date)) return False quant = quant * contract.quantity log.debug("last_date: %s next_date: %s quant: %d" % (last_date,next_date,quant)) return (last_date, next_date, quant) def create_invoice_batch(self, party=None, data=None): if data and data.get('form') and data['form'].get('invoice_date'): invoice_date = data['form']['invoice_date'] else: invoice_date = datetime.date.today() log.info("create invoice batch with invoice_date: %s" % invoice_date) contract_obj = self.pool.get('contract.contract') contract_ids = None if data and data.get('model') == 'contract.contract': contract_ids = data.get('ids') if not contract_ids: """ get a list of all active contracts """ query = [('state','=','active'), ('start_date','<=',invoice_date), ] """ filter on party if required """ if party: if type(party) != type([1,]): party = [party,] query.append(('party','in',party)) contract_ids = contract_obj.search(query) if not contract_ids: return [] """ build the list of all billable contracts and aggragate the result per party """ batch = {} contracts = contract_obj.browse(contract_ids) for contract in contracts: period = self._check_contract(contract, invoice_date) if period and period[2]: key = contract.party.id if not batch.get(key): batch[key] = [] batch[key].append((contract, period)) """ create draft invoices per party with lines for all billable contracts """ res = [] for party, info in batch.items(): invoice = self._invoice_init(info[0][0], invoice_date) for (contract, period) in info: self._invoice_append(invoice, contract, period) self.write(contract.id, {'opt_invoice_date': period[1]}) res.append(invoice.id) return res
class PurchaseRequest(ModelSQL, ModelView): 'Purchase Request' _name = 'purchase.request' _description = __doc__ product = fields.Many2One('product.product', 'Product', required=True, select=1, readonly=True, domain=[('purchasable', '=', True)]) party = fields.Many2One('party.party', 'Party', select=1) quantity = fields.Float('Quantity', required=True) uom = fields.Many2One('product.uom', 'UOM', required=True, select=1) computed_quantity = fields.Float('Computed Quantity', readonly=True) computed_uom = fields.Many2One('product.uom', 'Computed UOM', readonly=True) purchase_date = fields.Date('Best Purchase Date', readonly=True) supply_date = fields.Date('Expected Supply Date', readonly=True) stock_level = fields.Float('Stock at Supply Date', readonly=True) warehouse = fields.Many2One('stock.location', "Warehouse", required=True, domain=[('type', '=', 'warehouse')], readonly=True) purchase_line = fields.Many2One('purchase.line', 'Purchase Line', readonly=True) purchase = fields.Function( fields.Many2One('purchase.purchase', 'Purchase'), 'get_purchase') company = fields.Many2One('company.company', 'Company', required=True, readonly=True, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) origin = fields.Reference('Origin', selection='origin_get', readonly=True, required=True) state = fields.Function( fields.Selection([ ('purchased', 'Purchased'), ('done', 'Done'), ('draft', 'Draft'), ('cancel', 'Cancel'), ], 'State'), 'get_state') def __init__(self): super(PurchaseRequest, self).__init__() self._order[0] = ('id', 'DESC') self._error_messages.update({ 'create_request': 'Purchase requests are only created by the system.', }) def get_rec_name(self, ids, name): if isinstance(ids, (int, long)): ids = [ids] res = {} for pr in self.browse(ids): res[pr.id] = "%s@%s" % (pr.product.name, pr.warehouse.name) return res def search_rec_name(self, name, clause): res = [] names = clause[2].split('@', 1) res.append(('product.template.name', clause[1], names[0])) if len(names) != 1 and names[1]: res.append(('warehouse', clause[1], names[1])) return res def default_company(self): return Transaction().context.get('company') or False def get_purchase(self, ids, name): res = {} requests = self.browse(ids) for request in requests: if request.purchase_line: res[request.id] = request.purchase_line.purchase.id else: res[request.id] = False return res def get_state(self, ids, name): res = {}.fromkeys(ids, 'draft') for request in self.browse(ids): if request.purchase_line: if request.purchase_line.purchase.state == 'cancel': res[request.id] = 'cancel' elif request.purchase_line.purchase.state == 'done': res[request.id] = 'done' else: res[request.id] = 'purchased' return res def origin_get(self): model_obj = self.pool.get('ir.model') res = [] model_ids = model_obj.search([ ('model', '=', 'stock.order_point'), ]) for model in model_obj.browse(model_ids): res.append([model.model, model.name]) return res def generate_requests(self): """ For each product compute the purchase request that must be create today to meet product outputs. """ order_point_obj = self.pool.get('stock.order_point') purchase_request_obj = self.pool.get('purchase.request') product_obj = self.pool.get('product.product') location_obj = self.pool.get('stock.location') user_obj = self.pool.get('res.user') company = user_obj.browse(Transaction().user).company # fetch warehouses: warehouse_ids = location_obj.search([ ('type', '=', 'warehouse'), ]) # fetch order points order_point_ids = order_point_obj.search([ ('type', '=', 'purchase'), ]) # index them by product product2ops = {} for order_point in order_point_obj.browse(order_point_ids): product2ops[(order_point.warehouse_location.id, order_point.product.id)] = order_point # fetch stockable products product_ids = product_obj.search([ ('type', '=', 'stockable'), ('purchasable', '=', True), ]) #aggregate product by minimum supply date date2products = {} for product in product_obj.browse(product_ids): min_date, max_date = self.get_supply_dates(product) date2products.setdefault(min_date, []).append((product, max_date)) # compute requests new_requests = [] for min_date in date2products: product_ids = [x[0].id for x in date2products[min_date]] with Transaction().set_context(forecast=True, stock_date_end=min_date or datetime.date.max, stock_skip_warehouse=True): pbl = product_obj.products_by_location(warehouse_ids, product_ids, with_childs=True, skip_zero=False) for product, max_date in date2products[min_date]: for warehouse_id in warehouse_ids: qty = pbl.pop((warehouse_id, product.id)) order_point = product2ops.get((warehouse_id, product.id)) # Search for shortage between min-max shortage_date, product_quantity = self.get_shortage( warehouse_id, product.id, min_date, max_date, min_date_qty=qty, order_point=order_point) if shortage_date == None or product_quantity == None: continue # generate request values request_val = self.compute_request(product, warehouse_id, shortage_date, product_quantity, company, order_point) new_requests.append(request_val) new_requests = self.compare_requests(new_requests) self.create_requests(new_requests) return {} def create_requests(self, new_requests): request_obj = self.pool.get('purchase.request') for new_req in new_requests: if new_req['supply_date'] == datetime.date.max: new_req['supply_date'] = None if new_req['quantity'] > 0.0: new_req.update({ 'product': new_req['product'].id, 'party': new_req['party'] and new_req['party'].id, 'uom': new_req['uom'].id, 'computed_uom': new_req['computed_uom'].id, 'company': new_req['company'].id }) request_obj.create(new_req) def compare_requests(self, new_requests): """ Compare new_requests with already existing request to avoid to re-create existing requests. """ # delete purchase request without a purchase line uom_obj = self.pool.get('product.uom') request_obj = self.pool.get('purchase.request') product_supplier_obj = self.pool.get('purchase.product_supplier') req_ids = request_obj.search([ ('purchase_line', '=', False), ('origin', 'like', 'stock.order_point,%'), ]) request_obj.delete(req_ids) req_ids = request_obj.search([ ('purchase_line.moves', '=', False), ('purchase_line.purchase.state', '!=', 'cancel'), ('origin', 'like', 'stock.order_point,%'), ]) requests = request_obj.browse(req_ids) # Fetch data from existing requests existing_req = {} for request in requests: pline = request.purchase_line # Skip incoherent request if request.product.id != pline.product.id or \ request.warehouse.id != pline.purchase.warehouse.id: continue # Take smallest amount between request and purchase line req_qty = uom_obj.compute_qty(request.uom, request.quantity, pline.unit) if req_qty < pline.quantity: quantity = request.quantity uom = request.uom else: quantity = pline.quantity uom = pline.unit existing_req.setdefault( (request.product.id, request.warehouse.id), []).append({ 'supply_date': request.supply_date or datetime.date.max, 'quantity': quantity, 'uom': uom }) for i in existing_req.itervalues(): i.sort(lambda r, s: cmp(r['supply_date'], s['supply_date'])) # Update new requests to take existing requests into account new_requests.sort(lambda r, s: cmp(r['supply_date'], s['supply_date'])) for new_req in new_requests: for old_req in existing_req.get( (new_req['product'].id, new_req['warehouse']), []): if old_req['supply_date'] <= new_req['supply_date']: quantity = uom_obj.compute_qty(old_req['uom'], old_req['quantity'], new_req['uom']) new_req['quantity'] = max(0.0, new_req['quantity'] - quantity) new_req['computed_quantity'] = new_req['quantity'] old_req['quantity'] = uom_obj.compute_qty( new_req['uom'], max(0.0, quantity - new_req['quantity']), old_req['uom']) else: break return new_requests def get_supply_dates(self, product): """ Return the minimal interval of earliest supply dates for a product. :param product: a BrowseRecord of the Product :return: a tuple with the two dates """ product_supplier_obj = self.pool.get('purchase.product_supplier') date_obj = self.pool.get('ir.date') min_date = None max_date = None today = date_obj.today() for product_supplier in product.product_suppliers: supply_date, next_supply_date = product_supplier_obj.\ compute_supply_date(product_supplier, date=today) if (not min_date) or supply_date < min_date: min_date = supply_date if (not max_date): max_date = next_supply_date if supply_date > min_date and supply_date < max_date: max_date = supply_date if next_supply_date < max_date: max_date = next_supply_date if not min_date: min_date = datetime.date.max max_date = datetime.date.max return (min_date, max_date) def compute_request(self, product, location_id, shortage_date, product_quantity, company, order_point=None): """ Return the value of the purchase request which will answer to the needed quantity at the given date. I.e: the latest purchase date, the expected supply date and the prefered supplier. """ uom_obj = self.pool.get('product.uom') product_supplier_obj = self.pool.get('purchase.product_supplier') date_obj = self.pool.get('ir.date') supplier = None timedelta = datetime.timedelta.max today = date_obj.today() max_quantity = order_point and order_point.max_quantity or 0.0 for product_supplier in product.product_suppliers: supply_date = product_supplier_obj.compute_supply_date( product_supplier, date=today)[0] sup_timedelta = shortage_date - supply_date if not supplier: supplier = product_supplier.party timedelta = sup_timedelta continue if timedelta < datetime.timedelta(0) \ and (sup_timedelta >= datetime.timedelta(0) \ or sup_timedelta > timedelta): supplier = product_supplier.party timedelta = sup_timedelta if supplier: purchase_date = product_supplier_obj.compute_purchase_date( product_supplier, shortage_date) else: purchase_date = today quantity = uom_obj.compute_qty( product.default_uom, max_quantity - product_quantity, product.purchase_uom or product.default_uom) if order_point: origin = 'stock.order_point,%s' % order_point.id else: origin = 'stock.order_point,0' return { 'product': product, 'party': supplier and supplier or None, 'quantity': quantity, 'uom': product.purchase_uom or product.default_uom, 'computed_quantity': quantity, 'computed_uom': product.purchase_uom or product.default_uom, 'purchase_date': purchase_date, 'supply_date': shortage_date, 'stock_level': product_quantity, 'company': company, 'warehouse': location_id, 'origin': origin, } def get_shortage(self, location_id, product_id, min_date, max_date, min_date_qty, order_point=None): """ Return between min_date and max_date the first date where the stock quantity is less than the minimal quantity and the smallest stock quantity in the interval or None if there is no date where stock quantity is less than the minimal quantity The minimal quantity comes from the order_point or is zero :param location_id: the stock location id :param produc_id: the product id :param min_date: the minimal date :param max_date: the maximal date :param min_date_qty: the stock quantity at the minimal date :param order_point: a BrowseRecord of the Order Point :return: a tuple with the date and the quantity """ product_obj = self.pool.get('product.product') res_date = None res_qty = None min_quantity = order_point and order_point.min_quantity or 0.0 current_date = min_date current_qty = min_date_qty while (current_date < max_date) or (current_date == min_date): if current_qty < min_quantity: if not res_date: res_date = current_date if (not res_qty) or (current_qty < res_qty): res_qty = current_qty with Transaction().set_context(stock_date_start=current_date, stock_date_end=current_date): res = product_obj.products_by_location([location_id], [product_id], with_childs=True, skip_zero=False) for qty in res.itervalues(): current_qty += qty if current_date == datetime.date.max: break current_date += datetime.timedelta(1) return (res_date, res_qty) def create(self, vals): for field_name in ('product', 'quantity', 'uom', 'warehouse', 'company'): if not vals.get(field_name): self.raise_user_error('create_request') return super(PurchaseRequest, self).create(vals)
class OrderPoint(ModelSQL, ModelView): """ Order Point: Provide a way to define a supply policy for each product on each locations. Order points on warehouse are considered by the supply scheduler to generate purchase requests. """ _name = 'stock.order_point' _description = "Order Point" product = fields.Many2One('product.product', 'Product', required=True, select=1, domain=[ ('type', '=', 'stockable'), ('purchasable', 'in', If(Equal(Eval('type'), 'purchase'), [True], [True, False])), ], on_change=['product']) warehouse_location = fields.Many2One('stock.location', 'Warehouse Location', select=1, domain=[('type', '=', 'warehouse')], states={ 'invisible': Not( Equal(Eval('type'), 'purchase')), 'required': Equal(Eval('type'), 'purchase'), }) storage_location = fields.Many2One('stock.location', 'Storage Location', select=1, domain=[('type', '=', 'storage')], states={ 'invisible': Not(Equal(Eval('type'), 'internal')), 'required': Equal(Eval('type'), 'internal'), }) location = fields.Function(fields.Many2One('stock.location', 'Location'), 'get_location', searcher='search_location') provisioning_location = fields.Many2One( 'stock.location', 'Provisioning Location', domain=[('type', '=', 'storage')], states={ 'invisible': Not(Equal(Eval('type'), 'internal')), 'required': Equal(Eval('type'), 'internal'), }) type = fields.Selection([('internal', 'Internal'), ('purchase', 'Purchase')], 'Type', select=1, required=True) min_quantity = fields.Float('Minimal Quantity', required=True, digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) max_quantity = fields.Float('Maximal Quantity', required=True, digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) company = fields.Many2One('company.company', 'Company', required=True, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) unit = fields.Function(fields.Many2One('product.uom', 'Unit'), 'get_unit') unit_digits = fields.Function(fields.Integer('Unit Digits'), 'get_unit_digits') def __init__(self): super(OrderPoint, self).__init__() self._constraints += [ ('check_concurrent_internal', 'concurrent_internal_op'), ('check_uniqueness', 'unique_op'), ] self._sql_constraints += [ ('check_min_max_quantity', 'CHECK( max_quantity is null or min_quantity is null or max_quantity >= min_quantity )', 'Maximal quantity must be bigger than Minimal quantity'), ] self._error_messages.update( {'unique_op': 'Only one order point is allowed '\ 'for each product-location pair.', 'concurrent_internal_op': 'You can not define two order points '\ 'on the same product with opposite locations.',}) def default_type(self): return "purchase" def on_change_product(self, vals): product_obj = self.pool.get('product.product') res = { 'unit': False, 'unit.rec_name': '', 'unit_digits': 2, } if vals.get('product'): product = product_obj.browse(vals['product']) res['unit'] = product.default_uom.id res['unit.rec_name'] = product.default_uom.rec_name res['unit_digits'] = product.default_uom.digits return res def get_unit(self, ids, name): res = {} for order in self.browse(ids): res[order.id] = order.product.default_uom.id return res def get_unit_digits(self, ids, name): res = {} for order in self.browse(ids): res[order.id] = order.product.default_uom.digits return res def check_concurrent_internal(self, ids): """ Ensure that there is no 'concurrent' internal order points. I.E. no two order point with opposite location for the same product and same company. """ internal_ids = self.search([ ('id', 'in', ids), ('type', '=', 'internal'), ]) if not internal_ids: return True query = ['OR'] for op in self.browse(internal_ids): arg = [ 'AND', ('provisioning_location', '=', op.storage_location.id), ('storage_location', '=', op.provisioning_location.id), ('company', '=', op.company.id), ('type', '=', 'internal') ] query.append(arg) ids = self.search(query) return not bool(ids) def _type2field(self, type=None): t2f = { 'purchase': 'warehouse_location', 'internal': 'storage_location', } if type == None: return t2f else: return t2f[type] def check_uniqueness(self, ids): """ Ensure uniqueness of order points. I.E that there is no several order point for the same location, the same product and the same company. """ query = ['OR'] for op in self.browse(ids): field = self._type2field(op.type) arg = [ 'AND', ('product', '=', op.product.id), (field, '=', op[field].id), ('id', '!=', op.id), ('company', '=', op.company.id), ] query.append(arg) ids = self.search(query) return not bool(ids) def get_rec_name(self, ids, name): if not ids: return {} if isinstance(ids, (int, long)): ids = [ids] res = {} for op in self.browse(ids): res[op.id] = "%s@%s" % (op.product.name, op.location.name) return res def search_rec_name(self, name, clause): res = [] names = clause[2].split('@', 1) res.append(('product.template.name', clause[1], names[0])) if len(names) != 1 and names[1]: res.append(('location', clause[1], names[1])) return res def get_location(self, ids, name): location_obj = self.pool.get('stock.location') res = {} for op in self.browse(ids): if op.type == 'purchase': res[op.id] = op.warehouse_location.id elif op.type == 'internal': res[op.id] = op.storage_location.id else: res[op.id] = False return res def search_location(self, name, domain=None): ids = [] for type, field in self._type2field().iteritems(): args = [('type', '=', type)] for _, operator, operand in domain: args.append((field, operator, operand)) ids.extend(self.search(args)) return [('id', 'in', ids)] def default_company(self): return Transaction().context.get('company') or False
class SaleOpportunity(Workflow, ModelSQL, ModelView): 'Sale Opportunity' __name__ = "sale.opportunity" _history = True _rec_name = 'reference' reference = fields.Char('Reference', readonly=True, required=True, select=True) party = fields.Many2One( 'party.party', 'Party', select=True, on_change=['party'], states={ 'readonly': Eval('state').in_(['converted', 'lost', 'cancelled']), 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']), }, depends=['state']) address = fields.Many2One('party.address', 'Address', domain=[('party', '=', Eval('party'))], select=True, depends=['party', 'state'], states=_STATES_STOP) company = fields.Many2One('company.company', 'Company', required=True, select=True, states=_STATES_STOP, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ], on_change=['company'], depends=_DEPENDS_STOP) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'get_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits'], help='Estimated revenue amount') payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states={ 'required': Eval('state') == 'converted', 'readonly': In(Eval('state'), ['converted', 'lost', 'cancelled']), }, depends=['state']) employee = fields.Many2One('company.employee', 'Employee', required=True, states=_STATES_STOP, depends=['state', 'company'], domain=[('company', '=', Eval('company'))]) start_date = fields.Date('Start Date', required=True, select=True, states=_STATES_START, depends=_DEPENDS_START) end_date = fields.Date('End Date', select=True, readonly=True, states={ 'invisible': Not( In(Eval('state'), ['converted', 'cancelled', 'lost'])), }, depends=['state']) description = fields.Char('Description', required=True, states=_STATES_STOP, depends=_DEPENDS_STOP) comment = fields.Text('Comment', states=_STATES_STOP, depends=_DEPENDS_STOP) lines = fields.One2Many('sale.opportunity.line', 'opportunity', 'Lines', states=_STATES_STOP, depends=_DEPENDS_STOP) state = fields.Selection(STATES, 'State', required=True, select=True, sort=False, readonly=True) probability = fields.Integer('Conversion Probability', required=True, states={ 'readonly': Not( In(Eval('state'), ['opportunity', 'lead'])), }, depends=['state'], help="Percentage between 0 and 100") history = fields.One2Many('sale.opportunity.history', 'opportunity', 'History', readonly=True) lost_reason = fields.Text('Reason for loss', states={ 'invisible': Eval('state') != 'lost', }, depends=['state']) sale = fields.Many2One('sale.sale', 'Sale', readonly=True, states={ 'invisible': Eval('state') != 'converted', }, depends=['state']) @classmethod def __register__(cls, module_name): cursor = Transaction().cursor reference_exists = True if TableHandler.table_exist(cursor, cls._table): table = TableHandler(cursor, cls, module_name) reference_exists = table.column_exist('reference') super(SaleOpportunity, cls).__register__(module_name) table = TableHandler(cursor, cls, module_name) # Migration from 2.8: make party not required and add reference as # required table.not_null_action('party', action='remove') if not reference_exists: cursor.execute('UPDATE "' + cls._table + '" SET reference=id WHERE ' 'reference IS NULL') table.not_null_action('reference', action='add') @classmethod def __setup__(cls): super(SaleOpportunity, cls).__setup__() cls._order.insert(0, ('start_date', 'DESC')) cls._sql_constraints += [ ('check_percentage', 'CHECK(probability >= 0 AND probability <= 100)', 'Probability must be between 0 and 100.') ] cls._error_messages.update({ 'delete_cancel': ('Sale Opportunity "%s" must be cancelled ' 'before deletion.'), }) cls._transitions |= set(( ('lead', 'opportunity'), ('lead', 'lost'), ('lead', 'cancelled'), ('opportunity', 'converted'), ('opportunity', 'lead'), ('opportunity', 'lost'), ('opportunity', 'cancelled'), ('lost', 'lead'), ('cancelled', 'lead'), )) cls._buttons.update({ 'lead': { 'invisible': ~Eval('state').in_(['cancelled', 'lost', 'opportunity']), 'icon': If( Eval('state').in_(['cancelled', 'lost']), 'tryton-clear', 'tryton-go-previous'), }, 'opportunity': { 'invisible': ~Eval('state').in_(['lead']), }, 'convert': { 'invisible': ~Eval('state').in_(['opportunity']), }, 'lost': { 'invisible': ~Eval('state').in_(['lead', 'opportunity']), }, 'cancel': { 'invisible': ~Eval('state').in_(['lead', 'opportunity']), }, }) @staticmethod def default_state(): return 'lead' @staticmethod def default_start_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_probability(): return 50 @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_employee(): User = Pool().get('res.user') if Transaction().context.get('employee'): return Transaction().context['employee'] else: user = User(Transaction().user) if user.employee: return user.employee.id @classmethod def default_payment_term(cls): PaymentTerm = Pool().get('account.invoice.payment_term') payment_terms = PaymentTerm.search(cls.payment_term.domain) if len(payment_terms) == 1: return payment_terms[0].id @classmethod def create(cls, vlist): pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('sale.configuration') sequence = Config(1).sale_opportunity_sequence vlist = [x.copy() for x in vlist] for vals in vlist: vals['reference'] = Sequence.get_id(sequence.id) return super(SaleOpportunity, cls).create(vlist) @classmethod def copy(cls, opportunities, default=None): if default is None: default = {} default = default.copy() default.setdefault('reference', None) default.setdefault('history', None) return super(SaleOpportunity, cls).copy(opportunities, default=default) def get_currency(self, name): return self.company.currency.id def get_currency_digits(self, name): return self.company.currency.digits def on_change_company(self): res = {} if self.company: res['currency'] = self.company.currency.id res['currency.rec_name'] = self.company.currency.rec_name res['currency_digits'] = self.company.currency.digits return res def on_change_party(self): PaymentTerm = Pool().get('account.invoice.payment_term') res = { 'payment_term': None, } if self.party: if self.party.customer_payment_term: res['payment_term'] = self.party.customer_payment_term.id res['payment_term.rec_name'] = \ self.party.customer_payment_term.rec_name if not res['payment_term']: res['payment_term'] = self.default_payment_term() if res['payment_term']: res['payment_term.rec_name'] = PaymentTerm( res['payment_term']).rec_name return res def _get_sale_line_opportunity_line(self, sale): ''' Return sale lines for each opportunity line ''' res = {} for line in self.lines: if line.sale_line: continue sale_line = line.get_sale_line(sale) if sale_line: res[line.id] = sale_line return res def _get_sale_opportunity(self): ''' Return sale for an opportunity ''' Sale = Pool().get('sale.sale') with Transaction().set_user(0, set_context=True): return Sale( description=self.description, party=self.party, payment_term=self.payment_term, company=self.company, invoice_address=self.address, shipment_address=self.address, currency=self.company.currency, comment=self.comment, sale_date=None, ) def create_sale(self): ''' Create a sale for the opportunity and return the sale ''' Line = Pool().get('sale.opportunity.line') if self.sale: return sale = self._get_sale_opportunity() sale_lines = self._get_sale_line_opportunity_line(sale) sale.save() for line_id, sale_line in sale_lines.iteritems(): sale_line.sale = sale sale_line.save() Line.write([Line(line_id)], { 'sale_line': sale_line.id, }) self.write([self], { 'sale': sale.id, }) return sale @classmethod def delete(cls, opportunities): # Cancel before delete cls.cancel(opportunities) for opportunity in opportunities: if opportunity.state != 'cancelled': cls.raise_user_error('delete_cancel', opportunity.rec_name) super(SaleOpportunity, cls).delete(opportunities) @classmethod @ModelView.button @Workflow.transition('lead') def lead(cls, opportunities): pass @classmethod @ModelView.button @Workflow.transition('opportunity') def opportunity(cls, opportunities): pass @classmethod @ModelView.button @Workflow.transition('converted') def convert(cls, opportunities): Date = Pool().get('ir.date') cls.write(opportunities, { 'end_date': Date.today(), }) for opportunity in opportunities: opportunity.create_sale() @classmethod @ModelView.button @Workflow.transition('lost') def lost(cls, opportunities): Date = Pool().get('ir.date') cls.write(opportunities, { 'end_date': Date.today(), }) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, opportunities): Date = Pool().get('ir.date') cls.write(opportunities, { 'end_date': Date.today(), })