Files
tryton/modules/purchase/purchase.py
T

2066 lines
76 KiB
Python
Raw Normal View History

2014-12-02 12:55:47 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2010-08-19 22:17:54 +02:00
import datetime
2013-01-06 22:18:13 +01:00
from itertools import chain
2010-08-19 22:17:54 +02:00
from decimal import Decimal
2018-08-09 20:43:51 +02:00
from sql import Literal, Null
2013-09-06 22:34:36 +02:00
from sql.aggregate import Count
from sql.operators import Concat
2013-09-06 22:34:36 +02:00
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
from trytond.model import Workflow, Model, ModelView, ModelSQL, fields, \
2016-09-13 14:08:49 +02:00
sequence_ordered
2018-12-29 14:20:29 +01:00
from trytond.model.exceptions import AccessError
2009-10-02 15:11:25 +02:00
from trytond.modules.company import CompanyReport
2012-01-02 18:03:18 +01:00
from trytond.wizard import Wizard, StateAction, StateView, StateTransition, \
Button
2013-09-06 22:34:36 +02:00
from trytond import backend
2016-07-18 14:04:37 +02:00
from trytond.pyson import Eval, Bool, If, PYSONEncoder
2010-08-19 22:17:54 +02:00
from trytond.transaction import Transaction
2016-03-04 00:34:21 +01:00
from trytond.pool import Pool
2020-12-25 17:14:57 +01:00
from trytond.tools import firstline
2008-05-06 17:45:44 +02:00
from trytond.ir.attachment import AttachmentCopyMixin
from trytond.ir.note import NoteCopyMixin
2014-12-19 19:41:50 +01:00
from trytond.modules.account.tax import TaxableMixin
2018-12-29 14:20:29 +01:00
from trytond.modules.account_product.exceptions import AccountError
2020-04-10 18:53:44 +02:00
from trytond.modules.company.model import (
employee_field, set_employee, reset_employee)
2020-04-23 21:44:00 +02:00
from trytond.modules.product import price_digits, round_price
2014-12-19 19:41:50 +01:00
2018-12-29 14:20:29 +01:00
from .exceptions import PurchaseQuotationError, PartyLocationError
2008-05-06 17:45:44 +02:00
def get_shipments_returns(model_name):
"Computes the returns or shipments"
def method(self, name):
Model = Pool().get(model_name)
shipments = set()
for line in self.lines:
for move in line.moves:
if isinstance(move.shipment, Model):
shipments.add(move.shipment.id)
return list(shipments)
return method
def search_shipments_returns(model_name):
"Search on shipments or returns"
def method(self, name, clause):
nested = clause[0].lstrip(name)
if nested:
return [('lines.moves.shipment' + nested,)
+ tuple(clause[1:3]) + (model_name,) + tuple(clause[3:])]
else:
2018-06-26 19:11:51 +02:00
if isinstance(clause[2], str):
target = 'rec_name'
else:
target = 'id'
return [('lines.moves.shipment.' + target,)
+ tuple(clause[1:3]) + (model_name,)]
return classmethod(method)
class Purchase(
Workflow, ModelSQL, ModelView, TaxableMixin,
AttachmentCopyMixin, NoteCopyMixin):
2008-05-06 17:45:44 +02:00
'Purchase'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.purchase'
_rec_name = 'number'
_states = {
'readonly': Eval('state') != 'draft',
}
_depends = ['state']
2021-04-20 09:18:58 +02:00
company = fields.Many2One(
'company.company', "Company", required=True, select=True,
2011-08-04 16:57:38 +02:00
states={
'readonly': (
(Eval('state') != 'draft')
| Eval('lines', [0])
| Eval('party', True)
| Eval('invoice_party', True)),
2011-08-21 11:20:50 +02:00
},
2021-04-20 09:18:58 +02:00
depends=['state'])
number = fields.Char('Number', size=None, readonly=True, select=True)
reference = fields.Char('Reference', select=True)
description = fields.Char('Description', size=None, states=_states,
depends=_depends)
purchase_date = fields.Date('Purchase Date',
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
'required': ~Eval('state').in_(
['draft', 'quotation', 'cancelled']),
},
depends=['state'])
2008-05-06 17:45:44 +02:00
payment_term = fields.Many2One('account.invoice.payment_term',
'Payment Term', states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
},
depends=['state'])
2014-01-26 14:29:28 +01:00
party = fields.Many2One('party.party', 'Party', required=True,
2014-07-11 18:17:46 +02:00
states={
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('party'))),
},
context={
'company': Eval('company', -1),
},
select=True, depends=['state', 'company'])
2014-01-26 14:29:28 +01:00
party_lang = fields.Function(fields.Char('Party Language'),
'on_change_with_party_lang')
2020-03-28 14:25:57 +01:00
contact = fields.Many2One(
'party.contact_mechanism', "Contact",
context={
'company': Eval('company', -1),
},
2020-03-28 14:25:57 +01:00
search_context={
'related_party': Eval('party'),
},
depends=['party', 'company'])
2020-03-28 14:25:57 +01:00
invoice_party = fields.Many2One('party.party', "Invoice Party",
states={
'readonly': ((Eval('state') != 'draft')
| Eval('lines', [0])),
},
context={
'company': Eval('company', -1),
},
search_context={
'related_party': Eval('party'),
},
depends=['state', 'party', 'company'])
2008-10-22 20:12:14 +02:00
invoice_address = fields.Many2One('party.address', 'Invoice Address',
2020-03-28 14:25:57 +01:00
domain=[
('party', '=', If(Bool(Eval('invoice_party')),
Eval('invoice_party'), Eval('party'))),
],
2014-01-26 17:32:45 +01:00
states={
'readonly': Eval('state') != 'draft',
'required': ~Eval('state').in_(
['draft', 'quotation', 'cancelled']),
2014-01-26 17:32:45 +01:00
},
2020-03-28 14:25:57 +01:00
depends=['party', 'invoice_party', 'state'])
2008-05-06 17:45:44 +02:00
warehouse = fields.Many2One('stock.location', 'Warehouse',
domain=[('type', '=', 'warehouse')], states=_states,
depends=_depends)
2008-05-09 17:27:19 +02:00
currency = fields.Many2One('currency.currency', 'Currency', required=True,
states={
2011-08-21 11:20:50 +02:00
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('currency'))),
2011-08-04 16:57:38 +02:00
},
depends=['state'])
2014-01-26 14:29:28 +01:00
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
2008-05-06 17:45:44 +02:00
lines = fields.One2Many('purchase.line', 'purchase', 'Lines',
states=_states, depends=_depends)
2008-05-06 17:45:44 +02:00
comment = fields.Text('Comment')
2010-03-11 16:53:13 +01:00
untaxed_amount = fields.Function(fields.Numeric('Untaxed',
2011-08-04 16:57:38 +02:00
digits=(16, Eval('currency_digits', 2)),
2013-06-01 00:02:18 +02:00
depends=['currency_digits']), 'get_amount')
untaxed_amount_cache = fields.Numeric('Untaxed Cache',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
2010-03-11 16:53:13 +01:00
tax_amount = fields.Function(fields.Numeric('Tax',
2011-08-04 16:57:38 +02:00
digits=(16, Eval('currency_digits', 2)),
2013-06-01 00:02:18 +02:00
depends=['currency_digits']), 'get_amount')
tax_amount_cache = fields.Numeric('Tax Cache',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
2010-03-11 16:53:13 +01:00
total_amount = fields.Function(fields.Numeric('Total',
2011-08-04 16:57:38 +02:00
digits=(16, Eval('currency_digits', 2)),
2013-06-01 00:02:18 +02:00
depends=['currency_digits']), 'get_amount')
total_amount_cache = fields.Numeric('Total Cache',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
2008-05-06 17:45:44 +02:00
invoice_method = fields.Selection([
2011-08-04 16:57:38 +02:00
('manual', 'Manual'),
('order', 'Based On Order'),
('shipment', 'Based On Shipment'),
], 'Invoice Method', required=True, states=_states,
depends=_depends)
2008-05-06 17:45:44 +02:00
invoice_state = fields.Selection([
2012-03-25 00:18:10 +01:00
('none', 'None'),
('waiting', 'Waiting'),
('paid', 'Paid'),
('exception', 'Exception'),
], 'Invoice State', readonly=True, required=True)
invoices = fields.Function(fields.Many2Many(
'account.invoice', None, None, "Invoices"),
'get_invoices', searcher='search_invoices')
2009-03-05 14:53:03 +01:00
invoices_ignored = fields.Many2Many(
'purchase.purchase-ignored-account.invoice',
'purchase', 'invoice', 'Ignored Invoices', readonly=True)
invoices_recreated = fields.Many2Many(
'purchase.purchase-recreated-account.invoice',
'purchase', 'invoice', 'Recreated Invoices', readonly=True)
2021-03-27 21:21:33 +01:00
origin = fields.Reference('Origin', selection='get_origin', select=True,
states={
'readonly': Eval('state') != 'draft',
},
depends=['state'])
delivery_date = fields.Date(
"Delivery Date",
states={
'readonly': Eval('state').in_([
'processing', 'done', 'cancelled']),
},
depends=['state'],
help="The default delivery date for each line.")
shipment_state = fields.Selection([
2012-03-25 00:18:10 +01:00
('none', 'None'),
('waiting', 'Waiting'),
('received', 'Received'),
('exception', 'Exception'),
], 'Shipment State', readonly=True, required=True)
shipments = fields.Function(fields.Many2Many(
'stock.shipment.in', None, None, "Shipments"),
'get_shipments', searcher='search_shipments')
shipment_returns = fields.Function(fields.Many2Many(
'stock.shipment.in.return', None, None, "Shipment Returns"),
'get_shipment_returns', searcher='search_shipment_returns')
2015-09-26 21:14:03 +02:00
moves = fields.One2Many('stock.move', 'purchase', 'Moves', readonly=True)
2008-05-06 17:45:44 +02:00
2020-04-10 18:53:44 +02:00
quoted_by = employee_field(
"Quoted By",
states=['quotation', 'confirmed', 'processing', 'done', 'cancelled'])
2020-04-10 18:53:44 +02:00
confirmed_by = employee_field(
"Confirmed By",
states=['confirmed', 'processing', 'done', 'cancelled'])
2020-04-10 18:53:44 +02:00
state = fields.Selection([
('draft', "Draft"),
('quotation', "Quotation"),
('confirmed', "Confirmed"),
('processing', "Processing"),
('done', "Done"),
('cancelled', "Cancelled"),
2020-04-10 18:53:44 +02:00
], "State", readonly=True, required=True)
del _states, _depends
2012-09-24 22:18:09 +02:00
@classmethod
def __setup__(cls):
super(Purchase, cls).__setup__()
2022-02-03 22:29:08 +01:00
cls.create_date.select = True
cls._order = [
2021-02-14 15:48:44 +01:00
('purchase_date', 'DESC NULLS FIRST'),
('id', 'DESC'),
]
2012-09-24 22:18:09 +02:00
cls._transitions |= set((
2012-03-25 00:18:10 +01:00
('draft', 'quotation'),
('quotation', 'confirmed'),
('confirmed', 'processing'),
2018-07-27 09:27:49 +02:00
('confirmed', 'draft'),
('processing', 'processing'),
('processing', 'done'),
('done', 'processing'),
('draft', 'cancelled'),
('quotation', 'cancelled'),
2012-03-25 00:18:10 +01:00
('quotation', 'draft'),
('cancelled', 'draft'),
2012-03-25 00:18:10 +01:00
))
2012-09-24 22:18:09 +02:00
cls._buttons.update({
2012-03-25 00:18:10 +01:00
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'quotation']),
2018-02-19 11:40:08 +01:00
'depends': ['state'],
2012-03-25 00:18:10 +01:00
},
'draft': {
2018-07-27 09:27:49 +02:00
'invisible': ~Eval('state').in_(
['cancelled', 'quotation', 'confirmed']),
'icon': If(Eval('state') == 'cancelled', 'tryton-undo',
2018-08-19 20:02:58 +02:00
'tryton-back'),
2018-02-19 11:40:08 +01:00
'depends': ['state'],
2012-03-25 00:18:10 +01:00
},
'quote': {
2014-09-17 22:07:37 +02:00
'pre_validate': [
If(~Eval('purchase_date'),
('purchase_date', '!=', None),
()),
If(~Eval('invoice_address'),
('invoice_address', '!=', None),
()),
2014-09-17 22:07:37 +02:00
],
2012-03-25 00:18:10 +01:00
'invisible': Eval('state') != 'draft',
'readonly': ~Eval('lines', Eval('untaxed_amount', 0)),
2018-02-19 11:40:08 +01:00
'depends': ['state'],
2012-03-25 00:18:10 +01:00
},
'confirm': {
'invisible': Eval('state') != 'quotation',
2018-02-19 11:40:08 +01:00
'depends': ['state'],
2012-03-25 00:18:10 +01:00
},
'process': {
'invisible': ~Eval('state').in_(
['confirmed', 'processing']),
'icon': If(Eval('state') == 'confirmed',
'tryton-forward', 'tryton-refresh'),
2018-02-19 11:40:08 +01:00
'depends': ['state'],
},
2012-09-10 15:28:28 +02:00
'handle_invoice_exception': {
'invisible': ((Eval('invoice_state') != 'exception')
| (Eval('state') == 'cancelled')),
2018-02-19 11:40:08 +01:00
'depends': ['state', 'invoice_state'],
2012-09-10 15:28:28 +02:00
},
'handle_shipment_exception': {
'invisible': ((Eval('shipment_state') != 'exception')
| (Eval('state') == 'cancelled')),
2018-02-19 11:40:08 +01:00
'depends': ['state', 'shipment_state'],
2012-09-10 15:28:28 +02:00
},
'modify_header': {
'invisible': ((Eval('state') != 'draft')
| ~Eval('lines', [-1])),
2018-02-19 11:40:08 +01:00
'depends': ['state'],
},
2012-03-25 00:18:10 +01:00
})
# The states where amounts are cached
cls._states_cached = ['confirmed', 'done', 'cancelled']
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
@classmethod
def __register__(cls, module_name):
pool = Pool()
PurchaseLine = pool.get('purchase.line')
Move = pool.get('stock.move')
InvoiceLine = pool.get('account.invoice.line')
cursor = Transaction().connection.cursor()
2013-09-06 22:34:36 +02:00
sql_table = cls.__table__()
2018-07-24 22:01:08 +02:00
table = cls.__table_handler__(module_name)
# Migration from 3.8:
if table.column_exist('supplier_reference'):
table.column_rename('reference', 'number')
table.column_rename('supplier_reference', 'reference')
2012-09-24 22:18:09 +02:00
super(Purchase, cls).__register__(module_name)
2018-07-24 22:01:08 +02:00
table = cls.__table_handler__(module_name)
# Migration from 3.2
# state confirmed splitted into confirmed and processing
2019-12-06 17:33:46 +01:00
if (backend.TableHandler.table_exist(PurchaseLine._table)
and backend.TableHandler.table_exist(Move._table)
and backend.TableHandler.table_exist(InvoiceLine._table)):
purchase_line = PurchaseLine.__table__()
move = Move.__table__()
invoice_line = InvoiceLine.__table__()
# Wrap subquery inside an other inner subquery because MySQL syntax
# doesn't allow update a table and select from the same table in a
# subquery.
sub_query = sql_table.join(purchase_line,
condition=purchase_line.purchase == sql_table.id
).join(invoice_line, 'LEFT',
2020-03-01 12:33:53 +01:00
condition=(invoice_line.origin
== Concat(
PurchaseLine.__name__ + ',', purchase_line.id))
).join(move, 'LEFT',
condition=(move.origin == Concat(
PurchaseLine.__name__ + ',', purchase_line.id))
).select(sql_table.id,
where=((sql_table.state == 'confirmed')
& ((invoice_line.id != Null)
| (move.id != Null))))
cursor.execute(*sql_table.update(
columns=[sql_table.state],
values=['processing'],
where=sql_table.id.in_(sub_query.select(sub_query.id))))
# Migration from 5.6: rename state cancel to cancelled
cursor.execute(*sql_table.update(
[sql_table.state], ['cancelled'],
where=sql_table.state == 'cancel'))
2012-09-24 22:18:09 +02:00
@classmethod
def default_warehouse(cls):
Location = Pool().get('stock.location')
return Location.get_default_warehouse()
2012-09-24 22:18:09 +02:00
@staticmethod
def default_company():
2012-03-27 10:43:54 +02:00
return Transaction().context.get('company')
2008-05-06 17:45:44 +02:00
2021-04-03 14:27:10 +02:00
@fields.depends('company')
def on_change_company(self):
self.invoice_method = self.default_invoice_method(
company=self.company.id if self.company else None)
2012-09-24 22:18:09 +02:00
@staticmethod
def default_state():
2008-05-06 17:45:44 +02:00
return 'draft'
2012-09-24 22:18:09 +02:00
@staticmethod
def default_currency():
Company = Pool().get('company.company')
2010-08-19 22:17:54 +02:00
company = Transaction().context.get('company')
if company:
2012-09-24 22:18:09 +02:00
company = Company(company)
return company.currency.id
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
@staticmethod
def default_currency_digits():
Company = Pool().get('company.company')
2010-08-19 22:17:54 +02:00
company = Transaction().context.get('company')
if company:
2012-09-24 22:18:09 +02:00
company = Company(company)
return company.currency.digits
return 2
2021-04-03 14:27:10 +02:00
@classmethod
def default_invoice_method(cls, **pattern):
2012-09-24 22:18:09 +02:00
Configuration = Pool().get('purchase.configuration')
configuration = Configuration(1)
2021-04-03 14:27:10 +02:00
return configuration.get_multivalue(
'purchase_invoice_method', **pattern)
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
@staticmethod
def default_invoice_state():
2008-05-06 17:45:44 +02:00
return 'none'
2012-09-24 22:18:09 +02:00
@staticmethod
def default_shipment_state():
2008-05-06 17:45:44 +02:00
return 'none'
2020-03-28 14:25:57 +01:00
@fields.depends('party', 'invoice_party', 'payment_term', 'lines')
2012-09-24 22:18:09 +02:00
def on_change_party(self):
cursor = Transaction().connection.cursor()
2013-09-06 22:34:36 +02:00
table = self.__table__()
2020-03-28 14:25:57 +01:00
if not self.invoice_party:
self.invoice_address = None
self.invoice_method = self.default_invoice_method()
if not self.lines:
2014-10-29 20:40:09 +01:00
self.currency = self.default_currency()
self.currency_digits = self.default_currency_digits()
2012-09-24 22:18:09 +02:00
if self.party:
2020-03-28 14:25:57 +01:00
if not self.invoice_party:
self.invoice_address = self.party.address_get(type='invoice')
2008-05-06 17:45:44 +02:00
if not self.lines:
2020-03-28 14:25:57 +01:00
invoice_party = (
self.invoice_party.id if self.invoice_party else None)
subquery = table.select(
table.currency,
table.payment_term,
table.invoice_method,
2020-03-28 14:25:57 +01:00
where=(table.party == self.party.id)
& (table.invoice_party == invoice_party),
order_by=table.id,
limit=10)
cursor.execute(*subquery.select(
subquery.currency,
subquery.payment_term,
subquery.invoice_method,
group_by=[
subquery.currency,
subquery.payment_term,
subquery.invoice_method,
],
order_by=Count(Literal(1)).desc))
row = cursor.fetchone()
if row:
self.currency, self.payment_term, self.invoice_method = row
2014-10-29 20:40:09 +01:00
self.currency_digits = self.currency.digits
2008-05-06 17:45:44 +02:00
if self.party.supplier_payment_term:
self.payment_term = self.party.supplier_payment_term
2020-03-28 14:25:57 +01:00
@fields.depends('party', 'invoice_party')
def on_change_invoice_party(self):
if self.invoice_party:
self.invoice_address = self.invoice_party.address_get(
type='invoice')
elif self.party:
self.invoice_address = self.party.address_get(type='invoice')
2014-01-26 14:29:28 +01:00
@fields.depends('currency')
2012-09-24 22:18:09 +02:00
def on_change_with_currency_digits(self, name=None):
if self.currency:
return self.currency.digits
2008-07-28 18:10:52 +02:00
return 2
2014-01-26 14:29:28 +01:00
@fields.depends('party')
2012-09-24 22:18:09 +02:00
def on_change_with_party_lang(self, name=None):
2012-11-23 18:01:01 +01:00
Config = Pool().get('ir.configuration')
2012-09-24 22:18:09 +02:00
if self.party:
if self.party.lang:
return self.party.lang.code
2012-11-23 18:01:01 +01:00
return Config.get_language()
2021-04-03 14:27:10 +02:00
@fields.depends('party', 'company')
2014-12-19 19:41:50 +01:00
def _get_tax_context(self):
2012-09-24 22:18:09 +02:00
context = {}
if self.party and self.party.lang:
context['language'] = self.party.lang.code
2021-04-03 14:27:10 +02:00
if self.company:
context['company'] = self.company.id
2012-09-24 22:18:09 +02:00
return context
2020-09-03 09:01:26 +02:00
@fields.depends('lines', 'currency', methods=['get_tax_amount'])
2012-09-24 22:18:09 +02:00
def on_change_lines(self):
2014-10-29 20:40:09 +01:00
self.untaxed_amount = Decimal('0.0')
self.tax_amount = Decimal('0.0')
self.total_amount = Decimal('0.0')
2012-09-24 22:18:09 +02:00
if self.lines:
for line in self.lines:
2014-12-19 19:41:50 +01:00
self.untaxed_amount += getattr(line, 'amount', None) or 0
2020-09-03 09:01:26 +02:00
self.tax_amount = self.get_tax_amount()
2012-09-24 22:18:09 +02:00
if self.currency:
2014-10-29 20:40:09 +01:00
self.untaxed_amount = self.currency.round(self.untaxed_amount)
self.tax_amount = self.currency.round(self.tax_amount)
self.total_amount = self.untaxed_amount + self.tax_amount
2012-09-24 22:18:09 +02:00
if self.currency:
2014-10-29 20:40:09 +01:00
self.total_amount = self.currency.round(self.total_amount)
2014-12-19 19:41:50 +01:00
@property
def taxable_lines(self):
taxable_lines = []
# In case we're called from an on_change we have to use some sensible
# defaults
for line in self.lines:
if getattr(line, 'type', None) != 'line':
2014-12-19 19:41:50 +01:00
continue
2020-09-05 23:37:58 +02:00
taxable_lines.append((
getattr(line, 'taxes', None) or [],
getattr(line, 'unit_price', None) or Decimal(0),
getattr(line, 'quantity', None) or 0,
None,
))
2014-12-19 19:41:50 +01:00
return taxable_lines
2020-09-03 09:01:26 +02:00
@fields.depends(methods=['_get_taxes'])
2013-06-01 00:02:18 +02:00
def get_tax_amount(self):
2018-06-26 19:11:51 +02:00
taxes = iter(self._get_taxes().values())
2017-07-26 23:23:29 +02:00
return sum((tax['amount'] for tax in taxes), Decimal(0))
2008-05-06 17:45:44 +02:00
2013-06-01 00:02:18 +02:00
@classmethod
def get_amount(cls, purchases, names):
untaxed_amount = {}
tax_amount = {}
total_amount = {}
if {'tax_amount', 'total_amount'} & set(names):
compute_taxes = True
else:
compute_taxes = False
# Sort cached first and re-instanciate to optimize cache management
purchases = sorted(purchases,
key=lambda p: p.state in cls._states_cached, reverse=True)
purchases = cls.browse(purchases)
2013-06-01 00:02:18 +02:00
for purchase in purchases:
if (purchase.state in cls._states_cached
and purchase.untaxed_amount_cache is not None
and purchase.tax_amount_cache is not None
and purchase.total_amount_cache is not None):
untaxed_amount[purchase.id] = purchase.untaxed_amount_cache
if compute_taxes:
tax_amount[purchase.id] = purchase.tax_amount_cache
total_amount[purchase.id] = purchase.total_amount_cache
2013-06-01 00:02:18 +02:00
else:
untaxed_amount[purchase.id] = sum(
(line.amount for line in purchase.lines
if line.type == 'line'), Decimal(0))
if compute_taxes:
tax_amount[purchase.id] = purchase.get_tax_amount()
total_amount[purchase.id] = (
untaxed_amount[purchase.id] + tax_amount[purchase.id])
2013-06-01 00:02:18 +02:00
result = {
'untaxed_amount': untaxed_amount,
'tax_amount': tax_amount,
'total_amount': total_amount,
}
2018-06-26 19:11:51 +02:00
for key in list(result.keys()):
2013-06-01 00:02:18 +02:00
if key not in names:
del result[key]
return result
2008-05-06 17:45:44 +02:00
def get_invoices(self, name):
invoices = set()
for line in self.lines:
for invoice_line in line.invoice_lines:
if invoice_line.invoice:
invoices.add(invoice_line.invoice.id)
return list(invoices)
@classmethod
def search_invoices(cls, name, clause):
2017-09-27 18:37:31 +02:00
return [('lines.invoice_lines.invoice' + clause[0].lstrip(name),)
+ tuple(clause[1:])]
2012-09-24 22:18:09 +02:00
def get_invoice_state(self):
'''
2012-03-25 00:18:10 +01:00
Return the invoice state for the purchase.
'''
2012-09-24 22:18:09 +02:00
skip_ids = set(x.id for x in self.invoices_ignored)
skip_ids.update(x.id for x in self.invoices_recreated)
invoices = [i for i in self.invoices if i.id not in skip_ids]
2012-03-25 00:18:10 +01:00
if invoices:
if any(i.state == 'cancelled' for i in invoices):
2012-03-25 00:18:10 +01:00
return 'exception'
elif all(i.state == 'paid' for i in invoices):
return 'paid'
else:
return 'waiting'
return 'none'
2012-09-24 22:18:09 +02:00
def set_invoice_state(self):
'''
2012-03-25 00:18:10 +01:00
Set the invoice state.
'''
2012-09-24 22:18:09 +02:00
state = self.get_invoice_state()
if self.invoice_state != state:
self.write([self], {
2012-03-25 00:18:10 +01:00
'invoice_state': state,
})
2008-05-06 17:45:44 +02:00
get_shipments = get_shipments_returns('stock.shipment.in')
get_shipment_returns = get_shipments_returns('stock.shipment.in.return')
search_shipments = search_shipments_returns('stock.shipment.in')
search_shipment_returns = search_shipments_returns(
'stock.shipment.in.return')
2012-09-24 22:18:09 +02:00
def get_shipment_state(self):
'''
2012-03-25 00:18:10 +01:00
Return the shipment state for the purchase.
'''
2012-09-24 22:18:09 +02:00
if self.moves:
if any(l.move_exception for l in self.lines):
2012-03-25 00:18:10 +01:00
return 'exception'
2012-09-24 22:18:09 +02:00
elif all(l.move_done for l in self.lines):
2012-03-25 00:18:10 +01:00
return 'received'
else:
return 'waiting'
return 'none'
2012-09-24 22:18:09 +02:00
def set_shipment_state(self):
'''
2012-03-25 00:18:10 +01:00
Set the shipment state.
'''
2012-09-24 22:18:09 +02:00
state = self.get_shipment_state()
if self.shipment_state != state:
self.write([self], {
2012-03-25 00:18:10 +01:00
'shipment_state': state,
})
2008-05-06 17:45:44 +02:00
2021-03-27 21:21:33 +01:00
@classmethod
def _get_origin(cls):
"Return list of Model names for origin Reference"
return ['purchase.purchase']
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
get_name = Model.get_name
models = cls._get_origin()
return [(None, '')] + [(m, get_name(m)) for m in models]
2017-05-22 18:42:54 +02:00
@property
def report_address(self):
if self.invoice_address:
return self.invoice_address.full_address
else:
return ''
@property
def delivery_full_address(self):
if self.warehouse and self.warehouse.address:
return self.warehouse.address.full_address
return ''
2019-06-26 16:39:40 +02:00
@property
def full_number(self):
return self.number
2012-09-24 22:18:09 +02:00
def get_rec_name(self, name):
items = []
2016-07-04 12:08:26 +02:00
if self.number:
2019-06-26 16:39:40 +02:00
items.append(self.full_number)
if self.reference:
items.append('[%s]' % self.reference)
if not items:
items.append('(%s)' % self.id)
return ' '.join(items)
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
2015-04-05 23:38:20 +02:00
bool_op = 'AND'
else:
bool_op = 'OR'
domain = [bool_op,
2016-07-04 12:08:26 +02:00
('number', operator, value),
('reference', operator, value),
]
return domain
2008-05-06 17:45:44 +02:00
2015-03-09 19:42:02 +01:00
@classmethod
def view_attributes(cls):
2021-04-09 10:52:03 +02:00
attributes = super().view_attributes() + [
2019-09-20 11:47:15 +02:00
('/form//field[@name="comment"]', 'spell', Eval('party_lang')),
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
2019-09-20 11:47:15 +02:00
('/tree/field[@name="invoice_state"]', 'visual',
If(Eval('invoice_state') == 'exception', 'danger', '')),
('/tree/field[@name="shipment_state"]', 'visual',
If(Eval('shipment_state') == 'exception', 'danger', '')),
]
if Transaction().context.get('modify_header'):
attributes.extend([
('//group[@id="states"]', 'states', {'invisible': True}),
('//group[@id="amount"]', 'states', {'invisible': True}),
('//group[@id="links"]', 'states', {'invisible': True}),
('//group[@id="buttons"]', 'states', {'invisible': True}),
])
return attributes
2015-03-09 19:42:02 +01:00
@classmethod
def get_resources_to_copy(cls, name):
return {
'stock.shipment.in.return',
'account.invoice',
}
2012-09-24 22:18:09 +02:00
@classmethod
def copy(cls, purchases, default=None):
2008-05-06 17:45:44 +02:00
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('number', None)
default.setdefault('invoice_state', 'none')
default.setdefault('invoices_ignored', None)
default.setdefault('moves', None)
default.setdefault('shipment_state', 'none')
2012-03-27 10:43:54 +02:00
default.setdefault('purchase_date', None)
2021-04-05 16:24:13 +02:00
default.setdefault('quoted_by')
default.setdefault('confirmed_by')
2012-09-24 22:18:09 +02:00
return super(Purchase, cls).copy(purchases, default=default)
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
def check_for_quotation(self):
for line in self.lines:
if (not line.to_location
and line.product
and line.product.type in ('goods', 'assets')):
2018-12-29 14:20:29 +01:00
raise PurchaseQuotationError(
gettext('purchase.msg_warehouse_required_for_quotation',
purchase=self.rec_name))
2012-09-24 22:18:09 +02:00
@classmethod
def set_number(cls, purchases):
2012-03-25 00:18:10 +01:00
'''
Fill the number field with the purchase sequence
2012-03-25 00:18:10 +01:00
'''
2012-09-24 22:18:09 +02:00
pool = Pool()
Config = pool.get('purchase.configuration')
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
config = Config(1)
2012-03-25 00:18:10 +01:00
for purchase in purchases:
if purchase.number:
2012-03-25 00:18:10 +01:00
continue
2021-04-03 14:27:10 +02:00
purchase.number = config.get_multivalue(
'purchase_sequence', company=purchase.company.id).get()
cls.save(purchases)
2012-03-25 00:18:10 +01:00
2012-09-24 22:18:09 +02:00
@classmethod
def set_purchase_date(cls, purchases):
Date = Pool().get('ir.date')
for purchase in purchases:
if not purchase.purchase_date:
cls.write([purchase], {
'purchase_date': Date.today(),
})
@classmethod
def store_cache(cls, purchases):
for purchase in purchases:
2017-12-13 18:02:12 +01:00
purchase.untaxed_amount_cache = purchase.untaxed_amount
purchase.tax_amount_cache = purchase.tax_amount
purchase.total_amount_cache = purchase.total_amount
cls.save(purchases)
2008-09-05 18:47:02 +02:00
2016-03-20 22:59:22 +01:00
def _get_invoice_purchase(self):
'Return invoice'
2011-07-03 13:19:09 +02:00
pool = Pool()
2012-09-24 22:18:09 +02:00
Journal = pool.get('account.journal')
Invoice = pool.get('account.invoice')
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
journals = Journal.search([
('type', '=', 'expense'),
], limit=1)
if journals:
journal, = journals
else:
journal = None
2020-03-28 14:25:57 +01:00
party = self.invoice_party or self.party
2012-09-24 22:18:09 +02:00
2014-09-17 21:45:46 +02:00
return Invoice(
company=self.company,
2016-03-20 22:59:22 +01:00
type='in',
2014-09-17 21:45:46 +02:00
journal=journal,
2020-03-28 14:25:57 +01:00
party=party,
2014-09-17 21:45:46 +02:00
invoice_address=self.invoice_address,
currency=self.currency,
2020-03-28 14:25:57 +01:00
account=party.account_payable_used,
2014-09-17 21:45:46 +02:00
payment_term=self.payment_term,
)
2012-09-24 22:18:09 +02:00
2016-03-20 22:59:22 +01:00
def create_invoice(self):
'Create an invoice for the purchase and return it'
2012-09-24 22:18:09 +02:00
if self.invoice_method == 'manual':
return
invoice_lines = []
2015-12-13 14:57:02 +01:00
for line in self.lines:
invoice_lines.append(line.get_invoice_line())
invoice_lines = list(chain(*invoice_lines))
2008-05-06 17:45:44 +02:00
if not invoice_lines:
return
2016-03-20 22:59:22 +01:00
invoice = self._get_invoice_purchase()
if getattr(invoice, 'lines', None):
invoice_lines = list(invoice.lines) + invoice_lines
invoice.lines = invoice_lines
2012-09-24 22:18:09 +02:00
invoice.save()
2008-05-06 17:45:44 +02:00
invoice.update_taxes()
self.copy_resources_to(invoice)
2012-09-24 22:18:09 +02:00
return invoice
2008-05-06 17:45:44 +02:00
2012-09-30 21:19:45 +02:00
def create_move(self, move_type):
2008-05-06 17:45:44 +02:00
'''
Create move for each purchase lines
'''
2015-12-13 14:57:02 +01:00
pool = Pool()
Move = pool.get('stock.move')
moves = []
2012-09-24 22:18:09 +02:00
for line in self.lines:
2015-12-13 14:57:02 +01:00
move = line.get_move(move_type)
2012-09-30 21:19:45 +02:00
if move:
2015-12-13 14:57:02 +01:00
moves.append(move)
Move.save(moves)
return moves
2012-09-30 21:19:45 +02:00
@property
def return_from_location(self):
if self.warehouse:
return (self.warehouse.supplier_return_location
or self.warehouse.storage_location)
2012-09-30 21:19:45 +02:00
def _get_return_shipment(self):
ShipmentInReturn = Pool().get('stock.shipment.in.return')
2014-09-17 21:45:46 +02:00
return ShipmentInReturn(
company=self.company,
from_location=self.return_from_location,
2014-09-17 21:45:46 +02:00
to_location=self.party.supplier_location,
2016-03-17 15:31:11 +01:00
supplier=self.party,
delivery_address=self.party.address_get(type='delivery'),
2014-09-17 21:45:46 +02:00
)
2012-09-30 21:19:45 +02:00
def create_return_shipment(self, return_moves):
'''
Create return shipment and return the shipment id
'''
ShipmentInReturn = Pool().get('stock.shipment.in.return')
return_shipment = self._get_return_shipment()
return_shipment.moves = return_moves
return_shipment.save()
self.copy_resources_to(return_shipment)
2014-09-17 21:45:46 +02:00
ShipmentInReturn.wait([return_shipment])
2012-09-30 21:19:45 +02:00
return return_shipment
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
def is_done(self):
return ((self.invoice_state == 'paid'
or self.invoice_state == 'none')
and (self.shipment_state == 'received'
or self.shipment_state == 'none'
or all(l.product.type == 'service'
for l in self.lines if l.product)))
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
@classmethod
def delete(cls, purchases):
# Cancel before delete
2012-09-24 22:18:09 +02:00
cls.cancel(purchases)
for purchase in purchases:
if purchase.state != 'cancelled':
2018-12-29 14:20:29 +01:00
raise AccessError(
gettext('purchase.msg_purchase_delete_cancel',
purchase=purchase.rec_name))
2012-09-24 22:18:09 +02:00
super(Purchase, cls).delete(purchases)
def on_change(self, fieldnames):
pool = Pool()
ModelData = pool.get('ir.model.data')
context = Transaction().context
wizard_modify_header_id = ModelData.get_id(
'purchase', 'wizard_modify_header')
if (len(fieldnames) > 1
and context.get('action_id') == wizard_modify_header_id):
return []
return super().on_change(fieldnames)
2012-09-24 22:18:09 +02:00
@classmethod
2012-03-25 00:18:10 +01:00
@ModelView.button
@Workflow.transition('cancelled')
2012-09-24 22:18:09 +02:00
def cancel(cls, purchases):
cls.store_cache(purchases)
2012-09-24 22:18:09 +02:00
@classmethod
2012-03-25 00:18:10 +01:00
@ModelView.button
@Workflow.transition('draft')
2020-04-10 18:53:44 +02:00
@reset_employee('quoted_by', 'confirmed_by')
2012-09-24 22:18:09 +02:00
def draft(cls, purchases):
2012-03-25 00:18:10 +01:00
pass
2012-09-24 22:18:09 +02:00
@classmethod
2012-03-25 00:18:10 +01:00
@ModelView.button
@Workflow.transition('quotation')
2020-04-10 18:53:44 +02:00
@set_employee('quoted_by')
2012-09-24 22:18:09 +02:00
def quote(cls, purchases):
for purchase in purchases:
purchase.check_for_quotation()
cls.set_number(purchases)
2012-09-24 22:18:09 +02:00
@classmethod
2012-03-25 00:18:10 +01:00
@ModelView.button
@Workflow.transition('confirmed')
2020-04-10 18:53:44 +02:00
@set_employee('confirmed_by')
2012-09-24 22:18:09 +02:00
def confirm(cls, purchases):
2018-07-27 09:27:49 +02:00
pool = Pool()
Configuration = pool.get('purchase.configuration')
2012-09-24 22:18:09 +02:00
cls.set_purchase_date(purchases)
cls.store_cache(purchases)
2018-07-27 09:27:49 +02:00
config = Configuration(1)
with Transaction().set_context(
queue_name='purchase',
queue_scheduled_at=config.purchase_process_after):
cls.__queue__.process(purchases)
@classmethod
@Workflow.transition('processing')
def proceed(cls, purchases):
pass
2015-11-16 11:34:13 -03:00
@classmethod
@Workflow.transition('done')
def do(cls, purchases):
pass
2012-09-24 22:18:09 +02:00
@classmethod
2012-09-10 15:28:28 +02:00
@ModelView.button_action('purchase.wizard_invoice_handle_exception')
2012-09-24 22:18:09 +02:00
def handle_invoice_exception(cls, purchases):
2012-09-10 15:28:28 +02:00
pass
2012-09-24 22:18:09 +02:00
@classmethod
2012-09-10 15:28:28 +02:00
@ModelView.button_action('purchase.wizard_shipment_handle_exception')
2012-09-24 22:18:09 +02:00
def handle_shipment_exception(cls, purchases):
2012-09-10 15:28:28 +02:00
pass
2012-09-24 22:18:09 +02:00
@classmethod
@ModelView.button
2012-09-24 22:18:09 +02:00
def process(cls, purchases):
2020-04-14 09:27:53 +02:00
pool = Pool()
Line = pool.get('purchase.line')
lines = []
process, done = [], []
2019-01-30 09:45:59 +01:00
cls.lock(purchases)
2012-09-24 22:18:09 +02:00
for purchase in purchases:
2019-04-15 22:10:12 +02:00
if purchase.state not in {'confirmed', 'processing', 'done'}:
continue
2016-03-20 22:59:22 +01:00
purchase.create_invoice()
2012-09-24 22:18:09 +02:00
purchase.set_invoice_state()
2012-09-30 21:19:45 +02:00
purchase.create_move('in')
return_moves = purchase.create_move('return')
if return_moves:
purchase.create_return_shipment(return_moves)
2012-09-24 22:18:09 +02:00
purchase.set_shipment_state()
2020-04-14 09:27:53 +02:00
for line in purchase.lines:
line.set_actual_quantity()
lines.append(line)
2012-09-24 22:18:09 +02:00
if purchase.is_done():
2017-07-26 23:23:29 +02:00
if purchase.state != 'done':
if purchase.state == 'confirmed':
process.append(purchase)
done.append(purchase)
elif purchase.state != 'processing':
process.append(purchase)
2020-04-14 09:27:53 +02:00
Line.save(lines)
if process:
cls.proceed(process)
2012-03-25 00:18:10 +01:00
if done:
2015-11-16 11:34:13 -03:00
cls.do(done)
@classmethod
@ModelView.button_action('purchase.wizard_modify_header')
def modify_header(cls, purchases):
pass
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
class PurchaseIgnoredInvoice(ModelSQL):
2009-03-05 14:53:03 +01:00
'Purchase - Ignored Invoice'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.purchase-ignored-account.invoice'
2009-03-05 14:53:03 +01:00
_table = 'purchase_invoice_ignored_rel'
purchase = fields.Many2One('purchase.purchase', 'Purchase',
2012-03-05 10:53:06 +01:00
ondelete='CASCADE', select=True, required=True)
2009-03-05 14:53:03 +01:00
invoice = fields.Many2One('account.invoice', 'Invoice',
2012-03-05 10:53:06 +01:00
ondelete='RESTRICT', select=True, required=True)
2009-03-05 14:53:03 +01:00
2019-08-20 09:30:24 +02:00
class PurchaseRecreatedInvoice(ModelSQL):
2009-03-05 14:53:03 +01:00
'Purchase - Recreated Invoice'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.purchase-recreated-account.invoice'
2009-03-05 14:53:03 +01:00
_table = 'purchase_invoice_recreated_rel'
purchase = fields.Many2One('purchase.purchase', 'Purchase',
2012-03-05 10:53:06 +01:00
ondelete='CASCADE', select=True, required=True)
2009-03-05 14:53:03 +01:00
invoice = fields.Many2One('account.invoice', 'Invoice',
2012-03-05 10:53:06 +01:00
ondelete='RESTRICT', select=True, required=True)
2009-03-05 14:53:03 +01:00
2020-03-01 12:33:53 +01:00
class Line(sequence_ordered(), ModelSQL, ModelView):
2008-05-06 17:45:44 +02:00
'Purchase Line'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line'
2012-03-14 09:30:39 +01:00
purchase = fields.Many2One('purchase.purchase', 'Purchase',
2016-06-30 18:53:51 +02:00
ondelete='CASCADE', select=True, required=True,
states={
'readonly': ((Eval('purchase_state') != 'draft')
& Bool(Eval('purchase'))),
},
depends=['purchase_state'])
2008-05-06 17:45:44 +02:00
type = fields.Selection([
('line', 'Line'),
('subtotal', 'Subtotal'),
('title', 'Title'),
2009-01-12 14:28:51 +01:00
('comment', 'Comment'),
2016-06-30 18:53:51 +02:00
], 'Type', select=True, required=True,
states={
'readonly': Eval('purchase_state') != 'draft',
},
depends=['purchase_state'])
2008-05-06 17:45:44 +02:00
quantity = fields.Float('Quantity',
2011-08-04 16:57:38 +02:00
digits=(16, Eval('unit_digits', 2)),
states={
2011-08-21 11:20:50 +02:00
'invisible': Eval('type') != 'line',
'required': Eval('type') == 'line',
2016-06-30 18:53:51 +02:00
'readonly': Eval('purchase_state') != 'draft',
2014-01-26 14:29:28 +01:00
},
2016-06-30 18:53:51 +02:00
depends=['unit_digits', 'type', 'purchase_state'])
2020-04-14 09:27:53 +02:00
actual_quantity = fields.Float(
"Actual Quantity", digits=(16, Eval('unit_digits', 2)), readonly=True,
states={
'invisible': Eval('type') != 'line',
},
depends=['unit_digits', 'type'])
2008-05-06 17:45:44 +02:00
unit = fields.Many2One('product.uom', 'Unit',
ondelete='RESTRICT',
2011-08-04 16:57:38 +02:00
states={
'required': Bool(Eval('product')),
2011-08-21 11:20:50 +02:00
'invisible': Eval('type') != 'line',
2016-06-30 18:53:51 +02:00
'readonly': Eval('purchase_state') != 'draft',
2010-01-20 11:03:33 +01:00
},
2012-03-25 15:40:32 +02:00
domain=[
If(Bool(Eval('product_uom_category')),
('category', '=', Eval('product_uom_category')),
('category', '!=', -1)),
],
2016-06-30 18:53:51 +02:00
depends=['product', 'type', 'product_uom_category', 'purchase_state'])
2014-01-26 14:29:28 +01:00
unit_digits = fields.Function(fields.Integer('Unit Digits'),
'on_change_with_unit_digits')
2008-05-06 17:45:44 +02:00
product = fields.Many2One('product.product', 'Product',
2017-12-18 17:56:39 +01:00
ondelete='RESTRICT',
domain=[
If(Eval('purchase_state').in_(['draft', 'quotation']),
[('purchasable', '=', True)],
[]),
2017-12-18 17:56:39 +01:00
],
2010-03-11 16:53:13 +01:00
states={
2011-08-21 11:20:50 +02:00
'invisible': Eval('type') != 'line',
2016-06-30 18:53:51 +02:00
'readonly': Eval('purchase_state') != 'draft',
2017-12-18 17:56:39 +01:00
'required': Bool(Eval('product_supplier')),
2014-01-26 14:29:28 +01:00
},
2011-08-04 16:57:38 +02:00
context={
'company': Eval('company', None),
},
search_context={
2011-08-21 11:20:50 +02:00
'locations': If(Bool(Eval('_parent_purchase', {}).get(
2011-08-04 16:57:38 +02:00
'warehouse')),
2012-03-27 10:43:54 +02:00
[Eval('_parent_purchase', {}).get('warehouse', None)],
2011-08-04 16:57:38 +02:00
[]),
2011-08-21 11:20:50 +02:00
'stock_date_end': Eval('_parent_purchase', {}).get(
2011-08-04 16:57:38 +02:00
'purchase_date'),
'stock_skip_warehouse': True,
'currency': Eval('_parent_purchase', {}).get('currency'),
'supplier': Eval('_parent_purchase', {}).get('party'),
'purchase_date': Eval('_parent_purchase', {}).get('purchase_date'),
'uom': Eval('unit'),
'taxes': Eval('taxes', []),
'quantity': Eval('quantity'),
2020-04-25 09:18:21 +02:00
},
depends=['type', 'purchase_state', 'product_supplier', 'company'])
2017-12-18 17:56:39 +01:00
product_supplier = fields.Many2One(
'purchase.product_supplier', "Supplier's Product",
ondelete='RESTRICT',
domain=[
If(Bool(Eval('product')),
['OR',
[
('template.products', '=', Eval('product')),
('product', '=', None),
],
('product', '=', Eval('product')),
],
[]),
2017-12-18 17:56:39 +01:00
('party', '=', Eval('_parent_purchase', {}).get('party')),
],
states={
'invisible': Eval('type') != 'line',
'readonly': Eval('purchase_state') != 'draft',
},
depends=['product', 'type', 'purchase_state'])
2012-03-25 15:40:32 +02:00
product_uom_category = fields.Function(
2014-01-26 14:29:28 +01:00
fields.Many2One('product.uom.category', 'Product Uom Category'),
2012-09-24 22:18:09 +02:00
'on_change_with_product_uom_category')
2015-01-06 23:30:57 +01:00
unit_price = fields.Numeric('Unit Price', digits=price_digits,
2011-08-04 16:57:38 +02:00
states={
2011-08-21 11:20:50 +02:00
'invisible': Eval('type') != 'line',
'required': Eval('type') == 'line',
2016-06-30 18:53:51 +02:00
'readonly': Eval('purchase_state') != 'draft',
}, depends=['type', 'purchase_state'])
2011-08-04 16:57:38 +02:00
amount = fields.Function(fields.Numeric('Amount',
2011-08-21 11:20:50 +02:00
digits=(16,
Eval('_parent_purchase', {}).get('currency_digits', 2)),
2011-08-04 16:57:38 +02:00
states={
2011-08-21 11:20:50 +02:00
'invisible': ~Eval('type').in_(['line', 'subtotal']),
2014-01-26 14:29:28 +01:00
},
2011-08-04 16:57:38 +02:00
depends=['type']), 'get_amount')
2017-12-18 19:03:16 +01:00
description = fields.Text('Description', size=None,
2016-06-30 18:53:51 +02:00
states={
'readonly': Eval('purchase_state') != 'draft',
},
depends=['purchase_state'])
2020-12-25 17:14:57 +01:00
summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary')
note = fields.Text('Note')
2009-03-05 14:53:03 +01:00
taxes = fields.Many2Many('purchase.line-account.tax',
'line', 'tax', 'Taxes',
2015-03-16 15:40:51 +01:00
order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
domain=[('parent', '=', None), ['OR',
('group', '=', None),
('group.kind', 'in', ['purchase', 'both'])],
2014-12-31 17:18:13 +01:00
('company', '=',
Eval('_parent_purchase', {}).get('company', -1)),
],
2011-08-04 16:57:38 +02:00
states={
2011-08-21 11:20:50 +02:00
'invisible': Eval('type') != 'line',
2016-06-30 18:53:51 +02:00
'readonly': Eval('purchase_state') != 'draft',
}, depends=['type', 'purchase_state'])
invoice_lines = fields.One2Many('account.invoice.line', 'origin',
'Invoice Lines', readonly=True)
moves = fields.One2Many('stock.move', 'origin', 'Moves', readonly=True)
2009-03-05 14:53:03 +01:00
moves_ignored = fields.Many2Many('purchase.line-ignored-stock.move',
2009-02-23 18:53:30 +01:00
'purchase_line', 'move', 'Ignored Moves', readonly=True)
2009-03-05 14:53:03 +01:00
moves_recreated = fields.Many2Many('purchase.line-recreated-stock.move',
'purchase_line', 'move', 'Recreated Moves', readonly=True)
2010-03-11 16:53:13 +01:00
move_done = fields.Function(fields.Boolean('Moves Done'), 'get_move_done')
move_exception = fields.Function(fields.Boolean('Moves Exception'),
'get_move_exception')
2011-11-19 19:34:47 +01:00
from_location = fields.Function(fields.Many2One('stock.location',
'From Location'), 'get_from_location')
to_location = fields.Function(fields.Many2One('stock.location',
'To Location'), 'get_to_location')
delivery_date = fields.Function(fields.Date('Delivery Date',
states={
'invisible': ((Eval('type') != 'line')
| (Eval('delivery_date_edit', False)
& ~Eval('purchase_state').in_(
['processing', 'done', 'cancelled']))),
},
depends=['type', 'delivery_date_edit', 'purchase_state']),
2012-09-24 22:18:09 +02:00
'on_change_with_delivery_date')
delivery_date_edit = fields.Boolean(
"Edit Delivery Date",
states={
'invisible': Eval('type') != 'line',
'readonly': Eval('purchase_state').in_([
'processing', 'done', 'cancelled']),
},
depends=['type', 'purchase_state'],
help="Check to edit the delivery date.")
delivery_date_store = fields.Date(
"Delivery Date",
states={
'invisible': ((Eval('type') != 'line')
| ~Eval('delivery_date_edit', False)
| Eval('purchase_state').in_(
['processing', 'done', 'cancelled'])),
'readonly': Eval('purchase_state').in_([
'processing', 'done', 'cancelled']),
},
depends=['type', 'delivery_date_edit', 'purchase_state'])
2016-06-30 18:53:51 +02:00
purchase_state = fields.Function(
fields.Selection('get_purchase_states', 'Purchase State'),
2016-06-30 18:53:51 +02:00
'on_change_with_purchase_state')
2020-04-25 09:18:21 +02:00
company = fields.Function(
fields.Many2One('company.company', "Company"),
'on_change_with_company')
2020-10-09 19:09:05 +02:00
currency = fields.Function(
fields.Many2One('currency.currency', 'Currency'),
'on_change_with_currency')
2008-05-06 17:45:44 +02:00
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('purchase')
2012-09-24 22:18:09 +02:00
@classmethod
def __register__(cls, module_name):
2020-03-01 12:33:53 +01:00
super().__register__(module_name)
2018-07-24 22:01:08 +02:00
table = cls.__table_handler__(module_name)
2017-12-18 19:03:16 +01:00
# Migration from 4.6: drop required on description
table.not_null_action('description', action='remove')
2012-09-24 22:18:09 +02:00
@staticmethod
def default_type():
2008-05-06 17:45:44 +02:00
return 'line'
@classmethod
def default_delivery_date_edit(cls):
return False
2017-04-19 10:30:11 +02:00
@property
def _move_remaining_quantity(self):
"Compute the remaining quantity to receive"
pool = Pool()
Uom = pool.get('product.uom')
2016-03-03 09:37:13 +01:00
if self.type != 'line' or not self.product:
2017-04-19 10:30:11 +02:00
return
2012-09-24 22:18:09 +02:00
if self.product.type == 'service':
2017-04-19 10:30:11 +02:00
return
skip_ids = set(x.id for x in self.moves_ignored)
quantity = abs(self.quantity)
2012-09-24 22:18:09 +02:00
for move in self.moves:
2017-04-19 10:30:11 +02:00
if move.state == 'done' or move.id in skip_ids:
quantity -= Uom.compute_qty(move.uom, move.quantity, self.unit)
return quantity
def get_move_done(self, name):
quantity = self._move_remaining_quantity
if quantity is None:
return True
else:
return self.unit.round(quantity) <= 0
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
def get_move_exception(self, name):
skip_ids = set(x.id for x in self.moves_ignored
+ self.moves_recreated)
for move in self.moves:
2020-06-06 10:30:11 +01:00
if move.state == 'cancelled' \
2012-09-24 22:18:09 +02:00
and move.id not in skip_ids:
return True
return False
2008-05-06 17:45:44 +02:00
2014-01-26 14:29:28 +01:00
@fields.depends('unit')
2012-09-24 22:18:09 +02:00
def on_change_with_unit_digits(self, name=None):
if self.unit:
return self.unit.digits
return 2
2012-09-24 22:18:09 +02:00
def _get_tax_rule_pattern(self):
2009-03-28 19:32:53 +01:00
'''
Get tax rule pattern
'''
2012-09-24 22:18:09 +02:00
return {}
2009-03-28 19:32:53 +01:00
2019-10-01 22:23:55 +02:00
@fields.depends(
'purchase', '_parent_purchase.currency', '_parent_purchase.party',
2020-04-25 09:18:21 +02:00
'_parent_purchase.purchase_date', 'company',
2018-05-31 13:26:19 +02:00
'unit', 'product', 'product_supplier', 'taxes')
2012-09-24 22:18:09 +02:00
def _get_context_purchase_price(self):
context = {}
2018-05-31 13:26:19 +02:00
if self.purchase:
if self.purchase.currency:
2012-09-24 22:18:09 +02:00
context['currency'] = self.purchase.currency.id
2018-05-31 13:26:19 +02:00
if self.purchase.party:
2012-09-24 22:18:09 +02:00
context['supplier'] = self.purchase.party.id
2018-05-31 13:26:19 +02:00
context['purchase_date'] = self.purchase.purchase_date
2020-04-25 09:18:21 +02:00
if self.company:
context['company'] = self.company.id
2012-09-24 22:18:09 +02:00
if self.unit:
context['uom'] = self.unit.id
2018-05-31 13:26:19 +02:00
elif self.product:
2017-10-11 22:49:12 +02:00
context['uom'] = self.product.purchase_uom.id
2017-12-18 17:56:39 +01:00
if self.product_supplier:
context['product_supplier'] = self.product_supplier.id
2018-05-31 13:26:19 +02:00
context['taxes'] = [t.id for t in self.taxes or []]
2012-09-24 22:18:09 +02:00
return context
2009-03-28 19:32:53 +01:00
2020-04-25 09:18:21 +02:00
@fields.depends('purchase', 'company', '_parent_purchase.party',)
def _get_product_supplier_pattern(self):
return {
'party': (
self.purchase.party.id
if self.purchase and self.purchase.party else -1),
2020-04-25 09:18:21 +02:00
'company': (self.company.id if self.company else -1),
}
2020-03-28 14:25:57 +01:00
@fields.depends('product', 'unit', 'purchase',
'_parent_purchase.party', '_parent_purchase.invoice_party',
2019-09-29 18:30:25 +02:00
'product_supplier', methods=['compute_taxes', 'compute_unit_price',
'_get_product_supplier_pattern'])
2012-09-24 22:18:09 +02:00
def on_change_product(self):
if not self.product:
2014-10-29 20:40:09 +01:00
return
2008-05-06 17:45:44 +02:00
2008-05-21 18:14:38 +02:00
party = None
2020-03-28 14:25:57 +01:00
if self.purchase:
party = self.purchase.invoice_party or self.purchase.party
2008-05-06 17:45:44 +02:00
2015-02-27 11:25:10 +01:00
# Set taxes before unit_price to have taxes in context of purchase
# price
2019-09-29 18:30:25 +02:00
self.taxes = self.compute_taxes(party)
2008-05-06 17:45:44 +02:00
2015-02-27 11:25:10 +01:00
category = self.product.purchase_uom.category
if not self.unit or self.unit.category != category:
2015-02-27 11:25:10 +01:00
self.unit = self.product.purchase_uom
self.unit_digits = self.product.purchase_uom.digits
product_suppliers = list(self.product.product_suppliers_used(
**self._get_product_supplier_pattern()))
if len(product_suppliers) == 1:
self.product_supplier, = product_suppliers
elif (self.product_supplier
and self.product_supplier not in product_suppliers):
2017-12-18 17:56:39 +01:00
self.product_supplier = None
2019-09-29 18:30:25 +02:00
self.unit_price = self.compute_unit_price()
2015-02-27 11:25:10 +01:00
2012-09-24 22:18:09 +02:00
self.type = 'line'
2014-10-29 20:40:09 +01:00
self.amount = self.on_change_with_amount()
2008-05-06 17:45:44 +02:00
2019-09-29 18:30:25 +02:00
@fields.depends('product', methods=['_get_tax_rule_pattern'])
def compute_taxes(self, party):
2022-07-17 00:02:22 +02:00
taxes = set()
2019-09-29 18:30:25 +02:00
pattern = self._get_tax_rule_pattern()
for tax in self.product.supplier_taxes_used:
if party and party.supplier_tax_rule:
tax_ids = party.supplier_tax_rule.apply(tax, pattern)
if tax_ids:
2022-07-17 00:02:22 +02:00
taxes.update(tax_ids)
2019-09-29 18:30:25 +02:00
continue
2022-07-17 00:02:22 +02:00
taxes.add(tax.id)
2019-09-29 18:30:25 +02:00
if party and party.supplier_tax_rule:
tax_ids = party.supplier_tax_rule.apply(None, pattern)
if tax_ids:
2022-07-17 00:02:22 +02:00
taxes.update(tax_ids)
return list(taxes)
2019-09-29 18:30:25 +02:00
@fields.depends('product', 'quantity',
methods=['_get_context_purchase_price'])
def compute_unit_price(self):
pool = Pool()
Product = pool.get('product.product')
if not self.product:
return
with Transaction().set_context(self._get_context_purchase_price()):
unit_price = Product.get_purchase_price([self.product],
abs(self.quantity or 0))[self.product.id]
if unit_price:
2020-04-23 21:44:00 +02:00
unit_price = round_price(unit_price)
2019-09-29 18:30:25 +02:00
return unit_price
2018-05-31 13:26:19 +02:00
@fields.depends('product', 'product_supplier',
methods=['on_change_product'])
2017-12-18 17:56:39 +01:00
def on_change_product_supplier(self):
if self.product_supplier:
if self.product_supplier.product:
self.product = self.product_supplier.product
elif not self.product:
if len(self.product_supplier.template.products) == 1:
self.product, = self.product_supplier.template.products
2017-12-18 17:56:39 +01:00
self.on_change_product()
2014-01-26 14:29:28 +01:00
@fields.depends('product')
2012-09-24 22:18:09 +02:00
def on_change_with_product_uom_category(self, name=None):
if self.product:
return self.product.default_uom_category.id
2012-03-25 15:40:32 +02:00
2018-05-31 13:26:19 +02:00
@fields.depends('product', 'quantity', 'unit',
methods=['_get_context_purchase_price'])
2012-09-24 22:18:09 +02:00
def on_change_quantity(self):
Product = Pool().get('product.product')
2012-03-25 15:40:32 +02:00
2012-09-24 22:18:09 +02:00
if not self.product:
2014-10-29 20:40:09 +01:00
return
2008-09-01 11:02:49 +02:00
2012-09-24 22:18:09 +02:00
with Transaction().set_context(self._get_context_purchase_price()):
2014-10-29 20:40:09 +01:00
self.unit_price = Product.get_purchase_price([self.product],
2013-08-01 16:16:40 +02:00
abs(self.quantity or 0))[self.product.id]
2014-10-29 20:40:09 +01:00
if self.unit_price:
2020-04-23 21:44:00 +02:00
self.unit_price = round_price(self.unit_price)
2008-09-01 11:02:49 +02:00
2018-05-31 13:26:19 +02:00
@fields.depends(methods=['on_change_quantity'])
2012-09-24 22:18:09 +02:00
def on_change_unit(self):
2015-02-27 11:25:10 +01:00
self.on_change_quantity()
2020-12-25 17:14:57 +01:00
@fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
2018-03-24 13:47:39 +01:00
@fields.depends('type', 'quantity', 'unit_price', 'unit', 'purchase',
2014-01-26 14:29:28 +01:00
'_parent_purchase.currency')
2012-09-24 22:18:09 +02:00
def on_change_with_amount(self):
if self.type == 'line':
currency = self.purchase.currency if self.purchase else None
amount = Decimal(str(self.quantity or '0.0')) * \
(self.unit_price or Decimal('0.0'))
if currency:
2012-09-24 22:18:09 +02:00
return currency.round(amount)
return amount
return Decimal('0.0')
2012-09-24 22:18:09 +02:00
def get_amount(self, name):
if self.type == 'line':
2014-09-13 15:25:04 +02:00
return self.on_change_with_amount()
2012-09-24 22:18:09 +02:00
elif self.type == 'subtotal':
amount = Decimal('0.0')
for line2 in self.purchase.lines:
if line2.type == 'line':
amount += line2.purchase.currency.round(
Decimal(str(line2.quantity)) * line2.unit_price)
elif line2.type == 'subtotal':
if self == line2:
break
amount = Decimal('0.0')
return amount
return Decimal('0.0')
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
def get_from_location(self, name):
if (self.quantity or 0) >= 0:
2021-07-12 22:37:21 +02:00
if self.purchase.party.supplier_location:
return self.purchase.party.supplier_location.id
elif self.purchase.return_from_location:
return self.purchase.return_from_location.id
2011-11-19 19:34:47 +01:00
2012-09-24 22:18:09 +02:00
def get_to_location(self, name):
if (self.quantity or 0) >= 0:
2012-09-30 21:19:45 +02:00
if self.purchase.warehouse:
return self.purchase.warehouse.input_location.id
2021-07-12 22:37:21 +02:00
elif self.purchase.party.supplier_location:
2012-09-30 21:19:45 +02:00
return self.purchase.party.supplier_location.id
2011-11-19 19:34:47 +01:00
@fields.depends('moves', methods=['planned_delivery_date'])
2012-09-24 22:18:09 +02:00
def on_change_with_delivery_date(self, name=None):
moves = [m for m in self.moves if m.state != 'cancelled']
if moves:
dates = filter(
None, (m.effective_date or m.planned_date for m in moves))
2018-06-26 19:11:51 +02:00
return min(dates, default=None)
return self.planned_delivery_date
@property
@fields.depends(
'product_supplier', 'quantity', 'purchase',
'_parent_purchase.purchase_date',
'delivery_date_edit', 'delivery_date_store',
'_parent_purchase.delivery_date', '_parent_purchase.party')
def planned_delivery_date(self):
pool = Pool()
Date = pool.get('ir.date')
ProductSupplier = pool.get('purchase.product_supplier')
2021-01-19 14:59:54 +01:00
product_supplier = self.product_supplier
if not product_supplier:
product_supplier = ProductSupplier()
if self.purchase:
product_supplier.party = self.purchase.party
delivery_date = None
if self.delivery_date_edit:
delivery_date = self.delivery_date_store
elif self.purchase and self.purchase.delivery_date:
delivery_date = self.purchase.delivery_date
2021-01-19 14:59:54 +01:00
elif (product_supplier
2016-03-04 00:34:21 +01:00
and self.quantity is not None
and self.quantity > 0
and self.purchase):
2012-09-24 22:18:09 +02:00
date = self.purchase.purchase_date if self.purchase else None
2021-01-19 14:59:54 +01:00
delivery_date = product_supplier.compute_supply_date(date=date)
if delivery_date == datetime.date.max:
delivery_date = None
if delivery_date and delivery_date < Date.today():
delivery_date = None
return delivery_date
@classmethod
def get_purchase_states(cls):
pool = Pool()
Purchase = pool.get('purchase.purchase')
return Purchase.fields_get(['state'])['state']['selection']
2016-06-30 18:53:51 +02:00
@fields.depends('purchase', '_parent_purchase.state')
def on_change_with_purchase_state(self, name=None):
if self.purchase:
return self.purchase.state
2020-04-25 09:18:21 +02:00
@fields.depends('purchase', '_parent_purchase.company')
def on_change_with_company(self, name=None):
if self.purchase and self.purchase.company:
return self.purchase.company.id
2020-10-09 19:09:05 +02:00
@fields.depends('purchase', '_parent_purchase.currency')
def on_change_with_currency(self, name=None):
if self.purchase and self.purchase.currency:
return self.purchase.currency.id
2016-03-20 22:59:22 +01:00
def get_invoice_line(self):
'Return a list of invoice line for purchase line'
pool = Pool()
2012-09-24 22:18:09 +02:00
InvoiceLine = pool.get('account.invoice.line')
2017-04-01 00:33:03 +02:00
AccountConfiguration = pool.get('account.configuration')
account_config = AccountConfiguration(1)
invoice_line = InvoiceLine()
invoice_line.type = self.type
invoice_line.currency = self.currency
invoice_line.company = self.company
invoice_line.description = self.description
invoice_line.note = self.note
invoice_line.origin = self
2012-09-24 22:18:09 +02:00
if self.type != 'line':
2016-03-20 22:59:22 +01:00
if self._get_invoice_not_line():
return [invoice_line]
2012-04-21 01:20:31 +02:00
else:
return []
2012-09-30 21:19:45 +02:00
2016-03-20 22:59:22 +01:00
quantity = (self._get_invoice_line_quantity()
- self._get_invoiced_quantity())
2009-02-23 14:47:08 +01:00
if self.unit:
quantity = self.unit.round(quantity)
invoice_line.quantity = quantity
2016-01-11 10:29:44 -03:00
if not invoice_line.quantity:
return []
invoice_line.unit = self.unit
invoice_line.product = self.product
invoice_line.unit_price = self.unit_price
invoice_line.taxes = self.taxes
if self.company.purchase_taxes_expense:
invoice_line.taxes_deductible_rate = 0
elif self.product:
invoice_line.taxes_deductible_rate = (
self.product.supplier_taxes_deductible_rate_used)
invoice_line.invoice_type = 'in'
if self.product:
invoice_line.account = self.product.account_expense_used
if not invoice_line.account:
2018-12-29 14:20:29 +01:00
raise AccountError(
gettext('purchase'
'.msg_purchase_product_missing_account_expense',
purchase=self.purchase.rec_name,
product=self.product.rec_name))
2008-08-15 17:23:42 +02:00
else:
2018-06-20 12:09:50 +02:00
invoice_line.account = account_config.get_multivalue(
2021-04-03 14:27:10 +02:00
'default_category_account_expense', company=self.company.id)
if not invoice_line.account:
2018-12-29 14:20:29 +01:00
raise AccountError(
gettext('purchase'
'.msg_purchase_missing_account_expense',
purchase=self.purchase.rec_name))
invoice_line.stock_moves = self._get_invoice_line_moves()
return [invoice_line]
2008-05-06 17:45:44 +02:00
2016-03-20 22:59:22 +01:00
def _get_invoice_not_line(self):
2015-12-13 14:57:02 +01:00
'Return if the not line should be invoiced'
return (self.purchase.invoice_method == 'order'
2016-03-20 22:59:22 +01:00
and not self.invoice_lines)
2015-03-09 19:42:02 +01:00
2016-03-20 22:59:22 +01:00
def _get_invoice_line_quantity(self):
2015-12-13 14:57:02 +01:00
'Return the quantity that should be invoiced'
pool = Pool()
Uom = pool.get('product.uom')
2008-05-06 17:45:44 +02:00
2015-12-13 14:57:02 +01:00
if (self.purchase.invoice_method == 'order'
or not self.product
or self.product.type == 'service'):
2016-03-20 22:59:22 +01:00
return self.quantity
2015-12-13 14:57:02 +01:00
elif self.purchase.invoice_method == 'shipment':
quantity = 0.0
for move in self.moves:
if move.state != 'done':
continue
qty = Uom.compute_qty(move.uom, move.quantity, self.unit)
# Test only against from_location
# as it is what matters for purchase
src_type = self.from_location.type
if (move.from_location.type == src_type
and move.to_location.type != src_type):
quantity += qty
elif (move.to_location.type == src_type
and move.from_location.type != src_type):
quantity -= qty
2016-03-20 22:59:22 +01:00
if self.quantity < 0:
quantity *= -1
2015-12-13 14:57:02 +01:00
return quantity
2016-03-20 22:59:22 +01:00
def _get_invoiced_quantity(self):
2015-12-13 14:57:02 +01:00
'Return the quantity already invoiced'
pool = Pool()
Uom = pool.get('product.uom')
quantity = 0
skips = {l for i in self.purchase.invoices_recreated for l in i.lines}
for invoice_line in self.invoice_lines:
if invoice_line.type != 'line':
continue
if invoice_line not in skips:
quantity += Uom.compute_qty(invoice_line.unit,
2016-03-20 22:59:22 +01:00
invoice_line.quantity, self.unit)
2015-12-13 14:57:02 +01:00
return quantity
2016-03-20 22:59:22 +01:00
def _get_invoice_line_moves(self):
2015-12-13 14:57:02 +01:00
'Return the stock moves that should be invoiced'
moves = []
if self.purchase.invoice_method == 'order':
moves.extend(self.moves)
elif self.purchase.invoice_method == 'shipment':
for move in self.moves:
if move.state == 'done':
if move.invoiced_quantity < move.quantity:
moves.append(move)
return moves
def get_move(self, move_type):
2008-05-06 17:45:44 +02:00
'''
2012-02-27 10:42:01 +01:00
Return move values for purchase line
2008-05-06 17:45:44 +02:00
'''
2011-07-03 13:19:09 +02:00
pool = Pool()
2012-09-24 22:18:09 +02:00
Move = pool.get('stock.move')
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
if self.type != 'line':
2008-05-06 17:45:44 +02:00
return
2012-09-24 22:18:09 +02:00
if not self.product:
2008-07-23 15:31:49 +02:00
return
if self.product.type not in Move.get_product_types():
2008-05-26 12:32:11 +02:00
return
2015-12-13 14:57:02 +01:00
if (self.quantity >= 0) != (move_type == 'in'):
return
quantity = (self._get_move_quantity(move_type)
- self._get_shipped_quantity(move_type))
2016-01-11 10:29:44 -03:00
quantity = self.unit.round(quantity)
if quantity <= 0:
2008-05-06 17:45:44 +02:00
return
2012-09-24 22:18:09 +02:00
if not self.purchase.party.supplier_location:
2018-12-29 14:20:29 +01:00
raise PartyLocationError(
gettext('purchase.msg_purchase_supplier_location_required',
purchase=self.purchase.rec_name,
party=self.purchase.party.rec_name))
2014-09-17 21:45:46 +02:00
move = Move()
2012-09-24 22:18:09 +02:00
move.quantity = quantity
move.uom = self.unit
move.product = self.product
move.from_location = self.from_location
move.to_location = self.to_location
move.state = 'draft'
move.company = self.purchase.company
if move.on_change_with_unit_price_required():
move.unit_price = self.unit_price
move.currency = self.purchase.currency
move.planned_date = self.planned_delivery_date
2015-12-13 14:57:02 +01:00
move.invoice_lines = self._get_move_invoice_lines(move_type)
move.origin = self
2012-09-24 22:18:09 +02:00
return move
2008-05-06 17:45:44 +02:00
2015-12-13 14:57:02 +01:00
def _get_move_quantity(self, move_type):
'Return the quantity that should be shipped'
return abs(self.quantity)
def _get_shipped_quantity(self, move_type):
'Return the quantity already shipped'
pool = Pool()
Uom = pool.get('product.uom')
quantity = 0
skip = set(self.moves_recreated)
for move in self.moves:
if move not in skip:
quantity += Uom.compute_qty(move.uom, move.quantity,
self.unit)
return quantity
def _get_move_invoice_lines(self, move_type):
'Return the invoice lines that should be shipped'
if self.purchase.invoice_method == 'order':
return [l for l in self.invoice_lines]
else:
return [l for l in self.invoice_lines if not l.stock_moves]
2015-12-13 14:57:02 +01:00
2020-04-14 09:27:53 +02:00
def set_actual_quantity(self):
pool = Pool()
Uom = pool.get('product.uom')
if self.type != 'line':
return
moved_quantity = 0
for move in self.moves:
2022-05-31 23:57:32 +02:00
if move.state != 'cancelled' and self.unit:
2020-04-14 09:27:53 +02:00
moved_quantity += Uom.compute_qty(
2022-05-31 23:57:32 +02:00
move.uom, move.quantity, self.unit, round=False)
2020-04-14 09:27:53 +02:00
if self.quantity < 0:
moved_quantity *= -1
invoiced_quantity = 0
for invoice_line in self.invoice_lines:
2022-05-31 23:57:32 +02:00
if (
(not invoice_line.invoice
or invoice_line.invoice.state != 'cancelled')
and self.unit and invoice_line.unit):
2020-04-14 09:27:53 +02:00
invoiced_quantity += Uom.compute_qty(
2022-05-31 23:57:32 +02:00
invoice_line.unit, invoice_line.quantity, self.unit,
round=False)
actual_quantity = max(moved_quantity, invoiced_quantity, key=abs)
2022-05-31 23:57:32 +02:00
if self.unit:
actual_quantity = self.unit.round(actual_quantity)
if self.actual_quantity != actual_quantity:
self.actual_quantity = actual_quantity
2020-04-14 09:27:53 +02:00
def get_rec_name(self, name):
2019-11-26 22:20:24 +01:00
pool = Pool()
Lang = pool.get('ir.lang')
if self.product:
2019-11-26 22:20:24 +01:00
lang = Lang.get()
return (lang.format(
'%.*f', (self.unit.digits, self.quantity or 0))
+ '%s %s @ %s' % (
self.unit.symbol, self.product.rec_name,
self.purchase.rec_name))
else:
return self.purchase.rec_name
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
('purchase.rec_name', *clause[1:]),
('product.rec_name', *clause[1:]),
]
2015-12-13 14:57:02 +01:00
@classmethod
def view_attributes(cls):
2021-04-09 10:52:03 +02:00
return super().view_attributes() + [
2015-12-13 14:57:02 +01:00
('/form//field[@name="note"]|/form//field[@name="description"]',
'spell', Eval('_parent_purchase', {}).get('party_lang')),
('//label[@id="delivery_date"]', 'states', {
'invisible': Eval('type') != 'line',
}),
]
2015-12-13 14:57:02 +01:00
@classmethod
def delete(cls, lines):
for line in lines:
if line.purchase_state not in {'cancelled', 'draft'}:
2018-12-29 14:20:29 +01:00
raise AccessError(
gettext('purchase.msg_purchase_line_delete_cancel_draft',
line=line.rec_name,
purchase=line.purchase.rec_name))
2020-03-01 12:33:53 +01:00
super().delete(lines)
2015-12-13 14:57:02 +01:00
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('moves', None)
default.setdefault('moves_ignored', None)
default.setdefault('moves_recreated', None)
default.setdefault('invoice_lines', None)
2021-10-22 10:14:10 +02:00
default.setdefault('actual_quantity')
2020-03-01 12:33:53 +01:00
return super().copy(lines, default=default)
2008-05-06 17:45:44 +02:00
2020-03-01 12:33:53 +01:00
class LineTax(ModelSQL):
2009-03-05 14:53:03 +01:00
'Purchase Line - Tax'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line-account.tax'
2009-03-05 14:53:03 +01:00
_table = 'purchase_line_account_tax'
line = fields.Many2One('purchase.line', 'Purchase Line',
2012-03-05 10:53:06 +01:00
ondelete='CASCADE', select=True, required=True,
2009-03-05 14:53:03 +01:00
domain=[('type', '=', 'line')])
tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
2012-03-27 10:43:54 +02:00
select=True, required=True, domain=[('parent', '=', None)])
2009-03-05 14:53:03 +01:00
2020-03-01 12:33:53 +01:00
class LineIgnoredMove(ModelSQL):
2009-03-05 14:53:03 +01:00
'Purchase Line - Ignored Move'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line-ignored-stock.move'
2009-03-05 14:53:03 +01:00
_table = 'purchase_line_moves_ignored_rel'
purchase_line = fields.Many2One('purchase.line', 'Purchase Line',
2012-03-05 10:53:06 +01:00
ondelete='CASCADE', select=True, required=True)
2009-03-05 14:53:03 +01:00
move = fields.Many2One('stock.move', 'Move', ondelete='RESTRICT',
2012-03-05 10:53:06 +01:00
select=True, required=True)
2009-03-05 14:53:03 +01:00
2020-03-01 12:33:53 +01:00
class LineRecreatedMove(ModelSQL):
2009-03-05 14:53:03 +01:00
'Purchase Line - Ignored Move'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line-recreated-stock.move'
2009-03-05 14:53:03 +01:00
_table = 'purchase_line_moves_recreated_rel'
purchase_line = fields.Many2One('purchase.line', 'Purchase Line',
2012-03-05 10:53:06 +01:00
ondelete='CASCADE', select=True, required=True)
2009-03-05 14:53:03 +01:00
move = fields.Many2One('stock.move', 'Move', ondelete='RESTRICT',
2012-03-05 10:53:06 +01:00
select=True, required=True)
2009-03-05 14:53:03 +01:00
2008-05-14 11:12:32 +02:00
class PurchaseReport(CompanyReport):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.purchase'
2008-05-13 18:42:46 +02:00
2016-09-06 09:24:41 +02:00
@classmethod
def execute(cls, ids, data):
with Transaction().set_context(address_with_party=True):
return super(PurchaseReport, cls).execute(ids, data)
@classmethod
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
pool = Pool()
Date = pool.get('ir.date')
2021-02-21 19:19:26 +01:00
context = super().get_context(records, header, data)
context['today'] = Date.today()
return context
2008-05-13 18:42:46 +02:00
2008-05-29 09:38:37 +02:00
class OpenSupplier(Wizard):
'Open Suppliers'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.open_supplier'
2012-01-02 18:03:18 +01:00
start_state = 'open_'
open_ = StateAction('party.act_party_form')
2008-05-29 09:38:37 +02:00
2012-09-24 22:18:09 +02:00
def do_open_(self, action):
2011-07-03 13:19:09 +02:00
pool = Pool()
2012-09-24 22:18:09 +02:00
ModelData = pool.get('ir.model.data')
Wizard = pool.get('ir.action.wizard')
2013-09-06 22:34:36 +02:00
Purchase = pool.get('purchase.purchase')
cursor = Transaction().connection.cursor()
2013-09-06 22:34:36 +02:00
purchase = Purchase.__table__()
2010-08-19 22:17:54 +02:00
2013-09-06 22:34:36 +02:00
cursor.execute(*purchase.select(purchase.party,
group_by=purchase.party))
supplier_ids = [line[0] for line in cursor]
2012-01-02 18:03:18 +01:00
action['pyson_domain'] = PYSONEncoder().encode(
[('id', 'in', supplier_ids)])
2015-10-03 11:59:23 +02:00
wizard = Wizard(ModelData.get_id('purchase', 'act_open_supplier'))
2012-01-02 18:03:18 +01:00
action['name'] = wizard.name
return action, {}
2008-05-29 09:38:37 +02:00
class HandleShipmentExceptionAsk(ModelView):
2012-01-02 18:03:18 +01:00
'Handle Shipment Exception'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.handle.shipment.exception.ask'
2009-02-23 18:53:30 +01:00
recreate_moves = fields.Many2Many(
2009-03-05 14:53:03 +01:00
'stock.move', None, None, 'Recreate Moves',
2010-01-20 11:03:33 +01:00
domain=[('id', 'in', Eval('domain_moves'))], depends=['domain_moves'],
2013-02-24 15:45:26 +01:00
help=('The selected moves will be recreated. '
'The other ones will be ignored.'))
domain_moves = fields.Many2Many(
2009-03-05 14:53:03 +01:00
'stock.move', None, None, 'Domain Moves')
2012-04-03 20:07:31 +02:00
class HandleShipmentException(Wizard):
2009-03-20 12:10:50 +01:00
'Handle Shipment Exception'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.handle.shipment.exception'
2012-01-02 18:03:18 +01:00
start_state = 'ask'
ask = StateView('purchase.handle.shipment.exception.ask',
'purchase.handle_shipment_exception_ask_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
2015-03-10 18:43:37 +01:00
Button('OK', 'handle', 'tryton-ok', default=True),
2012-01-02 18:03:18 +01:00
])
handle = StateTransition()
2012-09-24 22:18:09 +02:00
def default_ask(self, fields):
2012-01-02 18:03:18 +01:00
moves = []
for line in self.record.lines:
2012-09-24 22:18:09 +02:00
skip = set(line.moves_ignored + line.moves_recreated)
2012-01-02 18:03:18 +01:00
for move in line.moves:
2020-06-06 10:30:11 +01:00
if move.state == 'cancelled' and move not in skip:
2012-01-02 18:03:18 +01:00
moves.append(move.id)
return {
2014-03-24 17:35:11 +01:00
'recreate_moves': moves,
2012-01-02 18:03:18 +01:00
'domain_moves': moves,
}
2012-09-24 22:18:09 +02:00
def transition_handle(self):
2011-07-03 13:19:09 +02:00
pool = Pool()
2012-09-24 22:18:09 +02:00
PurchaseLine = pool.get('purchase.line')
to_recreate = self.ask.recreate_moves
domain_moves = self.ask.domain_moves
for line in self.record.lines:
moves_ignored = []
2009-02-23 18:53:30 +01:00
moves_recreated = []
2012-09-24 22:18:09 +02:00
skip = set(line.moves_ignored)
skip.update(line.moves_recreated)
for move in line.moves:
2012-09-24 22:18:09 +02:00
if move not in domain_moves or move in skip:
continue
2012-09-24 22:18:09 +02:00
if move in to_recreate:
2009-02-23 18:53:30 +01:00
moves_recreated.append(move.id)
else:
moves_ignored.append(move.id)
2012-09-24 22:18:09 +02:00
PurchaseLine.write([line], {
2010-08-19 22:17:54 +02:00
'moves_ignored': [('add', moves_ignored)],
'moves_recreated': [('add', moves_recreated)],
})
self.model.__queue__.process([self.record])
return 'end'
2009-02-09 16:33:10 +01:00
2009-02-28 17:02:35 +01:00
class HandleInvoiceExceptionAsk(ModelView):
2012-01-02 18:03:18 +01:00
'Handle Invoice Exception'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.handle.invoice.exception.ask'
2009-02-23 14:47:08 +01:00
recreate_invoices = fields.Many2Many(
2009-03-05 14:53:03 +01:00
'account.invoice', None, None, 'Recreate Invoices',
2010-01-20 11:03:33 +01:00
domain=[('id', 'in', Eval('domain_invoices'))],
depends=['domain_invoices'],
2013-02-24 15:45:26 +01:00
help=('The selected invoices will be recreated. '
'The other ones will be ignored.'))
2009-02-09 16:33:10 +01:00
domain_invoices = fields.Many2Many(
2009-03-05 14:53:03 +01:00
'account.invoice', None, None, 'Domain Invoices')
2009-02-09 16:33:10 +01:00
2009-02-09 16:33:10 +01:00
class HandleInvoiceException(Wizard):
'Handle Invoice Exception'
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.handle.invoice.exception'
2012-01-02 18:03:18 +01:00
start_state = 'ask'
ask = StateView('purchase.handle.invoice.exception.ask',
'purchase.handle_invoice_exception_ask_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
2015-03-10 18:43:37 +01:00
Button('OK', 'handle', 'tryton-ok', default=True),
2012-01-02 18:03:18 +01:00
])
handle = StateTransition()
2009-02-09 16:33:10 +01:00
2012-09-24 22:18:09 +02:00
def default_ask(self, fields):
skip = set(self.record.invoices_ignored)
skip.update(self.record.invoices_recreated)
2012-01-02 18:03:18 +01:00
invoices = []
for invoice in self.record.invoices:
if invoice.state == 'cancelled' and invoice not in skip:
2012-01-02 18:03:18 +01:00
invoices.append(invoice.id)
return {
2014-03-24 17:35:11 +01:00
'recreate_invoices': invoices,
2012-01-02 18:03:18 +01:00
'domain_invoices': invoices,
}
2012-09-24 22:18:09 +02:00
def transition_handle(self):
2009-02-09 16:33:10 +01:00
invoices_ignored = []
2009-02-23 14:47:08 +01:00
invoices_recreated = []
for invoice in self.ask.domain_invoices:
if invoice in self.ask.recreate_invoices:
2009-02-23 14:47:08 +01:00
invoices_recreated.append(invoice.id)
2009-02-09 16:33:10 +01:00
else:
invoices_ignored.append(invoice.id)
self.model.write([self.record], {
2010-08-19 22:17:54 +02:00
'invoices_ignored': [('add', invoices_ignored)],
'invoices_recreated': [('add', invoices_recreated)],
})
2009-02-09 16:33:10 +01:00
self.model.__queue__.process([self.record])
return 'end'
class ModifyHeaderStateView(StateView):
def get_view(self, wizard, state_name):
with Transaction().set_context(modify_header=True):
return super(ModifyHeaderStateView, self).get_view(
wizard, state_name)
class ModifyHeader(Wizard):
"Modify Header"
__name__ = 'purchase.modify_header'
start = ModifyHeaderStateView('purchase.purchase',
'purchase.modify_header_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Modify", 'modify', 'tryton-ok', default=True),
])
modify = StateTransition()
def get_purchase(self):
if self.record.state != 'draft':
2018-12-29 14:20:29 +01:00
raise AccessError(
gettext('purchase.msg_purchase_modify_header_draft',
purchase=self.record.rec_name))
return self.record
def default_start(self, fields):
purchase = self.get_purchase()
defaults = {}
for fieldname in fields:
value = getattr(purchase, fieldname)
if isinstance(value, Model):
if getattr(purchase.__class__, fieldname)._type == 'reference':
value = str(value)
else:
value = value.id
elif isinstance(value, (list, tuple)):
value = [r.id for r in value]
defaults[fieldname] = value
# Mimic an empty sale in draft state to get the fields' states right
defaults['lines'] = []
return defaults
def transition_modify(self):
pool = Pool()
Line = pool.get('purchase.line')
purchase = self.get_purchase()
purchase.__class__.write([purchase], self.start._save_values)
# Call on_change after the save to ensure parent sale
# has the modified values
for line in purchase.lines:
line.on_change_product()
Line.save(purchase.lines)
return 'end'
2020-04-12 09:15:06 +02:00
class ReturnPurchaseStart(ModelView):
"Return Purchase"
__name__ = 'purchase.return_purchase.start'
class ReturnPurchase(Wizard):
"Return Purchase"
__name__ = 'purchase.return_purchase'
start = StateView('purchase.return_purchase.start',
'purchase.return_purchase_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Return", 'return_', 'tryton-ok', default=True),
])
return_ = StateAction('purchase.act_purchase_form')
def do_return_(self, action):
purchases = self.records
return_purchases = self.model.copy(purchases)
2020-04-12 09:15:06 +02:00
for return_purchase, purchase in zip(return_purchases, purchases):
2021-03-27 21:21:33 +01:00
return_purchase.origin = purchase
2020-04-12 09:15:06 +02:00
for line in return_purchase.lines:
if line.type == 'line':
line.quantity *= -1
return_purchase.lines = return_purchase.lines # Force saving
self.model.save(return_purchases)
2020-04-12 09:15:06 +02:00
data = {'res_id': [s.id for s in return_purchases]}
if len(return_purchases) == 1:
action['views'].reverse()
return action, data