mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-24 05:05:03 +00:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b95ab3425a | ||
|
|
16edf3ac36 | ||
|
|
22247cfa17 | ||
|
|
d2091cc22c | ||
|
|
9789b7bdef | ||
|
|
e8d7d30682 | ||
|
|
05f657e690 | ||
|
|
0350c69856 | ||
|
|
adc87f16a3 | ||
|
|
bd41cb221b | ||
|
|
7a5b454e97 | ||
|
|
256c3c81a4 | ||
|
|
9b2a84f259 | ||
|
|
c07548a612 | ||
|
|
0f98cc85e9 | ||
|
|
aad91c02f3 | ||
|
|
587283e787 | ||
|
|
78b0a52d41 | ||
|
|
c4d338a59b | ||
|
|
bc6bd81f87 | ||
|
|
fd4159423d | ||
|
|
95a6e1d855 | ||
|
|
e8dc63c89c | ||
|
|
6edfcf4de8 | ||
|
|
4fa07777e9 | ||
|
|
b1ebd81f77 | ||
|
|
20c45c7975 | ||
|
|
4a4cba0715 | ||
|
|
ab9da5281e | ||
|
|
c5080abd46 | ||
|
|
a8c53eeb93 | ||
|
|
d727a13562 | ||
|
|
4556c36736 | ||
|
|
d1b2ce35b6 | ||
|
|
a0a3519cd3 | ||
|
|
f16386bd07 | ||
|
|
94612b90ef | ||
|
|
6a9935c00e | ||
|
|
873ee384a1 | ||
|
|
13b1263723 | ||
|
|
6761874e28 | ||
|
|
19eb6af65f | ||
|
|
0dc0e287a3 | ||
|
|
a0575ed2b0 | ||
|
|
cb9aad3e87 | ||
|
|
37cee42561 | ||
|
|
928e475824 | ||
|
|
296a4d7a12 | ||
|
|
fe78076cde | ||
|
|
d082e68e83 | ||
|
|
4606079568 | ||
|
|
3634e80341 | ||
|
|
e1bd9a7e8d | ||
|
|
72d9dc6c85 | ||
|
|
9ec11d9502 | ||
|
|
2f92981afe | ||
|
|
c74a414313 | ||
|
|
e8f1c82089 | ||
|
|
6366f5aadb | ||
|
|
933d4bfceb | ||
|
|
de3b67c327 | ||
|
|
d5a596dff1 | ||
|
|
620b21fec5 | ||
|
|
5bd2a0923f | ||
|
|
e62ffa990d | ||
|
|
01ae513f70 | ||
|
|
37ef6e959b | ||
|
|
2a467a9fbf | ||
|
|
7ac35b496a | ||
|
|
cdb66bf198 | ||
|
|
8530a28c62 | ||
|
|
2c8c3a022c | ||
|
|
1269f2d301 | ||
|
|
11ba553dbd | ||
|
|
248d4082c0 | ||
|
|
f578c3219d | ||
|
|
c1f1a21714 | ||
|
|
3198f2669d | ||
|
|
a623469778 | ||
|
|
ee5a1d91ff | ||
|
|
0a4947a8f6 | ||
|
|
9668615f7e | ||
|
|
506ac10119 | ||
|
|
36147ec02c | ||
|
|
3dd642441b | ||
|
|
f186015f58 | ||
|
|
1f76c6972b | ||
|
|
c308bd5309 | ||
|
|
3f33d4cf76 | ||
|
|
47345e81a1 | ||
|
|
3e23e1fb91 | ||
|
|
b6134749c3 | ||
|
|
f8d6fe6be0 | ||
|
|
83cbc1bef6 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.35.1"
|
||||
__version__ = "14.37.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
company: d.company,
|
||||
root_type: ["in", ["Asset", "Liability"]],
|
||||
is_group: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
|
||||
frappe.set_route("List", frm.doc.document_type);
|
||||
|
||||
@@ -39,6 +39,8 @@ class AccountingDimension(Document):
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||
if doctype_before_save != self.document_type:
|
||||
@@ -46,6 +48,14 @@ class AccountingDimension(Document):
|
||||
message += _("Please create a new Accounting Dimension if required.")
|
||||
frappe.throw(message)
|
||||
|
||||
def validate_dimension_defaults(self):
|
||||
companies = []
|
||||
for default in self.get("dimension_defaults"):
|
||||
if default.company not in companies:
|
||||
companies.append(default.company)
|
||||
else:
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"reference_document",
|
||||
"default_dimension",
|
||||
"mandatory_for_bs",
|
||||
"mandatory_for_pl"
|
||||
"mandatory_for_pl",
|
||||
"column_break_lqns",
|
||||
"automatically_post_balancing_accounting_entry",
|
||||
"offsetting_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -50,6 +53,23 @@
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Mandatory For Profit and Loss Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "automatically_post_balancing_accounting_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically post balancing accounting entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "offsetting_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Offsetting Account",
|
||||
"mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lqns",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
|
||||
@@ -3,6 +3,296 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
class TestExchangeRateRevaluation(unittest.TestCase):
|
||||
pass
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.set_system_and_company_settings()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def set_system_and_company_settings(self):
|
||||
# set number and currency precision
|
||||
system_settings = frappe.get_doc("System Settings")
|
||||
system_settings.float_precision = 2
|
||||
system_settings.currency_precision = 2
|
||||
system_settings.save()
|
||||
|
||||
# Using Exchange Gain/Loss account for unrealized as well.
|
||||
company_doc = frappe.get_doc("Company", self.company)
|
||||
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||
company_doc.save()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_01_revaluation_of_forex_balance(self):
|
||||
"""
|
||||
Test Forex account balance and Journal creation post Revaluation
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = (self.company,)
|
||||
err.posting_date = today()
|
||||
accounts = err.get_accounts_data()
|
||||
err.extend("accounts", accounts)
|
||||
row = err.accounts[0]
|
||||
row.new_exchange_rate = 85
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
)
|
||||
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
|
||||
self.assertEqual(je.total_debit, 8500.0)
|
||||
self.assertEqual(je.total_credit, 8500.0)
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=["sum(debit)-sum(credit) as balance"],
|
||||
)[0]
|
||||
self.assertEqual(acc_balance.balance, 8500.0)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_02_accounts_only_with_base_currency_balance(self):
|
||||
"""
|
||||
Test Revaluation on Forex account with balance only in base currency
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.source_exchange_rate = 85
|
||||
pe.received_amount = 8500
|
||||
pe.save().submit()
|
||||
|
||||
# Cancel the auto created gain/loss JE to simulate balance only in base currency
|
||||
je = frappe.db.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
|
||||
)[0]
|
||||
frappe.get_doc("Journal Entry", je).cancel()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = (self.company,)
|
||||
err.posting_date = today()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
self.assertTrue(err.check_journal_entry_condition())
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
|
||||
self.assertEqual(len(je.accounts), 2)
|
||||
# Only base currency fields will be posted to
|
||||
for acc in je.accounts:
|
||||
self.assertEqual(acc.debit_in_account_currency, 0)
|
||||
self.assertEqual(acc.credit_in_account_currency, 0)
|
||||
|
||||
self.assertEqual(je.total_debit, 500.0)
|
||||
self.assertEqual(je.total_credit, 500.0)
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency
|
||||
self.assertEqual(acc_balance.balance, 0.0)
|
||||
self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_03_accounts_only_with_account_currency_balance(self):
|
||||
"""
|
||||
Test Revaluation on Forex account with balance only in account currency
|
||||
"""
|
||||
precision = frappe.db.get_single_value("System Settings", "currency_precision")
|
||||
|
||||
# posting on previous date to make sure that ERR picks up the Payment entry's exchange
|
||||
# rate while calculating gain/loss for account currency balance
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=add_days(today(), -1),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 95
|
||||
pe.source_exchange_rate = 84.211
|
||||
pe.received_amount = 8000
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account should have balance only in account currency
|
||||
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
|
||||
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = (self.company,)
|
||||
err.posting_date = today()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
self.assertTrue(err.check_journal_entry_condition())
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
|
||||
self.assertEqual(len(je.accounts), 2)
|
||||
# Only account currency fields will be posted to
|
||||
for acc in je.accounts:
|
||||
self.assertEqual(flt(acc.debit, precision), 0.0)
|
||||
self.assertEqual(flt(acc.credit, precision), 0.0)
|
||||
|
||||
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = [x for x in je.accounts if x.account != self.debtors_usd][0]
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # 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)
|
||||
self.assertEqual(flt(je.total_credit, precision), 0.0)
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency post revaluation
|
||||
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
|
||||
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_04_get_account_details_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
|
||||
get_account_details,
|
||||
)
|
||||
|
||||
account_details = get_account_details(
|
||||
self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
|
||||
)
|
||||
# not checking for new exchange rate and balances as it is dependent on live exchange rates
|
||||
expected_data = {
|
||||
"account_currency": "USD",
|
||||
"balance_in_base_currency": 8000.0,
|
||||
"balance_in_account_currency": 100.0,
|
||||
"current_exchange_rate": 80.0,
|
||||
"zero_balance": False,
|
||||
"new_balance_in_account_currency": 100.0,
|
||||
}
|
||||
|
||||
for key, val in expected_data.items():
|
||||
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -96,6 +96,8 @@ class JournalEntry(AccountsController):
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.update_advance_paid()
|
||||
|
||||
@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
|
||||
@@ -105,6 +105,8 @@ class PaymentEntry(AccountsController):
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
@@ -185,84 +187,87 @@ class PaymentEntry(AccountsController):
|
||||
return False
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
if self.references:
|
||||
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
|
||||
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -1463,6 +1468,7 @@ def get_outstanding_reference_documents(args):
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
accounting_dimensions=accounting_dimensions_filter,
|
||||
vouchers=args.get("vouchers") or None,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(
|
||||
@@ -1861,10 +1867,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
if not total_amount:
|
||||
if party_account_currency == company_currency:
|
||||
# for handling cases that don't have multi-currency (base field)
|
||||
total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
|
||||
total_amount = (
|
||||
ref_doc.get("base_rounded_total")
|
||||
or ref_doc.get("rounded_total")
|
||||
or ref_doc.get("base_grand_total")
|
||||
or ref_doc.get("grand_total")
|
||||
)
|
||||
exchange_rate = 1
|
||||
else:
|
||||
total_amount = ref_doc.get("grand_total")
|
||||
total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
|
||||
if not exchange_rate:
|
||||
# Get the exchange rate from the original ref doc
|
||||
# or get it based on the posting date of the ref doc.
|
||||
|
||||
@@ -1201,6 +1201,24 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
template.allocate_payment_based_on_payment_terms = 1
|
||||
template.save()
|
||||
|
||||
def test_allocation_validation_for_sales_order(self):
|
||||
so = make_sales_order(do_not_save=True)
|
||||
so.items[0].rate = 99.55
|
||||
so.save().submit()
|
||||
self.assertGreater(so.rounded_total, 0.0)
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_amount = 45.55
|
||||
pe.references[0].allocated_amount = 45.55
|
||||
pe.save().submit()
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
# No validation error should be thrown here.
|
||||
pe.save().submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, so.rounded_total)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -151,6 +151,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.refresh();
|
||||
}
|
||||
|
||||
invoice_name() {
|
||||
this.frm.trigger("get_unreconciled_entries");
|
||||
}
|
||||
|
||||
payment_name() {
|
||||
this.frm.trigger("get_unreconciled_entries");
|
||||
}
|
||||
|
||||
|
||||
clear_child_tables() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
"column_break_15",
|
||||
"payment_name",
|
||||
"payments",
|
||||
"sec_break2",
|
||||
"allocation"
|
||||
@@ -136,6 +138,7 @@
|
||||
"label": "Minimum Invoice Amount"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "invoice_limit",
|
||||
"fieldtype": "Int",
|
||||
@@ -166,6 +169,7 @@
|
||||
"label": "Maximum Payment Amount"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "payment_limit",
|
||||
"fieldtype": "Int",
|
||||
@@ -185,13 +189,23 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Payment"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-29 15:37:10.246831",
|
||||
"modified": "2023-08-15 05:35:50.109290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
@@ -218,4 +232,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
@@ -58,6 +59,9 @@ class PaymentReconciliation(Document):
|
||||
def get_payment_entries(self):
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = self.get_conditions(get_payments=True)
|
||||
if self.payment_name:
|
||||
condition += "name like '%%{0}%%'".format(self.payment_name)
|
||||
|
||||
payment_entries = get_advance_payment_entries(
|
||||
self.party_type,
|
||||
self.party,
|
||||
@@ -73,6 +77,9 @@ class PaymentReconciliation(Document):
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
|
||||
if self.payment_name:
|
||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||
|
||||
@@ -130,6 +137,15 @@ class PaymentReconciliation(Document):
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
|
||||
conditions = []
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
@@ -137,11 +153,7 @@ class PaymentReconciliation(Document):
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
@@ -210,6 +222,8 @@ class PaymentReconciliation(Document):
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
limit=self.invoice_limit,
|
||||
voucher_no=self.invoice_name,
|
||||
)
|
||||
|
||||
cr_dr_notes = (
|
||||
@@ -385,59 +399,6 @@ class PaymentReconciliation(Document):
|
||||
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def make_difference_entry(self, row):
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = nowdate()
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
party_account_currency = frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
)
|
||||
difference_account_currency = frappe.get_cached_value(
|
||||
"Account", row.difference_account, "account_currency"
|
||||
)
|
||||
|
||||
# Account Currency has balance
|
||||
dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": self.receivable_payable_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"account_currency": party_account_currency,
|
||||
"exchange_rate": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": row.against_voucher_type,
|
||||
"reference_name": row.against_voucher,
|
||||
dr_or_cr: flt(row.difference_amount),
|
||||
dr_or_cr + "_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": row.difference_account,
|
||||
"account_currency": difference_account_currency,
|
||||
"exchange_rate": 1,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
|
||||
reverse_dr_or_cr: flt(row.difference_amount),
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_entry.save()
|
||||
journal_entry.submit()
|
||||
|
||||
return journal_entry
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -603,16 +564,6 @@ class PaymentReconciliation(Document):
|
||||
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
def get_difference_row(inv):
|
||||
if inv.difference_amount != 0 and inv.difference_account:
|
||||
difference_row = {
|
||||
"account": inv.difference_account,
|
||||
inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
|
||||
reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
}
|
||||
return difference_row
|
||||
|
||||
for inv in dr_cr_notes:
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
args: { "pos_profile": frm.pos_profile },
|
||||
callback: ({ message: profile }) => {
|
||||
this.update_customer_groups_settings(profile?.customer_groups);
|
||||
this.frm.set_value("company", profile?.company);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_pos()
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -370,6 +371,14 @@ class POSInvoice(SalesInvoice):
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_company_with_pos_company(self):
|
||||
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
|
||||
frappe.throw(
|
||||
_("Company {} does not match with POS Profile Company {}").format(
|
||||
self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
|
||||
)
|
||||
)
|
||||
|
||||
def validate_loyalty_transaction(self):
|
||||
if self.redeem_loyalty_points and (
|
||||
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
|
||||
@@ -448,6 +457,7 @@ class POSInvoice(SalesInvoice):
|
||||
profile = {}
|
||||
if self.pos_profile:
|
||||
profile = frappe.get_doc("POS Profile", self.pos_profile)
|
||||
self.company = profile.get("company")
|
||||
|
||||
if not self.get("payments") and not for_validate:
|
||||
update_multi_mode_option(self, profile)
|
||||
|
||||
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
super.onload();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
|
||||
@@ -768,21 +768,22 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(amount["amount"]),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(amount["amount"]),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(item.rm_supp_cost):
|
||||
@@ -1422,6 +1423,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
|
||||
@@ -1771,23 +1771,101 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
0,
|
||||
)
|
||||
|
||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import (
|
||||
clear_dimension_defaults,
|
||||
create_accounting_dimension,
|
||||
disable_dimension,
|
||||
)
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
|
||||
order by posting_date asc, account asc""",
|
||||
(voucher_no, posting_date),
|
||||
as_dict=1,
|
||||
create_account(
|
||||
account_name="Offsetting",
|
||||
company="_Test Company",
|
||||
parent_account="Temporary Accounts - _TC",
|
||||
)
|
||||
|
||||
create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC")
|
||||
|
||||
branch1 = frappe.new_doc("Branch")
|
||||
branch1.branch = "Location 1"
|
||||
branch1.insert(ignore_if_duplicate=True)
|
||||
branch2 = frappe.new_doc("Branch")
|
||||
branch2.branch = "Location 2"
|
||||
branch2.insert(ignore_if_duplicate=True)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company="_Test Company",
|
||||
customer="_Test Supplier",
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
rate=1000,
|
||||
price_list_rate=1000,
|
||||
qty=1,
|
||||
)
|
||||
pi.branch = branch1.branch
|
||||
pi.items[0].branch = branch2.branch
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch],
|
||||
["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch],
|
||||
["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch],
|
||||
["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch],
|
||||
]
|
||||
|
||||
check_gl_entries(
|
||||
self,
|
||||
pi.name,
|
||||
expected_gle,
|
||||
nowdate(),
|
||||
voucher_type="Purchase Invoice",
|
||||
additional_columns=["branch"],
|
||||
)
|
||||
clear_dimension_defaults("Branch")
|
||||
disable_dimension()
|
||||
|
||||
|
||||
def check_gl_entries(
|
||||
doc,
|
||||
voucher_no,
|
||||
expected_gle,
|
||||
posting_date,
|
||||
voucher_type="Purchase Invoice",
|
||||
additional_columns=None,
|
||||
):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(gl.account, gl.debit, gl.credit, gl.posting_date)
|
||||
.where(
|
||||
(gl.voucher_type == voucher_type)
|
||||
& (gl.voucher_no == voucher_no)
|
||||
& (gl.posting_date >= posting_date)
|
||||
& (gl.is_cancelled == 0)
|
||||
)
|
||||
.orderby(gl.posting_date, gl.account, gl.creation)
|
||||
)
|
||||
|
||||
if additional_columns:
|
||||
for col in additional_columns:
|
||||
query = query.select(gl[col])
|
||||
|
||||
gl_entries = query.run(as_dict=True)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
doc.assertEqual(expected_gle[i][2], gle.credit)
|
||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
if additional_columns:
|
||||
j = 4
|
||||
for col in additional_columns:
|
||||
doc.assertEqual(expected_gle[i][j], gle[col])
|
||||
j += 1
|
||||
|
||||
|
||||
def create_tax_witholding_category(category_name, company, account):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<style>
|
||||
.print-format {
|
||||
padding: 4mm;
|
||||
font-size: 8.0pt !important;
|
||||
}
|
||||
.print-format td {
|
||||
vertical-align:middle !important;
|
||||
}
|
||||
.old {
|
||||
background-color: #FFB3C0;
|
||||
}
|
||||
.new {
|
||||
background-color: #B3FFCC;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<table class="table table-bordered table-condensed">
|
||||
<colgroup>
|
||||
{% for col in gl_columns%}
|
||||
<col style="width: 18mm;">
|
||||
{% endfor %}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in gl_columns%}
|
||||
<td>{{ col.label }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for gl in gl_data%}
|
||||
{% if gl["old"]%}
|
||||
<tr class="old">
|
||||
{% else %}
|
||||
<tr class="new">
|
||||
{% endif %}
|
||||
{% for col in gl_columns %}
|
||||
<td class="text-right">
|
||||
{{ gl[col.fieldname] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Repost Accounting Ledger", {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
|
||||
if (doc.company) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
docstatus: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__('Show Preview'), () => {
|
||||
frm.call({
|
||||
method: 'generate_preview',
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __('Generating Preview'),
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
let content = r.message;
|
||||
let opts = {
|
||||
title: "Preview",
|
||||
subtitle: "preview",
|
||||
content: content,
|
||||
print_settings: {orientation: "landscape"},
|
||||
columns: [],
|
||||
data: [],
|
||||
}
|
||||
frappe.render_grid(opts);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"column_break_vpup",
|
||||
"delete_cancelled_entries",
|
||||
"section_break_metl",
|
||||
"vouchers",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Repost Accounting Ledger",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "vouchers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Vouchers",
|
||||
"options": "Repost Accounting Ledger Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vpup",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_metl",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delete_cancelled_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Delete Cancelled Ledger Entries"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-27 15:47:58.975034",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
|
||||
self._allowed_types = set(
|
||||
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_vouchers()
|
||||
self.validate_for_closed_fiscal_year()
|
||||
self.validate_for_deferred_accounting()
|
||||
|
||||
def validate_for_deferred_accounting(self):
|
||||
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
|
||||
docs_with_deferred_revenue = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
|
||||
docs_with_deferred_expense = frappe.db.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
||||
frappe.throw(
|
||||
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
||||
frappe.bold(
|
||||
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_for_closed_fiscal_year(self):
|
||||
if self.vouchers:
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
order_by="posting_date desc",
|
||||
pluck="posting_date",
|
||||
limit=1,
|
||||
)
|
||||
or None
|
||||
)
|
||||
if not latest_pcv:
|
||||
return
|
||||
|
||||
for vtype in self._allowed_types:
|
||||
if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
|
||||
latest_voucher = frappe.db.get_all(
|
||||
vtype,
|
||||
filters={"name": ["in", names]},
|
||||
pluck="posting_date",
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)[0]
|
||||
if latest_voucher and latest_pcv[0] >= latest_voucher:
|
||||
frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
|
||||
|
||||
def validate_vouchers(self):
|
||||
if self.vouchers:
|
||||
# Validate voucher types
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
if disallowed_types := voucher_types.difference(self._allowed_types):
|
||||
frappe.throw(
|
||||
_("{0} types are not allowed. Only {1} are.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
frappe.bold(comma_and(list(self._allowed_types))),
|
||||
)
|
||||
)
|
||||
|
||||
def get_existing_ledger_entries(self):
|
||||
vouchers = [x.voucher_no for x in self.vouchers]
|
||||
gl = qb.DocType("GL Entry")
|
||||
existing_gles = (
|
||||
qb.from_(gl)
|
||||
.select(gl.star)
|
||||
.where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.gles = frappe._dict({})
|
||||
|
||||
for gle in existing_gles:
|
||||
self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
|
||||
"existing", []
|
||||
).append(gle.update({"old": True}))
|
||||
|
||||
def generate_preview_data(self):
|
||||
self.gl_entries = []
|
||||
self.get_existing_ledger_entries()
|
||||
for x in self.vouchers:
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
old_entries = self.gles.get((x.voucher_type, x.voucher_no))
|
||||
if old_entries:
|
||||
self.gl_entries.extend(old_entries.existing)
|
||||
self.gl_entries.extend(gle_map)
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_preview(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
|
||||
|
||||
gl_columns = []
|
||||
gl_data = []
|
||||
|
||||
self.generate_preview_data()
|
||||
if self.gl_entries:
|
||||
filters = {"company": self.company, "include_dimensions": 1}
|
||||
for x in get_gl_columns(filters):
|
||||
if x["fieldname"] == "gl_entry":
|
||||
x["fieldname"] = "name"
|
||||
gl_columns.append(x)
|
||||
|
||||
gl_data = self.gl_entries
|
||||
rendered_page = frappe.render_template(
|
||||
"erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
|
||||
{"gl_columns": gl_columns, "gl_data": gl_data},
|
||||
)
|
||||
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
|
||||
if repost_doc.docstatus == 1:
|
||||
# Prevent repost on invoices with deferred accounting
|
||||
repost_doc.validate_for_deferred_accounting()
|
||||
|
||||
for x in repost_doc.vouchers:
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
|
||||
if repost_doc.delete_cancelled_entries:
|
||||
frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
|
||||
frappe.db.delete(
|
||||
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
|
||||
)
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
@@ -0,0 +1,202 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def teadDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
preq = frappe.get_doc(
|
||||
make_payment_request(
|
||||
dt=si.doctype,
|
||||
dn=si.name,
|
||||
payment_request_type="Inward",
|
||||
party_type="Customer",
|
||||
party=si.customer,
|
||||
)
|
||||
)
|
||||
preq.save().submit()
|
||||
|
||||
# Test Validation Error
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append(
|
||||
"vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
|
||||
) # this should throw validation error
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
ral.vouchers.pop()
|
||||
preq.cancel()
|
||||
preq.delete()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# manually set an incorrect debit amount in DB
|
||||
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
|
||||
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
# Assert incorrect ledger balance
|
||||
self.assertNotEqual(res[0], (si.name, 100, 100))
|
||||
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
# Ledger should reflect correct amount post repost
|
||||
self.assertEqual(res[0], (si.name, 100, 100))
|
||||
|
||||
def test_02_deferred_accounting_valiations(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = True
|
||||
si.items[0].deferred_revenue_account = self.deferred_revenue
|
||||
si.items[0].service_start_date = nowdate()
|
||||
si.items[0].service_end_date = add_days(nowdate(), 90)
|
||||
si.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_04_pcv_validation(self):
|
||||
# Clear old GL entries so PCV can be submitted.
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
qb.from_(gl).delete().where(gl.company == self.company).run()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"company": self.company,
|
||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
pcv.reload()
|
||||
pcv.cancel()
|
||||
pcv.delete()
|
||||
|
||||
def test_03_deletion_flag_and_preview_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-07-04 14:14:01.243848",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 14:15:51.165584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Items",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class RepostAccountingLedgerItems(Document):
|
||||
pass
|
||||
@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
|
||||
@@ -714,6 +714,7 @@
|
||||
"fieldtype": "Table",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Items",
|
||||
"oldfieldname": "entries",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Invoice Item",
|
||||
|
||||
@@ -399,6 +399,8 @@ class SalesInvoice(SellingController):
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
|
||||
|
||||
@@ -268,9 +268,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
tax_amount = net_total * tax_details.rate / 100
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
tax_amount = net_total * tax_details.rate / 100
|
||||
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
|
||||
@@ -28,6 +28,7 @@ def make_gl_entries(
|
||||
):
|
||||
if gl_map:
|
||||
if not cancel:
|
||||
make_acc_dimensions_offsetting_entry(gl_map)
|
||||
validate_accounting_period(gl_map)
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
@@ -51,6 +52,63 @@ def make_gl_entries(
|
||||
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
|
||||
|
||||
def make_acc_dimensions_offsetting_entry(gl_map):
|
||||
accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
|
||||
gl_map, gl_map[0].company
|
||||
)
|
||||
no_of_dimensions = len(accounting_dimensions_to_offset)
|
||||
if no_of_dimensions == 0:
|
||||
return
|
||||
|
||||
offsetting_entries = []
|
||||
|
||||
for gle in gl_map:
|
||||
for dimension in accounting_dimensions_to_offset:
|
||||
offsetting_entry = gle.copy()
|
||||
debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
|
||||
credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
|
||||
offsetting_entry.update(
|
||||
{
|
||||
"account": dimension.offsetting_account,
|
||||
"debit": debit,
|
||||
"credit": credit,
|
||||
"debit_in_account_currency": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name),
|
||||
"against_voucher": None,
|
||||
}
|
||||
)
|
||||
offsetting_entry["against_voucher_type"] = None
|
||||
offsetting_entries.append(offsetting_entry)
|
||||
|
||||
gl_map += offsetting_entries
|
||||
|
||||
|
||||
def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
||||
acc_dimension = frappe.qb.DocType("Accounting Dimension")
|
||||
dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
|
||||
acc_dimensions = (
|
||||
frappe.qb.from_(acc_dimension)
|
||||
.inner_join(dimension_detail)
|
||||
.on(acc_dimension.name == dimension_detail.parent)
|
||||
.select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
|
||||
.where(
|
||||
(acc_dimension.disabled == 0)
|
||||
& (dimension_detail.company == company)
|
||||
& (dimension_detail.automatically_post_balancing_accounting_entry == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
accounting_dimensions_to_offset = []
|
||||
for acc_dimension in acc_dimensions:
|
||||
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
|
||||
if len(values) > 1:
|
||||
accounting_dimensions_to_offset.append(acc_dimension)
|
||||
|
||||
return accounting_dimensions_to_offset
|
||||
|
||||
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import (
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.query_builder.functions import Abs, Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -884,35 +884,34 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.party)
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.party, Abs(Sum(ple.amount).as_("amount")))
|
||||
.where(
|
||||
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
|
||||
(ple.party_type.isin(party_type))
|
||||
& (ple.amount < 0)
|
||||
& (ple.against_voucher_no == ple.voucher_no)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
.groupby(gle.party)
|
||||
.groupby(ple.party)
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
query = query.select(Sum(gle.credit).as_("amount"))
|
||||
else:
|
||||
query = query.select(Sum(gle.debit).as_("amount"))
|
||||
|
||||
if posting_date:
|
||||
if future_payment:
|
||||
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
|
||||
query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
|
||||
else:
|
||||
query = query.where(gle.posting_date <= posting_date)
|
||||
query = query.where(ple.posting_date <= posting_date)
|
||||
|
||||
if company:
|
||||
query = query.where(gle.company == company)
|
||||
query = query.where(ple.company == company)
|
||||
|
||||
if party:
|
||||
query = query.where(gle.party == party)
|
||||
query = query.where(ple.party == party)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
data = query.run()
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
@@ -214,8 +214,8 @@ class ReceivablePayableReport(object):
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
|
||||
# update voucher
|
||||
@@ -1090,7 +1090,10 @@ class ReceivablePayableReport(object):
|
||||
.where(
|
||||
(je.company == self.filters.company)
|
||||
& (je.posting_date.lte(self.filters.report_date))
|
||||
& (je.voucher_type == "Exchange Rate Revaluation")
|
||||
& (
|
||||
(je.voucher_type == "Exchange Rate Revaluation")
|
||||
| (je.voucher_type == "Exchange Gain Or Loss")
|
||||
)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@@ -8,20 +8,17 @@ from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAccountsReceivable(FrappeTestCase):
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
|
||||
|
||||
self.create_usd_account()
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_usd_receivable_account()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
@@ -49,29 +46,84 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
debtors_usd.account_type = debtors.account_type
|
||||
self.debtors_usd = debtors_usd.save().name
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def create_payment_entry(self, docname):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
def create_credit_note(self, docname):
|
||||
credit_note = create_sales_invoice(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item=self.item,
|
||||
qty=-1,
|
||||
debit_to=self.debit_to,
|
||||
cost_center=self.cost_center,
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
name = make_sales_invoice().name
|
||||
si = self.create_sales_invoice()
|
||||
name = si.name
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
make_payment(name)
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
|
||||
@@ -84,10 +136,10 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
make_credit_note(name)
|
||||
self.create_credit_note(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
@@ -108,21 +160,20 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
"""
|
||||
|
||||
so = make_sales_order(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
warehouse="Finished Goods - _TC2",
|
||||
currency="EUR",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
cost_center=self.cost_center,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name)
|
||||
pe = pe.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 0,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
@@ -147,34 +198,32 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_exchange_revaluation_for_party(self):
|
||||
"""
|
||||
Exchange Revaluation for party on Receivable/Payable shoule be included
|
||||
Exchange Revaluation for party on Receivable/Payable should be included
|
||||
"""
|
||||
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
# Using Exchange Gain/Loss account for unrealized as well.
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
company_doc = frappe.get_doc("Company", self.company)
|
||||
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||
company_doc.save()
|
||||
|
||||
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 0.90
|
||||
si.conversion_rate = 80
|
||||
si.debit_to = self.debtors_usd
|
||||
si = si.save().submit()
|
||||
|
||||
# Exchange Revaluation
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company
|
||||
err.company = self.company
|
||||
err.posting_date = today()
|
||||
accounts = err.get_accounts_data()
|
||||
err.extend("accounts", accounts)
|
||||
err.accounts[0].new_exchange_rate = 0.95
|
||||
err.accounts[0].new_exchange_rate = 85
|
||||
row = err.accounts[0]
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
@@ -189,7 +238,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
je = je.submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
@@ -198,7 +247,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
}
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_for_err = [0, -5, 0, 5]
|
||||
expected_data_for_err = [0, -500, 0, 500]
|
||||
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
|
||||
self.assertEqual(
|
||||
expected_data_for_err,
|
||||
@@ -214,46 +263,43 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
"""
|
||||
Payment against credit/debit note should be considered against the parent invoice
|
||||
"""
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
si1 = make_sales_invoice()
|
||||
si1 = self.create_sales_invoice()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
cr_note = make_credit_note(si1.name)
|
||||
cr_note = self.create_credit_note(si1.name)
|
||||
|
||||
si2 = make_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
|
||||
# manually link cr_note with si2 using journal entry
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = company
|
||||
je.company = self.company
|
||||
je.voucher_type = "Credit Note"
|
||||
je.posting_date = today()
|
||||
|
||||
debit_account = "Debtors - _TC2"
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"party": self.customer,
|
||||
"debit": 100,
|
||||
"debit_in_account_currency": 100,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
credit_entry = {
|
||||
"account": debit_account,
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"party": self.customer,
|
||||
"credit": 100,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si2.doctype,
|
||||
"reference_name": si2.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
@@ -261,7 +307,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
je = je.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
@@ -271,64 +317,254 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
|
||||
def test_group_by_party(self):
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.posting_date = add_days(today(), -1)
|
||||
si1.save().submit()
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.items[0].rate = 85
|
||||
si2.save().submit()
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"group_by_party": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 5)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
warehouse="Finished Goods - _TC2",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
do_not_save=1,
|
||||
)
|
||||
# assert voucher rows
|
||||
expected_voucher_rows = [
|
||||
[100.0, 100.0, 100.0, 100.0],
|
||||
[85.0, 85.0, 85.0, 85.0],
|
||||
]
|
||||
voucher_rows = []
|
||||
for x in report[0:2]:
|
||||
voucher_rows.append(
|
||||
[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
|
||||
)
|
||||
self.assertEqual(expected_voucher_rows, voucher_rows)
|
||||
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||
# assert total rows
|
||||
expected_total_rows = [
|
||||
[self.customer, 185.0, 185.0], # party total
|
||||
{}, # empty row for padding
|
||||
["Total", 185.0, 185.0], # grand total
|
||||
]
|
||||
party_total_row = report[2]
|
||||
self.assertEqual(
|
||||
expected_total_rows[0],
|
||||
[
|
||||
party_total_row.get("party"),
|
||||
party_total_row.get("invoiced"),
|
||||
party_total_row.get("outstanding"),
|
||||
],
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||
empty_row = report[3]
|
||||
self.assertEqual(expected_total_rows[1], empty_row)
|
||||
grand_total_row = report[4]
|
||||
self.assertEqual(
|
||||
expected_total_rows[2],
|
||||
[
|
||||
grand_total_row.get("party"),
|
||||
grand_total_row.get("invoiced"),
|
||||
grand_total_row.get("outstanding"),
|
||||
],
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
def test_future_payments(self):
|
||||
si = self.create_sales_invoice()
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.paid_amount = 90.0
|
||||
pe.references[0].allocated_amount = 90.0
|
||||
pe.save().submit()
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_future_payments": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
expected_data = [100.0, 100.0, 10.0, 90.0]
|
||||
|
||||
return si
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
pe.cancel()
|
||||
# full payment in future date
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.save().submit()
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, 0.0, 100.0]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def make_payment(docname):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
# over payment in future date
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.paid_amount = 110
|
||||
pe.save().submit()
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 2)
|
||||
expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
|
||||
for idx, row in enumerate(report):
|
||||
self.assertEqual(
|
||||
expected_data[idx],
|
||||
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
||||
)
|
||||
|
||||
def test_sales_person(self):
|
||||
sales_person = (
|
||||
frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
si = self.create_sales_invoice(do_not_submit=True)
|
||||
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
|
||||
si.save().submit()
|
||||
|
||||
def make_credit_note(docname):
|
||||
credit_note = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
qty=-1,
|
||||
warehouse="Finished Goods - _TC2",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"sales_person": sales_person.name,
|
||||
"show_sales_person": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
|
||||
return credit_note
|
||||
expected_data = [100.0, 100.0, sales_person.name]
|
||||
|
||||
row = report[0]
|
||||
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
|
||||
|
||||
def test_cost_center_filter(self):
|
||||
si = self.create_sales_invoice()
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, self.cost_center]
|
||||
row = report[0]
|
||||
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
|
||||
|
||||
def test_customer_group_filter(self):
|
||||
si = self.create_sales_invoice()
|
||||
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer_group": cus_group,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, cus_group]
|
||||
row = report[0]
|
||||
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
|
||||
|
||||
filters.update({"customer_group": "Individual"})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 0)
|
||||
|
||||
def test_party_account_filter(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
self.customer2 = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.posting_date = add_days(today(), -1)
|
||||
si2.customer = self.customer2
|
||||
si2.currency = "USD"
|
||||
si2.conversion_rate = 80
|
||||
si2.debit_to = self.debtors_usd
|
||||
si2.save().submit()
|
||||
|
||||
# Filter on company currency receivable account
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"party_account": self.debit_to,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, self.debit_to, si1.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
)
|
||||
|
||||
# Filter on USD receivable account
|
||||
filters.update({"party_account": self.debtors_usd})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
)
|
||||
|
||||
# without filter on party account
|
||||
filters.pop("party_account")
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 2)
|
||||
expected_data = [
|
||||
[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
|
||||
[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
|
||||
]
|
||||
for idx, row in enumerate(report):
|
||||
self.assertEqual(
|
||||
expected_data[idx],
|
||||
[
|
||||
row.invoiced,
|
||||
row.outstanding,
|
||||
row.invoiced_in_account_currency,
|
||||
row.outstanding_in_account_currency,
|
||||
row.party_account,
|
||||
row.account_currency,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.filters.show_future_payments,
|
||||
self.filters.company,
|
||||
party=party,
|
||||
account_type=self.account_type,
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date)
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
|
||||
|
||||
for party, party_dict in self.party_total.items():
|
||||
if party_dict.outstanding == 0:
|
||||
@@ -233,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
|
||||
|
||||
def get_gl_balance(report_date):
|
||||
def get_gl_balance(report_date, company):
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"GL Entry",
|
||||
fields=["party", "sum(debit - credit)"],
|
||||
filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
|
||||
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
|
||||
group_by="party",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_01_receivable_summary_output(self):
|
||||
"""
|
||||
Test for Invoices, Paid, Advance and Outstanding
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=200,
|
||||
price_list_rate=200,
|
||||
)
|
||||
|
||||
customer_group, customer_territory = frappe.db.get_all(
|
||||
"Customer",
|
||||
filters={"name": self.customer},
|
||||
fields=["customer_group", "territory"],
|
||||
as_list=True,
|
||||
)[0]
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
expected_data = {
|
||||
"party_type": "Customer",
|
||||
"advance": 0,
|
||||
"party": self.customer,
|
||||
"invoiced": 200.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 200.0,
|
||||
"range1": 200.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 200.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"currency": si.currency,
|
||||
"territory": customer_territory,
|
||||
"customer_group": customer_group,
|
||||
}
|
||||
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# simulate advance payment
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 50
|
||||
pe.references[0].allocated_amount = 0 # this essitially removes the reference
|
||||
pe.save().submit()
|
||||
|
||||
# update expected data with advance
|
||||
expected_data.update(
|
||||
{
|
||||
"advance": 50.0,
|
||||
"outstanding": 150.0,
|
||||
"range1": 150.0,
|
||||
"total_due": 150.0,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# make partial payment
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 125
|
||||
pe.references[0].allocated_amount = 125
|
||||
pe.save().submit()
|
||||
|
||||
# update expected data after advance and partial payment
|
||||
expected_data.update(
|
||||
{"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
@change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
|
||||
def test_02_various_filters_and_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=200,
|
||||
price_list_rate=200,
|
||||
)
|
||||
# make partial payment
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 150
|
||||
pe.references[0].allocated_amount = 150
|
||||
pe.save().submit()
|
||||
|
||||
customer_group, customer_territory = frappe.db.get_all(
|
||||
"Customer",
|
||||
filters={"name": self.customer},
|
||||
fields=["customer_group", "territory"],
|
||||
as_list=True,
|
||||
)[0]
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
expected_data = {
|
||||
"party_type": "Customer",
|
||||
"advance": 0,
|
||||
"party": self.customer,
|
||||
"party_name": self.customer,
|
||||
"invoiced": 200.0,
|
||||
"paid": 150.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 50.0,
|
||||
"range1": 50.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 50.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"currency": si.currency,
|
||||
"territory": customer_territory,
|
||||
"customer_group": customer_group,
|
||||
}
|
||||
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# with gl balance filter
|
||||
filters.update({"show_gl_balance": True})
|
||||
expected_data.update({"gl_balance": 50.0, "diff": 0.0})
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# with gl balance and future payments filter
|
||||
filters.update({"show_future_payments": True})
|
||||
expected_data.update({"remaining_balance": 50.0})
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# invoice fully paid
|
||||
pe = get_payment_entry(si.doctype, si.name).save().submit()
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 0)
|
||||
@@ -58,6 +58,9 @@ def get_data(filters):
|
||||
|
||||
|
||||
def get_asset_categories(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT asset_category,
|
||||
@@ -98,15 +101,25 @@ def get_asset_categories(filters):
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
|
||||
group by asset_category
|
||||
""",
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""".format(
|
||||
condition
|
||||
),
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset_category": filters.get("asset_category"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT results.asset_category,
|
||||
@@ -138,7 +151,7 @@ def get_assets(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
@@ -154,10 +167,12 @@ def get_assets(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""",
|
||||
""".format(
|
||||
condition
|
||||
),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
|
||||
if from_date:
|
||||
additional_conditions.append(gle.posting_date >= from_date)
|
||||
|
||||
finance_book = filters.get("finance_book")
|
||||
company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
|
||||
finance_books = []
|
||||
finance_books.append("")
|
||||
if filter_fb := filters.get("finance_book"):
|
||||
finance_books.append(filter_fb)
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
|
||||
if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"):
|
||||
finance_books.append(company_fb)
|
||||
|
||||
additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
|
||||
else:
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
|
||||
additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
|
||||
|
||||
return additional_conditions
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ def get_tds_docs(filters):
|
||||
}
|
||||
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query_filters.update({"against": ("in", party)})
|
||||
or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
|
||||
|
||||
if filters.get("party"):
|
||||
del query_filters["account"]
|
||||
@@ -294,7 +294,7 @@ def get_tds_docs(filters):
|
||||
|
||||
if journal_entries:
|
||||
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
|
||||
get_doc_info(journal_entries, "Journal Entry", tax_category_map)
|
||||
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
|
||||
|
||||
return (
|
||||
tds_documents,
|
||||
@@ -309,7 +309,11 @@ def get_journal_entry_party_map(journal_entries):
|
||||
journal_entry_party_map = {}
|
||||
for d in frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
|
||||
{
|
||||
"parent": ("in", journal_entries),
|
||||
"party_type": ("in", ("Supplier", "Customer")),
|
||||
"party": ("is", "set"),
|
||||
},
|
||||
["parent", "party"],
|
||||
):
|
||||
if d.parent not in journal_entry_party_map:
|
||||
@@ -320,41 +324,29 @@ def get_journal_entry_party_map(journal_entries):
|
||||
|
||||
|
||||
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
if doctype == "Purchase Invoice":
|
||||
fields = [
|
||||
"name",
|
||||
"tax_withholding_category",
|
||||
"base_tax_withholding_net_total",
|
||||
"grand_total",
|
||||
"base_total",
|
||||
]
|
||||
elif doctype == "Sales Invoice":
|
||||
fields = ["name", "base_net_total", "grand_total", "base_total"]
|
||||
elif doctype == "Payment Entry":
|
||||
fields = [
|
||||
"name",
|
||||
"tax_withholding_category",
|
||||
"paid_amount",
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
]
|
||||
else:
|
||||
fields = ["name", "tax_withholding_category"]
|
||||
common_fields = ["name", "tax_withholding_category"]
|
||||
fields_dict = {
|
||||
"Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"],
|
||||
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
|
||||
"Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"],
|
||||
"Journal Entry": ["total_amount"],
|
||||
}
|
||||
|
||||
entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields)
|
||||
entries = frappe.get_all(
|
||||
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
if doctype == "Purchase Invoice":
|
||||
net_total_map.update(
|
||||
{entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]}
|
||||
)
|
||||
value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
|
||||
elif doctype == "Sales Invoice":
|
||||
net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]})
|
||||
value = [entry.base_net_total, entry.grand_total, entry.base_total]
|
||||
elif doctype == "Payment Entry":
|
||||
net_total_map.update(
|
||||
{entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]}
|
||||
)
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
net_total_map.update({entry.name: value})
|
||||
|
||||
|
||||
def get_tax_rate_map(filters):
|
||||
|
||||
118
erpnext/accounts/report/trial_balance/test_trial_balance.py
Normal file
118
erpnext/accounts/report/trial_balance/test_trial_balance.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import execute
|
||||
|
||||
|
||||
class TestTrialBalance(FrappeTestCase):
|
||||
def setUp(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
self.company = create_company()
|
||||
create_cost_center(
|
||||
cost_center_name="Test Cost Center",
|
||||
company="Trial Balance Company",
|
||||
parent_cost_center="Trial Balance Company - TBC",
|
||||
)
|
||||
create_account(
|
||||
account_name="Offsetting",
|
||||
company="Trial Balance Company",
|
||||
parent_account="Temporary Accounts - TBC",
|
||||
)
|
||||
self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
|
||||
create_accounting_dimension()
|
||||
|
||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||
"""
|
||||
Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
|
||||
|
||||
branch1 = frappe.new_doc("Branch")
|
||||
branch1.branch = "Location 1"
|
||||
branch1.insert(ignore_if_duplicate=True)
|
||||
branch2 = frappe.new_doc("Branch")
|
||||
branch2.branch = "Location 2"
|
||||
branch2.insert(ignore_if_duplicate=True)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=self.company,
|
||||
debit_to="Debtors - TBC",
|
||||
cost_center="Test Cost Center - TBC",
|
||||
income_account="Sales - TBC",
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.branch = "Location 1"
|
||||
si.items[0].branch = "Location 2"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
{"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
|
||||
)
|
||||
total_row = execute(filters)[1][-1]
|
||||
self.assertEqual(total_row["debit"], total_row["credit"])
|
||||
|
||||
def tearDown(self):
|
||||
clear_dimension_defaults("Branch")
|
||||
disable_dimension()
|
||||
|
||||
|
||||
def create_company(**args):
|
||||
args = frappe._dict(args)
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": args.company_name or "Trial Balance Company",
|
||||
"country": args.country or "India",
|
||||
"default_currency": args.currency or "INR",
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
return company.name
|
||||
|
||||
|
||||
def create_accounting_dimension(**args):
|
||||
args = frappe._dict(args)
|
||||
document_type = args.document_type or "Branch"
|
||||
if frappe.db.exists("Accounting Dimension", document_type):
|
||||
accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
|
||||
accounting_dimension.disabled = 0
|
||||
else:
|
||||
accounting_dimension = frappe.new_doc("Accounting Dimension")
|
||||
accounting_dimension.document_type = document_type
|
||||
accounting_dimension.insert()
|
||||
|
||||
accounting_dimension.set("dimension_defaults", [])
|
||||
accounting_dimension.append(
|
||||
"dimension_defaults",
|
||||
{
|
||||
"company": args.company or "Trial Balance Company",
|
||||
"automatically_post_balancing_accounting_entry": 1,
|
||||
"offsetting_account": args.offsetting_account or "Offsetting - TBC",
|
||||
},
|
||||
)
|
||||
accounting_dimension.save()
|
||||
|
||||
|
||||
def disable_dimension(**args):
|
||||
args = frappe._dict(args)
|
||||
document_type = args.document_type or "Branch"
|
||||
dimension = frappe.get_doc("Accounting Dimension", document_type)
|
||||
dimension.disabled = 1
|
||||
dimension.save()
|
||||
|
||||
|
||||
def clear_dimension_defaults(dimension_name):
|
||||
accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
|
||||
accounting_dimension.dimension_defaults = []
|
||||
accounting_dimension.save()
|
||||
@@ -1,10 +1,11 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class AccountsTestMixin:
|
||||
def create_customer(self, customer_name, currency=None):
|
||||
def create_customer(self, customer_name="_Test Customer", currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
@@ -17,7 +18,7 @@ class AccountsTestMixin:
|
||||
else:
|
||||
self.customer = customer_name
|
||||
|
||||
def create_supplier(self, supplier_name, currency=None):
|
||||
def create_supplier(self, supplier_name="_Test Supplier", currency=None):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
@@ -31,7 +32,7 @@ class AccountsTestMixin:
|
||||
else:
|
||||
self.supplier = supplier_name
|
||||
|
||||
def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
|
||||
def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None):
|
||||
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
|
||||
self.item = item.name
|
||||
|
||||
@@ -59,22 +60,82 @@ class AccountsTestMixin:
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
self.debit_usd = "Debtors USD - " + abbr
|
||||
self.cash = "Cash - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
self.retained_earnings = "Retained Earnings - " + abbr
|
||||
|
||||
# create bank account
|
||||
bank_account = "HDFC - " + abbr
|
||||
if frappe.db.exists("Account", bank_account):
|
||||
self.bank = bank_account
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
# Deferred revenue, expense and bank accounts
|
||||
other_accounts = [
|
||||
frappe._dict(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"attribute_name": "deferred_revenue",
|
||||
"account_name": "Deferred Revenue",
|
||||
"parent_account": "Current Liabilities - " + abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "deferred_expense",
|
||||
"account_name": "Deferred Expense",
|
||||
"parent_account": "Current Assets - " + abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "bank",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - " + abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
),
|
||||
]
|
||||
for acc in other_accounts:
|
||||
acc_name = acc.account_name + " - " + abbr
|
||||
if frappe.db.exists("Account", acc_name):
|
||||
setattr(self, acc.attribute_name, acc_name)
|
||||
else:
|
||||
new_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": acc.account_name,
|
||||
"parent_account": acc.parent_account,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_acc.save()
|
||||
setattr(self, acc.attribute_name, new_acc.name)
|
||||
|
||||
def create_usd_receivable_account(self):
|
||||
account_name = "Debtors USD"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = account_name
|
||||
acc.parent_account = "Accounts Receivable - " + self.company_abbr
|
||||
acc.company = self.company
|
||||
acc.account_currency = "USD"
|
||||
acc.account_type = "Receivable"
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.debtors_usd = acc.name
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Sales Order",
|
||||
"Exchange Rate Revaluation",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
@@ -884,6 +884,9 @@ def get_outstanding_invoices(
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
accounting_dimensions=None,
|
||||
vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering
|
||||
limit=None, # passed by reconciliation tool
|
||||
voucher_no=None, # filter passed by reconciliation tool
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -909,12 +912,15 @@ def get_outstanding_invoices(
|
||||
|
||||
ple_query = QueryPaymentLedger()
|
||||
invoice_list = ple_query.get_voucher_outstandings(
|
||||
vouchers=vouchers,
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_date,
|
||||
min_outstanding=min_outstanding,
|
||||
max_outstanding=max_outstanding,
|
||||
get_invoices=True,
|
||||
accounting_dimensions=accounting_dimensions or [],
|
||||
limit=limit,
|
||||
voucher_no=voucher_no,
|
||||
)
|
||||
|
||||
for d in invoice_list:
|
||||
@@ -1646,12 +1652,13 @@ class QueryPaymentLedger(object):
|
||||
self.voucher_posting_date = []
|
||||
self.min_outstanding = None
|
||||
self.max_outstanding = None
|
||||
self.limit = self.voucher_no = None
|
||||
|
||||
def reset(self):
|
||||
# clear filters
|
||||
self.vouchers.clear()
|
||||
self.common_filter.clear()
|
||||
self.min_outstanding = self.max_outstanding = None
|
||||
self.min_outstanding = self.max_outstanding = self.limit = None
|
||||
|
||||
# clear result
|
||||
self.voucher_outstandings.clear()
|
||||
@@ -1665,6 +1672,7 @@ class QueryPaymentLedger(object):
|
||||
|
||||
filter_on_voucher_no = []
|
||||
filter_on_against_voucher_no = []
|
||||
|
||||
if self.vouchers:
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
voucher_nos = set([x.voucher_no for x in self.vouchers])
|
||||
@@ -1675,6 +1683,10 @@ class QueryPaymentLedger(object):
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
|
||||
|
||||
if self.voucher_no:
|
||||
filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%"))
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%"))
|
||||
|
||||
# build outstanding amount filter
|
||||
filter_on_outstanding_amount = []
|
||||
if self.min_outstanding:
|
||||
@@ -1790,6 +1802,11 @@ class QueryPaymentLedger(object):
|
||||
)
|
||||
)
|
||||
|
||||
if self.limit:
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
self.cte_query_voucher_amount_and_outstanding.limit(self.limit)
|
||||
)
|
||||
|
||||
# execute SQL
|
||||
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
|
||||
|
||||
@@ -1803,6 +1820,8 @@ class QueryPaymentLedger(object):
|
||||
get_payments=False,
|
||||
get_invoices=False,
|
||||
accounting_dimensions=None,
|
||||
limit=None,
|
||||
voucher_no=None,
|
||||
):
|
||||
"""
|
||||
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
|
||||
@@ -1824,6 +1843,8 @@ class QueryPaymentLedger(object):
|
||||
self.max_outstanding = max_outstanding
|
||||
self.get_payments = get_payments
|
||||
self.get_invoices = get_invoices
|
||||
self.limit = limit
|
||||
self.voucher_no = voucher_no
|
||||
self.query_for_outstanding()
|
||||
|
||||
return self.voucher_outstandings
|
||||
|
||||
@@ -81,18 +81,27 @@ class Asset(AccountsController):
|
||||
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
|
||||
)
|
||||
|
||||
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
|
||||
def prepare_depreciation_data(
|
||||
self,
|
||||
date_of_disposal=None,
|
||||
date_of_return=None,
|
||||
value_after_depreciation=None,
|
||||
ignore_booked_entry=False,
|
||||
):
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
if self.should_prepare_depreciation_schedule():
|
||||
self.make_depreciation_schedule(date_of_disposal)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||
self.make_depreciation_schedule(date_of_disposal, value_after_depreciation)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return, ignore_booked_entry)
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
if value_after_depreciation:
|
||||
self.value_after_depreciation = value_after_depreciation
|
||||
else:
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
|
||||
def should_prepare_depreciation_schedule(self):
|
||||
if not self.get("schedules"):
|
||||
@@ -285,7 +294,7 @@ class Asset(AccountsController):
|
||||
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
|
||||
)
|
||||
|
||||
def make_depreciation_schedule(self, date_of_disposal):
|
||||
def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None):
|
||||
if not self.get("schedules"):
|
||||
self.schedules = []
|
||||
|
||||
@@ -295,24 +304,30 @@ class Asset(AccountsController):
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
for finance_book in self.get("finance_books"):
|
||||
self._make_depreciation_schedule(finance_book, start, date_of_disposal)
|
||||
self._make_depreciation_schedule(
|
||||
finance_book, start, date_of_disposal, value_after_depreciation
|
||||
)
|
||||
|
||||
if len(self.get("finance_books")) > 1 and any(start):
|
||||
self.sort_depreciation_schedule()
|
||||
|
||||
def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
|
||||
def _make_depreciation_schedule(
|
||||
self, finance_book, start, date_of_disposal, value_after_depreciation=None
|
||||
):
|
||||
self.validate_asset_finance_books(finance_book)
|
||||
|
||||
value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book)
|
||||
if not value_after_depreciation:
|
||||
value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book)
|
||||
|
||||
finance_book.value_after_depreciation = value_after_depreciation
|
||||
|
||||
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
|
||||
final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
|
||||
self.number_of_depreciations_booked
|
||||
)
|
||||
|
||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||
if has_pro_rata:
|
||||
number_of_pending_depreciations += 1
|
||||
final_number_of_depreciations += 1
|
||||
|
||||
has_wdv_or_dd_non_yearly_pro_rata = False
|
||||
if (
|
||||
@@ -328,7 +343,9 @@ class Asset(AccountsController):
|
||||
|
||||
depreciation_amount = 0
|
||||
|
||||
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
|
||||
number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1]
|
||||
|
||||
for n in range(start[finance_book.idx - 1], final_number_of_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row:
|
||||
continue
|
||||
@@ -345,10 +362,11 @@ class Asset(AccountsController):
|
||||
n,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
number_of_pending_depreciations,
|
||||
)
|
||||
|
||||
if not has_pro_rata or (
|
||||
n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
|
||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||
):
|
||||
schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
|
||||
@@ -416,7 +434,7 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
# For last row
|
||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||
elif has_pro_rata and n == cint(final_number_of_depreciations) - 1:
|
||||
if not self.flags.increase_in_asset_life:
|
||||
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
|
||||
self.to_date = add_months(
|
||||
@@ -447,7 +465,7 @@ class Asset(AccountsController):
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if finance_book.expected_value_after_useful_life and (
|
||||
(
|
||||
n == cint(number_of_pending_depreciations) - 1
|
||||
n == cint(final_number_of_depreciations) - 1
|
||||
and value_after_depreciation != finance_book.expected_value_after_useful_life
|
||||
)
|
||||
or value_after_depreciation < finance_book.expected_value_after_useful_life
|
||||
@@ -690,7 +708,10 @@ class Asset(AccountsController):
|
||||
if s.finance_book_id == d.finance_book_id
|
||||
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual")
|
||||
]
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
if i > 0 and self.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
accumulated_depreciation = self.get("schedules")[i - 1].accumulated_depreciation_amount
|
||||
else:
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
value_after_depreciation = flt(
|
||||
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
|
||||
)
|
||||
@@ -1296,11 +1317,14 @@ def get_depreciation_amount(
|
||||
schedule_idx=0,
|
||||
prev_depreciation_amount=0,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
number_of_pending_depreciations=0,
|
||||
):
|
||||
frappe.flags.company = asset.company
|
||||
|
||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||
return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
|
||||
return get_straight_line_or_manual_depr_amount(
|
||||
asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||
)
|
||||
else:
|
||||
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset, depreciable_value, fb_row
|
||||
@@ -1320,7 +1344,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb
|
||||
return fb_row.rate_of_depreciation
|
||||
|
||||
|
||||
def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
|
||||
def get_straight_line_or_manual_depr_amount(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations
|
||||
):
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
|
||||
@@ -1331,6 +1357,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
|
||||
row.total_number_of_depreciations
|
||||
)
|
||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if row.daily_depreciation:
|
||||
daily_depr_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
* row.frequency_of_depreciation,
|
||||
),
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
)
|
||||
* row.frequency_of_depreciation,
|
||||
),
|
||||
)
|
||||
to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
from_date = add_months(
|
||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
||||
)
|
||||
return daily_depr_amount * date_diff(to_date, from_date)
|
||||
else:
|
||||
return (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / number_of_pending_depreciations
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_depreciation:
|
||||
|
||||
@@ -5,15 +5,12 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, date_diff, flt, formatdate, getdate
|
||||
from frappe.utils import flt, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_value_after_depreciation,
|
||||
get_depreciation_amount,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
|
||||
|
||||
@@ -25,10 +22,10 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
def on_submit(self):
|
||||
self.make_depreciation_entry()
|
||||
self.reschedule_depreciations(self.new_asset_value)
|
||||
self.update_asset(self.new_asset_value)
|
||||
|
||||
def on_cancel(self):
|
||||
self.reschedule_depreciations(self.current_asset_value)
|
||||
self.update_asset(self.current_asset_value)
|
||||
|
||||
def validate_date(self):
|
||||
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
|
||||
@@ -71,12 +68,16 @@ class AssetValueAdjustment(Document):
|
||||
"account": accumulated_depreciation_account,
|
||||
"credit_in_account_currency": self.difference_amount,
|
||||
"cost_center": depreciation_cost_center or self.cost_center,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": depreciation_expense_account,
|
||||
"debit_in_account_currency": self.difference_amount,
|
||||
"cost_center": depreciation_cost_center or self.cost_center,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
}
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
@@ -106,44 +107,11 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
self.db_set("journal_entry", je.name)
|
||||
|
||||
def reschedule_depreciations(self, asset_value):
|
||||
def update_asset(self, asset_value):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
country = frappe.get_value("Company", self.company, "country")
|
||||
|
||||
for d in asset.finance_books:
|
||||
d.value_after_depreciation = asset_value
|
||||
asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
|
||||
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
|
||||
total_days
|
||||
)
|
||||
from_date = self.date
|
||||
else:
|
||||
no_of_depreciations = len(
|
||||
[
|
||||
s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry)
|
||||
]
|
||||
)
|
||||
|
||||
value_after_depreciation = d.value_after_depreciation
|
||||
for data in asset.schedules:
|
||||
if cint(data.finance_book_id) == d.idx and not data.journal_entry:
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
days = date_diff(data.schedule_date, from_date)
|
||||
depreciation_amount = days * rate_per_day
|
||||
from_date = data.schedule_date
|
||||
else:
|
||||
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
|
||||
|
||||
if depreciation_amount:
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
data.depreciation_amount = depreciation_amount
|
||||
|
||||
d.db_update()
|
||||
|
||||
asset.set_accumulated_depreciation(ignore_booked_entry=True)
|
||||
for asset_data in asset.schedules:
|
||||
if not asset_data.journal_entry:
|
||||
asset_data.db_update()
|
||||
asset.prepare_depreciation_data(value_after_depreciation=asset_value, ignore_booked_entry=True)
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.save()
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, get_last_day, nowdate
|
||||
from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_data
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -46,40 +47,44 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
|
||||
def test_asset_depreciation_value_adjustment(self):
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
|
||||
item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location"
|
||||
)
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
|
||||
month_end_date = get_last_day(nowdate())
|
||||
purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
|
||||
|
||||
asset_doc.available_for_use_date = purchase_date
|
||||
asset_doc.purchase_date = purchase_date
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 200,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 3,
|
||||
"frequency_of_depreciation": 10,
|
||||
"depreciation_start_date": month_end_date,
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
|
||||
post_depreciation_entries(getdate("2023-08-21"))
|
||||
|
||||
current_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=current_value,
|
||||
new_asset_value=50000.0,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
|
||||
asset_doc.reload()
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
|
||||
("_Test Depreciations - _TC", 50000.0, 0.0),
|
||||
("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
|
||||
("_Test Depreciations - _TC", 4625.29, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -91,6 +96,29 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 5474.73, 5474.73],
|
||||
["2023-02-28", 9983.33, 15458.06],
|
||||
["2023-03-31", 9983.33, 25441.39],
|
||||
["2023-04-30", 9983.33, 35424.72],
|
||||
["2023-05-31", 9983.33, 45408.05],
|
||||
["2023-06-30", 9983.33, 55391.38],
|
||||
["2023-07-31", 9983.33, 65374.71],
|
||||
["2023-08-31", 8300.0, 73674.71],
|
||||
["2023-09-30", 8300.0, 81974.71],
|
||||
["2023-10-31", 8300.0, 90274.71],
|
||||
["2023-11-30", 8300.0, 98574.71],
|
||||
["2023-12-31", 8300.0, 106874.71],
|
||||
["2024-01-15", 8300.0, 115174.71],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in asset_doc.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -156,6 +156,8 @@ def get_data(filters):
|
||||
|
||||
|
||||
def prepare_chart_data(data, filters):
|
||||
if not data:
|
||||
return
|
||||
labels_values_map = {}
|
||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||
filters_filter_based_on = "Date Range"
|
||||
|
||||
@@ -245,19 +245,21 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
]
|
||||
});
|
||||
|
||||
dialog.fields_dict['supplier'].df.onchange = () => {
|
||||
var supplier = dialog.get_value('supplier');
|
||||
frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
|
||||
dialog.fields_dict["supplier"].df.onchange = () => {
|
||||
frm.call("get_supplier_email_preview", {
|
||||
supplier: dialog.get_value("supplier"),
|
||||
}).then(({ message }) => {
|
||||
dialog.fields_dict.email_preview.$wrapper.empty();
|
||||
dialog.fields_dict.email_preview.$wrapper.append(result.message);
|
||||
dialog.fields_dict.email_preview.$wrapper.append(
|
||||
message.message
|
||||
);
|
||||
dialog.set_value("subject", message.subject);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
|
||||
automatically be attached with the email.</p>`);
|
||||
|
||||
dialog.set_value("subject", frm.doc.subject);
|
||||
dialog.show();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,11 +20,10 @@
|
||||
"items_section",
|
||||
"items",
|
||||
"supplier_response_section",
|
||||
"salutation",
|
||||
"subject",
|
||||
"col_break_email_1",
|
||||
"email_template",
|
||||
"preview",
|
||||
"col_break_email_1",
|
||||
"html_llwp",
|
||||
"send_attached_files",
|
||||
"sec_break_email_2",
|
||||
"message_for_supplier",
|
||||
@@ -237,23 +236,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "email_template.subject",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
|
||||
"fieldname": "salutation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Salutation",
|
||||
"no_copy": 1,
|
||||
"options": "Salutation",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_email_1",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -287,6 +269,14 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_llwp",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p>In your <b>Email Template</b>, you can use the following special variables:\n</p>\n<ul>\n <li>\n <code>{{ update_password_link }}</code>: A link where your supplier can set a new password to log into your portal.\n </li>\n <li>\n <code>{{ portal_link }}</code>: A link to this RFQ in your supplier portal.\n </li>\n <li>\n <code>{{ supplier_name }}</code>: The company name of your supplier.\n </li>\n <li>\n <code>{{ contact.salutation }} {{ contact.last_name }}</code>: The contact person of your supplier.\n </li><li>\n <code>{{ user_fullname }}</code>: Your full name.\n </li>\n </ul>\n<p></p>\n<p>Apart from these, you can access all values in this RFQ, like <code>{{ message_for_supplier }}</code> or <code>{{ terms }}</code>.</p>",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, all files attached to this document will be attached to each email",
|
||||
@@ -299,7 +289,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-27 16:41:48.468873",
|
||||
"modified": "2023-08-08 16:30:10.870429",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -182,35 +182,28 @@ class RequestforQuotation(BuyingController):
|
||||
if full_name == "Guest":
|
||||
full_name = "Administrator"
|
||||
|
||||
# send document dict and some important data from suppliers row
|
||||
# to render message_for_supplier from any template
|
||||
doc_args = self.as_dict()
|
||||
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
|
||||
|
||||
# Get Contact Full Name
|
||||
supplier_name = None
|
||||
if data.get("contact"):
|
||||
contact_name = frappe.db.get_value(
|
||||
"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
|
||||
)
|
||||
supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
|
||||
contact = frappe.get_doc("Contact", data.get("contact"))
|
||||
doc_args["contact"] = contact.as_dict()
|
||||
|
||||
args = {
|
||||
"update_password_link": update_password_link,
|
||||
"message": frappe.render_template(self.message_for_supplier, doc_args),
|
||||
"rfq_link": rfq_link,
|
||||
"user_fullname": full_name,
|
||||
"supplier_name": supplier_name or data.get("supplier_name"),
|
||||
"supplier_salutation": self.salutation or "Dear Mx.",
|
||||
}
|
||||
|
||||
subject = self.subject or _("Request for Quotation")
|
||||
template = "templates/emails/request_for_quotation.html"
|
||||
doc_args.update(
|
||||
{
|
||||
"supplier": data.get("supplier"),
|
||||
"supplier_name": data.get("supplier_name"),
|
||||
"update_password_link": f'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
|
||||
"portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-xs" target="_blank"> {_("Submit your Quotation")} </a>',
|
||||
"user_fullname": full_name,
|
||||
}
|
||||
)
|
||||
email_template = frappe.get_doc("Email Template", self.email_template)
|
||||
message = frappe.render_template(email_template.response_, doc_args)
|
||||
subject = frappe.render_template(email_template.subject, doc_args)
|
||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||
message = frappe.get_template(template).render(args)
|
||||
|
||||
if preview:
|
||||
return message
|
||||
return {"message": message, "subject": subject}
|
||||
|
||||
attachments = None
|
||||
if self.send_attached_files:
|
||||
|
||||
@@ -154,31 +154,35 @@ def get_data(filters):
|
||||
procurement_record = []
|
||||
if procurement_record_against_mr:
|
||||
procurement_record += procurement_record_against_mr
|
||||
|
||||
for po in purchase_order_entry:
|
||||
# fetch material records linked to the purchase order item
|
||||
mr_record = mr_records.get(po.material_request_item, [{}])[0]
|
||||
procurement_detail = {
|
||||
"material_request_date": mr_record.get("transaction_date"),
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
"unit_of_measurement": po.stock_uom,
|
||||
"status": po.status,
|
||||
"purchase_order_date": po.transaction_date,
|
||||
"purchase_order": po.parent,
|
||||
"supplier": po.supplier,
|
||||
"estimated_cost": flt(mr_record.get("amount")),
|
||||
"actual_cost": flt(pi_records.get(po.name)),
|
||||
"purchase_order_amt": flt(po.amount),
|
||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||
"expected_delivery_date": po.schedule_date,
|
||||
"actual_delivery_date": pr_records.get(po.name),
|
||||
}
|
||||
procurement_record.append(procurement_detail)
|
||||
material_requests = mr_records.get(po.material_request_item, [{}])
|
||||
|
||||
for mr_record in material_requests:
|
||||
procurement_detail = {
|
||||
"material_request_date": mr_record.get("transaction_date"),
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
"unit_of_measurement": po.stock_uom,
|
||||
"status": po.status,
|
||||
"purchase_order_date": po.transaction_date,
|
||||
"purchase_order": po.parent,
|
||||
"supplier": po.supplier,
|
||||
"estimated_cost": flt(mr_record.get("amount")),
|
||||
"actual_cost": flt(pi_records.get(po.name)),
|
||||
"purchase_order_amt": flt(po.amount),
|
||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||
"expected_delivery_date": po.schedule_date,
|
||||
"actual_delivery_date": pr_records.get(po.name),
|
||||
}
|
||||
procurement_record.append(procurement_detail)
|
||||
|
||||
return procurement_record
|
||||
|
||||
|
||||
@@ -301,7 +305,7 @@ def get_po_entries(filters):
|
||||
& (parent.name == child.parent)
|
||||
& (parent.status.notin(("Closed", "Completed", "Cancelled")))
|
||||
)
|
||||
.groupby(parent.name, child.item_code)
|
||||
.groupby(parent.name, child.material_request_item)
|
||||
)
|
||||
query = apply_filters_on_query(filters, parent, child, query)
|
||||
|
||||
|
||||
@@ -200,9 +200,9 @@ class AccountsController(TransactionBase):
|
||||
# apply tax withholding only if checked and applicable
|
||||
self.set_tax_withholding()
|
||||
|
||||
validate_regional(self)
|
||||
|
||||
validate_einvoice_fields(self)
|
||||
with temporary_flag("company", self.company):
|
||||
validate_regional(self)
|
||||
validate_einvoice_fields(self)
|
||||
|
||||
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
|
||||
apply_pricing_rule_on_transaction(self)
|
||||
@@ -715,7 +715,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_enabled_taxes_and_charges(self):
|
||||
taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges")
|
||||
if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"):
|
||||
if self.taxes_and_charges and frappe.get_cached_value(
|
||||
taxes_and_charges_doctype, self.taxes_and_charges, "disabled"
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)
|
||||
)
|
||||
|
||||
@@ -345,6 +345,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
elif doctype == "Purchase Invoice":
|
||||
# look for Print Heading "Debit Note"
|
||||
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
|
||||
if source.tax_withholding_category:
|
||||
doc.set_onload("supplier_tds", source.tax_withholding_category)
|
||||
|
||||
for tax in doc.get("taxes") or []:
|
||||
if tax.charge_type == "Actual":
|
||||
|
||||
@@ -388,7 +388,7 @@ class SellingController(StockController):
|
||||
for d in self.get("items"):
|
||||
if d.get(ref_fieldname):
|
||||
status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status")
|
||||
if status in ("Closed", "On Hold"):
|
||||
if status in ("Closed", "On Hold") and not self.is_return:
|
||||
frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status))
|
||||
|
||||
def update_reserved_qty(self):
|
||||
@@ -404,7 +404,9 @@ class SellingController(StockController):
|
||||
if so and so_item_rows:
|
||||
sales_order = frappe.get_doc("Sales Order", so)
|
||||
|
||||
if sales_order.status in ["Closed", "Cancelled"]:
|
||||
if (sales_order.status == "Closed" and not self.is_return) or sales_order.status in [
|
||||
"Cancelled"
|
||||
]:
|
||||
frappe.throw(
|
||||
_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ class ProductionPlan(Document):
|
||||
data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
|
||||
|
||||
title = _("Production Plan Already Submitted")
|
||||
if not data:
|
||||
if not data and sales_orders:
|
||||
msg = _("No items are available in the sales order {0} for production").format(sales_orders[0])
|
||||
if len(sales_orders) > 1:
|
||||
sales_orders = ", ".join(sales_orders)
|
||||
@@ -347,7 +347,7 @@ class ProductionPlan(Document):
|
||||
if not data.pending_qty:
|
||||
continue
|
||||
|
||||
item_details = get_item_details(data.item_code)
|
||||
item_details = get_item_details(data.item_code, throw=False)
|
||||
if self.combine_items:
|
||||
if item_details.bom_no in refs:
|
||||
refs[item_details.bom_no]["so_details"].append(
|
||||
@@ -795,6 +795,9 @@ class ProductionPlan(Document):
|
||||
if not row.item_code:
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
|
||||
if not row.bom_no:
|
||||
frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx))
|
||||
|
||||
bom_data = []
|
||||
|
||||
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
|
||||
|
||||
@@ -1075,7 +1075,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item, project=None, skip_bom_info=False):
|
||||
def get_item_details(item, project=None, skip_bom_info=False, throw=True):
|
||||
res = frappe.db.sql(
|
||||
"""
|
||||
select stock_uom, description, item_name, allow_alternative_item,
|
||||
@@ -1111,12 +1111,15 @@ def get_item_details(item, project=None, skip_bom_info=False):
|
||||
|
||||
if not res["bom_no"]:
|
||||
if project:
|
||||
res = get_item_details(item)
|
||||
res = get_item_details(item, throw=throw)
|
||||
frappe.msgprint(
|
||||
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Default BOM for {0} not found").format(item))
|
||||
msg = _("Default BOM for {0} not found").format(item)
|
||||
frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw))
|
||||
|
||||
return res
|
||||
|
||||
bom_data = frappe.db.get_value(
|
||||
"BOM",
|
||||
|
||||
@@ -98,9 +98,11 @@ def get_timesheets(filters):
|
||||
record_filters = [
|
||||
["start_date", "<=", filters.to_date],
|
||||
["end_date", ">=", filters.from_date],
|
||||
["docstatus", "=", 1],
|
||||
]
|
||||
|
||||
if not filters.get("include_draft_timesheets"):
|
||||
record_filters.append(["docstatus", "=", 1])
|
||||
else:
|
||||
record_filters.append(["docstatus", "!=", 2])
|
||||
if "employee" in filters:
|
||||
record_filters.append(["employee", "=", filters.employee])
|
||||
|
||||
|
||||
@@ -25,5 +25,10 @@ frappe.query_reports["Employee Billing Summary"] = {
|
||||
default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"include_draft_timesheets",
|
||||
label: __("Include Timesheets in Draft Status"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,5 +25,10 @@ frappe.query_reports["Project Billing Summary"] = {
|
||||
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"include_draft_timesheets",
|
||||
label: __("Include Timesheets in Draft Status"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -117,6 +117,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
name: __("Document Name"),
|
||||
editable: false,
|
||||
width: 1,
|
||||
format: (value, row) => {
|
||||
return frappe.form.formatters.Link(value, {options: row[2].content});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: __("Reference Date"),
|
||||
|
||||
@@ -57,7 +57,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
|
||||
from_date: me.frm.doc.posting_date,
|
||||
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
|
||||
company: me.frm.doc.company,
|
||||
show_cancelled_entries: me.frm.doc.docstatus === 2
|
||||
show_cancelled_entries: me.frm.doc.docstatus === 2,
|
||||
ignore_prepared_report: true
|
||||
};
|
||||
frappe.set_route("query-report", "Stock Ledger");
|
||||
}, __("View"));
|
||||
@@ -75,7 +76,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
|
||||
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
|
||||
company: me.frm.doc.company,
|
||||
group_by: "Group by Voucher (Consolidated)",
|
||||
show_cancelled_entries: me.frm.doc.docstatus === 2
|
||||
show_cancelled_entries: me.frm.doc.docstatus === 2,
|
||||
ignore_prepared_report: true
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}, __("View"));
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt
|
||||
|
||||
import erpnext
|
||||
@@ -19,19 +20,7 @@ class LandedCostVoucher(Document):
|
||||
self.set("items", [])
|
||||
for pr in self.get("purchase_receipts"):
|
||||
if pr.receipt_document_type and pr.receipt_document:
|
||||
pr_items = frappe.db.sql(
|
||||
"""select pr_item.item_code, pr_item.description,
|
||||
pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name,
|
||||
pr_item.cost_center, pr_item.is_fixed_asset
|
||||
from `tab{doctype} Item` pr_item where parent = %s
|
||||
and exists(select name from tabItem
|
||||
where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1))
|
||||
""".format(
|
||||
doctype=pr.receipt_document_type
|
||||
),
|
||||
pr.receipt_document,
|
||||
as_dict=True,
|
||||
)
|
||||
pr_items = get_pr_items(pr)
|
||||
|
||||
for d in pr_items:
|
||||
item = self.append("items")
|
||||
@@ -247,3 +236,30 @@ class LandedCostVoucher(Document):
|
||||
),
|
||||
tuple([item.valuation_rate] + serial_nos),
|
||||
)
|
||||
|
||||
|
||||
def get_pr_items(purchase_receipt):
|
||||
item = frappe.qb.DocType("Item")
|
||||
pr_item = frappe.qb.DocType(purchase_receipt.receipt_document_type + " Item")
|
||||
return (
|
||||
frappe.qb.from_(pr_item)
|
||||
.inner_join(item)
|
||||
.on(item.name == pr_item.item_code)
|
||||
.select(
|
||||
pr_item.item_code,
|
||||
pr_item.description,
|
||||
pr_item.qty,
|
||||
pr_item.base_rate,
|
||||
pr_item.base_amount,
|
||||
pr_item.name,
|
||||
pr_item.cost_center,
|
||||
pr_item.is_fixed_asset,
|
||||
ConstantColumn(purchase_receipt.receipt_document_type).as_("receipt_document_type"),
|
||||
ConstantColumn(purchase_receipt.receipt_document).as_("receipt_document"),
|
||||
)
|
||||
.where(
|
||||
(pr_item.parent == purchase_receipt.receipt_document)
|
||||
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
@@ -660,7 +660,10 @@ def make_stock_entry(source_name, target_doc=None):
|
||||
"job_card_item": "job_card_item",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.ordered_qty < doc.stock_qty,
|
||||
"condition": lambda doc: (
|
||||
flt(doc.ordered_qty, doc.precision("ordered_qty"))
|
||||
< flt(doc.stock_qty, doc.precision("ordered_qty"))
|
||||
),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -6,6 +6,8 @@ def get_data():
|
||||
"fieldname": "material_request",
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"Project": ["items", "project"],
|
||||
"Cost Center": ["items", "cost_center"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
@@ -15,5 +17,6 @@ def get_data():
|
||||
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
|
||||
{"label": _("Manufacturing"), "items": ["Work Order"]},
|
||||
{"label": _("Internal Transfer"), "items": ["Sales Order"]},
|
||||
{"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -472,27 +472,28 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
# Amount added through landed-cos-voucher
|
||||
if d.landed_cost_voucher_amount and landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
|
||||
account_currency = get_account_currency(account)
|
||||
credit_amount = (
|
||||
flt(amount["base_amount"])
|
||||
if (amount["base_amount"] or account_currency != self.company_currency)
|
||||
else flt(amount["amount"])
|
||||
)
|
||||
if (d.item_code, d.name) in landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
|
||||
account_currency = get_account_currency(account)
|
||||
credit_amount = (
|
||||
flt(amount["base_amount"])
|
||||
if (amount["base_amount"] or account_currency != self.company_currency)
|
||||
else flt(amount["amount"])
|
||||
)
|
||||
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=account,
|
||||
cost_center=d.cost_center,
|
||||
debit=0.0,
|
||||
credit=credit_amount,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
credit_in_account_currency=flt(amount["amount"]),
|
||||
account_currency=account_currency,
|
||||
project=d.project,
|
||||
item=d,
|
||||
)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=account,
|
||||
cost_center=d.cost_center,
|
||||
debit=0.0,
|
||||
credit=credit_amount,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
credit_in_account_currency=flt(amount["amount"]),
|
||||
account_currency=account_currency,
|
||||
project=d.project,
|
||||
item=d,
|
||||
)
|
||||
|
||||
if d.rate_difference_with_purchase_invoice and stock_rbnb:
|
||||
account_currency = get_account_currency(stock_rbnb)
|
||||
|
||||
@@ -71,6 +71,14 @@ frappe.query_reports["Stock Balance"] = {
|
||||
"width": "80",
|
||||
"options": "Warehouse Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_field_type",
|
||||
"label": __("Valuation Field Type"),
|
||||
"fieldtype": "Select",
|
||||
"width": "80",
|
||||
"options": "Currency\nFloat",
|
||||
"default": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname":"include_uom",
|
||||
"label": __("Include UOM"),
|
||||
|
||||
@@ -430,9 +430,12 @@ class StockBalanceReport(object):
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldname": "val_rate",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": self.filters.valuation_field_type or "Currency",
|
||||
"width": 90,
|
||||
"convertible": "rate",
|
||||
"options": "Company:company:default_currency"
|
||||
if self.filters.valuation_field_type == "Currency"
|
||||
else None,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
|
||||
@@ -82,7 +82,15 @@ frappe.query_reports["Stock Ledger"] = {
|
||||
"label": __("Include UOM"),
|
||||
"fieldtype": "Link",
|
||||
"options": "UOM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_field_type",
|
||||
"label": __("Valuation Field Type"),
|
||||
"fieldtype": "Select",
|
||||
"width": "80",
|
||||
"options": "Currency\nFloat",
|
||||
"default": "Currency"
|
||||
},
|
||||
],
|
||||
"formatter": function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
@@ -196,17 +196,21 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Avg Rate (Balance Stock)"),
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": filters.valuation_field_type,
|
||||
"width": 180,
|
||||
"options": "Company:company:default_currency",
|
||||
"options": "Company:company:default_currency"
|
||||
if filters.valuation_field_type == "Currency"
|
||||
else None,
|
||||
"convertible": "rate",
|
||||
},
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldname": "in_out_rate",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": filters.valuation_field_type,
|
||||
"width": 140,
|
||||
"options": "Company:company:default_currency",
|
||||
"options": "Company:company:default_currency"
|
||||
if filters.valuation_field_type == "Currency"
|
||||
else None,
|
||||
"convertible": "rate",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -521,7 +521,7 @@ class update_entries_after(object):
|
||||
|
||||
def update_distinct_item_warehouses(self, dependant_sle):
|
||||
key = (dependant_sle.item_code, dependant_sle.warehouse)
|
||||
val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
|
||||
val = frappe._dict({"sle": dependant_sle})
|
||||
|
||||
if key not in self.distinct_item_warehouses:
|
||||
self.distinct_item_warehouses[key] = val
|
||||
@@ -535,6 +535,8 @@ class update_entries_after(object):
|
||||
|
||||
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
|
||||
val.sle_changed = True
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
|
||||
|
||||
@@ -268,17 +268,24 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
status = "Draft"
|
||||
elif self.docstatus == 1:
|
||||
status = "Completed"
|
||||
|
||||
if self.is_return:
|
||||
status = "Return"
|
||||
return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
|
||||
return_against.run_method("update_status")
|
||||
elif self.per_returned == 100:
|
||||
status = "Return Issued"
|
||||
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
|
||||
if self.is_return:
|
||||
frappe.get_doc("Subcontracting Receipt", self.return_against).update_status(
|
||||
update_modified=update_modified
|
||||
)
|
||||
|
||||
if status:
|
||||
frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
|
||||
frappe.db.set_value(
|
||||
"Subcontracting Receipt", self.name, "status", status, update_modified=update_modified
|
||||
)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<h4>{{_("Request for Quotation")}}</h4>
|
||||
<p>{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},</p>
|
||||
<p>{{ message }}</p>
|
||||
<p>{{_("The Request for Quotation can be accessed by clicking on the following button")}}:</p>
|
||||
<br>
|
||||
<a
|
||||
href="{{ rfq_link }}"
|
||||
class="btn btn-default btn-sm"
|
||||
target="_blank">
|
||||
{{ _("Submit your Quotation") }}
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
{% if update_password_link %}
|
||||
<br>
|
||||
<p>{{_("Please click on the following button to set your new password")}}:</p>
|
||||
<a
|
||||
href="{{ update_password_link }}"
|
||||
class="btn btn-default btn-xs"
|
||||
target="_blank">
|
||||
{{_("Set Password") }}
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
{% endif %}
|
||||
<p>
|
||||
{{_("Regards")}},<br>
|
||||
{{ user_fullname }}
|
||||
</p>
|
||||
@@ -3536,7 +3536,7 @@ Quality Feedback Template,Modèle de commentaires sur la qualité,
|
||||
Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels.,
|
||||
Shift,Décalage,
|
||||
Show {0},Montrer {0},
|
||||
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les séries de nommage {0}",
|
||||
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les masques de numérotation {0}",
|
||||
Target Details,Détails de la cible,
|
||||
{0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}.,
|
||||
API,API,
|
||||
@@ -3551,7 +3551,7 @@ Importing {0} of {1},Importer {0} de {1},
|
||||
Invalid URL,URL invalide,
|
||||
Landscape,Paysage,
|
||||
Last Sync On,Dernière synchronisation le,
|
||||
Naming Series,Nom de série,
|
||||
Naming Series,Masque de numérotation,
|
||||
No data to export,Aucune donnée à exporter,
|
||||
Portrait,Portrait,
|
||||
Print Heading,Imprimer Titre,
|
||||
@@ -4282,7 +4282,7 @@ Please set {0},Veuillez définir {0},supplier
|
||||
Draft,Brouillon,"docstatus,=,0"
|
||||
Cancelled,Annulé,"docstatus,=,2"
|
||||
Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation,
|
||||
Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration> Paramètres> Série de noms,
|
||||
Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration> Paramètres> Série de noms,
|
||||
UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2},
|
||||
Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque,
|
||||
Customer > Customer Group > Territory,Client> Groupe de clients> Territoire,
|
||||
@@ -4297,7 +4297,7 @@ Fetch Serial Numbers based on FIFO,Récupérer les numéros de série basés sur
|
||||
Current Odometer Value should be greater than Last Odometer Value {0},La valeur actuelle de l'odomètre doit être supérieure à la dernière valeur de l'odomètre {0},
|
||||
No additional expenses has been added,Aucune dépense supplémentaire n'a été ajoutée,
|
||||
Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {},
|
||||
Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {},
|
||||
Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {},
|
||||
Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement.,
|
||||
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}.,
|
||||
Invalid Account,Compte invalide,
|
||||
@@ -4321,7 +4321,7 @@ Advanced Settings,Réglages avancés,
|
||||
Path,Chemin,
|
||||
Components,Composants,
|
||||
Verified By,Vérifié Par,
|
||||
Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0},
|
||||
Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0},
|
||||
Filter Based On,Filtre basé sur,
|
||||
Reqd by date,Reqd par date,
|
||||
Manufacturer Part Number <b>{0}</b> is invalid,Le numéro de <b>pièce du</b> fabricant <b>{0}</b> n'est pas valide,
|
||||
@@ -5933,7 +5933,7 @@ Student Admission Program,Programme d'admission des étudiants,
|
||||
Minimum Age,Âge Minimum,
|
||||
Maximum Age,Âge Maximum,
|
||||
Application Fee,Frais de Dossier,
|
||||
Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant),
|
||||
Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant),
|
||||
LMS Only,LMS seulement,
|
||||
EDU-APP-.YYYY.-,EDU-APP-YYYY.-,
|
||||
Application Status,État de la Demande,
|
||||
@@ -6424,7 +6424,7 @@ Hotel Reservation User,Utilisateur chargé des réservations d'hôtel,
|
||||
Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel,
|
||||
Hotel Settings,Paramètres d'Hotel,
|
||||
Default Taxes and Charges,Taxes et frais par défaut,
|
||||
Default Invoice Naming Series,Numéro de série par défaut pour les factures,
|
||||
Default Invoice Naming Series,Masque de numérotation par défaut pour les factures,
|
||||
Additional Salary,Salaire supplémentaire,
|
||||
HR,RH,
|
||||
HR-ADS-.YY.-.MM.-,HR-ADS-.YY .-. MM.-,
|
||||
@@ -8034,7 +8034,7 @@ Default Unit of Measure,Unité de Mesure par Défaut,
|
||||
Maintain Stock,Maintenir Stock,
|
||||
Standard Selling Rate,Prix de Vente Standard,
|
||||
Auto Create Assets on Purchase,Création automatique d'actifs à l'achat,
|
||||
Asset Naming Series,Nom de série de l'actif,
|
||||
Asset Naming Series,Masque de numérotation de l'actif,
|
||||
Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%),
|
||||
Barcodes,Codes-barres,
|
||||
Shelf Life In Days,Durée de conservation en jours,
|
||||
@@ -8053,7 +8053,7 @@ Serial Nos and Batches,N° de Série et Lots,
|
||||
Has Batch No,A un Numéro de Lot,
|
||||
Automatically Create New Batch,Créer un Nouveau Lot Automatiquement,
|
||||
Batch Number Series,Série de numéros de lots,
|
||||
"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.",
|
||||
"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.",
|
||||
Has Expiry Date,A une date d'expiration,
|
||||
Retain Sample,Conserver l'échantillon,
|
||||
Max Sample Quantity,Quantité maximum d'échantillon,
|
||||
@@ -8353,8 +8353,8 @@ Inter Warehouse Transfer Settings,Paramètres de transfert entre entrepôts,
|
||||
Freeze Stock Entries,Geler les Entrées de Stocks,
|
||||
Stock Frozen Upto,Stock Gelé Jusqu'au,
|
||||
Batch Identification,Identification par lots,
|
||||
Use Naming Series,Utiliser la série de noms,
|
||||
Naming Series Prefix,Préfix du nom de série,
|
||||
Use Naming Series,Utiliser le masque de numérotation,
|
||||
Naming Series Prefix,Préfix du masque de numérotation,
|
||||
UOM Category,Catégorie d'unité de mesure (UdM),
|
||||
UOM Conversion Detail,Détails de Conversion de l'UdM,
|
||||
Variant Field,Champ de Variante,
|
||||
@@ -8824,7 +8824,7 @@ Is Inter State,Est Inter State,
|
||||
Purchase Details,Détails d'achat,
|
||||
Depreciation Posting Date,Date comptable de l'amortissement,
|
||||
"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un",
|
||||
choose the 'Naming Series' option.,choisissez l'option 'Naming Series'.,
|
||||
choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'.,
|
||||
Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix.,
|
||||
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.",
|
||||
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case "Autoriser la création de facture d'achat sans reçu d'achat" dans la fiche fournisseur.",
|
||||
@@ -9858,14 +9858,14 @@ Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de dé
|
||||
Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture
|
||||
Control Historical Stock Transactions,Controle de l'historique des stransaction de stock
|
||||
No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date.
|
||||
Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées
|
||||
Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée
|
||||
"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire"
|
||||
Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent
|
||||
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix
|
||||
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock
|
||||
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
|
||||
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries
|
||||
Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées,
|
||||
Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée,
|
||||
"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire
|
||||
Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent,
|
||||
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix,
|
||||
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock,
|
||||
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions,
|
||||
Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries,
|
||||
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
|
||||
Allowed Items,Articles autorisés
|
||||
Party Specific Item,Restriction d'article disponible
|
||||
@@ -9892,34 +9892,46 @@ Interview Feedback,Retour d'entretien
|
||||
Journal Energy Point,Historique des points d'énergies
|
||||
Billing Address Details,Adresse de facturation (détails)
|
||||
Supplier Address Details,Adresse Fournisseur (détails)
|
||||
Retail,Commerce
|
||||
Users,Utilisateurs
|
||||
Permission Manager,Gestion des permissions
|
||||
Fetch Timesheet,Récuprer les temps saisis
|
||||
Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur
|
||||
Quality Inspection(s),Inspection(s) Qualité
|
||||
Set Advances and Allocate (FIFO),Affecter les encours au réglement
|
||||
Apply Putaway Rule,Appliquer la régle de routage d'entrepot
|
||||
Delete Transactions,Supprimer les transactions
|
||||
Default Payment Discount Account,Compte par défaut des paiements de remise
|
||||
Unrealized Profit / Loss Account,Compte de perte
|
||||
Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés
|
||||
Publish in Website,Publier sur le Site Web
|
||||
List View,Vue en liste
|
||||
Allow Excess Material Transfer,Autoriser les transfert de stock supérieurs à l'attendue
|
||||
Allow transferring raw materials even after the Required Quantity is fulfilled,Autoriser les transfert de matiéres premiére mais si la quantité requise est atteinte
|
||||
Add Corrective Operation Cost in Finished Good Valuation,Ajouter des opérations de correction de coût pour la valorisation des produits finis
|
||||
Make Serial No / Batch from Work Order,Générer des numéros de séries / lots depuis les Ordres de Fabrications
|
||||
System will automatically create the serial numbers / batch for the Finished Good on submission of work order,le systéme va créer des numéros de séries / lots à la validation des produit finis depuis les Ordres de Fabrications
|
||||
Allow material consumptions without immediately manufacturing finished goods against a Work Order,Autoriser la consommation sans immédiatement fabriqué les produit fini dans les ordres de fabrication
|
||||
Quality Inspection Parameter,Paramétre des Inspection Qualité
|
||||
Parameter Group,Groupe de paramétre
|
||||
E Commerce Settings,Paramétrage E-Commerce
|
||||
Follow these steps to create a landing page for your store:,Suivez les intructions suivantes pour créer votre page d'accueil de boutique en ligne
|
||||
Show Price in Quotation,Afficher les prix sur les devis
|
||||
Add-ons,Extensions
|
||||
Enable Wishlist,Activer la liste de souhaits
|
||||
Enable Reviews and Ratings,Activer les avis et notes
|
||||
Enable Recommendations,Activer les recommendations
|
||||
Item Search Settings,Paramétrage de la recherche d'article
|
||||
Purchase demande,Demande de materiel
|
||||
Retail,Commerce,
|
||||
Users,Utilisateurs,
|
||||
Permission Manager,Gestion des permissions,
|
||||
Fetch Timesheet,Récuprer les temps saisis,
|
||||
Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur,
|
||||
Quality Inspection(s),Inspection(s) Qualite,
|
||||
Set Advances and Allocate (FIFO),Affecter les encours au réglement,
|
||||
Apply Putaway Rule,Appliquer la régle de routage d'entrepot,
|
||||
Delete Transactions,Supprimer les transactions,
|
||||
Default Payment Discount Account,Compte par défaut des paiements de remise,
|
||||
Unrealized Profit / Loss Account,Compte de perte,
|
||||
Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés,
|
||||
Publish in Website,Publier sur le Site Web,
|
||||
List View,Vue en liste,
|
||||
Allow Excess Material Transfer,Autoriser les transfert de stock supérieurs à l'attendue,
|
||||
Allow transferring raw materials even after the Required Quantity is fulfilled,Autoriser les transfert de matiéres premiére mais si la quantité requise est atteinte,
|
||||
Add Corrective Operation Cost in Finished Good Valuation,Ajouter des opérations de correction de coût pour la valorisation des produits finis,
|
||||
Make Serial No / Batch from Work Order,Générer des numéros de séries / lots depuis les Ordres de Fabrications,
|
||||
System will automatically create the serial numbers / batch for the Finished Good on submission of work order,le systéme va créer des numéros de séries / lots à la validation des produit finis depuis les Ordres de Fabrications,
|
||||
Allow material consumptions without immediately manufacturing finished goods against a Work Order,Autoriser la consommation sans immédiatement fabriqué les produit fini dans les ordres de fabrication,
|
||||
Quality Inspection Parameter,Paramétre des Inspection Qualite,
|
||||
Parameter Group,Groupe de paramétre,
|
||||
E Commerce Settings,Paramétrage E-Commerce,
|
||||
Follow these steps to create a landing page for your store:,Suivez les intructions suivantes pour créer votre page d'accueil de boutique en ligne,
|
||||
Show Price in Quotation,Afficher les prix sur les devis,
|
||||
Add-ons,Extensions,
|
||||
Enable Wishlist,Activer la liste de souhaits,
|
||||
Enable Reviews and Ratings,Activer les avis et notes,
|
||||
Enable Recommendations,Activer les recommendations,
|
||||
Item Search Settings,Paramétrage de la recherche d'article,
|
||||
Purchase demande,Demande de materiel,
|
||||
Internal Customer,Client interne
|
||||
Internal Supplier,Fournisseur interne
|
||||
Contact & Address,Contact et Adresse
|
||||
Primary Address and Contact,Adresse et contact principal
|
||||
Supplier Primary Contact,Contact fournisseur principal
|
||||
Supplier Primary Address,Adresse fournisseur principal
|
||||
From Opportunity,Depuis l'opportunité
|
||||
Default Receivable Accounts,Compte de débit par défaut
|
||||
Receivable Accounts,Compte de débit
|
||||
Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard
|
||||
Allow Purchase,Autoriser à l'achat
|
||||
Inventory Settings,Paramétrage de l'inventaire
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user