Files
tryton/modules/purchase/purchase.py
T

2505 lines
91 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
import math
from collections import defaultdict
2010-08-19 22:17:54 +02:00
from decimal import Decimal
from itertools import chain, groupby
2025-12-25 09:47:23 +01:00
from sql import Null
2013-09-06 22:34:36 +02:00
from sql.aggregate import Count
from sql.conditionals import Coalesce
from sql.functions import CharLength
2013-09-06 22:34:36 +02:00
2023-12-26 12:18:00 +01:00
from trytond import backend
2021-12-11 17:58:43 +01:00
from trytond.i18n import gettext
from trytond.ir.attachment import AttachmentCopyMixin
from trytond.ir.note import NoteCopyMixin
2021-12-11 17:58:43 +01:00
from trytond.model import (
2025-12-02 16:01:39 +01:00
ChatMixin, Index, ModelSQL, ModelView, Unique, Workflow, fields,
sequence_ordered, sort)
2021-12-11 17:58:43 +01:00
from trytond.model.exceptions import AccessError
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
2021-12-11 17:58:43 +01:00
from trytond.modules.company import CompanyReport
2020-04-10 18:53:44 +02:00
from trytond.modules.company.model import (
2021-12-11 17:58:43 +01:00
employee_field, reset_employee, set_employee)
2021-08-21 09:09:56 +02:00
from trytond.modules.currency.fields import Monetary
2023-08-17 17:57:11 +02:00
from trytond.modules.product import price_digits
2021-12-11 17:58:43 +01:00
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, If
from trytond.tools import cached_property, firstline
2021-12-11 17:58:43 +01:00
from trytond.transaction import Transaction
from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
2014-12-19 19:41:50 +01:00
from .exceptions import (
PartyLocationError, PurchaseMoveQuantity, PurchaseQuotationError)
2018-12-29 14:20:29 +01:00
2008-05-06 17:45:44 +02:00
def samesign(a, b):
return math.copysign(a, b) == a
def get_shipments_returns(model_name):
"Computes the returns or shipments"
def method(self, name):
pool = Pool()
Model = pool.get(model_name)
field = getattr(self.__class__, name)
order = field.order if field.order is not None else Model._order
shipments = set()
for line in self.line_lines:
for move in line.moves:
if isinstance(move.shipment, Model):
shipments.add(move.shipment)
return sort(shipments, order)
return method
def search_shipments_returns(model_name):
"Search on shipments or returns"
def method(self, name, clause):
_, operator, operand, *extra = clause
nested = clause[0][len(name):]
if not nested:
2018-06-26 19:11:51 +02:00
if isinstance(clause[2], str):
nested = '.rec_name'
else:
nested = '.id'
return [('lines.moves.shipment' + nested,
operator, operand, model_name, *extra)]
return classmethod(method)
class Purchase(
Workflow, ModelSQL, ModelView, TaxableMixin,
2025-12-02 16:01:39 +01:00
AttachmentCopyMixin, NoteCopyMixin, ChatMixin):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.purchase'
_rec_name = 'number'
_states = {
'readonly': Eval('state') != 'draft',
}
2021-04-20 09:18:58 +02:00
company = fields.Many2One(
'company.company', "Company", required=True,
2011-08-04 16:57:38 +02:00
states={
2026-08-31 23:21:03 +02:00
'readonly': Eval('state') != 'draft',
'editable': ~(
Eval('lines', [0])
| Eval('party', True)
| Eval('invoice_party', True)),
})
number = fields.Char("Number", readonly=True)
reference = fields.Char("Reference")
description = fields.Char('Description', size=None, states=_states)
2025-02-07 16:25:03 +01:00
quotation_expire = fields.Date(
"Quotation Expire",
states={
'invisible': ~Eval('state').in_(['draft', 'quotation']),
})
purchase_date = fields.Date('Purchase Date',
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
'required': ~Eval('state').in_(
['draft', 'quotation', 'cancelled']),
})
2023-02-21 12:55:12 +01:00
payment_term = fields.Many2One(
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
})
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={
2026-08-31 23:21:03 +02:00
'readonly': Eval('state') != 'draft',
'editable': ~(Eval('lines', [0]) & Eval('party')),
2014-07-11 18:17:46 +02:00
},
context={
'company': Eval('company', -1),
},
depends={'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={'company'})
2020-03-28 14:25:57 +01:00
invoice_party = fields.Many2One('party.party', "Invoice Party",
states={
2026-08-31 23:21:03 +02:00
'readonly': Eval('state') != 'draft',
'editable': ~Eval('lines', [0]),
2020-03-28 14:25:57 +01:00
},
context={
'company': Eval('company', -1),
'party_contact_mechanism_usage': 'invoice',
},
search_context={
'related_party': Eval('party'),
},
depends={'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', -1), Eval('party', -1))),
2020-03-28 14:25:57 +01:00
],
2014-01-26 17:32:45 +01:00
states={
'readonly': Eval('state') != 'draft',
'required': ~Eval('state').in_(
['draft', 'quotation', 'cancelled']),
})
2008-05-06 17:45:44 +02:00
warehouse = fields.Many2One('stock.location', 'Warehouse',
domain=[('type', '=', 'warehouse')], states=_states)
2008-05-09 17:27:19 +02:00
currency = fields.Many2One('currency.currency', 'Currency', required=True,
states={
2026-08-31 23:21:03 +02:00
'readonly': Eval('state') != 'draft',
'editable': ~(Eval('lines', [0]) & Eval('currency')),
})
lines = fields.One2Many(
'purchase.line', 'purchase', "Lines",
states={
2026-08-31 23:21:03 +02:00
'readonly': Eval('state') != 'draft',
'editable': Eval('company', None) & Eval('currency', None),
})
line_lines = fields.One2Many(
'purchase.line', 'purchase', "Line - Lines", readonly=True,
filter=[
('type', '=', 'line'),
])
2008-05-06 17:45:44 +02:00
comment = fields.Text('Comment')
2021-08-21 09:09:56 +02:00
untaxed_amount = fields.Function(Monetary(
"Untaxed", currency='currency', digits='currency'),
'get_amount')
untaxed_amount_cache = Monetary(
"Untaxed Cache", currency='currency', digits='currency')
tax_amount = fields.Function(Monetary(
"Tax", currency='currency', digits='currency'),
'get_amount')
tax_amount_cache = Monetary(
"Tax Cache", currency='currency', digits='currency')
total_amount = fields.Function(Monetary(
"Total", currency='currency', digits='currency'),
'get_amount')
total_amount_cache = Monetary(
"Total Cache", currency='currency', digits='currency')
2008-05-06 17:45:44 +02:00
invoice_method = fields.Selection([
2011-08-04 16:57:38 +02:00
('manual', 'Manual'),
('order', "On Order"),
('fulfillment', "On Fulfillment"),
],
"Invoice Method", required=True, states=_states)
2008-05-06 17:45:44 +02:00
invoice_state = fields.Selection([
2012-03-25 00:18:10 +01:00
('none', 'None'),
('pending', "Pending"),
('awaiting payment', "Awaiting Payment"),
('partially paid', "Partially Paid"),
2012-03-25 00:18:10 +01:00
('paid', 'Paid'),
('exception', 'Exception'),
2022-02-15 00:08:47 +01:00
], 'Invoice State', readonly=True, required=True, sort=False)
to_invoice = fields.Boolean("To Invoice", readonly=True)
invoices = fields.Function(fields.Many2Many(
'account.invoice', None, None, "Invoices",
order=[('id', 'ASC')]),
'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",
domain=[
('id', 'in', Eval('invoices', [])),
('state', '=', 'cancelled'),
],
order=[('id', 'ASC')],
states={
'invisible': ~Eval('invoices_ignored', []),
})
2009-03-05 14:53:03 +01:00
invoices_recreated = fields.Many2Many(
'purchase.purchase-recreated-account.invoice',
'purchase', 'invoice', 'Recreated Invoices', readonly=True,
order=[('id', 'ASC')])
origin = fields.Reference(
"Origin", selection='get_origin',
2021-03-27 21:21:33 +01:00
states={
'readonly': Eval('state') != 'draft',
})
delivery_date = fields.Date(
"Delivery Date",
states={
'readonly': Eval('state').in_([
'processing', 'done', 'cancelled']),
},
help="The default delivery date for each line.")
shipment_state = fields.Selection([
2012-03-25 00:18:10 +01:00
('none', 'None'),
('waiting', 'Waiting'),
2023-03-13 11:30:22 +01:00
('partially shipped', 'Partially Shipped'),
2012-03-25 00:18:10 +01:00
('received', 'Received'),
('exception', 'Exception'),
2022-02-15 00:08:47 +01:00
], 'Shipment State', readonly=True, required=True, sort=False)
shipments = fields.Function(fields.Many2Many(
'stock.shipment.in', None, None, "Shipments",
order=[('id', 'ASC')]),
'get_shipments', searcher='search_shipments')
shipment_returns = fields.Function(fields.Many2Many(
'stock.shipment.in.return', None, None, "Shipment Returns",
order=[('id', 'ASC')]),
'get_shipment_returns', searcher='search_shipment_returns')
2022-03-06 18:13:01 +01:00
moves = fields.Function(
2024-06-03 10:48:34 +02:00
fields.Many2Many('stock.move', None, None, "Stock Moves"),
2022-03-06 18:13:01 +01:00
'get_moves', searcher='search_moves')
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"),
2022-02-15 00:08:47 +01:00
], "State", readonly=True, required=True, sort=False)
2020-04-10 18:53:44 +02:00
del _states
2012-09-24 22:18:09 +02:00
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
cls.reference.search_unaccented = False
2025-04-04 00:45:18 +02:00
super().__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (Coalesce(t.reference, ''), Index.Similarity())),
2024-07-27 18:36:14 +02:00
Index(t, (t.party, Index.Range())),
Index(
t,
2024-07-27 18:36:14 +02:00
(t.state, Index.Equality(cardinality='low')),
where=t.state.in_([
'draft', 'quotation', 'confirmed', 'processing'])),
Index(
t,
2024-07-27 18:36:14 +02:00
(t.invoice_state, Index.Equality(cardinality='low')),
where=t.invoice_state.in_([
'none', 'waiting', 'exception'])),
Index(
t,
2024-07-27 18:36:14 +02:00
(t.shipment_state, Index.Equality(cardinality='low')),
where=t.shipment_state.in_([
'none', 'waiting', 'exception'])),
})
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': {
'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': {
'pre_validate': [
If(~Eval('invoice_address'),
('invoice_address', '!=', None),
()),
],
2012-03-25 00:18:10 +01:00
'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', 'done']),
'icon': If(Eval('state') == 'confirmed',
'tryton-forward', 'tryton-refresh'),
2018-02-19 11:40:08 +01:00
'depends': ['state'],
},
'manual_invoice': {
'invisible': (
~Eval('to_invoice', False)
| (Eval('invoice_method') != 'manual')
| ~Eval('state').in_(['processing', 'done'])),
'depends': ['to_invoice', 'invoice_method', '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):
cursor = Transaction().connection.cursor()
2013-09-06 22:34:36 +02:00
sql_table = cls.__table__()
2025-04-04 00:45:18 +02:00
super().__register__(module_name)
# Migration from 6.6: rename invoice state waiting to pending
cursor.execute(*sql_table.update(
[sql_table.invoice_state], ['pending'],
where=sql_table.invoice_state == 'waiting'))
# Migration from 7.8: rename invoice method shipment to fulfillment
cursor.execute(*sql_table.update(
[sql_table.invoice_method], ['fulfillment'],
where=sql_table.invoice_method == 'shipment'))
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [
~((table.state == 'cancelled') & (table.number == Null)),
CharLength(table.number), table.number]
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'
2022-04-11 21:24:28 +02:00
@classmethod
def default_currency(cls, **pattern):
pool = Pool()
Company = pool.get('company.company')
company = pattern.get('company')
if not company:
company = cls.default_company()
if company is not None and company >= 0:
2022-04-11 21:24:28 +02:00
return Company(company).currency.id
2008-05-06 17:45:44 +02:00
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'
@classmethod
def default_to_invoice(cls):
return False
2012-09-24 22:18:09 +02:00
@staticmethod
def default_shipment_state():
2008-05-06 17:45:44 +02:00
return 'none'
2022-04-11 21:24:28 +02:00
@fields.depends(
'company', '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
2022-04-11 21:24:28 +02:00
self.invoice_method = self.default_invoice_method(
company=self.company.id if self.company else None)
if not self.lines:
2022-04-11 21:24:28 +02:00
self.currency = self.default_currency(
company=self.company.id if self.company else None)
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.desc,
limit=10)
cursor.execute(*subquery.select(
subquery.currency,
subquery.payment_term,
subquery.invoice_method,
group_by=[
subquery.currency,
subquery.payment_term,
subquery.invoice_method,
],
2025-12-25 09:47:23 +01:00
order_by=Count().desc))
row = cursor.fetchone()
if row:
self.currency, self.payment_term, self.invoice_method = row
2022-04-11 21:24:28 +02:00
if self.party.supplier_currency:
self.currency = self.party.supplier_currency
if self.party.supplier_payment_term:
self.payment_term = self.party.supplier_payment_term
2021-10-08 19:32:57 +02:00
else:
self.payment_term = None
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('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()
@fields.depends('party_lang', '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_lang:
context['language'] = self.party_lang
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):
self.untaxed_amount = Decimal(0)
self.tax_amount = Decimal(0)
self.total_amount = Decimal(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
2026-01-17 12:54:09 +01:00
@fields.depends('lines')
2014-12-19 19:41:50 +01:00
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):
return sum((t.amount for t in self._get_taxes().values()), 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
# Browse separately not cached to limit number of lines read
cached, not_cached = [], []
2013-06-01 00:02:18 +02:00
for purchase in purchases:
if purchase.state in cls._states_cached:
cached.append(purchase)
else:
not_cached.append(purchase)
for purchase in chain(cached, cls.browse(not_cached)):
2013-06-01 00:02:18 +02:00
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.line_lines
if line.amount is not None),
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):
pool = Pool()
Invoice = pool.get('account.invoice')
field = getattr(self.__class__, name)
order = field.order if field.order is not None else Invoice._order
invoices = set()
for line in self.line_lines:
for invoice_line in line.invoice_lines:
if invoice_line.invoice:
invoices.add(invoice_line.invoice)
return sort(invoices, order)
@classmethod
def search_invoices(cls, name, clause):
return [
('lines', 'where', [
('invoice_lines.invoice' + clause[0][len(name):],
*clause[1:]),
('type', '=', 'line'),
]),
]
@property
def _invoices_for_state(self):
return self.invoices
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.
'''
skips = set(self.invoices_ignored)
skips.update(self.invoices_recreated)
invoices = [i for i in self._invoices_for_state if i not in skips]
def is_cancelled(invoice):
return invoice.state == 'cancelled' and not invoice.cancel_move
def is_paid(invoice):
return (
invoice.state == 'paid'
or (invoice.state == 'cancelled' and invoice.cancel_move))
2012-03-25 00:18:10 +01:00
if invoices:
if any(is_cancelled(i) for i in invoices):
2012-03-25 00:18:10 +01:00
return 'exception'
elif all(is_paid(i) for i in invoices):
2012-03-25 00:18:10 +01:00
return 'paid'
elif any(is_paid(i) for i in invoices):
return 'partially paid'
elif any(i.state == 'posted' for i in invoices):
return 'awaiting payment'
2012-03-25 00:18:10 +01:00
else:
return 'pending'
2012-03-25 00:18:10 +01:00
return 'none'
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.
'''
if any(l.moves_exception for l in self.line_lines):
return 'exception'
elif any(m.state != 'cancelled' for m in self.moves):
if all(l.moves_progress >= 1 for l in self.line_lines
2023-03-13 11:30:22 +01:00
if l.moves_progress is not None):
2012-03-25 00:18:10 +01:00
return 'received'
elif any(l.moves_progress for l in self.line_lines):
2023-03-13 11:30:22 +01:00
return 'partially shipped'
2012-03-25 00:18:10 +01:00
else:
return 'waiting'
return 'none'
2022-03-06 18:13:01 +01:00
def get_moves(self, name):
return [m.id for l in self.line_lines for m in l.moves]
2022-03-06 18:13:01 +01:00
@classmethod
def search_moves(cls, name, clause):
return [
('lines', 'where', [
clause,
('type', '=', 'line'),
]),
]
2022-03-06 18:13:01 +01: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
2025-12-02 16:01:39 +01:00
def chat_language(self, audience='internal'):
language = super().chat_language(audience=audience)
if audience == 'public':
language = self.party_lang
2025-12-02 16:01:39 +01:00
return language
@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)
2025-08-01 00:10:49 +02:00
default.setdefault('reference')
2018-09-01 01:02:03 +02:00
default.setdefault('invoice_state', 'none')
default.setdefault('to_invoice', cls.default_to_invoice())
2018-09-01 01:02:03 +02:00
default.setdefault('invoices_ignored', 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')
default.setdefault('untaxed_amount_cache')
default.setdefault('tax_amount_cache')
default.setdefault('total_amount_cache')
2025-04-04 00:45:18 +02:00
return super().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.line_lines:
if (line.quantity or 0) >= 0:
location = line.to_location
else:
location = line.from_location
if ((not location or not line.warehouse) and line.movable):
2018-12-29 14:20:29 +01:00
raise PurchaseQuotationError(
gettext('purchase.msg_warehouse_required_for_quotation',
purchase=self.rec_name,
line=line.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)
2024-08-25 12:16:12 +02:00
for company, c_purchases in groupby(
purchases, key=lambda p: p.company):
c_purchases = [p for p in c_purchases if not p.number]
if c_purchases:
sequence = config.get_multivalue(
'purchase_sequence', company=company.id)
for purchase, number in zip(
c_purchases, sequence.get_many(len(c_purchases))):
purchase.number = number
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 company, purchases in groupby(purchases, key=lambda p: p.company):
with Transaction().set_context(company=company.id):
today = Date.today()
cls.write([p for p in purchases if not p.purchase_date], {
'purchase_date': today,
})
2012-09-24 22:18:09 +02:00
@classmethod
def store_cache(cls, purchases):
purchases = list(purchases)
cls.write(purchases, {
'untaxed_amount_cache': None,
'tax_amount_cache': None,
'total_amount_cache': None,
})
2012-09-24 22:18:09 +02:00
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
def _get_invoice(self):
2016-03-20 22:59:22 +01:00
'Return invoice'
2011-07-03 13:19:09 +02:00
pool = Pool()
2012-09-24 22:18:09 +02:00
Invoice = pool.get('account.invoice')
2020-03-28 14:25:57 +01:00
party = self.invoice_party or self.party
2022-12-26 15:08:29 +01:00
invoice = Invoice(
2014-09-17 21:45:46 +02:00
company=self.company,
2016-03-20 22:59:22 +01:00
type='in',
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,
)
invoice.set_journal()
2022-12-26 15:08:29 +01:00
return invoice
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'
context = Transaction().context
if (self.invoice_method == 'manual'
and not context.get('_purchase_manual_invoice', False)):
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
invoice = self._get_invoice()
if getattr(invoice, 'lines', None):
invoice_lines = list(invoice.lines) + invoice_lines
invoice.lines = invoice_lines
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 = []
for line in self.line_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
'''
return_shipment = self._get_return_shipment()
return_shipment.moves = return_moves
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 all(
l.invoice_progress >= 1
for l in self.line_lines
if l.invoice_progress is not None)))
and (self.shipment_state == 'received'
or (self.shipment_state == 'none'
and all(
l.moves_progress >= 1
for l in self.line_lines
if l.moves_progress is not None))))
2008-05-06 17:45:44 +02:00
2012-09-24 22:18:09 +02:00
@classmethod
def check_modification(cls, mode, purchases, values=None, external=False):
super().check_modification(
mode, purchases, values=values, external=external)
if mode == 'delete':
for purchase in purchases:
if purchase.state not in {'cancelled', 'draft'}:
raise AccessError(gettext(
'purchase.msg_purchase_delete_cancel',
purchase=purchase.rec_name))
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):
cls.write(purchases, {
'tax_amount_cache': None,
'untaxed_amount_cache': None,
'total_amount_cache': None,
})
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)
2023-04-12 13:42:18 +02:00
@property
def process_after(self):
pool = Pool()
Configuration = pool.get('purchase.configuration')
config = Configuration(1)
return config.purchase_process_after
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):
pool = Pool()
Line = pool.get('purchase.line')
2022-03-09 00:57:17 +01:00
transaction = Transaction()
context = transaction.context
2012-09-24 22:18:09 +02:00
cls.set_purchase_date(purchases)
cls.write(purchases, {'state': 'confirmed'})
lines = list(sum((p.lines for p in purchases), ()))
Line._validate(lines, ['unit_price'])
2012-09-24 22:18:09 +02:00
cls.store_cache(purchases)
2023-04-12 13:42:18 +02:00
for process_after, sub_purchases in groupby(
purchases, lambda p: p.process_after):
with transaction.set_context(
queue_scheduled_at=process_after,
queue_batch=context.get('queue_batch', True)):
cls.__queue__.process(sub_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):
states = {'confirmed', 'processing', 'done'}
purchases = [p for p in purchases if p.state in states]
cls.lock(purchases)
cls._process_invoice(purchases)
cls._process_fulfillment(purchases)
cls._process_invoice_fulfillment_states(purchases)
cls._process_state(purchases)
@classmethod
def _process_invoice(cls, purchases):
invoices = {}
for purchase in purchases:
invoice = purchase.create_invoice()
if invoice:
invoices[purchase] = invoice
2021-10-22 00:38:12 +02:00
cls._save_invoice(invoices)
@classmethod
def _save_invoice(cls, invoices):
pool = Pool()
Invoice = pool.get('account.invoice')
Invoice.save(invoices.values())
for purchase, invoice in invoices.items():
purchase.copy_resources_to(invoice)
@classmethod
def _process_fulfillment(cls, purchases):
pool = Pool()
Move = pool.get('stock.move')
ShipmentInReturn = pool.get('stock.shipment.in.return')
moves, shipments_return = [], {}
for purchase in purchases:
moves.extend(purchase.create_move('in'))
return_moves = purchase.create_move('return')
if return_moves:
shipments_return[purchase] = purchase.create_return_shipment(
return_moves)
Move.save(moves)
ShipmentInReturn.save(shipments_return.values())
ShipmentInReturn.wait(shipments_return.values())
for purchase, shipment in shipments_return.items():
purchase.copy_resources_to(shipment)
@classmethod
def _process_invoice_fulfillment_states(cls, purchases):
2020-04-14 09:27:53 +02:00
pool = Pool()
Line = pool.get('purchase.line')
lines = []
purchases_to_invoice = defaultdict(list)
invoice_states, shipment_states = defaultdict(list), defaultdict(list)
2012-09-24 22:18:09 +02:00
for purchase in purchases:
invoice_state = purchase.get_invoice_state()
if purchase.invoice_state != invoice_state:
invoice_states[invoice_state].append(purchase)
shipment_state = purchase.get_shipment_state()
if purchase.shipment_state != shipment_state:
shipment_states[shipment_state].append(purchase)
2020-04-14 09:27:53 +02:00
to_invoice = False
for line in purchase.line_lines:
line.set_quantities()
to_invoice |= bool(line.quantity_to_invoice)
2020-04-14 09:27:53 +02:00
lines.append(line)
if purchase.to_invoice != to_invoice:
purchases_to_invoice[to_invoice].append(purchase)
2020-04-14 09:27:53 +02:00
for to_invoice, purchases in purchases_to_invoice.items():
cls.write(purchases, {'to_invoice': to_invoice})
for invoice_state, purchases in invoice_states.items():
cls.write(purchases, {'invoice_state': invoice_state})
cls.log(purchases, 'transition', f'invoice_state:{invoice_state}')
for shipment_state, purchases in shipment_states.items():
cls.write(purchases, {'shipment_state': shipment_state})
cls.log(
purchases, 'transition', f'shipment_state:{shipment_state}')
Line.save(lines)
@classmethod
def _process_state(cls, purchases):
process, done = [], []
for purchase in purchases:
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)
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
def manual_invoice(cls, purchases):
purchases = [p for p in purchases if p.invoice_method == 'manual']
with Transaction().set_context(_purchase_manual_invoice=True):
cls.process(purchases)
@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):
__name__ = 'purchase.purchase-ignored-account.invoice'
purchase = fields.Many2One(
'purchase.purchase', "Purchase", ondelete='CASCADE', required=True)
invoice = fields.Many2One(
'account.invoice', "Invoice", ondelete='RESTRICT', required=True,
domain=[
('purchases', '=', Eval('purchase', -1)),
('state', '=', 'cancelled'),
])
2009-03-05 14:53:03 +01:00
2023-12-26 12:18:00 +01:00
@classmethod
def __register__(cls, module):
# Migration from 7.0: rename to standard name
backend.TableHandler.table_rename(
'purchase_invoice_ignored_rel', cls._table)
super().__register__(module)
2009-03-05 14:53:03 +01:00
2019-08-20 09:30:24 +02:00
class PurchaseRecreatedInvoice(ModelSQL):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.purchase-recreated-account.invoice'
purchase = fields.Many2One(
'purchase.purchase', "Purchase", ondelete='CASCADE', required=True)
invoice = fields.Many2One(
'account.invoice', "Invoice", ondelete='RESTRICT', required=True,
domain=[
('purchases', '=', Eval('purchase', -1)),
('state', '=', 'cancelled'),
])
2009-03-05 14:53:03 +01:00
2023-12-26 12:18:00 +01:00
@classmethod
def __register__(cls, module):
# Migration from 7.0: rename to standard name
backend.TableHandler.table_rename(
'purchase_invoice_recreated_rel', cls._table)
super().__register__(module)
2009-03-05 14:53:03 +01:00
2020-03-01 12:33:53 +01:00
class Line(sequence_ordered(), ModelSQL, ModelView):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line'
purchase = fields.Many2One(
'purchase.purchase', "Purchase", ondelete='CASCADE', required=True,
2016-06-30 18:53:51 +02:00
states={
2026-08-31 23:21:03 +02:00
'readonly': Eval('purchase_state') != 'draft',
'editable': ~Eval('purchase'),
})
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'),
], "Type", required=True,
2016-06-30 18:53:51 +02:00
states={
'readonly': Eval('purchase_state') != 'draft',
})
2021-08-30 00:27:13 +02:00
quantity = fields.Float(
"Quantity", digits='unit',
domain=[
If(Eval('type') != 'line',
('quantity', '=', None),
()),
],
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',
})
2020-04-14 09:27:53 +02:00
actual_quantity = fields.Float(
2021-08-30 00:27:13 +02:00
"Actual Quantity", digits='unit', readonly=True,
domain=[
If(Eval('type') != 'line',
('actual_quantity', '=', None),
()),
],
2020-04-14 09:27:53 +02:00
states={
'invisible': ((Eval('type') != 'line') | ~Eval('actual_quantity')),
})
quantity_to_invoice = fields.Float(
"Quantity to Invoice", digits='unit', readonly=True,
states={
'invisible': ~Eval('quantity_to_invoice'),
})
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)),
If(Eval('type') != 'line',
('id', '=', None),
()),
])
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'])
& ~(Eval('quantity', 0) < 0),
('purchasable', '=', True),
()),
If(Eval('type') != 'line',
('id', '=', None),
()),
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={
'locations': If(Bool(Eval('warehouse')),
[Eval('warehouse', -1)], []),
'stock_date_end': Eval('purchase_date', None),
2011-08-04 16:57:38 +02:00
'stock_skip_warehouse': True,
'currency': Eval('currency', -1),
'supplier': Eval('supplier', -1),
'purchase_date': Eval('purchase_date', None),
'uom': Eval('unit'),
'taxes': Eval('taxes', []),
'quantity': Eval('quantity'),
2020-04-25 09:18:21 +02:00
},
depends={
'company', 'warehouse', 'purchase_date', 'currency', 'supplier'})
2017-12-18 17:56:39 +01:00
product_supplier = fields.Many2One(
'purchase.product_supplier', "Supplier's Product",
ondelete='RESTRICT',
domain=[
If(Eval('type') != 'line',
('id', '=', None),
()),
2017-12-18 17:56:39 +01:00
If(Bool(Eval('product')),
['OR',
[
('template.products', '=', Eval('product')),
('product', '=', None),
],
('product', '=', Eval('product')),
],
[]),
('party', '=', Eval('supplier', -1)),
2017-12-18 17:56:39 +01:00
],
states={
'invisible': Eval('type') != 'line',
'readonly': Eval('purchase_state') != 'draft',
})
2012-03-25 15:40:32 +02:00
product_uom_category = fields.Function(
2023-08-20 12:17:27 +02:00
fields.Many2One(
'product.uom.category', "Product UoM Category",
help="The category of Unit of Measure for the product."),
2012-09-24 22:18:09 +02:00
'on_change_with_product_uom_category')
2021-08-21 09:09:56 +02:00
unit_price = Monetary(
"Unit Price", currency='currency', digits=price_digits,
domain=[
If(Eval('type') != 'line',
('unit_price', '=', None),
()),
],
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')
& Eval('purchase_state').in_(
['confirmed', 'processing', 'done'])),
2016-06-30 18:53:51 +02:00
'readonly': Eval('purchase_state') != 'draft',
})
2021-08-21 09:09:56 +02:00
amount = fields.Function(Monetary(
"Amount", currency='currency', digits='currency',
2011-08-04 16:57:38 +02:00
states={
2011-08-21 11:20:50 +02:00
'invisible': ~Eval('type').in_(['line', 'subtotal']),
}),
'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',
})
2024-07-18 10:12:04 +02:00
summary = fields.Function(
fields.Char('Summary'), 'on_change_with_summary',
searcher='search_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']),
],
('company', '=', Eval('company', -1)),
If(Eval('type') != 'line',
('id', '=', None),
()),
],
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',
})
invoice_lines = fields.One2Many(
'account.invoice.line', 'origin', "Invoice Lines", readonly=True,
states={
'invisible': ~Eval('invoice_lines'),
})
invoice_progress = fields.Function(
fields.Float("Invoice Progress", digits=(1, 4)),
'get_invoice_progress')
moves = fields.One2Many(
2024-06-03 10:48:34 +02:00
'stock.move', 'origin', "Stock Moves", readonly=True,
states={
'invisible': ~Eval('moves'),
})
moves_ignored = fields.Many2Many(
'purchase.line-ignored-stock.move', 'purchase_line', 'move',
2024-06-03 10:48:34 +02:00
"Ignored Stock Moves",
domain=[
('id', 'in', Eval('moves', [])),
('state', '=', 'cancelled'),
],
states={
'invisible': ~Eval('moves_ignored'),
})
2024-06-03 10:48:34 +02:00
moves_recreated = fields.Many2Many(
'purchase.line-recreated-stock.move', 'purchase_line', 'move',
2023-11-18 00:24:33 +01:00
"Recreated Moves", readonly=True,
states={
'invisible': ~Eval('moves_recreated'),
})
moves_exception = fields.Function(fields.Boolean(
"Moves Exception",
states={
'invisible': ~Eval('movable'),
}),
'get_moves_exception')
moves_progress = fields.Function(fields.Float(
"Moves Progress", digits=(1, 4),
states={
'invisible': ~Eval('movable'),
}),
2023-03-13 11:30:22 +01:00
'get_moves_progress')
warehouse = fields.Function(fields.Many2One(
2023-11-18 00:24:33 +01:00
'stock.location', "Warehouse",
states={
'invisible': ~Eval('movable'),
}),
'on_change_with_warehouse')
2023-11-18 00:24:33 +01:00
from_location = fields.Function(fields.Many2One(
'stock.location', "From Location",
states={
'invisible': ~Eval('movable'),
}),
'get_from_location')
to_location = fields.Function(fields.Many2One(
'stock.location', "To Location",
states={
'invisible': ~Eval('movable'),
}),
'get_to_location')
movable = fields.Function(
fields.Boolean("Movable"), 'on_change_with_movable')
delivery_date = fields.Function(fields.Date('Delivery Date',
states={
'invisible': Eval('type') != 'line',
2026-08-31 23:21:03 +02:00
'readonly': Eval('purchase_state').in_(
['processing', 'done', 'cancelled']),
'editable': Eval('delivery_date_edit', False),
}),
'on_change_with_delivery_date', setter='set_delivery_date')
delivery_date_edit = fields.Boolean(
"Edit Delivery Date",
domain=[
If(Eval('type') != 'line',
('delivery_date_edit', '=', False),
()),
],
states={
'invisible': (
(Eval('type') != 'line')
| Eval('purchase_state').in_(
['processing', 'done', 'cancelled'])),
'readonly': Eval('purchase_state').in_(
['processing', 'done', 'cancelled']),
},
help="Check to edit the delivery date.")
delivery_date_store = fields.Date(
2026-08-31 23:21:03 +02:00
"Delivery Date",
domain=[
If(Eval('type') != 'line',
('delivery_date_store', '=', None),
()),
],
states={
'invisible': Eval('type') != 'line',
2026-08-31 23:21:03 +02:00
'editable': False
})
2016-06-30 18:53:51 +02:00
purchase_state = fields.Function(
fields.Selection('get_purchase_states', 'Purchase State'),
'on_change_with_purchase_state', searcher='search_purchase_state')
2020-04-25 09:18:21 +02:00
company = fields.Function(
fields.Many2One('company.company', "Company"),
'on_change_with_company')
supplier = fields.Function(
fields.Many2One(
'party.party', "Supplier",
context={
'company': Eval('company', -1),
}),
'on_change_with_supplier', searcher='search_supplier')
purchase_date = fields.Function(
fields.Date("Purchase Date"),
'on_change_with_purchase_date', searcher='search_purchase_date')
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__()
t = cls.__table__()
2021-03-07 11:13:53 +01:00
cls.__access__.add('purchase')
cls._sql_indexes.update({
Index(t,
(t.product, Index.Range()),
(t.purchase, Index.Equality()),
where=t.type == 'line'),
})
cls._order.insert(0, ('purchase.purchase_date', 'DESC NULLS FIRST'))
cls._order.insert(1, ('purchase.id', 'DESC'))
2021-03-07 11:13:53 +01:00
2012-09-24 22:18:09 +02:00
@staticmethod
def default_type():
2008-05-06 17:45:44 +02:00
return 'line'
@fields.depends('type', 'taxes')
def on_change_type(self):
if self.type != 'line':
self.product = None
self.product_supplier = None
self.unit = None
self.taxes = None
@classmethod
def default_delivery_date_edit(cls):
return False
@property
def _invoice_remaining_quantity(self):
"Compute the remaining quantity to be paid"
pool = Pool()
UoM = pool.get('product.uom')
if self.type != 'line':
return
skips = set(self.purchase.invoices_ignored)
quantity = self.quantity
if self.purchase.invoice_method == 'fulfillment':
moves_ignored = set(self.moves_ignored)
for move in self.moves:
if move in moves_ignored:
quantity -= UoM.compute_qty(
move.unit, math.copysign(move.quantity, self.quantity),
self.unit)
for invoice_line in self.invoice_lines:
if invoice_line.type != 'line':
continue
if (invoice_line.invoice
and (invoice_line.invoice.state == 'paid'
or invoice_line.invoice in skips)):
quantity -= UoM.compute_qty(
invoice_line.unit or self.unit, invoice_line.quantity,
self.unit)
return quantity
def get_invoice_progress(self, name):
progress = None
quantity = self._invoice_remaining_quantity
if quantity is not None and self.quantity:
progress = round((self.quantity - quantity) / self.quantity, 4)
progress = max(0., min(1., progress))
return progress
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
if not self.movable:
2017-04-19 10:30:11 +02:00
return
2023-03-16 13:31:20 +01:00
skips = set(self.moves_ignored)
2017-04-19 10:30:11 +02:00
quantity = abs(self.quantity)
2012-09-24 22:18:09 +02:00
for move in self.moves:
2023-03-16 13:31:20 +01:00
if move.state == 'done' or move in skips:
2023-08-22 13:18:33 +02:00
quantity -= Uom.compute_qty(
move.unit, move.quantity, self.unit)
2017-04-19 10:30:11 +02:00
return quantity
def get_moves_exception(self, name):
skips = set(self.moves_ignored)
skips.update(self.moves_recreated)
return any(
m.state == 'cancelled' for m in self.moves if m not in skips)
2008-05-06 17:45:44 +02:00
2023-03-13 11:30:22 +01:00
def get_moves_progress(self, name):
progress = None
quantity = self._move_remaining_quantity
if quantity is not None and self.quantity:
progress = round(
(abs(self.quantity) - quantity) / abs(self.quantity), 4)
progress = max(0., min(1., progress))
2023-03-13 11:30:22 +01:00
return progress
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
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),
}
@fields.depends(
'purchase', 'taxes',
'_parent_purchase.party', '_parent_purchase.invoice_party',
methods=['compute_taxes', 'on_change_with_amount'])
def on_change_purchase(self):
party = None
if self.purchase:
party = self.purchase.invoice_party or self.purchase.party
self.taxes = self.compute_taxes(party)
self.amount = self.on_change_with_amount()
@fields.depends(
'product', 'unit', 'purchase', 'taxes',
2020-03-28 14:25:57 +01:00
'_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):
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
if self.product:
category = self.product.purchase_uom.category
if not self.unit or self.unit.category != category:
self.unit = self.product.purchase_uom
2015-02-27 11:25:10 +01:00
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):
self.product_supplier = None
2017-12-18 17:56:39 +01:00
self.unit_price = self.compute_unit_price()
2015-02-27 11:25:10 +01:00
2014-10-29 20:40:09 +01:00
self.amount = self.on_change_with_amount()
2008-05-06 17:45:44 +02:00
@cached_property
def product_name(self):
if self.product_supplier:
return self.product_supplier.rec_name
elif self.product:
return self.product.rec_name
else:
return ''
@fields.depends(
'type', 'product',
methods=['on_change_with_company', '_get_tax_rule_pattern'])
2019-09-29 18:30:25 +02:00
def compute_taxes(self, party):
pool = Pool()
AccountConfiguration = pool.get('account.configuration')
if self.type != 'line':
return []
company = self.on_change_with_company()
2022-07-17 00:00:56 +02:00
taxes = set()
2019-09-29 18:30:25 +02:00
pattern = self._get_tax_rule_pattern()
taxes_used = []
if self.product:
taxes_used = self.product.supplier_taxes_used
elif company:
account_config = AccountConfiguration(1)
account = account_config.get_multivalue(
'default_category_account_expense', company=company.id)
if account:
taxes_used = account.taxes
for tax in taxes_used:
2019-09-29 18:30:25 +02:00
if party and party.supplier_tax_rule:
tax_ids = party.supplier_tax_rule.apply(tax, pattern)
if tax_ids:
2022-07-17 00:00:56 +02:00
taxes.update(tax_ids)
2019-09-29 18:30:25 +02:00
continue
2022-07-17 00:00:56 +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:00:56 +02:00
taxes.update(tax_ids)
return list(taxes)
2019-09-29 18:30:25 +02:00
@fields.depends('product', 'quantity', 'unit_price',
2019-09-29 18:30:25 +02:00
methods=['_get_context_purchase_price'])
def compute_unit_price(self):
pool = Pool()
Product = pool.get('product.product')
unit_price = None
if self.product:
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 is None:
unit_price = self.unit_price
return unit_price
2019-09-29 18:30:25 +02:00
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):
2023-03-26 09:49:00 +02:00
return self.product.default_uom_category if self.product else None
2012-03-25 15:40:32 +02:00
@fields.depends(methods=['compute_unit_price'])
2012-09-24 22:18:09 +02:00
def on_change_quantity(self):
self.unit_price = self.compute_unit_price()
2008-09-01 11:02:49 +02:00
@fields.depends(methods=['on_change_quantity', 'on_change_with_amount'])
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()
self.amount = self.on_change_with_amount()
@fields.depends(methods=['on_change_quantity', 'on_change_with_amount'])
def on_change_taxes(self):
self.on_change_quantity()
self.amount = self.on_change_with_amount()
2015-02-27 11:25:10 +01:00
2020-12-25 17:14:57 +01:00
@fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
2024-07-18 10:12:04 +02:00
@classmethod
def search_summary(cls, name, clause):
return [('description', *clause[1:])]
2022-07-30 11:14:26 +02:00
@fields.depends(
'type', 'quantity', 'unit_price',
'purchase', '_parent_purchase.currency')
2012-09-24 22:18:09 +02:00
def on_change_with_amount(self):
if (self.type == 'line'
and self.quantity is not None
and self.unit_price is not None):
2012-09-24 22:18:09 +02:00
currency = self.purchase.currency if self.purchase else None
amount = Decimal(str(self.quantity)) * self.unit_price
if currency:
2012-09-24 22:18:09 +02:00
return currency.round(amount)
return amount
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)
2012-09-24 22:18:09 +02:00
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)
2012-09-24 22:18:09 +02:00
return amount
2008-05-06 17:45:44 +02:00
@fields.depends('purchase', '_parent_purchase.warehouse')
def on_change_with_warehouse(self, name=None):
2023-03-26 09:49:00 +02:00
return self.purchase.warehouse if self.purchase else None
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
elif self.purchase.return_from_location:
return self.purchase.return_from_location
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
2021-07-12 22:37:21 +02:00
elif self.purchase.party.supplier_location:
return self.purchase.party.supplier_location
2011-11-19 19:34:47 +01:00
2023-11-18 00:24:33 +01:00
@fields.depends('product')
def on_change_with_movable(self, name=None):
pool = Pool()
Move = pool.get('stock.move')
if self.product:
return self.product.type in Move.get_product_types()
@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
@classmethod
def set_delivery_date(cls, lines, name, value):
cls.write([l for l in lines if l.delivery_date_edit], {
'delivery_date_store': value,
})
@fields.depends('delivery_date_edit', 'delivery_date',
methods=['planned_delivery_date'])
def on_change_delivery_date_edit(self):
if not self.delivery_date_edit:
self.delivery_date = 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',
'_parent_purchase.company')
def planned_delivery_date(self):
pool = Pool()
ProductSupplier = pool.get('purchase.product_supplier')
2021-01-19 14:59:54 +01:00
product_supplier = self.product_supplier
if not product_supplier and self.purchase and self.purchase.company:
product_supplier = ProductSupplier(
party=self.purchase.party,
company=self.purchase.company)
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
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
@classmethod
def search_purchase_state(cls, name, clause):
return [('purchase.state', *clause[1:])]
2020-04-25 09:18:21 +02:00
@fields.depends('purchase', '_parent_purchase.company')
def on_change_with_company(self, name=None):
2023-03-26 09:49:00 +02:00
return self.purchase.company if self.purchase else None
2020-04-25 09:18:21 +02:00
@fields.depends('purchase', '_parent_purchase.party')
def on_change_with_supplier(self, name=None):
2023-03-26 09:49:00 +02:00
return self.purchase.party if self.purchase else None
@classmethod
def search_supplier(cls, name, clause):
return [('purchase.party' + clause[0][len(name):], *clause[1:])]
@fields.depends('purchase', '_parent_purchase.purchase_date')
def on_change_with_purchase_date(self, name=None):
if self.purchase:
return self.purchase.purchase_date
@classmethod
def search_purchase_date(cls, name, clause):
return [('purchase.purchase_date', *clause[1:])]
@classmethod
def order_purchase_date(cls, tables):
return cls.purchase.convert_order(
'purchase.purchase_date', tables, cls)
2020-10-09 19:09:05 +02:00
@fields.depends('purchase', '_parent_purchase.currency')
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.purchase.currency if self.purchase else None
2020-10-09 19:09:05 +02:00
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)
2023-08-26 14:59:59 +02:00
if self.type != 'line':
return []
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-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(
invoice_line.quantity)
return [invoice_line]
2008-05-06 17:45:44 +02: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
if (self.purchase.invoice_method in {'order', 'manual'}
2015-12-13 14:57:02 +01:00
or not self.product
or self.product.type == 'service'):
2016-03-20 22:59:22 +01:00
return self.quantity
elif self.purchase.invoice_method == 'fulfillment':
2015-12-13 14:57:02 +01:00
quantity = 0.0
for move in self.moves:
if move.state != 'done':
continue
2023-08-22 13:18:33 +02:00
qty = Uom.compute_qty(move.unit, move.quantity, self.unit)
# Test only against from_location
# as it is what matters for purchase
src_type = 'supplier'
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
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:
if self.unit:
quantity += Uom.compute_qty(
invoice_line.unit or self.unit, invoice_line.quantity,
self.unit)
else:
quantity += invoice_line.quantity
2015-12-13 14:57:02 +01:00
return quantity
def _get_invoice_line_moves(self, quantity):
2015-12-13 14:57:02 +01:00
'Return the stock moves that should be invoiced'
moves = []
if self.purchase.invoice_method in {'order', 'manual'}:
2015-12-13 14:57:02 +01:00
moves.extend(self.moves)
elif (self.purchase.invoice_method == 'fulfillment'
and samesign(self.quantity, quantity)):
2015-12-13 14:57:02 +01:00
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')
Date = pool.get('ir.date')
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))
with Transaction().set_context(company=self.purchase.company.id):
today = Date.today()
2014-09-17 21:45:46 +02:00
move = Move()
2012-09-24 22:18:09 +02:00
move.quantity = quantity
2023-08-22 13:18:33 +02:00
move.unit = self.unit
2012-09-24 22:18:09 +02:00
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
else:
move.unit_price = None
move.currency = None
move.planned_date = self.planned_delivery_date
if move.planned_date and move.planned_date < today:
move.planned_date = None
2015-12-13 14:57:02 +01:00
move.invoice_lines = self._get_move_invoice_lines(move_type)
move.origin = self
move.origin_planned_date = move.planned_date
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:
2023-08-22 13:18:33 +02:00
quantity += Uom.compute_qty(
move.unit, move.quantity, self.unit)
2015-12-13 14:57:02 +01:00
return quantity
def check_move_quantity(self):
pool = Pool()
Lang = pool.get('ir.lang')
Warning = pool.get('res.user.warning')
lang = Lang.get()
move_type = 'in' if self.quantity >= 0 else 'return'
quantity = (
self._get_move_quantity(move_type)
- self._get_shipped_quantity(move_type))
2025-11-25 18:38:51 +01:00
if self.unit.round(quantity) < 0:
warning_name = Warning.format(
'check_move_quantity', [self])
if Warning.check(warning_name):
raise PurchaseMoveQuantity(warning_name, gettext(
'purchase.msg_purchase_line_move_quantity',
line=self.rec_name,
extra=lang.format_number_symbol(
-quantity, self.unit),
quantity=lang.format_number_symbol(
self.quantity, self.unit)))
2015-12-13 14:57:02 +01:00
def _get_move_invoice_lines(self, move_type):
'Return the invoice lines that should be shipped'
if self.purchase.invoice_method in {'order', 'manual'}:
lines = self.invoice_lines
else:
lines = filter(lambda l: not l.stock_moves, self.invoice_lines)
lines = filter(
lambda l: samesign(self.quantity, l.quantity), lines)
return list(lines)
2015-12-13 14:57:02 +01:00
def set_quantities(self):
2020-04-14 09:27:53 +02:00
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(
2023-08-22 13:18:33 +02:00
move.unit, 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:
if (not invoice_line.invoice
or invoice_line.invoice.state != 'cancelled'):
if self.unit:
invoiced_quantity += Uom.compute_qty(
invoice_line.unit or self.unit, invoice_line.quantity,
self.unit, round=False)
else:
invoiced_quantity += invoice_line.quantity
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
quantity_to_invoice = (
self._get_invoice_line_quantity()
- self._get_invoiced_quantity())
if self.unit:
quantity_to_invoice = self.unit.round(quantity_to_invoice)
if self.quantity_to_invoice != quantity_to_invoice:
self.quantity_to_invoice = quantity_to_invoice
def get_rec_name(self, name):
2019-11-26 22:20:24 +01:00
pool = Pool()
Lang = pool.get('ir.lang')
name = self.product.rec_name if self.product else self.summary
if self.type == 'line':
2019-11-26 22:20:24 +01:00
lang = Lang.get()
if self.unit:
quantity = lang.format_number_symbol(
self.quantity or 0, self.unit, digits=self.unit.digits)
else:
quantity = lang.format_number(self.quantity or 0)
if name:
name = f'{quantity} {name}'
else:
name = quantity
elif not name:
name = f'({self.id})'
return f'{name} @ {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:]),
('summary', *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 check_modification(cls, mode, lines, values=None, external=False):
super().check_modification(
mode, lines, values=values, external=external)
if mode == 'create':
for line in lines:
if line.purchase.state != 'draft':
raise AccessError(gettext(
'purchase.msg_purchase_line_create_draft',
purchase=line.purchase.rec_name))
elif mode == 'delete':
for line in lines:
if line.purchase.state not in {'cancelled', 'draft'}:
raise AccessError(gettext(
'purchase.msg_purchase_line_delete_cancel_draft',
line=line.rec_name,
purchase=line.purchase.rec_name))
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')
default.setdefault('quantity_to_invoice')
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):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line-account.tax'
line = fields.Many2One(
'purchase.line', "Purchase Line", ondelete='CASCADE', required=True,
domain=[('type', '=', 'line')])
tax = fields.Many2One(
'account.tax', "Tax", ondelete='RESTRICT', required=True,
domain=[('parent', '=', None)])
2009-03-05 14:53:03 +01:00
2021-10-21 00:03:36 +02:00
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints += [
('line_tax_unique', Unique(t, t.line, t.tax),
'purchase.msg_purchase_line_tax_unique'),
]
2023-12-26 12:18:00 +01:00
@classmethod
def __register__(cls, module):
# Migration from 7.0: rename to standard name
backend.TableHandler.table_rename(
'purchase_line_account_tax', cls._table)
super().__register__(module)
2009-03-05 14:53:03 +01:00
2020-03-01 12:33:53 +01:00
class LineIgnoredMove(ModelSQL):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line-ignored-stock.move'
purchase_line = fields.Many2One(
'purchase.line', "Purchase Line", ondelete='CASCADE', required=True)
move = fields.Many2One(
2024-06-03 10:48:34 +02:00
'stock.move', "Stock Move", ondelete='RESTRICT', required=True,
domain=[
('origin.id', '=', Eval('purchase_line', -1), 'purchase.line'),
('state', '=', 'cancelled'),
])
2009-03-05 14:53:03 +01:00
2023-12-26 12:18:00 +01:00
@classmethod
def __register__(cls, module):
# Migration from 7.0: rename to standard name
backend.TableHandler.table_rename(
'purchase_line_moves_ignored_rel', cls._table)
super().__register__(module)
2009-03-05 14:53:03 +01:00
2020-03-01 12:33:53 +01:00
class LineRecreatedMove(ModelSQL):
2012-09-24 22:18:09 +02:00
__name__ = 'purchase.line-recreated-stock.move'
purchase_line = fields.Many2One(
'purchase.line', "Purchase Line", ondelete='CASCADE', required=True)
move = fields.Many2One(
2024-06-03 10:48:34 +02:00
'stock.move', "Stock Move", ondelete='RESTRICT', required=True,
domain=[
('origin.id', '=', Eval('purchase_line', -1), 'purchase.line'),
('state', '=', 'cancelled'),
])
2009-03-05 14:53:03 +01:00
2023-12-26 12:18:00 +01:00
@classmethod
def __register__(cls, module):
# Migration from 7.0: rename to standard name
backend.TableHandler.table_rename(
'purchase_line_moves_recreated_rel', cls._table)
super().__register__(module)
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):
2025-04-04 00:45:18 +02:00
return super().execute(ids, data)
2016-09-06 09:24:41 +02:00
@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)
company = header.get('company')
with Transaction().set_context(
company=company.id if company else None):
context['today'] = Date.today()
return context
2008-05-13 18:42:46 +02:00
class HandleShipmentExceptionAsk(ModelView):
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(
2024-08-21 17:33:14 +02:00
'stock.move', None, None, "Stock Moves to Recreate",
domain=[
('id', 'in', Eval('domain_moves', [])),
('id', 'not in', Eval('ignore_moves', [])),
],
help="The selected cancelled stock moves will be recreated.")
ignore_moves = fields.Many2Many(
'stock.move', None, None, "Stock Moves to Ignore",
domain=[
('id', 'in', Eval('domain_moves', [])),
('id', 'not in', Eval('recreate_moves', [])),
],
help="The selected cancelled stock moves will be ignored.")
domain_moves = fields.Many2Many(
2024-06-03 10:48:34 +02:00
'stock.move', None, None, 'Domain Stock Moves')
2012-04-03 20:07:31 +02:00
class HandleShipmentException(Wizard):
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 {
'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')
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:
2024-08-21 17:33:14 +02:00
if move not in self.ask.domain_moves or move in skip:
continue
2024-08-21 17:33:14 +02:00
if move in self.ask.recreate_moves:
2009-02-23 18:53:30 +01:00
moves_recreated.append(move.id)
2024-08-21 17:33:14 +02:00
elif move in self.ask.ignore_moves:
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-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(
2024-08-21 17:33:14 +02:00
'account.invoice', None, None, "Invoices to Recreate",
domain=[
('id', 'in', Eval('domain_invoices', [])),
('id', 'not in', Eval('ignore_invoices', [])),
],
help="The selected cancelled invoices will be recreated.")
ignore_invoices = fields.Many2Many(
'account.invoice', None, None, "Invoices to Ignore",
domain=[
('id', 'in', Eval('domain_invoices', [])),
('id', 'not in', Eval('recreate_invoices', [])),
],
help="The selected cancelled invoices 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):
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 {
'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)
2024-08-21 17:33:14 +02:00
elif invoice in self.ask.ignore_invoices:
2009-02-09 16:33:10 +01:00
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):
2025-04-04 00:45:18 +02:00
return super().get_view(
wizard, state_name)
2022-10-01 14:45:59 +02:00
def get_defaults(self, wizard, state_name, fields):
return {}
class ModifyHeader(Wizard):
__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
2022-10-01 14:45:59 +02:00
def value_start(self, fields):
purchase = self.get_purchase()
2022-10-01 14:45:59 +02:00
values = {}
for fieldname in fields:
2024-11-20 12:08:20 +01:00
values[fieldname] = getattr(purchase, fieldname)
# Mimic an empty sale in draft state to get the fields' states right
2022-10-01 14:45:59 +02:00
values['lines'] = []
return values
def transition_modify(self):
pool = Pool()
Line = pool.get('purchase.line')
purchase = self.get_purchase()
values = self.start._save_values()
self.model.write([purchase], values)
self.model.log([purchase], 'write', ','.join(sorted(values.keys())))
# 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):
__name__ = 'purchase.return_purchase.start'
class ReturnPurchase(Wizard):
__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
2023-03-12 22:48:27 +01:00
return_purchases = self.model.copy(purchases, default={
'origin': lambda data: (
'%s,%s' % (self.model.__name__, data['id'])),
'lines.quantity': lambda data: (
data['quantity'] * -1 if data['type'] == 'line'
else data['quantity']),
})
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