Files
tryton/modules/account/party.py
T

359 lines
13 KiB
Python
Raw Normal View History

2014-12-02 12:54:29 +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.
from collections import defaultdict
2010-08-19 22:02:00 +02:00
from decimal import Decimal
2018-04-30 15:26:03 +02:00
from sql import Literal, Null
2013-09-06 22:33:47 +02:00
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from trytond import backend
2018-12-29 14:20:28 +01:00
from trytond.i18n import gettext
2017-04-01 00:25:49 +02:00
from trytond.model import ModelSQL, fields
from trytond.modules.company.model import (
CompanyMultiValueMixin, CompanyValueMixin)
2021-08-21 09:09:55 +02:00
from trytond.modules.currency.fields import Monetary
2021-12-11 17:58:41 +01:00
from trytond.modules.party.exceptions import EraseError
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If
2026-03-22 21:05:26 +01:00
from trytond.tools import sqlite_apply_types
from trytond.tools import timezone as tz
2021-12-11 17:58:41 +01:00
from trytond.transaction import Transaction
2018-12-29 14:20:28 +01:00
from .exceptions import AccountMissing
2012-09-24 22:17:29 +02:00
2017-04-01 00:25:49 +02:00
account_names = [
'account_payable', 'account_receivable',
'customer_tax_rule', 'supplier_tax_rule']
2018-06-26 19:11:40 +02:00
class Party(CompanyMultiValueMixin, metaclass=PoolMeta):
2012-09-24 22:17:29 +02:00
__name__ = 'party.party'
2017-04-01 00:25:49 +02:00
accounts = fields.One2Many('party.party.account', 'party', "Accounts")
account_payable = fields.MultiValue(fields.Many2One(
'account.account', "Account Payable",
domain=[
2019-12-28 17:48:50 +01:00
('closed', '!=', True),
2019-03-24 18:00:01 +01:00
('type.payable', '=', True),
('party_required', '=', True),
2013-10-09 18:25:04 +02:00
('company', '=', Eval('context', {}).get('company', -1)),
2011-08-04 16:57:14 +02:00
],
states={
2011-08-21 11:20:29 +02:00
'invisible': ~Eval('context', {}).get('company'),
2011-08-04 16:57:14 +02:00
}))
2017-04-01 00:25:49 +02:00
account_receivable = fields.MultiValue(fields.Many2One(
'account.account', "Account Receivable",
domain=[
2019-12-28 17:48:50 +01:00
('closed', '!=', True),
2019-03-24 18:00:01 +01:00
('type.receivable', '=', True),
('party_required', '=', True),
2013-10-09 18:25:04 +02:00
('company', '=', Eval('context', {}).get('company', -1)),
2011-08-04 16:57:14 +02:00
],
states={
2011-08-21 11:20:29 +02:00
'invisible': ~Eval('context', {}).get('company'),
2011-08-04 16:57:14 +02:00
}))
2017-04-01 00:25:49 +02:00
customer_tax_rule = fields.MultiValue(fields.Many2One(
'account.tax.rule', "Customer Tax Rule",
domain=[
2013-10-09 18:25:04 +02:00
('company', '=', Eval('context', {}).get('company', -1)),
('kind', 'in', ['sale', 'both']),
],
2011-08-04 16:57:14 +02:00
states={
2011-08-21 11:20:29 +02:00
'invisible': ~Eval('context', {}).get('company'),
2011-08-04 16:57:14 +02:00
}, help='Apply this rule on taxes when party is customer.'))
2017-04-01 00:25:49 +02:00
supplier_tax_rule = fields.MultiValue(fields.Many2One(
'account.tax.rule', "Supplier Tax Rule",
domain=[
2013-10-09 18:25:04 +02:00
('company', '=', Eval('context', {}).get('company', -1)),
('kind', 'in', ['purchase', 'both']),
],
2011-08-04 16:57:14 +02:00
states={
2011-08-21 11:20:29 +02:00
'invisible': ~Eval('context', {}).get('company'),
2011-08-04 16:57:14 +02:00
}, help='Apply this rule on taxes when party is supplier.'))
2021-08-21 09:09:55 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"), 'get_currency')
receivable = fields.Function(Monetary(
"Receivable", currency='currency', digits='currency'),
'get_receivable_payable', searcher='search_receivable_payable')
payable = fields.Function(Monetary(
"Payable", currency='currency', digits='currency'),
'get_receivable_payable', searcher='search_receivable_payable')
receivable_today = fields.Function(Monetary(
"Receivable Today", currency='currency', digits='currency'),
'get_receivable_payable', searcher='search_receivable_payable')
payable_today = fields.Function(Monetary(
"Payable Today", currency='currency', digits='currency'),
'get_receivable_payable', searcher='search_receivable_payable')
2017-04-01 00:25:49 +02:00
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field in account_names:
return pool.get('party.party.account')
2025-04-04 00:45:18 +02:00
return super().multivalue_model(field)
2017-04-01 00:25:49 +02:00
2019-08-18 19:06:19 +02:00
@classmethod
def _default_tax_rule(cls, type_, **pattern):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
assert type_ in {'customer', 'supplier'}
tax_rule = config.get_multivalue(
'default_%s_tax_rule' % type_, **pattern)
return tax_rule.id if tax_rule else None
@classmethod
def default_customer_tax_rule(cls, **pattern):
return cls._default_tax_rule('customer', **pattern)
@classmethod
def default_supplier_tax_rule(cls, **pattern):
return cls._default_tax_rule('supplier', **pattern)
2021-08-21 09:09:55 +02:00
def get_currency(self, name):
pool = Pool()
Company = pool.get('company.company')
company_id = Transaction().context.get('company')
if company_id is not None and company_id >= 0:
company = Company(company_id)
return company.currency
2012-09-24 22:17:29 +02:00
@classmethod
def _receivable_payable_query(cls, names, company, parties=None):
2011-07-03 13:18:40 +02:00
pool = Pool()
2013-09-06 22:33:47 +02:00
Account = pool.get('account.account')
2019-03-24 18:00:01 +01:00
AccountType = pool.get('account.account.type')
2012-09-24 22:17:29 +02:00
Date = pool.get('ir.date')
Move = pool.get('account.move')
MoveLine = pool.get('account.move.line')
transaction = Transaction()
context = transaction.context
2013-09-06 22:33:47 +02:00
account = Account.__table__()
2019-03-24 18:00:01 +01:00
account_type = AccountType.__table__()
move = Move.__table__()
move_line = MoveLine.__table__()
2013-09-06 22:33:47 +02:00
if date := context.get('date'):
today = date
elif datetime := context.get('_datetime'):
if company.timezone:
timezone = tz.ZoneInfo(company.timezone)
datetime = datetime.astimezone(timezone)
today = datetime.date()
else:
with transaction.set_context(company=company.id):
today = Date.today()
date = Coalesce(move_line.maturity_date, move.date)
expressions = {
'receivable': Sum(
move_line.debit - move_line.credit,
filter_=account_type.receivable),
'receivable_today': Sum(
move_line.debit - move_line.credit,
filter_=account_type.receivable & (date <= today)),
'payable': Sum(
move_line.debit - move_line.credit,
filter_=account_type.payable),
'payable_today': Sum(
move_line.debit - move_line.credit,
filter_=account_type.payable & (date <= today)),
}
columns = []
for name in names:
columns.append(Coalesce(expressions[name], Decimal()).as_(name))
if parties is not None:
party_where = fields.SQL_OPERATORS['in'](
move_line.party, [p.id for p in parties])
else:
party_where = Literal(True)
query = (
move_line
.join(move, condition=move_line.move == move.id)
.join(account, condition=move_line.account == account.id)
.join(account_type, condition=account.type == account_type.id)
.select(
move_line.party.as_('party'),
*columns,
where=(
(move_line.reconciliation == Null)
& (account.company == company.id)
& party_where),
group_by=move_line.party))
return query
@classmethod
def get_receivable_payable(cls, parties, names):
pool = Pool()
User = pool.get('res.user')
transaction = Transaction()
cursor = transaction.connection.cursor()
amounts = {name: defaultdict(Decimal) for name in names}
user = User(transaction.user)
2012-12-05 13:43:06 +01:00
if not user.company:
return amounts
company = user.company
currency = company.currency
2026-03-22 21:05:26 +01:00
query = cls._receivable_payable_query(names, company, parties)
if backend.name == 'sqlite':
sqlite_apply_types(query, [None] + ['NUMERIC'] * 4)
cursor.execute(*query)
for party_id, *values in cursor:
for name, value in zip(names, values):
amounts[name][party_id] = currency.round(value)
return amounts
2012-09-24 22:17:29 +02:00
@classmethod
def search_receivable_payable(cls, name, clause):
2011-07-03 13:18:40 +02:00
pool = Pool()
2012-09-24 22:17:29 +02:00
User = pool.get('res.user')
MoveLine = pool.get('account.move.line')
transaction = Transaction()
user = User(transaction.user)
2014-12-20 19:12:19 +01:00
if not user.company:
return [('id', '=', -1)]
company = user.company
query = cls._receivable_payable_query([name], company)
columns = list(query.columns)
amount = columns.pop(1).expression
_, operator, value = clause
Operator = fields.SQL_OPERATORS[operator]
if backend.name == 'sqlite':
cast_ = MoveLine.debit.sql_cast
if operator in {'in', 'not in'}:
value = [cast_(Literal(Decimal(v or 0))) for v in value]
else:
value = cast_(Literal(Decimal(value or 0)))
amount = cast_(amount)
query.having = Operator(amount, value)
query.columns = columns
2013-09-06 22:33:47 +02:00
return [('id', 'in', query)]
2016-10-02 15:19:34 +02:00
@property
def account_payable_used(self):
pool = Pool()
Configuration = pool.get('account.configuration')
account = self.account_payable
if not account:
2021-04-03 14:27:09 +02:00
with Transaction().set_context(self._context):
config = Configuration(1)
account = config.get_multivalue('default_account_payable')
# Allow empty values on on_change
if not account and not Transaction().readonly:
2018-12-29 14:20:28 +01:00
raise AccountMissing(
gettext('account.msg_party_missing_payable_account',
party=self.rec_name))
2018-08-31 14:58:05 +02:00
if account:
return account.current()
@property
def account_receivable_used(self):
pool = Pool()
Configuration = pool.get('account.configuration')
account = self.account_receivable
if not account:
2021-04-03 14:27:09 +02:00
with Transaction().set_context(self._context):
config = Configuration(1)
account = config.get_multivalue('default_account_receivable')
# Allow empty values on on_change
if not account and not Transaction().readonly:
2018-12-29 14:20:28 +01:00
raise AccountMissing(
gettext('account.msg_party_missing_receivable_account',
party=self.rec_name))
2018-08-31 14:58:05 +02:00
if account:
return account.current()
2019-09-20 11:47:15 +02:00
@classmethod
def view_attributes(cls):
2021-04-09 10:52:03 +02:00
return super().view_attributes() + [
2019-09-20 11:47:15 +02:00
('/tree/field[@name="receivable_today"]',
'visual', If(Eval('receivable_today', 0) > 0, 'danger', '')),
('/tree/field[@name="payable_today"]',
'visual', If(Eval('payable_today', 0) < 0, 'warning', '')),
]
2021-10-11 15:35:46 +02:00
@classmethod
def copy(cls, parties, default=None):
default = default.copy() if default else {}
if Transaction().check_access:
2021-10-11 15:35:46 +02:00
fields = [
'accounts',
'account_payable', 'account_receivable',
'customer_tax_rule', 'supplier_tax_rule']
default_values = cls.default_get(fields, with_rec_name=False)
for fname in fields:
default.setdefault(fname, default_values.get(fname))
return super().copy(parties, default=default)
2016-10-02 15:19:34 +02:00
2017-04-01 00:25:49 +02:00
class PartyAccount(ModelSQL, CompanyValueMixin):
__name__ = 'party.party.account'
party = fields.Many2One(
'party.party', "Party", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
2017-04-01 00:25:49 +02:00
account_payable = fields.Many2One(
'account.account', "Account Payable",
domain=[
2019-03-24 18:00:01 +01:00
('type.payable', '=', True),
('party_required', '=', True),
2017-04-01 00:25:49 +02:00
('company', '=', Eval('company', -1)),
],
ondelete='RESTRICT')
2017-04-01 00:25:49 +02:00
account_receivable = fields.Many2One(
'account.account', "Account Receivable",
domain=[
2019-03-24 18:00:01 +01:00
('type.receivable', '=', True),
('party_required', '=', True),
2017-04-01 00:25:49 +02:00
('company', '=', Eval('company', -1)),
],
ondelete='RESTRICT')
2017-04-01 00:25:49 +02:00
customer_tax_rule = fields.Many2One(
'account.tax.rule', "Customer Tax Rule",
domain=[
('company', '=', Eval('company', -1)),
('kind', 'in', ['sale', 'both']),
],
ondelete='RESTRICT')
2017-04-01 00:25:49 +02:00
supplier_tax_rule = fields.Many2One(
'account.tax.rule', "Supplier Tax Rule",
domain=[
('company', '=', Eval('company', -1)),
('kind', 'in', ['purchase', 'both']),
],
ondelete='RESTRICT')
2017-04-01 00:25:49 +02:00
2018-06-26 19:11:40 +02:00
class PartyReplace(metaclass=PoolMeta):
2016-10-02 15:19:34 +02:00
__name__ = 'party.replace'
@classmethod
def fields_to_replace(cls):
2025-04-04 00:45:18 +02:00
return super().fields_to_replace() + [
2016-10-02 15:19:34 +02:00
('account.move.line', 'party'),
]
2018-03-24 00:04:07 +01:00
2018-06-26 19:11:40 +02:00
class PartyErase(metaclass=PoolMeta):
2018-03-24 00:04:07 +01:00
__name__ = 'party.erase'
def check_erase_company(self, party, company):
2025-04-04 00:45:18 +02:00
super().check_erase_company(party, company)
2018-03-24 00:04:07 +01:00
if party.receivable or party.payable:
2018-12-29 14:20:28 +01:00
raise EraseError(
gettext('account.msg_erase_party_receivable_payable',
party=party.rec_name,
company=company.rec_name))