Author SHA1 Message Date
tryton-sync 4e7d12a621 Merge remote-tracking branch 'upstream/8.0' into automation/upstream-8.0 2026-10-15 00:37:18 +00:00
Cédric Krier 3ea0c2a04e Set the default cache timeout to 300 seconds as stated in the documentation
Closes #15160
(grafted from eda9f5c7918eeaf17f97bc7c75c50dedef9964ae)

--HG--
branch : 8.0
2026-10-12 17:40:15 +02:00
Cédric Krier e35702038c Sum internal quantity to calculate the stock inventory reporting
The quantity is in the unit of the stock move which is not always the same.

Closes #15166
(grafted from fb72b8d1cd1d3181935e6aff4e92dc4853af9ca9)

--HG--
branch : 8.0
2026-10-13 18:53:13 +02:00
Cédric Krier 8237437e53 Convert only Number instances to number when evaluate domain for inversion
Closes #15159
(grafted from 88cc5493f11bb7dc50026f87e34b416812eae666)

--HG--
branch : 8.0
2026-10-12 12:42:56 +02:00
Cédric Krier 082564ceda Convert account move line check_modification into on_modification
The method is calling validate_move which test the value in the database but
check_modification is called before the update of the database for write.
But on_modification is always called after the database modification.

Closes #15157
(grafted from a4629adf43a5dbf915a7eb031ae01e5763004396)

--HG--
branch : 8.0
2026-10-09 23:28:16 +02:00
Cédric Krier a9448b7ec8 Cast currency rounding when comparing against sum of debit - credit for SQLite
The Decimal value is adapted as string for SQLite backend so without column
affinity to trigger conversion, SQLite compares REAL against TEXT. And for
SQLite's comparison ordering, numeric values sort before text values.

Closes #15158
(grafted from 5c67a5358f10ebc9192f680acdc9e61b976719fc)

--HG--
branch : 8.0
2026-10-09 23:25:45 +02:00
Nicolas Évrard 37c0dd9d7c Early exits cache synchronization call when initializing the Pool
As threads could be launched when clean_timeout is 0, preforking WSGI could
unfortunately fork while in the cache listening thread making the server
unresponsive.

Closes #15120
(grafted from 69857991edf1f6dd9d0b535f7c0abe861c2b0c7f)

--HG--
branch : 8.0
2026-09-23 10:41:33 +02:00
Nicolas Évrard 05af3a2056 Turn the assert on the threads after Pool initialization into an exception
People running with PYTHONOPTIMIZE set deserve to know that something is going
wrong
(grafted from 261e0c174ddf435fda240d13ae518cc961a47e8a)

--HG--
branch : 8.0
2026-09-23 10:50:27 +02:00
Cédric Krier fa125204a7 Allow to create any action from the UI
The action field must not be used to create the main action and the inherited
fields from the parent action must not be set a second time.

Closes #15149
(grafted from 08c5c82cd1e2cb19a8877b706803dd03481863eb)

--HG--
branch : 8.0
2026-10-05 18:21:13 +02:00
Cédric Krier ca1a608c13 Catch all SubprocessError and OSError when converting report
The subprocess run may raises one of those exceptions.

Closes #15141
(grafted from 2dfbdcb8e6969bbf8d2021dab131386974cc5515)

--HG--
branch : 8.0
2026-10-02 23:00:51 +02:00
Cédric Krier 24f805356d Test if parent is not null before testing classList
Closes #15152
(grafted from 45cf3086a1f63732e3298845a1fa183375ec76b4)

--HG--
branch : 8.0
2026-10-06 13:12:24 +02:00
Nicolas Évrard 407e6b9a7f Do not go further than the modal window when computing the scrollable ancestors
Closes #15143
(grafted from ba1fb062e5bc109884cb8af66afd4c4de9e52037)

--HG--
branch : 8.0
2026-10-02 16:07:28 +02:00
Cédric Krier 2ff8d421a5 Fallback to 0 when cost price is empty for product of production
Closes #15138
(grafted from aaa87c6471b33c02922121d6f8849d2759a4208f)

--HG--
branch : 8.0
2026-10-02 11:11:48 +02:00
Cédric Krier 3f72a62365 Support phantom input and output when setting moves to production
Closes #14908
(grafted from 38aba6163a892562f07fdfce1ac10901638ca8c7)

--HG--
branch : 8.0
2026-10-02 10:48:48 +02:00
Cédric Krier 07aaf4393a Validate stdnum code before compacting and formatting it
Closes #15017
(grafted from 7d7d0ccfb5ab052e3a83b8d33c581d41dcf9eb38)

