mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +00:00
Merge pull request #36293 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -122,13 +122,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
|
||||
let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
|
||||
|
||||
payment_term_list = payment_term_list.map(pt => pt.payment_term);
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_payment_terms_for_references",
|
||||
filters: {
|
||||
'name': ['in', payment_term_list]
|
||||
'reference': child.reference_name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,20 @@ class PaymentEntry(AccountsController):
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def term_based_allocation_enabled_for_reference(
|
||||
self, reference_doctype: str, reference_name: str
|
||||
) -> bool:
|
||||
if (
|
||||
reference_doctype
|
||||
and reference_doctype in ["Sales Invoice", "Sales Order", "Purchase Order", "Purchase Invoice"]
|
||||
and reference_name
|
||||
):
|
||||
if template := frappe.db.get_value(reference_doctype, reference_name, "payment_terms_template"):
|
||||
return frappe.db.get_value(
|
||||
"Payment Terms Template", template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
return False
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
@@ -184,10 +198,23 @@ class PaymentEntry(AccountsController):
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
|
||||
for d in self.get("references"):
|
||||
latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get(
|
||||
d.payment_term
|
||||
)
|
||||
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:
|
||||
@@ -1510,6 +1537,9 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": flt(d.outstanding_amount),
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"allocated_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
|
||||
@@ -1061,6 +1061,101 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
}
|
||||
self.assertDictEqual(ref_details, expected_response)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_overallocation_validation_on_payment_terms(self):
|
||||
"""
|
||||
Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
# Validate allocation on base/company currency
|
||||
si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
si1.payment_terms_template = "Test Receivable Template"
|
||||
si1.save().submit()
|
||||
|
||||
si1.reload()
|
||||
pe = get_payment_entry(si1.doctype, si1.name).save()
|
||||
# Allocated amount should be according to the payment schedule
|
||||
for idx, schedule in enumerate(si1.payment_schedule):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 400
|
||||
pe.references[0].allocated_amount = 200
|
||||
pe.references[1].allocated_amount = 200
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si1.cancel()
|
||||
si1.delete()
|
||||
|
||||
# Validate allocation on foreign currency
|
||||
si2 = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
do_not_save=1,
|
||||
)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.save().submit()
|
||||
|
||||
si2.reload()
|
||||
pe = get_payment_entry(si2.doctype, si2.name).save()
|
||||
# Allocated amount should be according to the payment schedule
|
||||
for idx, schedule in enumerate(si2.payment_schedule):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.references[1].allocated_amount = 100
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si2.cancel()
|
||||
si2.delete()
|
||||
|
||||
# Validate allocation in base/company currency on a foreign currency document
|
||||
# when invoice is made is foreign currency, but posted to base/company currency debtors account
|
||||
si3 = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
do_not_save=1,
|
||||
)
|
||||
si3.payment_terms_template = "Test Receivable Template"
|
||||
si3.save().submit()
|
||||
|
||||
si3.reload()
|
||||
pe = get_payment_entry(si3.doctype, si3.name).save()
|
||||
# Allocated amount should be equal to payment term outstanding
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
for idx, ref in enumerate(pe.references):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 16000
|
||||
pe.references[0].allocated_amount = 8000
|
||||
pe.references[1].allocated_amount = 8000
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si3.cancel()
|
||||
si3.delete()
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -1150,3 +1245,17 @@ def create_payment_terms_template_with_discount(
|
||||
def create_payment_term(name):
|
||||
if not frappe.db.exists("Payment Term", name):
|
||||
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
|
||||
|
||||
|
||||
def create_customer(name="_Test Customer 2 USD", currency="USD"):
|
||||
customer = None
|
||||
if frappe.db.exists("Customer", name):
|
||||
customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.default_currency = currency
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
customer = customer.name
|
||||
return customer
|
||||
|
||||
@@ -138,7 +138,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
def get_ar_filters(doc, entry):
|
||||
return {
|
||||
"report_date": doc.posting_date if doc.posting_date else None,
|
||||
"customer_name": entry.customer,
|
||||
"customer": entry.customer,
|
||||
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
|
||||
"sales_partner": doc.sales_partner if doc.sales_partner else None,
|
||||
"sales_person": doc.sales_person if doc.sales_person else None,
|
||||
|
||||
@@ -10,16 +10,12 @@
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h4 class="text-center">
|
||||
{% if (filters.customer_name) %}
|
||||
{{ filters.customer_name }}
|
||||
{% else %}
|
||||
{{ filters.customer ~ filters.supplier }}
|
||||
{% endif %}
|
||||
{{ filters.customer }}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
{% if (filters.tax_id) %}
|
||||
{{ _("Tax Id: ") }}{{ filters.tax_id }}
|
||||
{% endif %}
|
||||
{% if (filters.tax_id) %}
|
||||
{{ _("Tax Id: ") }}{{ filters.tax_id }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h5 class="text-center">
|
||||
{{ _(filters.ageing_based_on) }}
|
||||
|
||||
@@ -887,6 +887,8 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
||||
}
|
||||
});
|
||||
frm.refresh_field("timesheets");
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
},
|
||||
|
||||
async get_exchange_rate(frm, from_currency, to_currency) {
|
||||
@@ -926,9 +928,6 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
||||
row.timesheet_detail = time_log.name;
|
||||
row.project_name = time_log.project_name;
|
||||
|
||||
frm.refresh_field("timesheets");
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
},
|
||||
|
||||
calculate_timesheet_totals: function(frm) {
|
||||
|
||||
@@ -1900,16 +1900,22 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
si = self.create_si_to_test_tax_breakup()
|
||||
|
||||
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
|
||||
itemised_tax_data = get_itemised_tax_breakup_data(si)
|
||||
|
||||
expected_itemised_tax = {
|
||||
"_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
|
||||
"_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
|
||||
}
|
||||
expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
|
||||
expected_itemised_tax = [
|
||||
{
|
||||
"item": "_Test Item",
|
||||
"taxable_amount": 10000.0,
|
||||
"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0},
|
||||
},
|
||||
{
|
||||
"item": "_Test Item 2",
|
||||
"taxable_amount": 5000.0,
|
||||
"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0},
|
||||
},
|
||||
]
|
||||
|
||||
self.assertEqual(itemised_tax, expected_itemised_tax)
|
||||
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
|
||||
self.assertEqual(itemised_tax_data, expected_itemised_tax)
|
||||
|
||||
frappe.flags.country = None
|
||||
|
||||
|
||||
@@ -603,7 +603,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break5",
|
||||
@@ -890,7 +891,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-17 12:51:44.825398",
|
||||
"modified": "2023-07-25 11:58:10.723833",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -33,6 +33,7 @@ import erpnext
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
|
||||
SALES_TRANSACTION_TYPES = {
|
||||
@@ -261,9 +262,8 @@ def set_address_details(
|
||||
)
|
||||
|
||||
if doctype in TRANSACTION_TYPES:
|
||||
# required to set correct region
|
||||
frappe.flags.company = company
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
with temporary_flag("company", company):
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_address, shipping_address
|
||||
|
||||
|
||||
@@ -16,9 +16,30 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
"fieldname": "based_on",
|
||||
"label": __("Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Cost Center", "Project"],
|
||||
"options": ["Cost Center", "Project", "Accounting Dimension"],
|
||||
"default": "Cost Center",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"on_change": function(query_report){
|
||||
let based_on = query_report.get_values().based_on;
|
||||
if(based_on!='Accounting Dimension'){
|
||||
frappe.query_report.set_filter_value({
|
||||
accounting_dimension: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimension",
|
||||
"label": __("Accounting Dimension"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Accounting Dimension",
|
||||
"get_query": () =>{
|
||||
return {
|
||||
filters: {
|
||||
"disabled": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
@@ -16,10 +17,12 @@ value_fields = ("income", "expense", "gross_profit_loss")
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters.get("based_on"):
|
||||
filters["based_on"] = "Cost Center"
|
||||
if filters.get("based_on") == "Accounting Dimension" and not filters.get("accounting_dimension"):
|
||||
frappe.throw(_("Select Accounting Dimension."))
|
||||
|
||||
based_on = filters.based_on.replace(" ", "_").lower()
|
||||
based_on = (
|
||||
filters.based_on if filters.based_on != "Accounting Dimension" else filters.accounting_dimension
|
||||
)
|
||||
validate_filters(filters)
|
||||
accounts = get_accounts_data(based_on, filters.get("company"))
|
||||
data = get_data(accounts, filters, based_on)
|
||||
@@ -28,14 +31,14 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_accounts_data(based_on, company):
|
||||
if based_on == "cost_center":
|
||||
if based_on == "Cost Center":
|
||||
return frappe.db.sql(
|
||||
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
|
||||
from `tabCost Center` where company=%s order by name""",
|
||||
company,
|
||||
as_dict=True,
|
||||
)
|
||||
elif based_on == "project":
|
||||
elif based_on == "Project":
|
||||
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
|
||||
else:
|
||||
filters = {}
|
||||
@@ -56,11 +59,17 @@ def get_data(accounts, filters, based_on):
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)[0]
|
||||
fieldname = ""
|
||||
for dimension in accounting_dimensions:
|
||||
if dimension["document_type"] == based_on:
|
||||
fieldname = dimension["fieldname"]
|
||||
|
||||
set_gl_entries_by_account(
|
||||
filters.get("company"),
|
||||
filters.get("from_date"),
|
||||
filters.get("to_date"),
|
||||
based_on,
|
||||
fieldname,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=not flt(filters.get("with_period_closing_entry")),
|
||||
)
|
||||
|
||||
@@ -1413,6 +1413,8 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
)
|
||||
|
||||
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
||||
if asset.purchase_receipt_amount:
|
||||
new_asset.purchase_receipt_amount = new_gross_purchase_amount
|
||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||
new_asset.asset_quantity = split_qty
|
||||
new_asset.split_from = asset.name
|
||||
|
||||
@@ -62,20 +62,21 @@ class AssetMovement(Document):
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee):
|
||||
if not (d.source_location) and not (d.target_location or d.to_employee):
|
||||
frappe.throw(
|
||||
_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)
|
||||
)
|
||||
elif d.from_employee and not d.target_location:
|
||||
frappe.throw(
|
||||
_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
|
||||
)
|
||||
elif d.to_employee and d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} cannot be received at a location and given to an employee in a single movement"
|
||||
).format(d.asset)
|
||||
)
|
||||
elif d.source_location:
|
||||
if d.from_employee and not d.target_location:
|
||||
frappe.throw(
|
||||
_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
|
||||
)
|
||||
elif d.to_employee and d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} cannot be received at a location and given to an employee in a single movement"
|
||||
).format(d.asset)
|
||||
)
|
||||
|
||||
def validate_employee(self):
|
||||
for d in self.assets:
|
||||
|
||||
@@ -54,12 +54,12 @@ def get_conditions(filters):
|
||||
conditions["cost_center"] = filters.get("cost_center")
|
||||
|
||||
if status:
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
# In Store assets are those that are not sold or scrapped or capitalized or decapitalized
|
||||
operand = "not in"
|
||||
if status not in "In Location":
|
||||
operand = "in"
|
||||
|
||||
conditions["status"] = (operand, ["Sold", "Scrapped"])
|
||||
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"])
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -71,36 +71,6 @@ def get_data(filters):
|
||||
pr_supplier_map = get_purchase_receipt_supplier_map()
|
||||
pi_supplier_map = get_purchase_invoice_supplier_map()
|
||||
|
||||
group_by = frappe.scrub(filters.get("group_by"))
|
||||
|
||||
if group_by == "asset_category":
|
||||
fields = ["asset_category", "gross_purchase_amount", "opening_accumulated_depreciation"]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by)
|
||||
|
||||
elif group_by == "location":
|
||||
fields = ["location", "gross_purchase_amount", "opening_accumulated_depreciation"]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by)
|
||||
|
||||
else:
|
||||
fields = [
|
||||
"name as asset_id",
|
||||
"asset_name",
|
||||
"status",
|
||||
"department",
|
||||
"company",
|
||||
"cost_center",
|
||||
"calculate_depreciation",
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
"gross_purchase_amount",
|
||||
"location",
|
||||
"available_for_use_date",
|
||||
"purchase_invoice",
|
||||
"opening_accumulated_depreciation",
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
assets_linked_to_fb = get_assets_linked_to_fb(filters)
|
||||
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
@@ -114,6 +84,31 @@ def get_data(filters):
|
||||
|
||||
depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book)
|
||||
|
||||
group_by = frappe.scrub(filters.get("group_by"))
|
||||
|
||||
if group_by in ("asset_category", "location"):
|
||||
data = get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map)
|
||||
return data
|
||||
|
||||
fields = [
|
||||
"name as asset_id",
|
||||
"asset_name",
|
||||
"status",
|
||||
"department",
|
||||
"company",
|
||||
"cost_center",
|
||||
"calculate_depreciation",
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
"gross_purchase_amount",
|
||||
"location",
|
||||
"available_for_use_date",
|
||||
"purchase_invoice",
|
||||
"opening_accumulated_depreciation",
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
for asset in assets_record:
|
||||
if (
|
||||
assets_linked_to_fb
|
||||
@@ -136,7 +131,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map),
|
||||
"depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
@@ -230,12 +225,11 @@ def get_assets_linked_to_fb(filters):
|
||||
return assets_linked_to_fb
|
||||
|
||||
|
||||
def get_depreciation_amount_of_asset(asset, depreciation_amount_map):
|
||||
return depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
|
||||
|
||||
def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
start_date = (
|
||||
filters.from_date if filters.filter_based_on == "Date Range" else filters.year_start_date
|
||||
)
|
||||
end_date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
@@ -256,25 +250,77 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(company.name == filters.company)
|
||||
.where(asset.docstatus == 1)
|
||||
.groupby(asset.name)
|
||||
)
|
||||
|
||||
if filters.only_existing_assets:
|
||||
query = query.where(asset.is_existing_asset == 1)
|
||||
if filters.asset_category:
|
||||
query = query.where(asset.asset_category == filters.asset_category)
|
||||
if filters.cost_center:
|
||||
query = query.where(asset.cost_center == filters.cost_center)
|
||||
if filters.status:
|
||||
if filters.status == "In Location":
|
||||
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
|
||||
else:
|
||||
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
|
||||
if finance_book:
|
||||
query = query.where(
|
||||
(gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull()))
|
||||
|
||||
if filters.filter_based_on in ("Date Range", "Fiscal Year"):
|
||||
query = query.where(gle.posting_date <= date)
|
||||
query = query.where(gle.posting_date >= start_date)
|
||||
query = query.where(gle.posting_date <= end_date)
|
||||
|
||||
query = query.groupby(asset.name)
|
||||
|
||||
asset_depr_amount_map = query.run()
|
||||
|
||||
return dict(asset_depr_amount_map)
|
||||
|
||||
|
||||
def get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map):
|
||||
fields = [
|
||||
group_by,
|
||||
"name",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"calculate_depreciation",
|
||||
]
|
||||
assets = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
data = []
|
||||
|
||||
for a in assets:
|
||||
if assets_linked_to_fb and a.calculate_depreciation and a.name not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
a["depreciated_amount"] = depreciation_amount_map.get(a["name"], 0.0)
|
||||
a["asset_value"] = (
|
||||
a["gross_purchase_amount"] - a["opening_accumulated_depreciation"] - a["depreciated_amount"]
|
||||
)
|
||||
|
||||
del a["name"]
|
||||
del a["calculate_depreciation"]
|
||||
|
||||
idx = ([i for i, d in enumerate(data) if a[group_by] == d[group_by]] or [None])[0]
|
||||
if idx is None:
|
||||
data.append(a)
|
||||
else:
|
||||
for field in (
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"depreciated_amount",
|
||||
"asset_value",
|
||||
):
|
||||
data[idx][field] = data[idx][field] + a[field]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_purchase_receipt_supplier_map():
|
||||
return frappe._dict(
|
||||
frappe.db.sql(
|
||||
@@ -313,35 +359,35 @@ def get_columns(filters):
|
||||
"fieldtype": "Link",
|
||||
"fieldname": frappe.scrub(filters.get("group_by")),
|
||||
"options": filters.get("group_by"),
|
||||
"width": 120,
|
||||
"width": 216,
|
||||
},
|
||||
{
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 100,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Opening Accumulated Depreciation"),
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 90,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciated Amount"),
|
||||
"fieldname": "depreciated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 100,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Value"),
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"options": "company:currency",
|
||||
"width": 100,
|
||||
"width": 250,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ from erpnext.stock.get_item_details import (
|
||||
get_item_tax_map,
|
||||
get_item_warehouse,
|
||||
)
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
|
||||
@@ -758,7 +759,9 @@ class AccountsController(TransactionBase):
|
||||
}
|
||||
)
|
||||
|
||||
update_gl_dict_with_regional_fields(self, gl_dict)
|
||||
with temporary_flag("company", self.company):
|
||||
update_gl_dict_with_regional_fields(self, gl_dict)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
dimension_dict = frappe._dict()
|
||||
|
||||
|
||||
@@ -823,3 +823,18 @@ def get_fields(doctype, fields=None):
|
||||
fields.insert(1, meta.title_field.strip())
|
||||
|
||||
return unique(fields)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list:
|
||||
terms = []
|
||||
if filters:
|
||||
terms = frappe.db.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": filters.get("reference")},
|
||||
fields=["payment_term"],
|
||||
limit=page_len,
|
||||
as_list=1,
|
||||
)
|
||||
return terms
|
||||
|
||||
@@ -18,6 +18,7 @@ from erpnext.controllers.accounts_controller import (
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
|
||||
class calculate_taxes_and_totals(object):
|
||||
@@ -942,7 +943,6 @@ class calculate_taxes_and_totals(object):
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
return
|
||||
frappe.flags.company = doc.company
|
||||
|
||||
# get headers
|
||||
tax_accounts = []
|
||||
@@ -952,22 +952,17 @@ def get_itemised_tax_breakup_html(doc):
|
||||
if tax.description not in tax_accounts:
|
||||
tax_accounts.append(tax.description)
|
||||
|
||||
headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts)
|
||||
|
||||
# get tax breakup data
|
||||
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(doc)
|
||||
|
||||
get_rounded_tax_amount(itemised_tax, doc.precision("tax_amount", "taxes"))
|
||||
|
||||
update_itemised_tax_data(doc)
|
||||
frappe.flags.company = None
|
||||
with temporary_flag("company", doc.company):
|
||||
headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts)
|
||||
itemised_tax_data = get_itemised_tax_breakup_data(doc)
|
||||
get_rounded_tax_amount(itemised_tax_data, doc.precision("tax_amount", "taxes"))
|
||||
update_itemised_tax_data(doc)
|
||||
|
||||
return frappe.render_template(
|
||||
"templates/includes/itemised_tax_breakup.html",
|
||||
dict(
|
||||
headers=headers,
|
||||
itemised_tax=itemised_tax,
|
||||
itemised_taxable_amount=itemised_taxable_amount,
|
||||
itemised_tax_data=itemised_tax_data,
|
||||
tax_accounts=tax_accounts,
|
||||
doc=doc,
|
||||
),
|
||||
@@ -977,10 +972,8 @@ def get_itemised_tax_breakup_html(doc):
|
||||
@frappe.whitelist()
|
||||
def get_round_off_applicable_accounts(company, account_list):
|
||||
# required to set correct region
|
||||
frappe.flags.company = company
|
||||
account_list = get_regional_round_off_accounts(company, account_list)
|
||||
|
||||
return account_list
|
||||
with temporary_flag("company", company):
|
||||
return get_regional_round_off_accounts(company, account_list)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
@@ -1005,7 +998,15 @@ def get_itemised_tax_breakup_data(doc):
|
||||
|
||||
itemised_taxable_amount = get_itemised_taxable_amount(doc.items)
|
||||
|
||||
return itemised_tax, itemised_taxable_amount
|
||||
itemised_tax_data = []
|
||||
for item_code, taxes in itemised_tax.items():
|
||||
itemised_tax_data.append(
|
||||
frappe._dict(
|
||||
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes}
|
||||
)
|
||||
)
|
||||
|
||||
return itemised_tax_data
|
||||
|
||||
|
||||
def get_itemised_tax(taxes, with_tax_account=False):
|
||||
@@ -1050,9 +1051,10 @@ def get_itemised_taxable_amount(items):
|
||||
|
||||
def get_rounded_tax_amount(itemised_tax, precision):
|
||||
# Rounding based on tax_amount precision
|
||||
for taxes in itemised_tax.values():
|
||||
for tax_account in taxes:
|
||||
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
|
||||
for taxes in itemised_tax:
|
||||
for row in taxes.values():
|
||||
if isinstance(row, dict) and isinstance(row["tax_amount"], float):
|
||||
row["tax_amount"] = flt(row["tax_amount"], precision)
|
||||
|
||||
|
||||
class init_landed_taxes_and_totals(object):
|
||||
|
||||
@@ -969,6 +969,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
}
|
||||
|
||||
apply_discount_on_item(doc, cdt, cdn, field) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item.price_list_rate) {
|
||||
item[field] = 0.0;
|
||||
} else {
|
||||
this.price_list_rate(doc, cdt, cdn);
|
||||
}
|
||||
this.set_gross_profit(item);
|
||||
}
|
||||
|
||||
shipping_rule() {
|
||||
var me = this;
|
||||
if(this.frm.doc.shipping_rule) {
|
||||
@@ -1639,6 +1649,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
() => {
|
||||
if(args.items.length) {
|
||||
me._set_values_for_item_list(r.message.children);
|
||||
$.each(r.message.children || [], function(i, d) {
|
||||
me.apply_discount_on_item(d, d.doctype, d.name, 'discount_percentage');
|
||||
});
|
||||
}
|
||||
},
|
||||
() => { me.in_apply_price_list = false; }
|
||||
|
||||
@@ -145,16 +145,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount');
|
||||
}
|
||||
|
||||
apply_discount_on_item(doc, cdt, cdn, field) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item.price_list_rate) {
|
||||
item[field] = 0.0;
|
||||
} else {
|
||||
this.price_list_rate(doc, cdt, cdn);
|
||||
}
|
||||
this.set_gross_profit(item);
|
||||
}
|
||||
|
||||
commission_rate() {
|
||||
this.calculate_commission();
|
||||
}
|
||||
|
||||
@@ -515,7 +515,8 @@
|
||||
"oldfieldname": "batch_no",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Batch",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -867,7 +868,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-01 21:05:14.175640",
|
||||
"modified": "2023-07-25 11:58:28.101919",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
@@ -877,4 +878,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nDraft\nSubmitted\nStopped\nCancelled\nPending\nPartially Ordered\nOrdered\nIssued\nTransferred\nReceived",
|
||||
"options": "\nDraft\nSubmitted\nStopped\nCancelled\nPending\nPartially Ordered\nPartially Received\nOrdered\nIssued\nTransferred\nReceived",
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
@@ -356,7 +356,7 @@
|
||||
"idx": 70,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-07 20:17:29.108095",
|
||||
"modified": "2023-07-25 17:19:31.662662",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request",
|
||||
|
||||
@@ -79,7 +79,8 @@
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch"
|
||||
"options": "Batch",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
@@ -192,7 +193,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-16 14:05:51.719959",
|
||||
"modified": "2023-07-25 11:56:23.361867",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
@@ -203,4 +204,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,11 @@ def repost(doc):
|
||||
message += "<br>" + "Traceback: <br>" + traceback
|
||||
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
||||
|
||||
if not isinstance(e, RecoverableErrors):
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
|
||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
finally:
|
||||
|
||||
@@ -168,7 +168,8 @@
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch"
|
||||
"options": "Batch",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -189,7 +190,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-09 18:42:19.224916",
|
||||
"modified": "2023-07-25 11:58:44.992419",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation Item",
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item, taxes in itemised_tax.items() %}
|
||||
{% for taxes in itemised_tax_data %}
|
||||
<tr>
|
||||
<td>{{ item }}</td>
|
||||
<td>{{ taxes.item }}</td>
|
||||
<td class="text-right">
|
||||
{% if doc.get('is_return') %}
|
||||
{{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }}
|
||||
{{ frappe.utils.fmt_money(taxes.taxable_amount|abs, None, doc.currency) }}
|
||||
{% else %}
|
||||
{{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, doc.currency) }}
|
||||
{{ frappe.utils.fmt_money(taxes.taxable_amount, None, doc.currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% for tax_account in tax_accounts %}
|
||||
|
||||
13
erpnext/utilities/regional.py
Normal file
13
erpnext/utilities/regional.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_flag(flag_name, value):
|
||||
flags = frappe.local.flags
|
||||
flags[flag_name] = value
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
flags.pop(flag_name, None)
|
||||
Reference in New Issue
Block a user