Compare commits

..

121 Commits

Author SHA1 Message Date
Deepesh Garg
ed231abb54 fix: Ledger repost support for extending app doctypes 2024-12-19 17:06:54 +05:30
rohitwaghchaure
024c442087 fix: not able to make PO for returned qty from material request (#44540) 2024-12-05 15:29:34 +05:30
rohitwaghchaure
7249cf0001 fix: update qty in SABB if qty changed in stock reco (#44542) 2024-12-05 15:28:38 +05:30
Khushi Rawat
322c788760 Merge pull request #44537 from mahsem/patch-9
fix: add title for translation in  asset.js
2024-12-05 15:12:39 +05:30
mahsem
61439132a4 fix: add title for translation in asset.js 2024-12-05 08:37:43 +01:00
rohitwaghchaure
14f2b0ab0e fix: consider zero valuation rate for serial nos (#44532) 2024-12-05 12:50:27 +05:30
rohitwaghchaure
5413cf9f1f fix: incorrect stock UOM for BOM raw materials (#44528)
fix: incorrect stock uom for BOM raw materials
2024-12-05 11:00:11 +05:30
ruthra kumar
707d8eddc5 Merge pull request #44503 from mahsem/patch-7
fix: add strings for translation in pos_item_cart.js
2024-12-05 10:18:10 +05:30
ruthra kumar
abd2314894 Merge pull request #44511 from frappe/l10n_develop
fix: sync translations from crowdin
2024-12-05 10:13:24 +05:30
ruthra kumar
fb5cbc43a2 Merge pull request #44512 from mahsem/patch-8
fix: add label strings for translation in pos_controller.js
2024-12-05 10:11:40 +05:30
ruthra kumar
31efaf6dbf chore: linter fix 2024-12-05 10:10:34 +05:30
ruthra kumar
2f38390b48 Merge pull request #44501 from mahsem/patch-5
fix: strings for translation in pos_past_order_summary.js
2024-12-05 10:00:17 +05:30
mahsem
138ffc4e93 refactor: translatable label on pos payments (#42081)
* Use better description in pos_payment.js 

Use Change Amount instead of Change and To Be Paid in pos_payment.js and be consistent with other strings

* change_amount_pos_payment.js
2024-12-05 09:59:43 +05:30
ruthra kumar
ba8ba79335 Merge pull request #44500 from mahsem/patch-4
fix: add labels for translation in sales_order.js
2024-12-05 09:58:05 +05:30
rohitwaghchaure
353610ce61 fix: inv dimensions fields not creating for standard doctype (#44504) 2024-12-04 21:26:15 +05:30
mahsem
bd77a5557d fix: add label strings for translation in pos_controller.js 2024-12-04 15:40:55 +01:00
Frappe PR Bot
163ff71ece fix: Persian translations 2024-12-04 19:48:36 +05:30
Frappe PR Bot
6a9a4f10f0 fix: Turkish translations 2024-12-04 19:48:28 +05:30
Frappe PR Bot
663b66ca1d fix: Swedish translations 2024-12-04 19:48:24 +05:30
David Arnold
029dc948fe fix: client-side taxes calculation (#44510)
closes: #44328
2024-12-04 13:08:50 +00:00
rohitwaghchaure
4001166ecc fix: required by date in the reorder material request (#44497) 2024-12-04 17:45:07 +05:30
rohitwaghchaure
b4534e56e4 fix: duplicate required items in the CSV (#44498) 2024-12-04 17:43:51 +05:30
mahsem
4b72b60f1a fix: add strings for translation in pos_item_cart.js 2024-12-04 10:12:34 +01:00
mahsem
23c846d4b9 fix: strings for translation in pos_past_order_summary.js 2024-12-04 09:07:09 +01:00
mahsem
d544328ffe fix: add labels for translation in sales_order.js 2024-12-04 08:58:16 +01:00
mahsem
6585fabdb1 fix: add strings for translation in timesheet.js (#44496) 2024-12-04 13:05:48 +05:30
ruthra kumar
e6be1021f9 Merge pull request #44453 from n-traore/vf_ohada_charts_accounts
feat: Create Syscohada charts of accounts
2024-12-04 11:44:05 +05:30
Frappe PR Bot
f74c99be9d fix: sync translations from crowdin (#44486)
fix: Turkish translations
2024-12-03 21:09:01 +01:00
Raffael Meyer
032ef62b14 fix: if-block + indentation (#44494) 2024-12-03 20:06:50 +00:00
Raffael Meyer
657c85638c fix: get value from dict (#44492)
`doc.taxes` is not always a `frappe._dict`, it can also be a regular `dict`. Resolves #44328
2024-12-03 19:03:57 +00:00
ruthra kumar
483fd124fc Merge pull request #44339 from vishakhdesai/hd-ticket-20084
fix: move `validate_total_debit_and_credit` from`validate` to`on_submit` in Journal Entry
2024-12-03 18:08:59 +05:30
ruthra kumar
c3ace82db8 refactor: validate debit and credit on before_submit 2024-12-03 17:42:11 +05:30
ruthra kumar
88b0af1696 Merge pull request #44467 from ljain112/fix-multi-gp
fix: Multiple Fixes in Gross Profit Report
2024-12-03 17:29:31 +05:30
ruthra kumar
fc0122ce76 chore: fix typo 2024-12-03 15:39:30 +05:30
ruthra kumar
d0c522ee46 Merge pull request #44450 from aerele/payment-request-partial-payment-pr
fix: calculate submitted payment entry amount for grand total
2024-12-03 14:51:47 +05:30
ruthra kumar
f11eab06c3 Merge pull request #44415 from ljain112/fix-incoming-rate
fix: adjusted incoming rate for zero rated item in purchase receipt
2024-12-03 14:24:33 +05:30
ruthra kumar
9bbb953e26 Merge pull request #44373 from Ninad1306/valuation_rate_fix
fix: Always Calculate `sales_incoming_rate` for Internal Transfers
2024-12-03 14:17:04 +05:30
ruthra kumar
f21283d829 Merge pull request #44470 from ruthra-kumar/rearrange_fields_in_company
refactor(UI): Rearranging fields under new sections
2024-12-03 10:29:25 +05:30
ruthra kumar
94de5c4e7e Merge pull request #44473 from frappe/l10n_develop
fix: sync translations from crowdin
2024-12-03 10:16:39 +05:30
Frappe PR Bot
dc645b3906 fix: Persian translations 2024-12-02 17:26:34 +05:30
Frappe PR Bot
f758dfcbe1 fix: Swedish translations 2024-12-02 17:26:22 +05:30
Frappe PR Bot
fdfd51c0fd fix: Spanish translations 2024-12-02 17:26:07 +05:30
ruthra kumar
7244754d28 refactor(UI): Rearranging fields under new sections 2024-12-02 15:37:38 +05:30
ljain112
4e6a5893e7 fix: correct buying amount for product bundel 2024-12-02 15:20:20 +05:30
Sagar Vora
7cc111f790 fix: set correct unallocated amount in Payment Entry (#43958)
* fix: set correct unallocated amount in Payment Entry

* fix: add checkbox and other logic fix

* fix: patch to set is_exchange_gain_loss in Payment Entry deductions

* fix: consider deductions except exch. gain/loss

* fix: set exchange gain loss in payment entry

* fix: separate function to set exchange gain loss

* fix: failing test cases

* fix: add cash disc. row first

* fix: review changes

* fix: changes as per review

* fix: failing test cases

* fix: review

* fix: wait for request to complete before updating exchange gain loss

* fix: review

---------

Co-authored-by: vishakhdesai <vishakhdesai@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2024-12-02 14:54:29 +05:30
rohitwaghchaure
7de9c14a2c fix: incorrect Gross Margin on project (#44461) 2024-12-02 14:22:41 +05:30
ljain112
a86b223aed fix: remove queries 2024-12-02 14:08:21 +05:30
ruthra kumar
810a2c8fc1 Merge pull request #44412 from ljain112/fix-common-party
fix: handle multi currency in common party journal entry
2024-12-02 12:38:34 +05:30
ruthra kumar
6b6798bee6 Merge pull request #44437 from Abdeali099/report-data-trnslation
fix: Added translation for `Account` column
2024-12-02 12:22:38 +05:30
Abdeali Chharchhoda
e355b18c0c Merge branch 'develop' into report-data-trnslation 2024-12-02 11:33:00 +05:30
Abdeali Chharchhoda
a4f8315602 fix: Translate Party Account column label 2024-12-02 11:23:10 +05:30
Frappe PR Bot
6df0ea153d fix: sync translations from crowdin (#44432)
* fix: Swedish translations

* fix: Turkish translations

* fix: Swedish translations

* fix: Persian translations

* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Hungarian translations

* fix: Polish translations

* fix: Russian translations

* fix: German translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Persian translations

* fix: Bosnian translations

* fix: Esperanto translations
2024-12-01 22:35:17 +00:00
Khushi Rawat
74624828e7 Merge pull request #44456 from khushi8112/missing-ctx-argument
fix: missing ctx in API function calls
2024-12-01 20:28:54 +05:30
Khushi Rawat
55cb1c54e0 fix: missing ctx in API function calls 2024-12-01 17:59:44 +05:30
Frappe PR Bot
bd042d0fff chore: update POT file (#44455) 2024-12-01 11:18:19 +01:00
Nelly Traore
aa8254963c feat: add Syscohada charts of accounts 2024-11-30 19:51:48 +00:00
Nabin Hait
04c7c0bb66 fix: Reordered fields for asset doctype (#44423) 2024-11-30 22:00:17 +05:30
Sagar Vora
279dcabf38 Merge pull request #44443 from sagarvora/minor-perfs
perf: reduce queries during transaction save
2024-11-30 00:45:46 +05:30
Sagar Vora
9ee4f58e1b Merge pull request #44439 from Abdeali099/translation-bug
fix: added fieldname to avoid fieldname to translate
2024-11-30 00:24:53 +05:30
Sagar Vora
b6b8a06fda perf: reduce queries during transaction save 2024-11-30 00:20:18 +05:30
Sagar Vora
6de7320ef4 perf: cache product bundle items at document level (#44440) 2024-11-29 17:21:32 +00:00
Abdeali Chharchhoda
b80022133c fix: added fieldname to avoid fieldname to translate 2024-11-29 18:13:03 +05:30
rohitwaghchaure
810c72a30c fix: number format in the message (#44435) 2024-11-29 18:12:48 +05:30
Sugesh393
9bee2d430c test: add new unit test to validate paid amount in payment request 2024-11-29 17:44:22 +05:30
Abdeali Chharchhoda
de6cbd382f fix: Added translation for Account column 2024-11-29 17:42:11 +05:30
Sugesh393
561a159aec fix: calculate submitted payment entry as paid amount 2024-11-29 17:39:40 +05:30
rohitwaghchaure
7f7564b581 fix: precision calculation causing 0.1 discrepancy (#44431) 2024-11-29 17:09:41 +05:30
rohitwaghchaure
4050ea07eb fix: source warehouse not set in required items of WO (#44426)
fix: source warehouse not set in required items of WO on data import
2024-11-29 17:01:08 +05:30
rohitwaghchaure
5266f236b7 fix: SABB print for packed items (#44413) 2024-11-29 15:49:00 +05:30
rohitwaghchaure
d37d7b9811 fix: do not validate stock during inward (#44417) 2024-11-29 15:24:06 +05:30
ruthra kumar
76bd1017f4 Merge pull request #44302 from Abdeali099/refactor-payment-request
fix: Minor Updates in `Payment Request` and `Payment Entry`
2024-11-29 14:39:49 +05:30
ruthra kumar
2b3c829662 Merge pull request #44414 from blaggacao/fix/configure-test-type-checking
test: configure test-time type checking
2024-11-29 12:16:35 +05:30
ruthra kumar
da5dba997d Merge pull request #44411 from frappe/l10n_develop
fix: sync translations from crowdin
2024-11-29 10:36:59 +05:30
David
2394da419e test: configure test-time type checking
see: https://github.com/frappe/frappe/pull/28554
2024-11-28 23:36:24 +01:00
ljain112
3182c6981c fix: adjusted incoming rate for zero rated item in purchase receipt 2024-11-28 20:45:17 +05:30
ljain112
e371f68d66 fix: handle multi currency in common party journal entry 2024-11-28 19:50:01 +05:30
Frappe PR Bot
44c33cd12a fix: Turkish translations 2024-11-28 16:25:23 +05:30
Frappe PR Bot
4066df8652 fix: Swedish translations 2024-11-28 16:25:18 +05:30
Frappe PR Bot
a6d8383e43 fix: Spanish translations 2024-11-28 16:25:11 +05:30
Ninad Parikh
69bd90b038 fix: Data Should be Computed in Backend to Maintain Consistent Behaviour (#44195) 2024-11-28 15:59:52 +05:30
ruthra kumar
5de7db2be0 Merge pull request #44405 from ruthra-kumar/typeerror_on_transactionjs
fix: typeerror on transaction.js
2024-11-28 14:46:10 +05:30
ruthra kumar
46ce8780f2 fix: typeerror on transaction.js 2024-11-28 14:41:26 +05:30
Khushi Rawat
af5dae8682 Merge pull request #44400 from khushi8112/asset-depreciation-error-index-error
fix: IndexError in Asset Depreciation Ledger when query result is empty
2024-11-28 12:56:21 +05:30
Khushi Rawat
1737de7c10 chore: removed print statement 2024-11-28 11:41:00 +05:30
Khushi Rawat
7c393e5aa0 fix: IndexError in Asset Depreciation Ledger when query result is empty 2024-11-28 11:36:05 +05:30
Khushi Rawat
9a5d68f1f4 Merge pull request #44187 from khushi8112/progressive-disclosure-assets
style: added progressive disclosure to assets
2024-11-27 22:00:19 +05:30
Raffael Meyer
f6776c7d6b feat: add Company Contact Person in selling transactions (#44362) 2024-11-27 15:51:59 +01:00
ruthra kumar
ea6ddd5df6 Merge pull request #44346 from aerele/payment-request-partial-payment-pr
fix: reduce paid amount from grand total
2024-11-27 20:08:22 +05:30
ruthra kumar
e7808981cf Merge pull request #44382 from frappe/l10n_develop
fix: sync translations from crowdin
2024-11-27 19:56:32 +05:30
ruthra kumar
78a1b211a1 Merge pull request #44316 from Ninad1306/pos_closing_entry_fix
fix: Initially Closing Amount Should be Equal to Expected Amount
2024-11-27 19:56:05 +05:30
ruthra kumar
4d164d5854 Merge pull request #44392 from aerele/purchase_receipt_typo
chore: Fix typo "Purchase Reecipt"
2024-11-27 19:53:58 +05:30
vimalraj27
21049bae91 chore: Fix typo "Purchase Reecipt" 2024-11-27 18:03:39 +05:30
Sugesh393
82907672d9 fix: reduce paid amount from grand total 2024-11-27 17:41:16 +05:30
ruthra kumar
7efe05baf2 Merge pull request #44386 from Abdeali099/popup-msg-translation
fix: Add translation for showing mandatory fields in error msg
2024-11-27 17:28:54 +05:30
ruthra kumar
46b15f6040 Merge pull request #44327 from aerele/gl-transaction-currency
fix: set debit transaction currency in gl entry
2024-11-27 17:28:00 +05:30
akashdubey22
3de5ce74e1 refactor: updated print format for general ledger (#44057)
* refactor: update General Ledger print format

* Update general_ledger.html

* Update general_ledger.html

Removed extra spaces

* refactor: use letter-spacing for titles

* Update general_ledger.html

Comment added back

* Update general_ledger.html

* refactor: adding Remarks conditions & print party_type

* refactor: added Remarks column & adjusted spaces

Remarks column will be printed when Show Remarks is checked.

* Update general_ledger.html

Removed whitespace

* Update general_ledger.html

Fixed by removing colspan=2 in Opening Balance.

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2024-11-27 17:26:47 +05:30
ruthra kumar
676c93411e Merge pull request #44378 from aerele/pb-item-filter
fix: filter item with search fields
2024-11-27 17:19:39 +05:30
Abdeali Chharchhoda
214dfab269 fix: Add filter for outstanding_amount to fetch open PRs 2024-11-27 17:18:10 +05:30
Abdeali Chharchhoda
e1b3193b04 Merge branch 'develop' into refactor-payment-request 2024-11-27 17:05:39 +05:30
Abdeali Chharchhoda
f42ec6a124 fix: Add translation for showing mandatory fields in error msg 2024-11-27 16:53:50 +05:30
ruthra kumar
e3770bc9e1 Merge pull request #44359 from aerele/period-closing-voucher
fix: check difference with company currency
2024-11-27 16:53:22 +05:30
ruthra kumar
4dbd8054e8 Merge pull request #44323 from ljain112/fix-gross-profit-returned-invoices
fix: update gross profit for returned invoices
2024-11-27 16:49:17 +05:30
ruthra kumar
1ea5c5d821 Merge pull request #44376 from vishakhdesai/remove-precision
fix: remove field precision in Sales and Purchase Order for percentage fields
2024-11-27 16:48:31 +05:30
Frappe PR Bot
c9fb59a158 fix: Persian translations 2024-11-27 16:28:19 +05:30
Frappe PR Bot
02225e6a33 fix: Turkish translations 2024-11-27 16:28:14 +05:30
venkat102
ebfbee3da5 fix: filter item with search fields 2024-11-27 14:02:11 +05:30
vishakhdesai
eff9cd10cd fix: remove field precision in SO and PO for percentage fields 2024-11-27 12:43:34 +05:30
Ninad1306
94d3fc9fde test: validate buying workflow 2024-11-27 12:06:11 +05:30
Ninad1306
d049c97884 fix: always set sales incoming rate for internal transfers 2024-11-27 12:05:39 +05:30
venkat102
e2bae4cf07 fix: check difference with company currency 2024-11-26 22:13:57 +05:30
vishakhdesai
8b5d4c0236 fix: move validate_total_debit_and_credit from validate to on_submit in Journal Entry 2024-11-26 14:26:02 +05:30
Sugesh393
bbe3bc95d0 test: add unit test to validate outstanding amount in payment request 2024-11-26 13:59:45 +05:30
Sugesh393
38e7d0a41e fix: set outstanding amount while creating payment request for invoices 2024-11-26 13:58:52 +05:30
venkat102
6e19c06e58 fix: set debit transaction currency in gl entry 2024-11-26 00:03:42 +05:30
ljain112
af5a3e5a48 fix: test case 2024-11-25 19:05:08 +05:30
ljain112
00403515a8 fix: gp for return invoice 2024-11-25 18:45:17 +05:30
ljain112
8a42601e99 fix: update gross profit for returned invoices 2024-11-25 17:31:07 +05:30
Ninad1306
af9524920b fix: initially closing amt should be equal to expected amt 2024-11-25 10:44:25 +05:30
Khushi Rawat
4bdc6a0021 style: move depreciation details to a new tab 2024-11-20 11:56:34 +05:30
Khushi Rawat
87065d0387 chore: pre-commit check 2024-11-18 14:45:02 +05:30
Khushi Rawat
e3d734c890 style: added progressive disclosure to assets 2024-11-18 14:40:32 +05:30
131 changed files with 123485 additions and 11950 deletions

View File

@@ -0,0 +1,34 @@
import json
from pathlib import Path
syscohada_countries = [
"bj", # Bénin
"bf", # Burkina-Faso
"cm", # Cameroun
"cf", # Centrafrique
"ci", # Côte d'Ivoire
"cg", # Congo
"km", # Comores
"ga", # Gabon
"gn", # Guinée
"gw", # Guinée-Bissau
"gq", # Guinée Equatoriale
"ml", # Mali
"ne", # Niger
"cd", # République Démocratique du Congo
"sn", # Sénégal
"td", # Tchad
"tg", # Togo
]
folder = Path(__file__).parent
generic_charts = Path(folder).glob("syscohada*.json")
for file in generic_charts:
with open(file) as f:
chart = json.load(f)
for country in syscohada_countries:
chart["country_code"] = country
json_object = json.dumps(chart, indent=4)
with open(Path(folder, file.name.replace("syscohada", country)), "w") as outfile:
outfile.write(json_object)

View File

@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = 95
pe.source_exchange_rate = 84.211
pe.source_exchange_rate = 84.2105
pe.received_amount = 8000
pe.references = []
pe.save().submit()
@@ -229,7 +229,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
row = next(x for x in je.accounts if x.account == self.debtors_usd)
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
row = next(x for x in je.accounts if x.account != self.debtors_usd)
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
self.assertEqual(flt(je.total_debit, precision), 0.0)

View File

@@ -127,9 +127,6 @@ class JournalEntry(AccountsController):
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.set_total_debit_credit()
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
if not frappe.flags.is_reverse_depr_entry:
self.validate_against_jv()
@@ -184,6 +181,11 @@ class JournalEntry(AccountsController):
else:
return self._cancel()
def before_submit(self):
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
def on_submit(self):
self.validate_cheque_info()
self.check_credit_limit()

View File

@@ -324,11 +324,6 @@ frappe.ui.form.on("Payment Entry", {
"write_off_difference_amount",
frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount
);
frm.toggle_display(
"set_exchange_gain_loss",
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount
);
},
set_dynamic_labels: function (frm) {
@@ -1119,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", {
},
set_unallocated_amount: function (frm) {
var unallocated_amount = 0;
var total_deductions = frappe.utils.sum(
$.map(frm.doc.deductions || [], function (d) {
return flt(d.amount);
})
);
let unallocated_amount = 0;
let deductions_to_consider = 0;
for (const row of frm.doc.deductions || []) {
if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount);
}
const included_taxes = get_included_taxes(frm);
if (frm.doc.party) {
if (
frm.doc.payment_type == "Receive" &&
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions &&
frm.doc.total_allocated_amount <
frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate
) {
unallocated_amount =
(frm.doc.base_received_amount +
total_deductions -
flt(frm.doc.base_total_taxes_and_charges) -
frm.doc.base_total_allocated_amount) /
frm.doc.source_exchange_rate;
} else if (
frm.doc.payment_type == "Pay" &&
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions &&
frm.doc.total_allocated_amount <
frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider
) {
unallocated_amount =
(frm.doc.base_paid_amount +
flt(frm.doc.base_total_taxes_and_charges) -
(total_deductions + frm.doc.base_total_allocated_amount)) /
deductions_to_consider -
frm.doc.base_total_allocated_amount -
included_taxes) /
frm.doc.source_exchange_rate;
} else if (
frm.doc.payment_type == "Pay" &&
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider
) {
unallocated_amount =
(frm.doc.base_received_amount -
deductions_to_consider -
frm.doc.base_total_allocated_amount -
included_taxes) /
frm.doc.target_exchange_rate;
}
}
@@ -1242,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", {
},
write_off_difference_amount: function (frm) {
frm.events.set_deductions_entry(frm, "write_off_account");
frm.events.set_write_off_deduction(frm);
},
set_exchange_gain_loss: function (frm) {
frm.events.set_deductions_entry(frm, "exchange_gain_loss_account");
base_paid_amount: function (frm) {
frm.events.set_exchange_gain_loss_deduction(frm);
},
set_deductions_entry: function (frm, account) {
if (frm.doc.difference_amount) {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
args: {
company: frm.doc.company,
},
callback: function (r, rt) {
if (r.message) {
const write_off_row = $.map(frm.doc["deductions"] || [], function (t) {
return t.account == r.message[account] ? t : null;
});
base_received_amount: function (frm) {
frm.events.set_exchange_gain_loss_deduction(frm);
},
const difference_amount = flt(
frm.doc.difference_amount,
precision("difference_amount")
);
set_exchange_gain_loss_deduction: async function (frm) {
// wait for allocate_party_amount_against_ref_docs to finish
await frappe.after_ajax();
const base_paid_amount = frm.doc.base_paid_amount || 0;
const base_received_amount = frm.doc.base_received_amount || 0;
const exchange_gain_loss = flt(
base_paid_amount - base_received_amount,
get_deduction_amount_precision()
);
const add_deductions = (details) => {
let row = null;
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = details[account];
row.cost_center = details["cost_center"];
} else {
row = write_off_row[0];
}
if (row) {
row.amount = flt(row.amount) + difference_amount;
} else {
frappe.msgprint(__("No gain or loss in the exchange rate"));
}
refresh_field("deductions");
};
if (!r.message[account]) {
frappe.prompt(
{
label: __("Please Specify Account"),
fieldname: account,
fieldtype: "Link",
options: "Account",
get_query: () => ({
filters: {
company: frm.doc.company,
},
}),
},
(values) => {
const details = Object.assign({}, r.message, values);
add_deductions(details);
},
__(frappe.unscrub(account))
);
} else {
add_deductions(r.message);
}
frm.events.set_unallocated_amount(frm);
}
},
});
if (!exchange_gain_loss) {
frm.events.delete_exchange_gain_loss(frm);
return;
}
const account_fieldname = "exchange_gain_loss_account";
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
if (!row) {
const response = await get_company_defaults(frm.doc.company);
const account =
response.message?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
row = frm.add_child("deductions");
row.account = account;
row.cost_center = response.message?.cost_center;
row.is_exchange_gain_loss = 1;
}
row.amount = exchange_gain_loss;
frm.refresh_field("deductions");
frm.events.set_unallocated_amount(frm);
},
delete_exchange_gain_loss: function (frm) {
const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss);
if (!exchange_gain_loss_row) return;
exchange_gain_loss_row.amount = 0;
frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove();
frm.refresh_field("deductions");
},
set_write_off_deduction: async function (frm) {
const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision());
if (!difference_amount) return;
const account_fieldname = "write_off_account";
const response = await get_company_defaults(frm.doc.company);
const write_off_account =
response.message?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
if (!write_off_account) return;
let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account);
if (!row) {
row = frm.add_child("deductions");
row.account = write_off_account;
row.cost_center = response.message?.cost_center;
}
row.amount = flt(row.amount) + difference_amount;
frm.refresh_field("deductions");
frm.events.set_unallocated_amount(frm);
},
bank_account: function (frm) {
@@ -1778,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", {
});
frappe.ui.form.on("Payment Entry Deduction", {
before_deductions_remove: function (doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
if (row.is_exchange_gain_loss && row.amount) {
frappe.throw(__("Cannot delete Exchange Gain/Loss row"));
}
},
amount: function (frm) {
frm.events.set_unallocated_amount(frm);
},
@@ -1799,3 +1807,53 @@ function set_default_party_type(frm) {
if (party_type) frm.set_value("party_type", party_type);
}
function get_included_taxes(frm) {
let included_taxes = 0;
for (const tax of frm.doc.taxes) {
if (!tax.included_in_paid_amount) continue;
if (tax.add_deduct_tax == "Add") {
included_taxes += tax.base_tax_amount;
} else {
included_taxes -= tax.base_tax_amount;
}
}
return included_taxes;
}
function get_company_defaults(company) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
args: {
company: company,
},
});
}
function prompt_for_missing_account(frm, account) {
return new Promise((resolve) => {
const dialog = frappe.prompt(
{
label: __(frappe.unscrub(account)),
fieldname: account,
fieldtype: "Link",
options: "Account",
get_query: () => ({
filters: {
company: frm.doc.company,
},
}),
},
(values) => resolve(values?.[account]),
__("Please Specify Account")
);
dialog.on_hide = () => resolve("");
});
}
function get_deduction_amount_precision() {
return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount"));
}

View File

@@ -56,7 +56,6 @@
"section_break_34",
"total_allocated_amount",
"base_total_allocated_amount",
"set_exchange_gain_loss",
"column_break_36",
"unallocated_amount",
"difference_amount",
@@ -391,11 +390,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "set_exchange_gain_loss",
"fieldtype": "Button",
"label": "Set Exchange Gain / Loss"
},
{
"fieldname": "column_break_36",
"fieldtype": "Column Break"
@@ -802,7 +796,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-05-31 17:07:06.197249",
"modified": "2024-11-07 11:19:19.320883",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -985,6 +985,7 @@ class PaymentEntry(AccountsController):
self.set_amounts_in_company_currency()
self.set_total_allocated_amount()
self.set_unallocated_amount()
self.set_exchange_gain_loss()
self.set_difference_amount()
def validate_amounts(self):
@@ -1083,10 +1084,10 @@ class PaymentEntry(AccountsController):
if d.exchange_rate is None:
d.exchange_rate = 1
allocated_amount_in_pe_exchange_rate = flt(
allocated_amount_in_ref_exchange_rate = flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate
return base_allocated_amount
def set_total_allocated_amount(self):
@@ -1104,29 +1105,80 @@ class PaymentEntry(AccountsController):
def set_unallocated_amount(self):
self.unallocated_amount = 0
if self.party:
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
if (
self.payment_type == "Receive"
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
and self.total_allocated_amount
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
):
self.unallocated_amount = (
self.base_received_amount + total_deductions - self.base_total_allocated_amount
) / self.source_exchange_rate
self.unallocated_amount -= included_taxes
elif (
self.payment_type == "Pay"
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
and self.total_allocated_amount
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
):
self.unallocated_amount = (
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
) / self.target_exchange_rate
self.unallocated_amount -= included_taxes
if not self.party:
return
deductions_to_consider = sum(
flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss
)
included_taxes = self.get_included_taxes()
if self.payment_type == "Receive" and self.base_total_allocated_amount < (
self.base_paid_amount + deductions_to_consider
):
self.unallocated_amount = (
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.source_exchange_rate
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
self.base_received_amount - deductions_to_consider
):
self.unallocated_amount = (
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.target_exchange_rate
def set_exchange_gain_loss(self):
exchange_gain_loss = flt(
self.base_paid_amount - self.base_received_amount,
self.precision("amount", "deductions"),
)
exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss]
exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None
for row in exchange_gain_loss_rows:
self.remove(row)
if not exchange_gain_loss:
if exchange_gain_loss_row:
self.remove(exchange_gain_loss_row)
return
if not exchange_gain_loss_row:
values = frappe.get_cached_value(
"Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True
)
for fieldname, value in values.items():
if value:
continue
label = _(frappe.get_meta("Company").get_label(fieldname))
return frappe.msgprint(
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
label, get_link_to_form("Company", self.company)
),
title=_("Missing Default in Company"),
indicator="red" if self.docstatus.is_submitted() else "yellow",
raise_exception=self.docstatus.is_submitted(),
)
exchange_gain_loss_row = self.append(
"deductions",
{
"account": values.exchange_gain_loss_account,
"cost_center": values.cost_center,
"is_exchange_gain_loss": 1,
},
)
exchange_gain_loss_row.amount = exchange_gain_loss
def set_difference_amount(self):
base_unallocated_amount = flt(self.unallocated_amount) * (
@@ -1154,11 +1206,13 @@ class PaymentEntry(AccountsController):
def get_included_taxes(self):
included_taxes = 0
for tax in self.get("taxes"):
if tax.included_in_paid_amount:
if tax.add_deduct_tax == "Add":
included_taxes += tax.base_tax_amount
else:
included_taxes -= tax.base_tax_amount
if not tax.included_in_paid_amount:
continue
if tax.add_deduct_tax == "Add":
included_taxes += tax.base_tax_amount
else:
included_taxes -= tax.base_tax_amount
return included_taxes
@@ -1318,11 +1372,19 @@ class PaymentEntry(AccountsController):
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update(
{
dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr + "_in_account_currency": d.allocated_amount,
"cost_center": cost_center,
}
self.get_gl_dict(
{
"account": self.party_account,
"party_type": self.party_type,
"party": self.party,
"against": against_account,
"account_currency": self.party_account_currency,
"cost_center": cost_center,
dr_or_cr + "_in_account_currency": d.allocated_amount,
dr_or_cr: allocated_amount_in_company_currency,
},
item=self,
)
)
if self.book_advance_payments_in_separate_party_account:
@@ -2006,8 +2068,8 @@ class PaymentEntry(AccountsController):
def get_matched_payment_request_of_references(references=None):
"""
Get those `Payment Requests` which are matched with `References`.\n
- Amount must be same.
- Only single `Payment Request` available for this amount.
- Amount must be same.
- Only single `Payment Request` available for this amount.
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
@@ -2109,7 +2171,7 @@ def get_outstanding_of_references_with_payment_term(references=None):
def get_outstanding_of_references_with_no_payment_term(references):
"""
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
- Fetch outstanding amount from `References` it self.
- Fetch outstanding amount from `References` it self.
Note: `None` is used for allocation of `Payment Request`
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
@@ -2923,9 +2985,6 @@ def get_payment_entry(
update_accounting_dimensions(pe, doc)
if party_account and bank:
pe.set_exchange_rate(ref_doc=doc)
pe.set_amounts()
if discount_amount:
base_total_discount_loss = 0
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
@@ -2935,7 +2994,8 @@ def get_payment_entry(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
pe.set_difference_amount()
pe.set_exchange_rate(ref_doc=doc)
pe.set_amounts()
# If PE is created from PR directly, then no need to find open PRs for the references
if not created_from_payment_request:
@@ -2947,7 +3007,7 @@ def get_payment_entry(
def get_open_payment_requests_for_references(references=None):
"""
Fetch all unpaid Payment Requests for the references. \n
- Each reference can have multiple Payment Requests. \n
- Each reference can have multiple Payment Requests. \n
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
"""
@@ -2971,6 +3031,7 @@ def get_open_payment_requests_for_references(references=None):
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
).run(as_dict=True)
@@ -3285,13 +3346,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
pe.set_gain_or_loss(
account_details={
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, account_type),
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": discount_amount * positive_negative,
}
},
)

View File

@@ -488,16 +488,9 @@ class TestPaymentEntry(IntegrationTestCase):
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
# Exchange loss
self.assertEqual(pe.difference_amount, 300.0)
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 300.0,
},
)
self.assertEqual(pe.deductions[-1].amount, 300.0)
pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
pe.deductions[-1].cost_center = "_Test Cost Center - _TC"
pe.insert()
pe.submit()
@@ -561,16 +554,10 @@ class TestPaymentEntry(IntegrationTestCase):
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
self.assertEqual(pe.difference_amount, 100)
self.assertEqual(pe.deductions[0].amount, 100)
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 100,
},
)
pe.insert()
pe.submit()
@@ -660,16 +647,9 @@ class TestPaymentEntry(IntegrationTestCase):
pe.set_exchange_rate()
pe.set_amounts()
self.assertEqual(pe.difference_amount, 500)
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 500,
},
)
self.assertEqual(pe.deductions[0].amount, 500)
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
pe.insert()
pe.submit()

View File

@@ -9,6 +9,7 @@
"cost_center",
"amount",
"column_break_2",
"is_exchange_gain_loss",
"description"
],
"fields": [
@@ -45,12 +46,20 @@
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"default": "0",
"depends_on": "eval:doc.is_exchange_gain_loss",
"fieldname": "is_exchange_gain_loss",
"fieldtype": "Check",
"label": "Is Exchange Gain / Loss?",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:09.454552",
"modified": "2024-11-05 16:07:47.307971",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",

View File

@@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document):
amount: DF.Currency
cost_center: DF.Link
description: DF.SmallText | None
is_exchange_gain_loss: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -3,7 +3,7 @@ import json
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
@@ -563,6 +563,8 @@ def make_payment_request(**args):
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
grand_total -= existing_payment_request_amount
if not grand_total:
@@ -582,6 +584,15 @@ def make_payment_request(**args):
else:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
if existing_paid_amount:
if ref_doc.party_account_currency == ref_doc.currency:
if ref_doc.conversion_rate:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
else:
grand_total -= flt(existing_paid_amount)
else:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -758,6 +769,29 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
return response[0][0] if response[0] else 0
def get_existing_paid_amount(doctype, name):
PL = frappe.qb.DocType("Payment Ledger Entry")
PER = frappe.qb.DocType("Payment Entry Reference")
query = (
frappe.qb.from_(PL)
.left_join(PER)
.on(
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
)
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
.where(PL.against_voucher_type.eq(doctype))
.where(PL.against_voucher_no.eq(name))
.where(PL.amount < 0)
.where(PL.delinked == 0)
.where(PER.docstatus == 1)
.where(PER.payment_request.isnull())
)
response = query.run()
return response[0][0] if response[0] else 0
def get_gateway_details(args): # nosemgrep
"""
Return gateway and payment account of default payment gateway

View File

@@ -8,6 +8,7 @@ from unittest.mock import patch
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -689,3 +690,48 @@ class TestPaymentRequest(IntegrationTestCase):
so.load_from_db()
self.assertEqual(so.advance_payment_status, "Requested")
def test_partial_paid_invoice_with_payment_request(self):
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
si.save()
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PAYEE0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
si.load_from_db()
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount)
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
pi.save()
pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0001"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe.cancel()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pi.load_from_db()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
self.assertEqual(pr.grand_total, pi.outstanding_amount)

View File

@@ -171,9 +171,7 @@ class PeriodClosingVoucher(AccountsController):
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
for dimensions, account_balances in pl_account_balances.items():
for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt(
balances.credit_in_account_currency
)
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
if balance_in_company_currency and acc != "balances":
self.pl_accounts_reverse_gle.append(
self.get_gle_for_pl_account(acc, balances, dimensions)

View File

@@ -147,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_payments(doc, frm, false);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
@@ -172,7 +172,7 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty);
refresh_payments(d, frm);
refresh_payments(d, frm, true);
refresh_taxes(d, frm);
});
}
@@ -186,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
});
}
function refresh_payments(d, frm) {
function refresh_payments(d, frm, is_new) {
d.payments.forEach((p) => {
const payment = frm.doc.payment_reconciliation.find(
(pay) => pay.mode_of_payment === p.mode_of_payment
@@ -196,9 +196,7 @@ function refresh_payments(d, frm) {
}
if (payment) {
payment.expected_amount += flt(p.amount);
if (payment.closing_amount === 0) {
payment.closing_amount = payment.expected_amount;
}
if (is_new) payment.closing_amount = payment.expected_amount;
payment.difference = payment.closing_amount - payment.expected_amount;
} else {
frm.add_child("payment_reconciliation", {

View File

@@ -48,6 +48,7 @@
"shipping_address",
"company_address",
"company_address_display",
"company_contact_person",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -1549,10 +1550,10 @@
},
{
"fieldname": "utm_medium",
"print_hide": 1,
"fieldtype": "Link",
"label": "Medium",
"options": "UTM Medium"
"options": "UTM Medium",
"print_hide": 1
},
{
"fieldname": "utm_campaign",
@@ -1571,12 +1572,19 @@
"oldfieldtype": "Select",
"options": "UTM Source",
"print_hide": 1
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2024-06-28 10:55:34.941200",
"modified": "2024-11-26 13:10:50.309570",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1630,4 +1638,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -32,12 +32,8 @@ class POSInvoice(SalesInvoice):
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
SalesInvoiceAdvance,
)
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
SalesInvoicePayment,
)
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
SalesInvoiceTimesheet,
)
@@ -74,6 +70,7 @@ class POSInvoice(SalesInvoice):
company: DF.Link
company_address: DF.Link | None
company_address_display: DF.TextEditor | None
company_contact_person: DF.Link | None
consolidated_invoice: DF.Link | None
contact_display: DF.SmallText | None
contact_email: DF.Data | None

View File

@@ -1742,6 +1742,30 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
# Cost of Item is zero in Purchase Receipt
pr = make_purchase_receipt(qty=1, rate=0)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 0)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 150
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 150)
# Increase the cost of the item
pr = make_purchase_receipt(qty=1, rate=100)

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import inspect
import frappe
from frappe import _, qb
from frappe.model.document import Document
@@ -142,6 +144,8 @@ class RepostAccountingLedger(Document):
@frappe.whitelist()
def start_repost(account_repost_doc=str) -> None:
from erpnext.accounts.general_ledger import make_reverse_gl_entries
frappe.flags.through_repost_accounting_ledger = True
if account_repost_doc:
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
@@ -177,6 +181,14 @@ def start_repost(account_repost_doc=str) -> None:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)
doc.make_gl_entries()
else:
if hasattr(doc, "make_gl_entries") and callable(doc.make_gl_entries):
if not repost_doc.delete_cancelled_entries:
if "cancel" in inspect.getfullargspec(doc.make_gl_entries):
doc.make_gl_entries(cancel=1)
else:
make_reverse_gl_entries(voucher_type=doc.doctype, voucher_no=doc.name)
doc.make_gl_entries()
def get_allowed_types_from_settings():

View File

@@ -161,8 +161,9 @@
"dispatch_address",
"company_address_section",
"company_address",
"company_addr_col_break",
"company_address_display",
"company_addr_col_break",
"company_contact_person",
"terms_tab",
"payment_schedule_section",
"ignore_default_payment_terms_template",
@@ -2203,6 +2204,13 @@
"oldfieldtype": "Select",
"options": "UTM Source",
"print_hide": 1
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
@@ -2215,7 +2223,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-07-18 15:30:39.428519",
"modified": "2024-11-26 12:34:09.110690",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2270,4 +2278,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -98,6 +98,7 @@ class SalesInvoice(SellingController):
company: DF.Link
company_address: DF.Link | None
company_address_display: DF.TextEditor | None
company_contact_person: DF.Link | None
company_tax_id: DF.Data | None
contact_display: DF.SmallText | None
contact_email: DF.Data | None
@@ -153,7 +154,6 @@ class SalesInvoice(SellingController):
party_account_currency: DF.Link | None
payment_schedule: DF.Table[PaymentSchedule]
payment_terms_template: DF.Link | None
payment_url: DF.Data | None
payments: DF.Table[SalesInvoicePayment]
plc_conversion_rate: DF.Float
po_date: DF.Date | None
@@ -1713,6 +1713,9 @@ class SalesInvoice(SellingController):
def update_project(self):
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
if self.project and self.project not in unique_projects:
unique_projects.append(self.project)
for p in unique_projects:
project = frappe.get_doc("Project", p)
project.update_billed_amount()

View File

@@ -4167,6 +4167,88 @@ class TestSalesInvoice(IntegrationTestCase):
self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total)
@IntegrationTestCase.change_settings("Accounts Settings", {"enable_common_party_accounting": True})
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.setup.utils import get_exchange_rate
creditors = create_account(
account_name="Creditors INR",
parent_account="Accounts Payable - _TC",
company="_Test Company",
account_currency="INR",
account_type="Payable",
)
debtors = create_account(
account_name="Debtors USD",
parent_account="Accounts Receivable - _TC",
company="_Test Company",
account_currency="USD",
account_type="Receivable",
)
# create a customer
customer = make_customer(customer="_Test Common Party USD")
cust_doc = frappe.get_doc("Customer", customer)
cust_doc.default_currency = "USD"
test_account_details = {
"company": "_Test Company",
"account": debtors,
}
cust_doc.append("accounts", test_account_details)
cust_doc.save()
# create a supplier
supplier = create_supplier(supplier_name="_Test Common Party INR").name
supp_doc = frappe.get_doc("Supplier", supplier)
supp_doc.default_currency = "INR"
test_account_details = {
"company": "_Test Company",
"account": creditors,
}
supp_doc.append("accounts", test_account_details)
supp_doc.save()
# create a party link between customer & supplier
create_party_link("Supplier", supplier, customer)
# create a sales invoice
si = create_sales_invoice(
customer=customer,
currency="USD",
conversion_rate=get_exchange_rate("USD", "INR"),
debit_to=debtors,
do_not_save=1,
)
si.party_account_currency = "USD"
si.save()
si.submit()
# check outstanding of sales invoice
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(flt(si.outstanding_amount), 0.0)
# check creation of journal entry
jv = frappe.get_all(
"Journal Entry Account",
{
"account": si.debit_to,
"party_type": "Customer",
"party": si.customer,
"reference_type": si.doctype,
"reference_name": si.name,
},
pluck="credit_in_account_currency",
)
self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total)
def test_invoice_remarks(self):
si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][0])
si.po_no = "Test PO"
@@ -4194,6 +4276,20 @@ class TestSalesInvoice(IntegrationTestCase):
self.assertTrue(all([x == "Credit Note" for x in gl_entries]))
def test_total_billed_amount(self):
si = create_sales_invoice(do_not_submit=True)
project = frappe.new_doc("Project")
project.project_name = "Test Total Billed Amount"
project.save()
si.project = project.name
si.save()
si.submit()
doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -262,6 +262,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
pe1.paid_from = self.debtors_usd
pe1.paid_from_account_currency = "USD"
pe1.source_exchange_rate = 75
pe1.paid_amount = 100
pe1.received_amount = 75 * 100
pe1.save()
# Allocate payment against both invoices
@@ -279,6 +280,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
pe2.paid_from = self.debtors_usd
pe2.paid_from_account_currency = "USD"
pe2.source_exchange_rate = 75
pe2.paid_amount = 100
pe2.received_amount = 75 * 100
pe2.save()
# Allocate payment against both invoices

View File

@@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag
try:
from frappe.contacts.doctype.address.address import render_address as _render_address
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render_address
PURCHASE_TRANSACTION_TYPES = {
"Supplier Quotation",
"Purchase Order",
@@ -987,10 +993,4 @@ def add_party_account(party_type, party, company, account):
def render_address(address, check_permissions=True):
try:
from frappe.contacts.doctype.address.address import render_address as _render
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render
return frappe.call(_render, address, check_permissions=check_permissions)
return frappe.call(_render_address, address, check_permissions=check_permissions)

View File

@@ -1013,7 +1013,7 @@ class ReceivablePayableReport:
def get_columns(self):
self.columns = []
self.add_column(_("Posting Date"), fieldtype="Date")
self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
self.add_column(
label=_("Party Type"),
fieldname="party_type",
@@ -1027,8 +1027,15 @@ class ReceivablePayableReport:
options="party_type",
width=180,
)
if self.account_type == "Receivable":
label = _("Receivable Account")
elif self.account_type == "Payable":
label = _("Payable Account")
else:
label = _("Party Account")
self.add_column(
label=self.account_type + " Account",
label=label,
fieldname="party_account",
fieldtype="Link",
options="Account",
@@ -1066,7 +1073,7 @@ class ReceivablePayableReport:
width=180,
)
self.add_column(label=_("Due Date"), fieldtype="Date")
self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date")
if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")

View File

@@ -89,7 +89,9 @@ def get_data(filters):
& (DepreciationSchedule.schedule_date == d.posting_date)
)
).run(as_dict=True)
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
asset_data.accumulated_depreciation_amount = (
query[0]["accumulated_depreciation_amount"] if query else 0
)
else:
asset_data.accumulated_depreciation_amount += d.debit

View File

@@ -7,6 +7,7 @@ from frappe import _
from frappe.utils import cint, flt
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
get_columns,
get_data,
get_filtered_list_for_consolidated_report,
@@ -101,6 +102,9 @@ def execute(filters=None):
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
)
if filters.get("selected_view") == "Growth":
compute_growth_view_data(data, period_list)
return columns, data, message, chart, report_summary, primitive_summary

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import copy
import functools
import math
import re
@@ -668,3 +669,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list):
filtered_summary_list.append(period)
return filtered_summary_list
def compute_growth_view_data(data, columns):
data_copy = copy.deepcopy(data)
for row_idx in range(len(data_copy)):
for column_idx in range(1, len(columns)):
previous_period_key = columns[column_idx - 1].get("key")
current_period_key = columns[column_idx].get("key")
current_period_value = data_copy[row_idx].get(current_period_key)
previous_period_value = data_copy[row_idx].get(previous_period_key)
annual_growth = 0
if current_period_value is None:
data[row_idx][current_period_key] = None
continue
if previous_period_value == 0 and current_period_value > 0:
annual_growth = 1
elif previous_period_value > 0:
annual_growth = (current_period_value - previous_period_value) / previous_period_value
growth_percent = round(annual_growth * 100, 2)
data[row_idx][current_period_key] = growth_percent
def compute_margin_view_data(data, columns, accumulated_values):
if not columns:
return
if not accumulated_values:
columns.append({"key": "total"})
data_copy = copy.deepcopy(data)
base_row = None
for row in data_copy:
if row.get("account_name") == _("Income"):
base_row = row
break
if not base_row:
return
for row_idx in range(len(data_copy)):
# Taking the total income from each column (for all the financial years) as the base (100%)
row = data_copy[row_idx]
if not row:
continue
for column in columns:
curr_period = column.get("key")
base_value = base_row[curr_period]
curr_value = row[curr_period]
if curr_value is None or base_value <= 0:
data[row_idx][curr_period] = None
continue
margin_percent = round((curr_value / base_value) * 100, 2)
data[row_idx][curr_period] = margin_percent

View File

@@ -1,82 +1,180 @@
<h2 class="text-center">{%= __("Statement of Account") %}</h2>
<h4 class="text-center">
{% if (filters.party_name) { %}
{%= filters.party_name %}
{% } else if (filters.party && filters.party.length) { %}
{%= filters.party %}
{% } else if (filters.account) { %}
{%= filters.account %}
{% } %}
</h4>
<!-- Modified on 25-11-2024
-->
<h6 class="text-center">
{% if (filters.tax_id) { %}
{%= __("Tax Id: ")%} {%= filters.tax_id %}
{% } %}
</h6>
<style type="text/css">
/* General styles for both screen display and print */
body, html {
margin-top: 10;
padding: 0;
width: 100%;
height: auto; /* Allow content to expand */
font-family: Arial, sans-serif; /* Example font */
}
<h5 class="text-center">
{%= frappe.datetime.str_to_user(filters.from_date) %}
{%= __("to") %}
{%= frappe.datetime.str_to_user(filters.to_date) %}
</h5>
<hr>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 12%">{%= __("Date") %}</th>
<th style="width: 15%">{%= __("Reference") %}</th>
<th style="width: 25%">{%= __("Remarks") %}</th>
<th style="width: 15%">{%= __("Debit") %}</th>
<th style="width: 15%">{%= __("Credit") %}</th>
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
{% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %}
<br>{%= data[i].voucher_no %}
</td>
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
<span>
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
<br>
{% } %}
/* Ensure consistent letter spacing across all media */
.title-letter-spacing {
letter-spacing: .2rem;
}
{% if(data[i].remarks) { %}
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
{% } else if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</span>
</td>
<td style="text-align: right">
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
</td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
</td>
{% } else { %}
<td></td>
<td></td>
<td><b>{%= frappe.format(data[i].account, {fieldtype: "Link"}) || "&nbsp;" %}</b></td>
<td style="text-align: right">
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
</td>
<td style="text-align: right">
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
</td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i].balance, filters.presentation_currency || data[i].account_currency) %}
</td>
/* Styles specific to printing and PDF generation */
@media print {
/* Set page size and margins for printing */
@page {
size: A4; /* Use fixed A4 page size */
margin-top: 10mm;
}
/* Force a page break before elements with the class "page-break" */
.page-break {
page-break-before: always;
margin-top: 10mm; /* Add some space after the break */
}
/* Ensure table headers repeat on each printed page */
thead {
display: table-header-group;
}
/* Ensure table footers repeat on each printed page */
tfoot {
display: table-footer-group;
}
th, td {
padding: 1px;
border: 1px solid black; /* Example border for clarity */
}
/* Hide elements that should not appear in print (optional) */
.no-print {
display: none !important;
}
}
</style>
<br>
<div style="font-family:Arial">
<div>
<div class="title-letter-spacing" style="text-align:center; font-size:15px; text-decoration:underline;">
<b>
{%= __("STATEMENT OF ACCOUNTS") %}<br>
{% if (filters.party_name) { %}
<br>{%= filters.party_name %}
{% } else if (filters.party && filters.party.length) { %}
<br>{%= filters.party %}
{% } else if (filters.account) { %}
<br>{%= filters.account %}
{% } else { %}
<br>{%= __("All Parties ") %}
{% } %}
</b>
</div>
<div style="text-align:center; font-size:13px;">
<b>
{% if(filters.party_type) { %}
[ {%= filters.party_type %} ]<br>
{% } %}
{%= frappe.datetime.str_to_user(filters.from_date) %}
{%= __("to") %}
{%= frappe.datetime.str_to_user(filters.to_date) %}<br><br>
</b>
</div>
</div>
<table style="width:100%; font-size: 11px">
<thead>
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
<td style="border: 1.5px solid black; width: 7em">DATE</td>
<td style="border: 1.5px solid black">PARTICULARS</td>
{% if(filters.show_remarks) { %}
<td style="border: 1.5px solid black">REMARKS</td>
{% } %}
<td style="border: 1.5px solid black; width: 9em">DEBIT</td>
<td style="border: 1.5px solid black; width: 9em">CREDIT</td>
<td style="border: 1.5px solid black; width: 10.2em">BALANCE</td>
</tr>
{% } %}
</tbody>
</table>
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr style="border-bottom: 1px solid black">
{% if(data[i].posting_date) { %}
<td style="text-align: center; border: 1px dotted black">
{%= frappe.datetime.str_to_user(data[i].posting_date) %}
</td>
<td style="border-right: 1px dotted black">
{%= data[i].voucher_type %} {%= data[i].voucher_no %}
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
{% } %}<br>
{% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td style="border-right: 1px dotted black; font-size: 10px">
{% if(data[i].remarks != "No Remarks" && data[i].remarks != "") { %}
{%= __("Remarks") %}: {%= data[i].remarks %}<br>
{% } %}
</td>
{% } %}
<td style="text-align: right; border-right: 1px dotted black">
{% if data[i].debit != 0 %}
{%= format_currency(data[i].debit, filters.presentation_currency) %}
{% } %}
</td>
<td style="text-align: right; border-right: 1px dotted black">
{% if data[i].credit != 0 %}
{%= format_currency(data[i].credit, filters.presentation_currency) %}
{% } %}
</td>
{% } else { %}
<td style="text-align: center; border: 1px dotted black">
{% if(i == 0) { %}
{%= frappe.datetime.str_to_user(filters.from_date) %}
{% } %}
</td>
<td style="text-align: left; border-right: 1px dotted black"><b>
{% if(i == l-2) { %}
{%= "Total" %}
{% } else { %}
{% if(i == l-1) { %}
{%= "Closing [Opening + Total] " %}
{% } else { %}
{%= frappe.format(data[i].account, {fieldtype: "Link"}) || "&nbsp;" %}
{% } %}
{% } %}</b>
</td>
{% if(filters.show_remarks) { %} <td style="text-align: left; border-right: 1px dotted black"></td>{% } %}
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != 0){ %}
{% if(i != l-1){ %}
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency) %}
{% } %}
{% } %}
</td>
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != 0){ %}
{% if(i != l-1){ %}
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency) %}
{% } %}
{% } %}
</td>
{% } %}
{% if(i == l-1) { %}
<td style="text-align: right; font-weight:bold; border-right: 1px dotted black">
{%= format_currency(data[i].balance, filters.presentation_currency) %}
{% if(data[i].balance < 0){ %}Cr{% } %}
{% if(data[i].balance > 0){ %}Dr{% } %}
</td>
{% } else { %}
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != l-2) { %}
{%= format_currency(data[i].balance, filters.presentation_currency) %}
{% } %}
</td>
{% } %}
</tr>
{% endfor%}
</tbody>
</table>
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
</div>

View File

@@ -421,10 +421,10 @@ class GrossProfitGenerator:
self.load_invoice_items()
self.get_delivery_notes()
self.load_product_bundle()
if filters.group_by == "Invoice":
self.group_items_by_invoice()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
self.process()
@@ -440,6 +440,7 @@ class GrossProfitGenerator:
if grouped_by_invoice:
buying_amount = 0
base_amount = 0
for row in reversed(self.si_list):
if self.filters.get("group_by") == "Monthly":
@@ -480,12 +481,11 @@ class GrossProfitGenerator:
else:
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
if grouped_by_invoice:
if row.indent == 1.0:
buying_amount += row.buying_amount
elif row.indent == 0.0:
row.buying_amount = buying_amount
buying_amount = 0
if grouped_by_invoice and row.indent == 0.0:
row.buying_amount = buying_amount
row.base_amount = base_amount
buying_amount = 0
base_amount = 0
# get buying rate
if flt(row.qty):
@@ -495,11 +495,19 @@ class GrossProfitGenerator:
if self.is_not_invoice_row(row):
row.buying_rate, row.base_rate = 0.0, 0.0
if self.is_not_invoice_row(row):
self.update_return_invoices(row)
if grouped_by_invoice and row.indent == 1.0:
buying_amount += row.buying_amount
base_amount += row.base_amount
# calculate gross profit
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
if row.base_amount:
row.gross_profit_percent = flt(
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision
(row.gross_profit / row.base_amount) * 100.0,
self.currency_precision,
)
else:
row.gross_profit_percent = 0.0
@@ -510,33 +518,29 @@ class GrossProfitGenerator:
if self.grouped:
self.get_average_rate_based_on_group_by()
def update_return_invoices(self, row):
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
returned_item_row.qty = 0
returned_item_row.base_amount = 0
else:
row.qty = 0
row.base_amount = 0
returned_item_row.qty += row.qty
returned_item_row.base_amount += row.base_amount
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
def get_average_rate_based_on_group_by(self):
for key in list(self.grouped):
if self.filters.get("group_by") == "Invoice":
for row in self.grouped[key]:
if row.indent == 1.0:
if (
row.parent in self.returned_invoices
and row.item_code in self.returned_invoices[row.parent]
):
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
returned_item_row.qty = 0
else:
row.qty = 0
returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(
flt(row.qty) * flt(row.buying_rate), self.currency_precision
)
if flt(row.qty) or row.base_amount:
row = self.set_average_rate(row)
self.grouped_data.append(row)
elif self.filters.get("group_by") == "Payment Term":
if self.filters.get("group_by") == "Payment Term":
for i, row in enumerate(self.grouped[key]):
invoice_portion = 0
@@ -556,7 +560,7 @@ class GrossProfitGenerator:
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
else:
elif self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]):
if i == 0:
new_row = row
@@ -632,6 +636,7 @@ class GrossProfitGenerator:
if packed_item.get("parent_detail_docname") == row.item_row:
packed_item_row = row.copy()
packed_item_row.warehouse = packed_item.warehouse
packed_item_row.qty = packed_item.total_qty * -1
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
return flt(buying_amount, self.currency_precision)
@@ -664,7 +669,9 @@ class GrossProfitGenerator:
else:
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
parenttype = row.parenttype
parent = row.invoice or row.parent
if row.dn_detail:
parenttype, parent = "Delivery Note", row.delivery_note
@@ -847,6 +854,7 @@ class GrossProfitGenerator:
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group,
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
@@ -907,6 +915,7 @@ class GrossProfitGenerator:
"""
grouped = OrderedDict()
product_bundles = self.product_bundles.get("Sales Invoice", {})
for row in self.si_list:
# initialize list with a header row for each new parent
@@ -917,8 +926,7 @@ class GrossProfitGenerator:
)
# if item is a bundle, add it's components as seperate rows
if frappe.db.exists("Product Bundle", row.item_code):
bundled_items = self.get_bundle_items(row)
if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code):
for x in bundled_items:
bundle_item = self.get_bundle_item_row(row, x)
grouped.get(row.parent).append(bundle_item)
@@ -954,47 +962,40 @@ class GrossProfitGenerator:
"item_row": None,
"is_return": row.is_return,
"cost_center": row.cost_center,
"base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"),
"base_net_amount": row.invoice_base_net_total,
}
)
def get_bundle_items(self, product_bundle):
return frappe.get_all(
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
)
def get_bundle_item_row(self, product_bundle, item):
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
def get_bundle_item_row(self, row, item):
return frappe._dict(
{
"parent_invoice": product_bundle.item_code,
"indent": product_bundle.indent + 1,
"parent_invoice": row.item_code,
"parenttype": row.parenttype,
"indent": row.indent + 1,
"parent": None,
"invoice_or_item": item.item_code,
"posting_date": product_bundle.posting_date,
"posting_time": product_bundle.posting_time,
"project": product_bundle.project,
"customer": product_bundle.customer,
"customer_group": product_bundle.customer_group,
"posting_date": row.posting_date,
"posting_time": row.posting_time,
"project": row.project,
"customer": row.customer,
"customer_group": row.customer_group,
"item_code": item.item_code,
"item_name": item_name,
"description": description,
"warehouse": product_bundle.warehouse,
"item_group": item_group,
"brand": brand,
"dn_detail": product_bundle.dn_detail,
"delivery_note": product_bundle.delivery_note,
"qty": (flt(product_bundle.qty) * flt(item.qty)),
"item_row": None,
"is_return": product_bundle.is_return,
"cost_center": product_bundle.cost_center,
"item_name": item.item_name,
"description": item.description,
"warehouse": item.warehouse or row.warehouse,
"update_stock": row.update_stock,
"item_group": "",
"brand": "",
"dn_detail": row.dn_detail,
"delivery_note": row.delivery_note,
"qty": item.total_qty * -1,
"item_row": row.item_row,
"is_return": row.is_return,
"cost_center": row.cost_center,
"invoice": row.parent,
}
)
def get_bundle_item_details(self, item_code):
return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"])
def get_stock_ledger_entries(self, item_code, warehouse):
if item_code and warehouse:
if (item_code, warehouse) not in self.sle:

View File

@@ -421,12 +421,12 @@ class TestGrossProfit(IntegrationTestCase):
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 0.0,
"avg._selling_rate": 0.0,
"avg._selling_rate": 100,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"selling_amount": 0.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
"gross_profit": 0.0,
"gross_profit_%": 0.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
# Both items of Invoice should have '0' qty

View File

@@ -7,6 +7,8 @@ from frappe import _
from frappe.utils import flt
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
compute_margin_view_data,
get_columns,
get_data,
get_filtered_list_for_consolidated_report,
@@ -68,6 +70,12 @@ def execute(filters=None):
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
)
if filters.get("selected_view") == "Growth":
compute_growth_view_data(data, period_list)
if filters.get("selected_view") == "Margin":
compute_margin_view_data(data, period_list, filters.accumulated_values)
return columns, data, None, chart, report_summary, primitive_summary

View File

@@ -93,14 +93,14 @@ class TestUtils(IntegrationTestCase):
payment_entry.deductions = []
payment_entry.save()
# below is the difference between base_received_amount and base_paid_amount
self.assertEqual(payment_entry.difference_amount, -4855.0)
# below is the difference between base_paid_amount and base_received_amount (exchange gain)
self.assertEqual(payment_entry.deductions[0].amount, -4855.0)
payment_entry.target_exchange_rate = 62.9
payment_entry.save()
# below is due to change in exchange rate
self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
# after changing the exchange rate, there is no exchange gain / loss
self.assertEqual(payment_entry.deductions, [])
payment_entry.references = []
self.assertEqual(payment_entry.difference_amount, 0.0)

View File

@@ -416,7 +416,7 @@ frappe.ui.form.on("Asset", {
}
frm.dashboard.render_graph({
title: "Asset Value",
title: __("Asset Value"),
data: {
labels: x_intervals,
datasets: [

View File

@@ -8,29 +8,20 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"company",
"naming_series",
"item_code",
"item_name",
"asset_name",
"asset_category",
"location",
"image",
"column_break_3",
"status",
"company",
"asset_owner",
"asset_owner_company",
"is_existing_asset",
"is_composite_asset",
"supplier",
"customer",
"image",
"journal_entry_for_scrap",
"column_break_3",
"naming_series",
"asset_name",
"asset_category",
"location",
"split_from",
"custodian",
"department",
"disposal_date",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"purchase_details_section",
"purchase_receipt",
"purchase_receipt_item",
@@ -40,10 +31,12 @@
"available_for_use_date",
"column_break_23",
"gross_purchase_amount",
"purchase_amount",
"asset_quantity",
"additional_asset_cost",
"total_asset_cost",
"section_break_23",
"disposal_date",
"depreciation_tab",
"calculate_depreciation",
"column_break_33",
"opening_accumulated_depreciation",
@@ -60,7 +53,7 @@
"next_depreciation_date",
"depreciation_schedule_sb",
"depreciation_schedule_view",
"insurance_details",
"insurance_details_tab",
"policy_number",
"insurer",
"insured_value",
@@ -68,22 +61,29 @@
"insurance_start_date",
"insurance_end_date",
"comprehensive_insurance",
"section_break_31",
"maintenance_required",
"other_details",
"status",
"booked_fixed_asset",
"column_break_51",
"purchase_amount",
"other_info_tab",
"accounting_dimensions_section",
"cost_center",
"section_break_jtou",
"custodian",
"default_finance_book",
"depr_entry_posting_status",
"amended_from"
"booked_fixed_asset",
"customer",
"supplier",
"column_break_51",
"department",
"split_from",
"journal_entry_for_scrap",
"amended_from",
"maintenance_required",
"connections_tab"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"label": "Series",
"options": "ACC-ASS-.YYYY.-"
},
{
@@ -124,6 +124,7 @@
"read_only": 1
},
{
"default": "Company",
"fieldname": "asset_owner",
"fieldtype": "Select",
"label": "Asset Owner",
@@ -257,18 +258,10 @@
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
"fieldname": "section_break_23",
"fieldtype": "Section Break",
"label": "Depreciation"
},
{
"columns": 10,
"fieldname": "finance_books",
"fieldtype": "Table",
"label": "Finance Books",
"options": "Asset Finance Book"
},
{
@@ -310,12 +303,6 @@
"label": "Next Depreciation Date",
"no_copy": 1
},
{
"collapsible": 1,
"fieldname": "insurance_details",
"fieldtype": "Section Break",
"label": "Insurance details"
},
{
"fieldname": "policy_number",
"fieldtype": "Data",
@@ -350,25 +337,13 @@
"fieldtype": "Data",
"label": "Comprehensive Insurance"
},
{
"fieldname": "section_break_31",
"fieldtype": "Section Break",
"label": "Maintenance"
},
{
"allow_on_submit": 1,
"default": "0",
"description": "Check if Asset requires Preventive Maintenance or Calibration",
"fieldname": "maintenance_required",
"fieldtype": "Check",
"label": "Maintenance Required"
},
{
"collapsible": 1,
"fieldname": "other_details",
"fieldtype": "Section Break",
"label": "Other Details"
},
{
"allow_on_submit": 1,
"default": "Draft",
@@ -385,6 +360,7 @@
"default": "0",
"fieldname": "booked_fixed_asset",
"fieldtype": "Check",
"hidden": 1,
"label": "Booked Fixed Asset",
"no_copy": 1,
"read_only": 1
@@ -428,16 +404,6 @@
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"collapsible_depends_on": "is_existing_asset",
@@ -552,6 +518,37 @@
"hidden": 1,
"label": "Purchase Invoice Item",
"options": "Purchase Invoice Item"
},
{
"fieldname": "insurance_details_tab",
"fieldtype": "Tab Break",
"label": "Insurance"
},
{
"fieldname": "other_info_tab",
"fieldtype": "Tab Break",
"label": "Other Info"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "depreciation_tab",
"fieldtype": "Tab Break",
"label": "Depreciation"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "section_break_jtou",
"fieldtype": "Section Break",
"label": "Additional Info"
}
],
"idx": 72,
@@ -595,7 +592,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2024-08-26 23:28:29.095139",
"modified": "2024-11-29 14:25:56.436124",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -322,7 +322,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details",
child: row,
args: {
args: {
ctx: {
item_code: row.item_code,
warehouse: row.warehouse,
stock_qty: flt(row.stock_qty),
@@ -350,7 +350,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details",
child: row,
args: {
args: {
ctx: {
asset: row.asset,
doctype: me.frm.doc.doctype,
name: me.frm.doc.name,
@@ -377,7 +377,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details",
child: row,
args: {
args: {
ctx: {
item_code: row.item_code,
qty: flt(row.qty),
expense_account: row.expense_account,

View File

@@ -7,17 +7,19 @@
"field_order": [
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
"total_number_of_booked_depreciations",
"daily_prorata_based",
"shift_based",
"column_break_5",
"frequency_of_depreciation",
"total_number_of_depreciations",
"depreciation_start_date",
"column_break_5",
"salvage_value_percentage",
"expected_value_after_useful_life",
"rate_of_depreciation",
"daily_prorata_based",
"shift_based",
"section_break_jkdf",
"value_after_depreciation",
"rate_of_depreciation"
"column_break_sigk",
"total_number_of_booked_depreciations"
],
"fields": [
{
@@ -67,9 +69,10 @@
"columns": 1,
"default": "0",
"depends_on": "eval:parent.doctype == 'Asset'",
"description": "Expected Value After Useful Life",
"fieldname": "expected_value_after_useful_life",
"fieldtype": "Currency",
"label": "Expected Value After Useful Life",
"label": "Salvage Value",
"options": "Company:company:default_currency"
},
{
@@ -83,10 +86,9 @@
},
{
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"description": "In Percentage",
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
"label": "Rate of Depreciation"
"label": "Rate of Depreciation (%)"
},
{
"fieldname": "salvage_value_percentage",
@@ -108,16 +110,25 @@
},
{
"default": "0",
"depends_on": "total_number_of_booked_depreciations",
"fieldname": "total_number_of_booked_depreciations",
"fieldtype": "Int",
"label": "Total Number of Booked Depreciations ",
"read_only": 1
},
{
"fieldname": "section_break_jkdf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_sigk",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-05-21 15:48:20.907250",
"modified": "2024-11-29 14:36:54.399034",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@@ -93,10 +93,7 @@ frappe.ui.form.on("Purchase Order", {
get_materials_from_supplier: function (frm) {
let po_details = [];
if (
frm.doc.supplied_items &&
(flt(frm.doc.per_received, precision("per_received")) == 100 || frm.doc.status === "Closed")
) {
if (frm.doc.supplied_items && (flt(frm.doc.per_received) == 100 || frm.doc.status === "Closed")) {
frm.doc.supplied_items.forEach((d) => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name);
@@ -332,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (!["Closed", "Delivered"].includes(doc.status)) {
if (
this.frm.doc.status !== "Closed" &&
flt(this.frm.doc.per_received, precision("per_received")) < 100 &&
flt(this.frm.doc.per_billed, precision("per_billed")) < 100
flt(this.frm.doc.per_received) < 100 &&
flt(this.frm.doc.per_billed) < 100
) {
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
this.frm.add_custom_button(__("Update Items"), () => {
@@ -347,10 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
if (this.frm.has_perm("submit")) {
if (
flt(doc.per_billed, precision("per_billed")) < 100 ||
flt(doc.per_received, precision("per_received")) < 100
) {
if (flt(doc.per_billed) < 100 || flt(doc.per_received) < 100) {
if (doc.status != "On Hold") {
this.frm.add_custom_button(
__("Hold"),
@@ -388,7 +382,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
if (doc.status != "Closed") {
if (doc.status != "On Hold") {
if (flt(doc.per_received, precision("per_received")) < 100 && allow_receipt) {
if (flt(doc.per_received) < 100 && allow_receipt) {
this.frm.add_custom_button(
__("Purchase Receipt"),
() => {
@@ -419,7 +413,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
// Please do not add precision in the below flt function
if (flt(doc.per_billed, precision("per_billed")) < 100)
if (flt(doc.per_billed) < 100)
this.frm.add_custom_button(
__("Purchase Invoice"),
() => {
@@ -428,7 +422,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed, precision("per_billed")) < 100 && doc.status != "Delivered") {
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -436,7 +430,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed, precision("per_billed")) < 100) {
if (flt(doc.per_billed) < 100) {
this.frm.add_custom_button(
__("Payment Request"),
function () {

View File

@@ -2493,6 +2493,12 @@ class AccountsController(TransactionBase):
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
primary_account_currency = get_account_currency(primary_account)
secondary_account_currency = get_account_currency(secondary_account)
default_currency = erpnext.get_company_currency(self.company)
# Determine if multi-currency journal entry is needed
multi_currency = (
primary_account_currency != default_currency or secondary_account_currency != default_currency
)
jv = frappe.new_doc("Journal Entry")
jv.voucher_type = "Journal Entry"
@@ -2517,7 +2523,7 @@ class AccountsController(TransactionBase):
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
# update dimesions
# Update dimensions
dimensions_dict = frappe._dict()
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
@@ -2526,17 +2532,58 @@ class AccountsController(TransactionBase):
reconcilation_entry.update(dimensions_dict)
advance_entry.update(dimensions_dict)
if self.doctype == "Sales Invoice":
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount
# Calculate exchange rates if necessary
if multi_currency:
# Exchange rates for primary and secondary accounts
exc_rate_primary_to_default = (
1
if primary_account_currency == default_currency
else get_exchange_rate(primary_account_currency, default_currency, self.posting_date)
)
exc_rate_secondary_to_default = (
1
if secondary_account_currency == default_currency
else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date)
)
exc_rate_secondary_to_primary = (
1
if secondary_account_currency == primary_account_currency
else get_exchange_rate(
secondary_account_currency, primary_account_currency, self.posting_date
)
)
# Convert outstanding amount from secondary to primary account currency, if needed
os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default
os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary
if self.doctype == "Sales Invoice":
# Calculate credit and debit values for reconciliation and advance entries
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
reconcilation_entry.credit = os_in_default_currency
advance_entry.debit_in_account_currency = os_in_primary_currency
advance_entry.debit = os_in_default_currency
else:
advance_entry.credit_in_account_currency = os_in_primary_currency
advance_entry.credit = os_in_default_currency
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
reconcilation_entry.debit = os_in_default_currency
# Set exchange rates for entries
reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
advance_entry.exchange_rate = exc_rate_primary_to_default
else:
advance_entry.credit_in_account_currency = self.outstanding_amount
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
default_currency = erpnext.get_company_currency(self.company)
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
jv.multi_currency = 1
if self.doctype == "Sales Invoice":
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount
else:
advance_entry.credit_in_account_currency = self.outstanding_amount
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
jv.multi_currency = multi_currency
jv.append("accounts", reconcilation_entry)
jv.append("accounts", advance_entry)

View File

@@ -356,14 +356,14 @@ class BuyingController(SubcontractingController):
if not self.is_internal_transfer():
return
self.set_sales_incoming_rate_for_internal_transfer()
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
if allow_at_arms_length_price:
return
self.set_sales_incoming_rate_for_internal_transfer()
for d in self.get("items"):
d.discount_percentage = 0.0
d.discount_amount = 0.0

View File

@@ -11,7 +11,13 @@ def set_print_templates_for_item_table(doc, settings):
"items": {
"qty": "templates/print_formats/includes/item_table_qty.html",
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
}
},
"packed_items": {
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
},
"supplied_items": {
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
},
}
doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]

View File

@@ -1036,7 +1036,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
available_serial_nos.append(serial_no)
if available_serial_nos:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
if len(available_serial_nos) > qty:
@@ -1052,7 +1052,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
if batch_qty <= 0:
continue
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
batch_qty = get_available_batch_qty(
parent_doc,
batch_no,

View File

@@ -74,19 +74,13 @@ class SellingController(StockController):
if customer:
from erpnext.accounts.party import _get_party_details
fetch_payment_terms_template = False
if self.get("__islocal") or self.company != frappe.db.get_value(
self.doctype, self.name, "company"
):
fetch_payment_terms_template = True
party_details = _get_party_details(
customer,
ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype,
company=self.company,
posting_date=self.get("posting_date"),
fetch_payment_terms_template=fetch_payment_terms_template,
fetch_payment_terms_template=self.has_value_changed("company"),
party_address=self.customer_address,
shipping_address=self.shipping_address_name,
company_address=self.get("company_address"),
@@ -378,12 +372,32 @@ class SellingController(StockController):
return il
def has_product_bundle(self, item_code):
product_bundle = frappe.qb.DocType("Product Bundle")
return (
frappe.qb.from_(product_bundle)
.select(product_bundle.name)
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
).run()
product_bundle_items = getattr(self, "_product_bundle_items", None)
if product_bundle_items is None:
self._product_bundle_items = product_bundle_items = {}
if item_code not in product_bundle_items:
self._fetch_product_bundle_items(item_code)
return product_bundle_items[item_code]
def _fetch_product_bundle_items(self, item_code):
product_bundle_items = self._product_bundle_items
items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items}
# fetch for requisite item_code even if it is not in items
items_to_fetch.add(item_code)
items_with_product_bundle = {
row.new_item_code
for row in frappe.get_all(
"Product Bundle",
filters={"new_item_code": ("in", items_to_fetch), "disabled": 0},
fields="new_item_code",
)
}
for item_code in items_to_fetch:
product_bundle_items[item_code] = item_code in items_with_product_bundle
def get_already_delivered_qty(self, current_docname, so, so_detail):
delivered_via_dn = frappe.db.sql(

View File

@@ -809,6 +809,7 @@ class TestAccountsController(IntegrationTestCase):
"Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1}
)
def test_16_internal_transfer_at_arms_length_price(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
prepare_data_for_internal_transfer()
@@ -842,6 +843,31 @@ class TestAccountsController(IntegrationTestCase):
# rate should reset to incoming rate
self.assertEqual(si.items[0].rate, 100)
si.update_stock = 0
si.save()
si.submit()
pi = make_inter_company_purchase_invoice(si.name)
pi.update_stock = 1
pi.items[0].rate = arms_length_price
pi.items[0].warehouse = target_warehouse
pi.items[0].from_warehouse = warehouse
pi.save()
self.assertEqual(pi.items[0].rate, 100)
self.assertEqual(pi.items[0].valuation_rate, 100)
frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 1)
pi = make_inter_company_purchase_invoice(si.name)
pi.update_stock = 1
pi.items[0].rate = arms_length_price
pi.items[0].warehouse = target_warehouse
pi.items[0].from_warehouse = warehouse
pi.save()
self.assertEqual(pi.items[0].rate, arms_length_price)
self.assertEqual(pi.items[0].valuation_rate, 100)
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -264,6 +264,24 @@ class BOM(WebsiteGenerator):
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
self.set_process_loss_qty()
self.validate_scrap_items()
self.set_default_uom()
def set_default_uom(self):
if not self.get("items"):
return
item_wise_uom = frappe._dict(
frappe.get_all(
"Item",
filters={"name": ("in", [item.item_code for item in self.items])},
fields=["name", "stock_uom"],
as_list=1,
)
)
for row in self.get("items"):
if row.stock_uom != item_wise_uom.get(row.item_code):
row.stock_uom = item_wise_uom.get(row.item_code)
def get_context(self, context):
context.parents = [{"name": "boms", "title": _("All BOMs")}]

View File

@@ -763,6 +763,26 @@ class TestBOM(IntegrationTestCase):
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
def test_bom_raw_materials_stock_uom(self):
rm_item = make_item(
properties={"is_stock_item": 1, "valuation_rate": 1000.0, "stock_uom": "Nos"}
).name
fg_item = make_item(properties={"is_stock_item": 1}).name
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True)
for row in bom.items:
self.assertEqual(row.stock_uom, "Nos")
frappe.db.set_value("Item", rm_item, "stock_uom", "Kg")
bom.items[0].qty = 2
bom.save()
for row in bom.items:
self.assertEqual(row.stock_uom, "Kg")
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -243,7 +243,7 @@
"depends_on": "eval:!doc.__islocal",
"fieldname": "download_materials_required",
"fieldtype": "Button",
"label": "Download Materials Request Plan"
"label": "Download Required Materials"
},
{
"fieldname": "get_items_for_mr",
@@ -398,7 +398,7 @@
"collapsible": 1,
"fieldname": "download_materials_request_plan_section_section",
"fieldtype": "Section Break",
"label": "Download Materials Request Plan Section"
"label": "Preview Required Materials"
},
{
"default": "0",
@@ -439,7 +439,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:19.928403",
"modified": "2024-12-04 11:55:03.108971",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -44,9 +44,7 @@ class ProductionPlan(Document):
from erpnext.manufacturing.doctype.material_request_plan_item.material_request_plan_item import (
MaterialRequestPlanItem,
)
from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import (
ProductionPlanItem,
)
from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import ProductionPlanItem
from erpnext.manufacturing.doctype.production_plan_item_reference.production_plan_item_reference import (
ProductionPlanItemReference,
)
@@ -1085,24 +1083,33 @@ def download_raw_materials(doc, warehouses=None):
frappe.flags.show_qty_in_stock_uom = 1
items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True)
duplicate_item_wh_list = frappe._dict()
for d in items:
item_list.append(
[
d.get("item_code"),
d.get("item_name"),
d.get("description"),
d.get("stock_uom"),
d.get("warehouse"),
d.get("required_bom_qty"),
d.get("projected_qty"),
d.get("actual_qty"),
d.get("ordered_qty"),
d.get("planned_qty"),
d.get("reserved_qty_for_production"),
d.get("safety_stock"),
d.get("quantity"),
]
)
key = (d.get("item_code"), d.get("warehouse"))
if key in duplicate_item_wh_list:
rm_data = duplicate_item_wh_list[key]
rm_data[12] += d.get("quantity")
continue
rm_data = [
d.get("item_code"),
d.get("item_name"),
d.get("description"),
d.get("stock_uom"),
d.get("warehouse"),
d.get("required_bom_qty"),
d.get("projected_qty"),
d.get("actual_qty"),
d.get("ordered_qty"),
d.get("planned_qty"),
d.get("reserved_qty_for_production"),
d.get("safety_stock"),
d.get("quantity"),
]
duplicate_item_wh_list[key] = rm_data
item_list.append(rm_data)
if not doc.get("for_warehouse"):
row = {"item_code": d.get("item_code")}

View File

@@ -178,10 +178,18 @@ class WorkOrder(Document):
self.validate_workstation_type()
self.reset_use_multi_level_bom()
if self.source_warehouse:
self.set_warehouses()
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items")))
def set_warehouses(self):
for row in self.required_items:
if not row.source_warehouse:
row.source_warehouse = self.source_warehouse
def reset_use_multi_level_bom(self):
if self.is_new():
return

View File

@@ -389,4 +389,5 @@ erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_format
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions
erpnext.patches.v14_0.update_stock_uom_in_work_order_item

View File

@@ -0,0 +1,22 @@
import frappe
def execute():
default_exchange_gain_loss_accounts = frappe.get_all(
"Company",
filters={"exchange_gain_loss_account": ["!=", ""]},
pluck="exchange_gain_loss_account",
)
if not default_exchange_gain_loss_accounts:
return
payment_entry = frappe.qb.DocType("Payment Entry")
payment_entry_deduction = frappe.qb.DocType("Payment Entry Deduction")
frappe.qb.update(payment_entry_deduction).set(payment_entry_deduction.is_exchange_gain_loss, 1).join(
payment_entry,
).on(payment_entry.name == payment_entry_deduction.parent).where(
(payment_entry.paid_to_account_currency != payment_entry.paid_from_account_currency)
& (payment_entry_deduction.account.isin(default_exchange_gain_loss_accounts))
).run()

View File

@@ -58,10 +58,10 @@ frappe.ui.form.on("Timesheet", {
}
if (frm.doc.docstatus < 1) {
let button = "Start Timer";
let button = __("Start Timer");
$.each(frm.doc.time_logs || [], function (i, row) {
if (row.from_time <= frappe.datetime.now_datetime() && !row.completed) {
button = "Resume Timer";
button = __("Resume Timer");
}
});

Some files were not shown because too many files have changed in this diff Show More