--HG--
branch : 8.0
2026-09-30 18:30:11 +02:00
Cédric Krier 072cd3b91f Show price and currency fields on stock move when they are not required but still filled
This is to let the user clear the invalid fields from past moves.

Closes #14919
(grafted from d5ace112f8a9315a6740ef75c2bda4cc8b94b90e)

--HG--
branch : 8.0
2026-10-01 09:54:29 +02:00
Cédric Krier 175cfc21b5 Use total package weight and volume as shipment weight for PDP
The shipment weight must not be less than the sum of parcel weights.

Closes #15121
(grafted from ed5fc0ff2cf3a191335821306d91349285393bb7)

--HG--
branch : 8.0
2026-10-01 15:30:57 +02:00
Cédric Krier 830d337d15 Do not normalize Decimal value in XML document
Some third party expect to always have the trailing 0 in the format of currency
amount. So the amount coming from the database must already have the right
precision to format.

Closes #15085
(grafted from 0ace319afd0fdf51ae4d6df4f6e1a39fc9005525)

--HG--
branch : 8.0
2026-09-28 11:26:56 +02:00
Nicolas Évrard 5d93555c35 Compute the minimal width of a treeview on display as it's done when parsing
Closes #15094
(grafted from 38edbf96602b3d3cc7f7fcea94d5154086513120)

