diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index ec55e60fd1f..ced04ced3fd 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -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)) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 127768071a0..765e69b75bd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1867,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. diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 21379458874..2de009f8c43 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -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") diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 07f35c9fe11..6f1f34bc9f3 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -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"); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 18d34850850..0dc9c135b8c 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -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 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index cc3ec26066f..abffd262961 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -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 = ( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 6161e5b36ee..2340f486f14 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -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): diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index d17ca08c408..943c0057f99 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -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 = {} diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 6f1889b34e1..0c7d931d2d5 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -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, + ], + ) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index d67eee3552d..bdc8d8504f8 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -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, ) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 7d166614722..7191720c57e 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -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): diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index debfffdcbb3..bf01362c97f 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -60,7 +60,6 @@ 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 @@ -105,6 +104,28 @@ class AccountsTestMixin: 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, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -113,6 +134,8 @@ class AccountsTestMixin: "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() diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2df3387b83e..0c01ff78c8c 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -884,7 +884,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=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") @@ -917,6 +919,8 @@ def get_outstanding_invoices( max_outstanding=max_outstanding, get_invoices=True, accounting_dimensions=accounting_dimensions or [], + limit=limit, + voucher_no=voucher_no, ) for d in invoice_list: @@ -1648,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() @@ -1667,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]) @@ -1677,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: @@ -1792,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) @@ -1805,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 @@ -1826,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 diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index bf62a8fb39c..383be973477 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -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" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 76fe6a91182..53039cfd8b5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -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) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index b5d8246c116..a69b21c7c1d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -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": diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index ef289ff6a67..8948b7e3f7c 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -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 ) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index db4003bc585..a494550423f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -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) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 111a0861b71..7f0dc2df9f3 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -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) + ) diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index 2bba52a4e25..f91ea6a0bba 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -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"]}, ], } diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c793529e843..4f6c6364a47 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -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) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 33ed955a5c4..6de5f00ece8 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -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"), diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index d362d818ebf..7e81a72028f 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -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"), diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 0def161d283..b00b422a67a 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -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); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index ed28ed3ee46..eeef39641b0 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -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", }, { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index ecec73e265c..cf457dfe82d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -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 diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index fde3f57211e..eb9076b4062 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -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 {0} is invalid,Le numéro de pièce du fabricant {0} 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