--HG--
branch : 8.0
2026-10-01 15:32:58 +02:00
29 changed files with 346 additions and 141 deletions
+9 -9
View File
@@ -355,9 +355,11 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
'''
pool = Pool()
MoveLine = pool.get('account.move.line')
Currency = pool.get('currency.currency')
line = MoveLine.__table__()
transaction = Transaction()
cursor = transaction.connection.cursor()
cast = Currency.rounding.sql_cast
for company, moves in groupby(moves, key=lambda m: m.company):
currency = company.currency
@@ -370,7 +372,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) < abs(currency.rounding))
currency.digits)) < cast(abs(currency.rounding)))
cursor.execute(*line.update(
[line.state],
['valid'],
@@ -382,7 +384,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) >= abs(currency.rounding))
currency.digits)) >= cast(abs(currency.rounding)))
cursor.execute(*line.update(
[line.state],
['draft'],
@@ -1444,16 +1446,13 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
}])
@classmethod
def check_modification(cls, mode, lines, values=None, external=False):
def on_modification(cls, mode, lines, field_names=None):
pool = Pool()
Move = pool.get('account.move')
super().check_modification(
mode, lines, values=values, external=external)
super().on_modification(mode, lines, field_names=field_names)
if (mode in {'create', 'delete'}
or (mode == 'write'
and values.keys() - cls._check_modify_exclude)):
if field_names is None or (field_names - cls._check_modify_exclude):
journal_period_done = set()
for line in lines:
if line.move.state == 'posted':
@@ -1474,7 +1473,8 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
'account.msg_delete_line_reconciled',
line=line.rec_name))
if (mode == 'write'
and values.keys() & cls._reconciliation_modify_disallow):
and field_names is not None
and field_names & cls._reconciliation_modify_disallow):
for line in lines:
if line.reconciliation:
raise AccessError(gettext(
+63
View File
@@ -767,6 +767,69 @@ class AccountTestCase(
]
move.save()
@with_transaction()
def test_move_validation(self):
"Test move validation"
pool = Pool()
Party = pool.get('party.party')
FiscalYear = pool.get('account.fiscalyear')
Journal = pool.get('account.journal')
Account = pool.get('account.account')
Move = pool.get('account.move')
Line = pool.get('account.move.line')
party = Party(name='Party')
party.save()
company = create_company()
with set_company(company):
fiscalyear = get_fiscalyear(company)
fiscalyear.save()
FiscalYear.create_period([fiscalyear])
period = fiscalyear.periods[0]
create_chart(company)
journal_revenue, = Journal.search([
('code', '=', 'REV'),
])
revenue, = Account.search([
('type.revenue', '=', True),
('closed', '=', False),
], limit=1)
receivable, = Account.search([
('type.receivable', '=', True),
('closed', '=', False),
], limit=1)
move = Move()
move.period = period
move.journal = journal_revenue
move.date = period.start_date
move.lines = [
Line(account=revenue, credit=Decimal(100)),
]
move.save()
line, = move.lines
self.assertEqual(line.state, 'draft')
move.lines += (
Line(account=receivable, debit=Decimal(100), party=party),
)
move.save()
self.assertEqual([l.state for l in move.lines], ['valid', 'valid'])
line = move.lines[-1]
line.credit = Decimal(50)
line.save()
self.assertEqual(line.state, 'draft')
line.credit = Decimal(100)
line.save()
self.assertEqual(line.state, 'valid')
@with_transaction()
def test_find_best_reconciliation(self):
"Test find best reconciliation"
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -45,7 +45,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -77,7 +77,7 @@ this repository contains the full copyright notices and license terms. -->
</PmtId>
<!-- PmtTpInf --> <!-- EPC only at payment information level -->
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- EqvtAmt -->
</Amt>
<!-- XchgRateInf -->
@@ -13,7 +13,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -26,7 +26,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -59,7 +59,7 @@ this repository contains the full copyright notices and license terms. -->
</PmtId>
<!-- PmtTpInf --> <!-- EPC only at payment information level -->
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- EqvtAmt -->
</Amt>
<!-- XchgRateInf -->
@@ -15,7 +15,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party, id=group.journal.sepa_payable_initiator_id, with_address=False)}
</InitgPty>
@@ -25,7 +25,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -52,7 +52,7 @@ this repository contains the full copyright notices and license terms. -->
</PmtId>
<!-- PmtTpInf --> <!-- EPC only at payment information level -->
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
</Amt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<!-- UltmtDbtr -->
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -45,7 +45,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -83,7 +83,7 @@ this repository contains the full copyright notices and license terms. -->
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<!-- PmtTpInf -->
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
@@ -13,7 +13,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${len(group.payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -26,7 +26,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -64,7 +64,7 @@ this repository contains the full copyright notices and license terms. -->
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<!-- PmtTpInf -->
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
@@ -15,7 +15,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party, id=group.journal.sepa_receivable_initiator_id, with_address=False)}
</InitgPty>
@@ -25,7 +25,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
@@ -56,7 +56,7 @@ this repository contains the full copyright notices and license terms. -->
<InstrId>${payment.sepa_instruction_id}</InstrId>
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
@@ -26,7 +26,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party)}
</InitgPty>
@@ -36,7 +36,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -62,7 +62,7 @@ this repository contains the full copyright notices and license terms. -->
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- EqvtAmt -->
</Amt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party)}
</InitgPty>
@@ -42,7 +42,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
@@ -73,7 +73,7 @@ this repository contains the full copyright notices and license terms. -->
<InstrId>${payment.sepa_instruction_id}</InstrId>
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
<MndtId>${mandate.identification}</MndtId>
+13 -13
View File
@@ -90,7 +90,7 @@ this repository contains the full copyright notices and license terms. -->
<py:otherwise>
<cac:PaymentTerms py:for="line in this.invoice.lines_to_pay">
<cbc:Note py:if="this.invoice.payment_term and this.invoice.payment_term.description">${this.invoice.payment_term.description}</cbc:Note>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:Amount>
<cbc:PaymentDueDate>${line.maturity_date}</cbc:PaymentDueDate>
</cac:PaymentTerms>
</py:otherwise>
@@ -101,16 +101,16 @@ this repository contains the full copyright notices and license terms. -->
<cbc:AllowanceChargeReasonCode py:if="reason_code">${reason_code}</cbc:AllowanceChargeReasonCode>
</py:with>
<cbc:AllowanceChargeReason>${allowance_charge.product.name}</cbc:AllowanceChargeReason>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount).normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount), 'f')}</cbc:Amount>
<cac:TaxCategory py:for="tax_line in allowance_charge.invoice_taxes">
${TaxCategory(tax_line.tax)}
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal py:for="group, lines, amount in this.taxes">
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-amount).normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-amount, 'f')}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.base).normalize(), 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.amount).normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.base, 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.amount, 'f')}</cbc:TaxAmount>
<cbc:Percent py:if="line.tax.type == 'percentage' and not (specification or '').startswith('peppol')">${format((line.tax.rate * 100).normalize(), 'f')}</cbc:Percent>
<cac:TaxCategory>
${TaxCategory(line.tax)}
@@ -118,19 +118,19 @@ this repository contains the full copyright notices and license terms. -->
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.invoice.untaxed_amount).normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.invoice.untaxed_amount).normalize(), 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-(this.invoice.untaxed_amount + this.invoice.tax_amount)).normalize(), 'f')}</cbc:TaxInclusiveAmount>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.invoice.untaxed_amount, 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.invoice.untaxed_amount, 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-(this.invoice.untaxed_amount + this.invoice.tax_amount), 'f')}</cbc:TaxInclusiveAmount>
<py:if test="this.invoice.total_amount != (this.invoice.untaxed_amount + this.invoice.tax_amount)">
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-(this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount)).normalize(), 'f')}</cbc:PayableRoundingAmount>
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-(this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount), 'f')}</cbc:PayableRoundingAmount>
</py:if>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.prepaid_amount).normalize(), 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.invoice.total_amount + this.prepaid_amount).normalize(), 'f')}</cbc:PayableAmount>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.prepaid_amount, 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.invoice.total_amount + this.prepaid_amount, 'f')}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:CreditNoteLine py:for="line_id, line in enumerate(this.lines, 1)">
<cbc:ID>${line_id}</cbc:ID>
<cbc:CreditedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(-Decimal(str(line.quantity)), 'f')}</cbc:CreditedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.amount).normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:CreditedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(-Decimal(str(line.quantity)).normalize(), 'f')}</cbc:CreditedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.amount, 'f')}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description py:if="line.description">${line.description}</cbc:Description>
<cbc:Name>${line.product.name if line.product else line.summary}</cbc:Name>
+13 -13
View File
@@ -90,7 +90,7 @@ this repository contains the full copyright notices and license terms. -->
<py:otherwise>
<cac:PaymentTerms py:for="line in this.invoice.lines_to_pay">
<cbc:Note py:if="this.invoice.payment_term and this.invoice.payment_term.description">${this.invoice.payment_term.description}</cbc:Note>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:Amount>
<cbc:PaymentDueDate>${line.maturity_date}</cbc:PaymentDueDate>
</cac:PaymentTerms>
</py:otherwise>
@@ -101,16 +101,16 @@ this repository contains the full copyright notices and license terms. -->
<cbc:AllowanceChargeReasonCode py:if="reason_code">${reason_code}</cbc:AllowanceChargeReasonCode>
</py:with>
<cbc:AllowanceChargeReason>${allowance_charge.product.name}</cbc:AllowanceChargeReason>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount).normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount), 'f')}</cbc:Amount>
<cac:TaxCategory py:for="tax_line in allowance_charge.invoice_taxes">
${TaxCategory(tax_line.tax)}
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal py:for="group, lines, amount in this.taxes">
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount.normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount, 'f')}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.base.normalize(), 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.base, 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:TaxAmount>
<cbc:Percent py:if="line.tax.type == 'percentage' and not (specification or '').startswith('peppol')">${format((line.tax.rate * 100).normalize(), 'f')}</cbc:Percent>
<cac:TaxCategory>
${TaxCategory(line.tax)}
@@ -118,19 +118,19 @@ this repository contains the full copyright notices and license terms. -->
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount.normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount.normalize(), 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount + this.invoice.tax_amount).normalize(), 'f')}</cbc:TaxInclusiveAmount>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount, 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount, 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount + this.invoice.tax_amount), 'f')}</cbc:TaxInclusiveAmount>
<py:if test="this.invoice.total_amount != (this.invoice.untaxed_amount + this.invoice.tax_amount)">
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount).normalize(), 'f')}</cbc:PayableRoundingAmount>
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount), 'f')}</cbc:PayableRoundingAmount>
</py:if>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.prepaid_amount.normalize(), 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount - this.prepaid_amount).normalize(), 'f')}</cbc:PayableAmount>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.prepaid_amount, 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.total_amount - this.prepaid_amount, 'f')}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine py:for="line_id, line in enumerate(this.lines, 1)">
<cbc:ID>${line_id}</cbc:ID>
<cbc:InvoicedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(Decimal(str(line.quantity)), 'f')}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:InvoicedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(Decimal(str(line.quantity)).normalize(), 'f')}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description py:if="line.description">${line.description}</cbc:Description>
<cbc:Name>${line.product.name if line.product else line.summary}</cbc:Name>
@@ -44,11 +44,11 @@ this repository contains the full copyright notices and license terms. -->
</py:def>
<py:def function="TradeTax(tax, amount=None, base=None)">
<ram:ApplicableTradeTax>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format((amount * this.type_sign).normalize(), 'f')}</ram:CalculatedAmount>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount * this.type_sign, 'f')}</ram:CalculatedAmount>
<ram:TypeCode py:if="tax.unece_code">${tax.unece_code}</ram:TypeCode>
<ram:ExemptionReasonCode py:if="hasattr(tax, 'vatex_code') and tax.vatex_code">${tax.vatex_code}</ram:ExemptionReasonCode>
<ram:ExemptionReason py:if="tax.legal_notice">${tax.legal_notice}</ram:ExemptionReason>
<ram:BasisAmount py:if="base">${format((base * this.type_sign).normalize(), 'f')}</ram:BasisAmount>
<ram:BasisAmount py:if="base">${format(base * this.type_sign, 'f')}</ram:BasisAmount>
<ram:CategoryCode py:if="tax.unece_category_code">${tax.unece_category_code}</ram:CategoryCode>
<ram:RateApplicablePercent py:if="tax.type == 'percentage'">${format((tax.rate * 100).normalize(), 'f')}</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
@@ -74,18 +74,18 @@ this repository contains the full copyright notices and license terms. -->
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.currency.round(line.unit_price).normalize(), 'f')}</ram:ChargeAmount>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.currency.round(line.unit_price), 'f')}</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity py:attrs="{'unitCode': line.unit.unece_code} if line.unit and line.unit.unece_code else {}">${format(abs(Decimal(str(line.quantity))), 'f')}</ram:BilledQuantity>
<ram:BilledQuantity py:attrs="{'unitCode': line.unit.unece_code} if line.unit and line.unit.unece_code else {}">${format(abs(Decimal(str(line.quantity)).normalize()), 'f')}</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<py:for each="tax in line.invoice_taxes">
${TradeTax(tax.tax)}
</py:for>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((line.amount * this.type_sign).normalize(), 'f')}</ram:LineTotalAmount>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount * this.type_sign, 'f')}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
@@ -158,14 +158,14 @@ this repository contains the full copyright notices and license terms. -->
<ram:DueDateDateTime>
${DateTime(line.maturity_date)}
</ram:DueDateDateTime>
<ram:PartialPaymentAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(((line.amount_second_currency or (line.debit - line.credit)) * this.type_sign).normalize(), 'f')}</ram:PartialPaymentAmount>
<ram:PartialPaymentAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((line.amount_second_currency or (line.debit - line.credit)) * this.type_sign, 'f')}</ram:PartialPaymentAmount>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount * this.type_sign).normalize(), 'f')}</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount * this.type_sign).normalize(), 'f')}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.tax_amount * this.type_sign).normalize(), 'f')}</ram:TaxTotalAmount>
<ram:GrandTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount * this.type_sign).normalize(), 'f')}</ram:GrandTotalAmount>
<ram:DuePayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.amount_to_pay * this.type_sign).normalize(), 'f')}</ram:DuePayableAmount>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount * this.type_sign, 'f')}</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount * this.type_sign, 'f')}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.tax_amount * this.type_sign, 'f')}</ram:TaxTotalAmount>
<ram:GrandTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.total_amount * this.type_sign, 'f')}</ram:GrandTotalAmount>
<ram:DuePayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.amount_to_pay * this.type_sign, 'f')}</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
+40 -31
View File
@@ -395,12 +395,15 @@ class Party(
else:
for country in eu_vat.guess_country(text):
if 'eu_vat' in identifier_types:
code = eu_vat.compact(country + text)
add('eu_vat', code)
code = country + text
if eu_vat.is_valid(code):
code = eu_vat.compact(code)
add('eu_vat', code)
elif f'{country}_vat' in identifier_types:
vat = get_cc_module(country, 'vat')
code = vat.compact(text)
add(f'{country}_vat', code)
if vat.is_valid(text):
code = vat.compact(text)
add(f'{country}_vat', code)
return result
@classmethod
@@ -913,18 +916,20 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
@fields.depends('type', 'code')
def on_change_with_code(self):
code = self.code
if self.type and '_' in self.type:
if code and self.type and '_' in self.type:
if module := get_cc_module(*self.type.split('_', 1)):
if hasattr(module, 'compact'):
try:
code = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'format'):
try:
code = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if (hasattr(module, 'is_valid')
and module.is_valid(code)):
if hasattr(module, 'compact'):
try:
code = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'format'):
try:
code = module.format(code)
except stdnum.exceptions.ValidationError:
pass
return code
def pre_validate(self):
@@ -955,16 +960,18 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
if ((type := values.get('type')) and '_' in type
and (code := values.get('code'))):
if module := get_cc_module(*type.split('_', 1)):
if hasattr(module, 'format'):
try:
values['code'] = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'compact'):
try:
values['code_compact'] = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if (hasattr(module, 'is_valid')
and module.is_valid(code)):
if hasattr(module, 'format'):
try:
values['code'] = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'compact'):
try:
values['code_compact'] = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
return values
def compute_fields(self, field_names=None):
@@ -974,12 +981,14 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
code_compact = getattr(self, 'code_compact', None)
if (type := getattr(self, 'type', None)) and '_' in type and code:
if module := get_cc_module(*type.split('_', 1)):
if hasattr(module, 'format'):
code = module.format(code)
if hasattr(module, 'compact'):
code_compact = module.compact(code)
else:
code_compact = code
if (hasattr(module, 'is_valid')
and module.is_valid(code)):
if hasattr(module, 'format'):
code = module.format(code)
if hasattr(module, 'compact'):
code_compact = module.compact(code)
else:
code_compact = code
if getattr(self, 'code', None) != code:
values['code'] = code
if getattr(self, 'code_compact', None) != code_compact:
+13 -11
View File
@@ -470,7 +470,7 @@ class Production(
for input_ in self.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price()
cost_price = input_.get_cost_price() or 0
cost += (Decimal(str(input_.internal_quantity)) * cost_price)
return round_price(cost)
@@ -491,7 +491,7 @@ class Production(
product = input_.product
quantity = Uom.compute_qty(
input_.unit, input_.quantity, product.default_uom)
cost += Decimal(str(quantity)) * product.cost_price
cost += Decimal(str(quantity)) * (product.cost_price or 0)
return cost
@dualmethod
@@ -516,18 +516,20 @@ class Production(
for input_ in production.bom.inputs:
quantity = input_.compute_quantity(factor)
product = input_.product
move = production._move(
'input', product, input_.unit, quantity)
move.planned_date = input_date
to_save.append(input_.prepare_move(production, move))
for line, quantity in input_.lines_for_quantity(quantity):
move = production._move(
'input', product, input_.unit, quantity)
move.planned_date = input_date
to_save.append(input_.prepare_move(production, move))
for output in production.bom.outputs:
quantity = output.compute_quantity(factor)
product = output.product
move = production._move(
'output', product, output.unit, quantity)
move.planned_date = output_date
to_save.append(output.prepare_move(production, move))
for line, quantity in output.lines_for_quantity(quantity):
move = production._move(
'output', product, output.unit, quantity)
move.planned_date = output_date
to_save.append(output.prepare_move(production, move))
Move.save(to_save)
@classmethod
@@ -564,7 +566,7 @@ class Production(
for input_ in production.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price()
cost_price = input_.get_cost_price() or 0
input_quantities[input_.product] += (
Decimal(str(input_.internal_quantity)))
input_costs[input_.product] += (
+9 -5
View File
@@ -322,8 +322,10 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('unit_price_required'),
'required': Bool(Eval('unit_price_required')),
'invisible': (
~Eval('unit_price_required', False)
& (Eval('unit_price', None) == None)), # noqa: E711
'required': Eval('unit_price_required', False),
'readonly': Eval('state') != 'draft',
})
unit_price_company = fields.Function(
@@ -346,7 +348,7 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('cost_price_required'),
'invisible': ~Eval('cost_price_required', False),
'required': (
(Eval('state') == 'done')
& Eval('cost_price_required', False)),
@@ -370,8 +372,10 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('unit_price_required'),
'required': Bool(Eval('unit_price_required')),
'invisible': (
~Eval('unit_price_required')
& (Eval('currency', None) == None)), # noqa: E711
'required': Eval('unit_price_required', False),
'readonly': Eval('state') != 'draft',
},
help="The currency in which the unit price is specified.")
+3 -3
View File
@@ -154,13 +154,13 @@ class _InventoryMixin:
output_clause = (
(from_location.left >= left) & (from_location.right <= right))
start_quantity = Sum(
move.quantity * Case((output_clause, -1), else_=1),
move.internal_quantity * Case((output_clause, -1), else_=1),
filter_=(move_date < from_date))
input_quantity = Sum(
move.quantity,
move.internal_quantity,
filter_=input_clause & (move_date >= from_date))
output_quantity = Sum(
move.quantity,
move.internal_quantity,
filter_=output_clause & (move_date >= from_date))
date_column = Greatest(move_date, from_date)
@@ -50,6 +50,8 @@ Get currency::
Create product::
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> box2, = unit.duplicate(default={'factor': 2, 'rate': .5})
>>> box10, = unit.duplicate(default={'factor': 10, 'rate': .1})
>>> template = ProductTemplate()
>>> template.name = 'Product'
@@ -96,8 +98,8 @@ Fill warehouse::
>>> move.product = product
>>> move.from_location = storage_loc
>>> move.to_location = customer_loc
>>> move.unit = unit
>>> move.quantity = 2
>>> move.unit = box2
>>> move.quantity = 1
>>> move.effective_date = dt.date(2025, 1, 10)
>>> move.unit_price = Decimal('20.0000')
>>> move.currency = currency
@@ -111,7 +113,8 @@ Forecast some moves::
>>> move.product = product
>>> move.from_location = supplier_loc
>>> move.to_location = storage_loc
>>> move.quantity = 10
>>> move.unit = box10
>>> move.quantity = 1
>>> move.effective_date = dt.date(2025, 1, 5)
>>> move.unit_price = Decimal('10.0000')
>>> move.currency = currency
+10 -2
View File
@@ -281,10 +281,18 @@ class CreateDPDShipping(Wizard):
cm3 = UoM(ModelData.get_id('product', 'uom_cubic_centimeter'))
kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
volume = sum(
UoM.compute_qty(
p.volume_uom, p.total_volume, shipment.volume_uom, round=False)
for p in shipment.root_packages)
volume = round(UoM.compute_qty(
shipment.volume_uom, shipment.volume, cm3, round=False))
shipment.volume_uom, volume, cm3, round=False))
weight = sum(
UoM.compute_qty(
p.weight_uom, p.total_weight, shipment.weight_uom, round=False)
for p in shipment.root_packages)
weight = round(UoM.compute_qty(
shipment.weight_uom, shipment.weight, kg, round=False), 2)
shipment.weight_uom, weight, kg, round=False), 2)
return {
'generalShipmentData': {
'identificationNumber': shipment.number,
@@ -136,7 +136,9 @@ Create Package Type::
... name='Box',
... length=10, length_uom=cm,
... height=8, height_uom=cm,
... width=5, width_uom=cm)
... width=5, width_uom=cm,
... packaging_weight=50,
... packaging_weight_uom=g)
>>> box.save()
Create a DPD Carrier and the related credential::
+11 -4
View File
@@ -89,13 +89,16 @@
function getScrollableAncestors(el) {
let ancestors = [];
let parent = el.parentElement;
while (parent && parent !== document.body) {
while (parent && (parent !== document.body ||
!parent.classList.contains('modal'))) {
if (isScrollable(parent)) {
ancestors.push(parent);
}
parent = parent.parentElement;
}
ancestors.push(window);
if (parent && !parent.classList.contains('modal')) {
ancestors.push(window);
}
return ancestors;
}
@@ -2978,9 +2981,13 @@
return ((a.isDate == b.isDate) &&
(a.isDateTime == b.isDateTime) &&
(a.valueOf() == b.valueOf()));
} else if ((a instanceof Number) || (b instanceof Number)) {
return (Number(a) === Number(b));
} else {
if (a instanceof Number) {
a = a.valueOf();
}
if (b instanceof Number) {
b = b.valueOf();
}
return (a === b);
}
};
+5 -3
View File
@@ -1102,11 +1102,13 @@
width = column.col.data('default-width');
}
if (width && width.endsWith('%')) {
width = parseInt(width.slice(0, -1), 10) / 100;
// Reverse the calculation from _set_column_width
let factor = 1;
if (column.attributes.expand) {
width /= parseInt(column.attributes.expand, 10);
factor += parseInt(column.attributes.expand, 10);
}
width = `${width}em`;
width = parseInt(width.slice(0, -1), 10);
width = `${width / 100 / factor}em`;
}
min_width.push(width);
column.col.sao_show();
+38
View File
@@ -2969,6 +2969,44 @@
var now = new Date();
[
[[['x', '=', 42]], {'x': 42}, true],
[[['x', '=', 42]], {'x': new Number(42)}, true],
[[['x', '=', 42]], {'x': 0}, false],
[[['x', '=', 42]], {'x': new Number(0)}, false],
[[['x', '=', 42]], {'x': null}, false],
[[['x', '=', new Number(42)]], {'x': 42}, true],
[[['x', '=', new Number(42)]], {'x': new Number(42)}, true],
[[['x', '=', new Number(42)]], {'x': 0}, false],
[[['x', '=', new Number(42)]], {'x': new Number(0)}, false],
[[['x', '=', new Number(42)]], {'x': null}, false],
[[['x', '=', 0]], {'x': 0}, true],
[[['x', '=', 0]], {'x': new Number(0)}, true],
[[['x', '=', 0]], {'x': null}, false],
[[['x', '=', new Number(0)]], {'x': 0}, true],
[[['x', '=', new Number(0)]], {'x': new Number(0)}, true],
[[['x', '=', new Number(0)]], {'x': null}, false],
[[['x', '=', null]], {'x': 0}, false],
[[['x', '=', null]], {'x': new Number(0)}, false],
[[['x', '=', null]], {'x': null}, true],
[[['x', '!=', 42]], {'x': 42}, false],
[[['x', '!=', 42]], {'x': new Number(42)}, false],
[[['x', '!=', 42]], {'x': 0}, true],
[[['x', '!=', 42]], {'x': new Number(0)}, true],
[[['x', '!=', 42]], {'x': null}, true],
[[['x', '!=', new Number(42)]], {'x': 42}, false],
[[['x', '!=', new Number(42)]], {'x': new Number(42)}, false],
[[['x', '!=', new Number(42)]], {'x': 0}, true],
[[['x', '!=', new Number(42)]], {'x': new Number(0)}, true],
[[['x', '!=', new Number(42)]], {'x': null}, true],
[[['x', '!=', 0]], {'x': 0}, false],
[[['x', '!=', 0]], {'x': new Number(0)}, false],
[[['x', '!=', 0]], {'x': null}, true],
[[['x', '!=', new Number(0)]], {'x': 0}, false],
[[['x', '!=', new Number(0)]], {'x': new Number(0)}, false],
[[['x', '!=', new Number(0)]], {'x': null}, true],
[[['x', '!=', null]], {'x': 0}, true],
[[['x', '!=', null]], {'x': new Number(0)}, true],
[[['x', '!=', null]], {'x': null}, false],
[[['x', '>', 5]], {'x': 6}, true],
[[['x', '>', 5]], {'x': 4}, false],
[[['x', '>', null]], {'x': today}, false],
+4 -1
View File
@@ -46,4 +46,7 @@ if db_names:
for thread in threads:
thread.join()
assert len(threads := threading.enumerate()) == 1, f"len({threads}) != 1"
if len(threads := threading.enumerate()) != 1:
raise RuntimeError(
f"There are {len(threads)} threads running after initializing"
" the Pool")
+3 -1
View File
@@ -256,6 +256,8 @@ class MemoryCache(BaseCache):
@classmethod
def sync(cls, transaction):
if not Pool().ready:
return
if cls._clean_last is None:
cls._clean_last = dt.datetime.now()
return
@@ -298,7 +300,7 @@ class MemoryCache(BaseCache):
cls._clean_last = dt.datetime.now()
def sync_since(self, value):
return self._clean_last > value
return self._clean_last is not None and self._clean_last > value
@classmethod
def commit(cls, transaction):
+1 -1
View File
@@ -104,7 +104,7 @@ class TrytonConfigParser(configparser.ConfigParser):
self.add_section('bus')
self.set('bus', 'allow_subscribe', 'False')
self.set('bus', 'long_polling_timeout', str(5 * 60))
self.set('bus', 'cache_timeout', '5')
self.set('bus', 'cache_timeout', str(5 * 60))
self.set('bus', 'select_timeout', '5')
self.add_section('report')
self.add_section('html')
+9 -4
View File
@@ -345,7 +345,11 @@ class ActionMixin(ModelSQL):
action = fields.Many2One(
'ir.action', "Action",
required=True, readonly=True, ondelete='CASCADE')
required=True, ondelete='CASCADE',
states={
'required': Eval('id', -1) >= 0,
'readonly': Eval('id', -1) >= 0,
})
@classmethod
def __setup__(cls):
@@ -456,8 +460,9 @@ class ActionMixin(ModelSQL):
values = values.copy()
for field in values:
if field in Action._fields:
action_values[field] = values[field]
if hasattr(getattr(cls, field), 'set'):
if field != 'action':
action_values[field] = values[field]
elif hasattr(getattr(cls, field), 'set'):
later[field] = values[field]
for field in later:
del values[field]
@@ -468,7 +473,7 @@ class ActionMixin(ModelSQL):
if database.nextid(transaction.connection, cls._table):
database.setnextid(transaction.connection, cls._table,
database.currid(transaction.connection, Action._table))
if 'action' not in values:
if not values.get('action'):
action, = Action.create([action_values])
values['action'] = action.id
else:
+1 -1
View File
@@ -464,7 +464,7 @@ class Report(URLMixin, PoolBase):
time.sleep(0.02 * (retry - count))
try:
subprocess.run(args, timeout=timeout, check=True)
except subprocess.CalledProcessError:
except (subprocess.SubprocessError, OSError):
if count:
continue
logger.error(
+57
View File
@@ -884,6 +884,63 @@ class DomainInversionTestCase(TestCase):
unique_value(domain, single_value=False), (False, None, None))
def test_evaldomain(self):
domain = [['x', '=', 42]]
self.assertTrue(eval_domain(domain, {'x': 42}))
self.assertTrue(eval_domain(domain, {'x': Decimal('42')}))
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', Decimal('42')]]
self.assertTrue(eval_domain(domain, {'x': 42}))
self.assertTrue(eval_domain(domain, {'x': Decimal('42')}))
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', 0]]
self.assertTrue(eval_domain(domain, {'x': 0}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', Decimal('0')]]
self.assertTrue(eval_domain(domain, {'x': 0}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', None]]
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', 42]]
self.assertFalse(eval_domain(domain, {'x': 42}))
self.assertFalse(eval_domain(domain, {'x': Decimal('42')}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', Decimal('42')]]
self.assertFalse(eval_domain(domain, {'x': 42}))
self.assertFalse(eval_domain(domain, {'x': Decimal('42')}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', 0]]
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', Decimal('0')]]
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', None]]
self.assertTrue(eval_domain(domain, {'x': 0}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '>', 5]]
self.assertTrue(eval_domain(domain, {'x': 6}))
self.assertFalse(eval_domain(domain, {'x': 4}))