mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-20 21:49:18 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into dev_fix_french_chart_of_account
This commit is contained in:
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not amount:
|
if not amount:
|
||||||
return
|
|
||||||
|
|
||||||
gl_posting_date = end_date
|
|
||||||
prev_posting_date = None
|
|
||||||
# check if books nor frozen till endate:
|
|
||||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
|
||||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
|
||||||
prev_posting_date = end_date
|
prev_posting_date = end_date
|
||||||
|
|
||||||
if via_journal_entry:
|
|
||||||
book_revenue_via_journal_entry(
|
|
||||||
doc,
|
|
||||||
credit_account,
|
|
||||||
debit_account,
|
|
||||||
amount,
|
|
||||||
base_amount,
|
|
||||||
gl_posting_date,
|
|
||||||
project,
|
|
||||||
account_currency,
|
|
||||||
item.cost_center,
|
|
||||||
item,
|
|
||||||
deferred_process,
|
|
||||||
submit_journal_entry,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
make_gl_entries(
|
gl_posting_date = end_date
|
||||||
doc,
|
prev_posting_date = None
|
||||||
credit_account,
|
# check if books nor frozen till endate:
|
||||||
debit_account,
|
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||||
against,
|
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||||
amount,
|
prev_posting_date = end_date
|
||||||
base_amount,
|
|
||||||
gl_posting_date,
|
if via_journal_entry:
|
||||||
project,
|
book_revenue_via_journal_entry(
|
||||||
account_currency,
|
doc,
|
||||||
item.cost_center,
|
credit_account,
|
||||||
item,
|
debit_account,
|
||||||
deferred_process,
|
amount,
|
||||||
)
|
base_amount,
|
||||||
|
gl_posting_date,
|
||||||
|
project,
|
||||||
|
account_currency,
|
||||||
|
item.cost_center,
|
||||||
|
item,
|
||||||
|
deferred_process,
|
||||||
|
submit_journal_entry,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
make_gl_entries(
|
||||||
|
doc,
|
||||||
|
credit_account,
|
||||||
|
debit_account,
|
||||||
|
against,
|
||||||
|
amount,
|
||||||
|
base_amount,
|
||||||
|
gl_posting_date,
|
||||||
|
project,
|
||||||
|
account_currency,
|
||||||
|
item.cost_center,
|
||||||
|
item,
|
||||||
|
deferred_process,
|
||||||
|
)
|
||||||
|
|
||||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||||
if frappe.flags.deferred_accounting_error:
|
if frappe.flags.deferred_accounting_error:
|
||||||
|
|||||||
@@ -65,6 +65,8 @@
|
|||||||
"label": "Is Group"
|
"label": "Is Group"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "parent_account.company",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ frappe.ui.form.on("Accounting Dimension", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
label: function (frm) {
|
||||||
|
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
|
||||||
|
},
|
||||||
|
|
||||||
document_type: function (frm) {
|
document_type: function (frm) {
|
||||||
frm.set_value("label", frm.doc.document_type);
|
frm.set_value("label", frm.doc.document_type);
|
||||||
frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type));
|
|
||||||
|
|
||||||
frappe.db.get_value(
|
frappe.db.get_value(
|
||||||
"Accounting Dimension",
|
"Accounting Dimension",
|
||||||
|
|||||||
@@ -3,4 +3,23 @@
|
|||||||
|
|
||||||
frappe.ui.form.on("Accounts Settings", {
|
frappe.ui.form.on("Accounts Settings", {
|
||||||
refresh: function (frm) {},
|
refresh: function (frm) {},
|
||||||
|
enable_immutable_ledger: function (frm) {
|
||||||
|
if (!frm.doc.enable_immutable_ledger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = __("Enabling this will change the way how cancelled transactions are handled.");
|
||||||
|
msg += " ";
|
||||||
|
msg += __("Please enable only if the understand the effects of enabling this.");
|
||||||
|
msg += "<br>";
|
||||||
|
msg += "Do you still want to enable immutable ledger?";
|
||||||
|
|
||||||
|
frappe.confirm(
|
||||||
|
msg,
|
||||||
|
() => {},
|
||||||
|
() => {
|
||||||
|
frm.set_value("enable_immutable_ledger", 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"unlink_advance_payment_on_cancelation_of_order",
|
"unlink_advance_payment_on_cancelation_of_order",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"delete_linked_ledger_entries",
|
"delete_linked_ledger_entries",
|
||||||
|
"enable_immutable_ledger",
|
||||||
"invoicing_features_section",
|
"invoicing_features_section",
|
||||||
"check_supplier_invoice_uniqueness",
|
"check_supplier_invoice_uniqueness",
|
||||||
"automatically_fetch_payment_terms",
|
"automatically_fetch_payment_terms",
|
||||||
@@ -105,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
|
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
|
||||||
"fieldname": "check_supplier_invoice_uniqueness",
|
"fieldname": "check_supplier_invoice_uniqueness",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Check Supplier Invoice Number Uniqueness"
|
"label": "Check Supplier Invoice Number Uniqueness"
|
||||||
@@ -454,6 +455,13 @@
|
|||||||
"fieldname": "remarks_section",
|
"fieldname": "remarks_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Remarks Column Length"
|
"label": "Remarks Column Length"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well",
|
||||||
|
"fieldname": "enable_immutable_ledger",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Immutable Ledger"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -461,7 +469,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:05:57.568638",
|
"modified": "2024-05-11 23:19:44.673975",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class AccountsSettings(Document):
|
|||||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||||
enable_common_party_accounting: DF.Check
|
enable_common_party_accounting: DF.Check
|
||||||
enable_fuzzy_matching: DF.Check
|
enable_fuzzy_matching: DF.Check
|
||||||
|
enable_immutable_ledger: DF.Check
|
||||||
enable_party_matching: DF.Check
|
enable_party_matching: DF.Check
|
||||||
frozen_accounts_modifier: DF.Link | None
|
frozen_accounts_modifier: DF.Link | None
|
||||||
general_ledger_remarks_length: DF.Int
|
general_ledger_remarks_length: DF.Int
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company"
|
"options": "Company"
|
||||||
},
|
},
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:37.922473",
|
"modified": "2024-04-28 14:40:50.910884",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Reconciliation Tool",
|
"name": "Bank Reconciliation Tool",
|
||||||
|
|||||||
@@ -719,7 +719,7 @@ def get_pe_matching_query(
|
|||||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||||
ConstantColumn("Payment Entry").as_("doctype"),
|
ConstantColumn("Payment Entry").as_("doctype"),
|
||||||
pe.name,
|
pe.name,
|
||||||
pe.paid_amount,
|
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||||
pe.reference_no,
|
pe.reference_no,
|
||||||
pe.reference_date,
|
pe.reference_date,
|
||||||
pe.party,
|
pe.party,
|
||||||
|
|||||||
@@ -224,12 +224,18 @@ def validate_expense_against_budget(args, expense_amount=0):
|
|||||||
def validate_budget_records(args, budget_records, expense_amount):
|
def validate_budget_records(args, budget_records, expense_amount):
|
||||||
for budget in budget_records:
|
for budget in budget_records:
|
||||||
if flt(budget.budget_amount):
|
if flt(budget.budget_amount):
|
||||||
amount = expense_amount or get_amount(args, budget)
|
|
||||||
yearly_action, monthly_action = get_actions(args, budget)
|
yearly_action, monthly_action = get_actions(args, budget)
|
||||||
|
args["for_material_request"] = budget.for_material_request
|
||||||
|
args["for_purchase_order"] = budget.for_purchase_order
|
||||||
|
|
||||||
if yearly_action in ("Stop", "Warn"):
|
if yearly_action in ("Stop", "Warn"):
|
||||||
compare_expense_with_budget(
|
compare_expense_with_budget(
|
||||||
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
|
args,
|
||||||
|
flt(budget.budget_amount),
|
||||||
|
_("Annual"),
|
||||||
|
yearly_action,
|
||||||
|
budget.budget_against,
|
||||||
|
expense_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
if monthly_action in ["Stop", "Warn"]:
|
if monthly_action in ["Stop", "Warn"]:
|
||||||
@@ -245,18 +251,27 @@ def validate_budget_records(args, budget_records, expense_amount):
|
|||||||
_("Accumulated Monthly"),
|
_("Accumulated Monthly"),
|
||||||
monthly_action,
|
monthly_action,
|
||||||
budget.budget_against,
|
budget.budget_against,
|
||||||
amount,
|
expense_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||||
actual_expense = get_actual_expense(args)
|
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
|
||||||
total_expense = actual_expense + amount
|
if not amount:
|
||||||
|
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
|
||||||
|
|
||||||
|
if args.get("doctype") == "Material Request" and args.for_material_request:
|
||||||
|
amount = args.requested_amount + args.ordered_amount
|
||||||
|
|
||||||
|
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
|
||||||
|
amount = args.ordered_amount
|
||||||
|
|
||||||
|
total_expense = args.actual_expense + amount
|
||||||
|
|
||||||
if total_expense > budget_amount:
|
if total_expense > budget_amount:
|
||||||
if actual_expense > budget_amount:
|
if args.actual_expense > budget_amount:
|
||||||
error_tense = _("is already")
|
error_tense = _("is already")
|
||||||
diff = actual_expense - budget_amount
|
diff = args.actual_expense - budget_amount
|
||||||
else:
|
else:
|
||||||
error_tense = _("will be")
|
error_tense = _("will be")
|
||||||
diff = total_expense - budget_amount
|
diff = total_expense - budget_amount
|
||||||
@@ -273,6 +288,8 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
|||||||
frappe.bold(fmt_money(diff, currency=currency)),
|
frappe.bold(fmt_money(diff, currency=currency)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
msg += get_expense_breakup(args, currency, budget_against)
|
||||||
|
|
||||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||||
frappe.session.user
|
frappe.session.user
|
||||||
):
|
):
|
||||||
@@ -284,6 +301,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
|||||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_expense_breakup(args, currency, budget_against):
|
||||||
|
msg = "<hr>Total Expenses booked through - <ul>"
|
||||||
|
|
||||||
|
common_filters = frappe._dict(
|
||||||
|
{
|
||||||
|
args.budget_against_field: budget_against,
|
||||||
|
"account": args.account,
|
||||||
|
"company": args.company,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg += (
|
||||||
|
"<li>"
|
||||||
|
+ frappe.utils.get_link_to_report(
|
||||||
|
"General Ledger",
|
||||||
|
label="Actual Expenses",
|
||||||
|
filters=common_filters.copy().update(
|
||||||
|
{
|
||||||
|
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||||
|
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
|
||||||
|
"is_cancelled": 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ " - "
|
||||||
|
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
||||||
|
+ "</li>"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg += (
|
||||||
|
"<li>"
|
||||||
|
+ frappe.utils.get_link_to_report(
|
||||||
|
"Material Request",
|
||||||
|
label="Material Requests",
|
||||||
|
report_type="Report Builder",
|
||||||
|
doctype="Material Request",
|
||||||
|
filters=common_filters.copy().update(
|
||||||
|
{
|
||||||
|
"status": [["!=", "Stopped"]],
|
||||||
|
"docstatus": 1,
|
||||||
|
"material_request_type": "Purchase",
|
||||||
|
"schedule_date": [["fiscal year", "2023-2024"]],
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"per_ordered": [["<", 100]],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ " - "
|
||||||
|
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||||
|
+ "</li>"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg += (
|
||||||
|
"<li>"
|
||||||
|
+ frappe.utils.get_link_to_report(
|
||||||
|
"Purchase Order",
|
||||||
|
label="Unbilled Orders",
|
||||||
|
report_type="Report Builder",
|
||||||
|
doctype="Purchase Order",
|
||||||
|
filters=common_filters.copy().update(
|
||||||
|
{
|
||||||
|
"status": [["!=", "Closed"]],
|
||||||
|
"docstatus": 1,
|
||||||
|
"transaction_date": [["fiscal year", "2023-2024"]],
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"per_billed": [["<", 100]],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ " - "
|
||||||
|
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||||
|
+ "</li></ul>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def get_actions(args, budget):
|
def get_actions(args, budget):
|
||||||
yearly_action = budget.action_if_annual_budget_exceeded
|
yearly_action = budget.action_if_annual_budget_exceeded
|
||||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||||
@@ -299,23 +393,9 @@ def get_actions(args, budget):
|
|||||||
return yearly_action, monthly_action
|
return yearly_action, monthly_action
|
||||||
|
|
||||||
|
|
||||||
def get_amount(args, budget):
|
def get_requested_amount(args):
|
||||||
amount = 0
|
|
||||||
|
|
||||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
|
||||||
amount = (
|
|
||||||
get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
|
||||||
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
|
|
||||||
|
|
||||||
return amount
|
|
||||||
|
|
||||||
|
|
||||||
def get_requested_amount(args, budget):
|
|
||||||
item_code = args.get("item_code")
|
item_code = args.get("item_code")
|
||||||
condition = get_other_condition(args, budget, "Material Request")
|
condition = get_other_condition(args, "Material Request")
|
||||||
|
|
||||||
data = frappe.db.sql(
|
data = frappe.db.sql(
|
||||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||||
@@ -329,9 +409,9 @@ def get_requested_amount(args, budget):
|
|||||||
return data[0][0] if data else 0
|
return data[0][0] if data else 0
|
||||||
|
|
||||||
|
|
||||||
def get_ordered_amount(args, budget):
|
def get_ordered_amount(args):
|
||||||
item_code = args.get("item_code")
|
item_code = args.get("item_code")
|
||||||
condition = get_other_condition(args, budget, "Purchase Order")
|
condition = get_other_condition(args, "Purchase Order")
|
||||||
|
|
||||||
data = frappe.db.sql(
|
data = frappe.db.sql(
|
||||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||||
@@ -345,7 +425,7 @@ def get_ordered_amount(args, budget):
|
|||||||
return data[0][0] if data else 0
|
return data[0][0] if data else 0
|
||||||
|
|
||||||
|
|
||||||
def get_other_condition(args, budget, for_doc):
|
def get_other_condition(args, for_doc):
|
||||||
condition = "expense_account = '%s'" % (args.expense_account)
|
condition = "expense_account = '%s'" % (args.expense_account)
|
||||||
budget_against_field = args.get("budget_against_field")
|
budget_against_field = args.get("budget_against_field")
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:46.762208",
|
"modified": "2024-04-24 10:55:54.083042",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Cost Center",
|
"name": "Cost Center",
|
||||||
@@ -163,6 +163,15 @@
|
|||||||
{
|
{
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Purchase User"
|
"role": "Purchase User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Employee",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "parent_cost_center, is_group",
|
"search_fields": "parent_cost_center, is_group",
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ class Dunning(AccountsController):
|
|||||||
)
|
)
|
||||||
row.dunning_level = len(past_dunnings) + 1
|
row.dunning_level = len(past_dunnings) + 1
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
super().on_cancel()
|
||||||
|
self.ignore_linked_doctypes = ["GL Entry"]
|
||||||
|
|
||||||
|
|
||||||
def resolve_dunning(doc, state):
|
def resolve_dunning(doc, state):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint, scrub
|
from frappe import _, msgprint, scrub
|
||||||
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||||
@@ -146,6 +146,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.validate_empty_accounts_table()
|
self.validate_empty_accounts_table()
|
||||||
self.validate_inter_company_accounts()
|
self.validate_inter_company_accounts()
|
||||||
self.validate_depr_entry_voucher_type()
|
self.validate_depr_entry_voucher_type()
|
||||||
|
self.validate_advance_accounts()
|
||||||
|
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.apply_tax_withholding()
|
self.apply_tax_withholding()
|
||||||
@@ -153,6 +154,20 @@ class JournalEntry(AccountsController):
|
|||||||
if not self.title:
|
if not self.title:
|
||||||
self.title = self.get_title()
|
self.title = self.get_title()
|
||||||
|
|
||||||
|
def validate_advance_accounts(self):
|
||||||
|
journal_accounts = set([x.account for x in self.accounts])
|
||||||
|
advance_accounts = set()
|
||||||
|
advance_accounts.add(
|
||||||
|
frappe.get_cached_value("Company", self.company, "default_advance_received_account")
|
||||||
|
)
|
||||||
|
advance_accounts.add(frappe.get_cached_value("Company", self.company, "default_advance_paid_account"))
|
||||||
|
if advance_accounts_used := journal_accounts & advance_accounts:
|
||||||
|
frappe.msgprint(
|
||||||
|
_(
|
||||||
|
"Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation."
|
||||||
|
).format(frappe.bold(comma_and(advance_accounts_used)))
|
||||||
|
)
|
||||||
|
|
||||||
def validate_for_repost(self):
|
def validate_for_repost(self):
|
||||||
validate_docs_for_voucher_types(["Journal Entry"])
|
validate_docs_for_voucher_types(["Journal Entry"])
|
||||||
validate_docs_for_deferred_accounting([self.name], [])
|
validate_docs_for_deferred_accounting([self.name], [])
|
||||||
@@ -442,7 +457,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.voucher_type == "Depreciation Entry"
|
self.voucher_type == "Depreciation Entry"
|
||||||
and d.reference_type == "Asset"
|
and d.reference_type == "Asset"
|
||||||
and d.reference_name
|
and d.reference_name
|
||||||
and d.account_type == "Depreciation"
|
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||||
and d.debit
|
and d.debit
|
||||||
):
|
):
|
||||||
asset = frappe.get_doc("Asset", d.reference_name)
|
asset = frappe.get_doc("Asset", d.reference_name)
|
||||||
|
|||||||
@@ -1334,7 +1334,9 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
frm.set_value(field, r.message.account);
|
if (!frm.doc.mode_of_payment) {
|
||||||
|
frm.set_value(field, r.message.account);
|
||||||
|
}
|
||||||
frm.set_value("bank", r.message.bank);
|
frm.set_value("bank", r.message.bank);
|
||||||
frm.set_value("bank_account_no", r.message.bank_account_no);
|
frm.set_value("bank_account_no", r.message.bank_account_no);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
@@ -477,6 +478,7 @@
|
|||||||
"label": "More Information"
|
"label": "More Information"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
@@ -777,7 +779,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:10:09.131139",
|
"modified": "2024-04-11 11:25:07.366347",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self.setup_party_account_field()
|
self.setup_party_account_field()
|
||||||
self.set_missing_values()
|
self.set_missing_values()
|
||||||
self.set_liability_account()
|
self.set_liability_account()
|
||||||
|
self.validate_advance_account_currency()
|
||||||
self.set_missing_ref_details(force=True)
|
self.set_missing_ref_details(force=True)
|
||||||
self.validate_payment_type()
|
self.validate_payment_type()
|
||||||
self.validate_party_details()
|
self.validate_party_details()
|
||||||
@@ -196,6 +197,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
if self.party_type not in ("Customer", "Supplier"):
|
if self.party_type not in ("Customer", "Supplier"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -240,6 +242,22 @@ class PaymentEntry(AccountsController):
|
|||||||
alert=True,
|
alert=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_advance_account_currency(self):
|
||||||
|
if self.book_advance_payments_in_separate_party_account is True:
|
||||||
|
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||||
|
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
|
||||||
|
frappe.throw(
|
||||||
|
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||||
|
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
|
||||||
|
frappe.throw(
|
||||||
|
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||||
|
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.ignore_linked_doctypes = (
|
self.ignore_linked_doctypes = (
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
@@ -1208,88 +1226,71 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||||
if self.book_advance_payments_in_separate_party_account:
|
|
||||||
|
for d in self.get("references"):
|
||||||
|
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||||
|
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||||
|
cost_center = self.cost_center
|
||||||
|
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||||
|
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
gle = party_gl_dict.copy()
|
||||||
|
|
||||||
if self.payment_type == "Receive":
|
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||||
amount = self.base_paid_amount
|
reverse_dr_or_cr = 0
|
||||||
else:
|
|
||||||
amount = self.base_received_amount
|
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
|
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||||
|
payable_party_types = get_party_types_from_account_type("Payable")
|
||||||
|
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||||
|
if (
|
||||||
|
is_return
|
||||||
|
and self.party_type in receivable_party_types
|
||||||
|
and (self.payment_type == "Pay")
|
||||||
|
):
|
||||||
|
reverse_dr_or_cr = 1
|
||||||
|
elif (
|
||||||
|
is_return
|
||||||
|
and self.party_type in payable_party_types
|
||||||
|
and (self.payment_type == "Receive")
|
||||||
|
):
|
||||||
|
reverse_dr_or_cr = 1
|
||||||
|
|
||||||
|
if is_return and not reverse_dr_or_cr:
|
||||||
|
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
exchange_rate = self.get_exchange_rate()
|
|
||||||
amount_in_account_currency = amount * exchange_rate
|
|
||||||
gle.update(
|
gle.update(
|
||||||
{
|
{
|
||||||
dr_or_cr: amount,
|
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||||
dr_or_cr + "_in_account_currency": amount_in_account_currency,
|
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||||
"against_voucher_type": "Payment Entry",
|
"against_voucher_type": d.reference_doctype,
|
||||||
"against_voucher": self.name,
|
"against_voucher": d.reference_name,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": cost_center,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
gl_entries.append(gle)
|
gl_entries.append(gle)
|
||||||
else:
|
|
||||||
for d in self.get("references"):
|
|
||||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
|
||||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
|
||||||
cost_center = self.cost_center
|
|
||||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
|
||||||
cost_center = frappe.db.get_value(
|
|
||||||
d.reference_doctype, d.reference_name, "cost_center"
|
|
||||||
)
|
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
if self.unallocated_amount:
|
||||||
|
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||||
|
exchange_rate = self.get_exchange_rate()
|
||||||
|
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||||
|
|
||||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
|
gle = party_gl_dict.copy()
|
||||||
d
|
gle.update(
|
||||||
)
|
{
|
||||||
reverse_dr_or_cr = 0
|
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||||
|
dr_or_cr: base_unallocated_amount,
|
||||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
}
|
||||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
)
|
||||||
payable_party_types = get_party_types_from_account_type("Payable")
|
|
||||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
|
||||||
if (
|
|
||||||
is_return
|
|
||||||
and self.party_type in receivable_party_types
|
|
||||||
and (self.payment_type == "Pay")
|
|
||||||
):
|
|
||||||
reverse_dr_or_cr = 1
|
|
||||||
elif (
|
|
||||||
is_return
|
|
||||||
and self.party_type in payable_party_types
|
|
||||||
and (self.payment_type == "Receive")
|
|
||||||
):
|
|
||||||
reverse_dr_or_cr = 1
|
|
||||||
|
|
||||||
if is_return and not reverse_dr_or_cr:
|
|
||||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
|
||||||
|
|
||||||
|
if self.book_advance_payments_in_separate_party_account:
|
||||||
gle.update(
|
gle.update(
|
||||||
{
|
{
|
||||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
"against_voucher_type": "Payment Entry",
|
||||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
"against_voucher": self.name,
|
||||||
"against_voucher_type": d.reference_doctype,
|
|
||||||
"against_voucher": d.reference_name,
|
|
||||||
"cost_center": cost_center,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
gl_entries.append(gle)
|
gl_entries.append(gle)
|
||||||
|
|
||||||
if self.unallocated_amount:
|
|
||||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
|
||||||
exchange_rate = self.get_exchange_rate()
|
|
||||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
|
||||||
gle.update(
|
|
||||||
{
|
|
||||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
|
||||||
dr_or_cr: base_unallocated_amount,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
gl_entries.append(gle)
|
|
||||||
|
|
||||||
def make_advance_gl_entries(
|
def make_advance_gl_entries(
|
||||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||||
@@ -1304,7 +1305,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
||||||
"""
|
"""
|
||||||
If 'entry' is passed, GL enties only for that reference is added.
|
If 'entry' is passed, GL entries only for that reference is added.
|
||||||
"""
|
"""
|
||||||
if self.book_advance_payments_in_separate_party_account:
|
if self.book_advance_payments_in_separate_party_account:
|
||||||
references = [x for x in self.get("references")]
|
references = [x for x in self.get("references")]
|
||||||
@@ -1316,8 +1317,6 @@ class PaymentEntry(AccountsController):
|
|||||||
"Sales Invoice",
|
"Sales Invoice",
|
||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
"Journal Entry",
|
"Journal Entry",
|
||||||
"Sales Order",
|
|
||||||
"Purchase Order",
|
|
||||||
"Payment Entry",
|
"Payment Entry",
|
||||||
):
|
):
|
||||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||||
@@ -2126,6 +2125,8 @@ def get_negative_outstanding_invoices(
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_party_details(company, party_type, party, date, cost_center=None):
|
def get_party_details(company, party_type, party, date, cost_center=None):
|
||||||
bank_account = ""
|
bank_account = ""
|
||||||
|
party_bank_account = ""
|
||||||
|
|
||||||
if not frappe.db.exists(party_type, party):
|
if not frappe.db.exists(party_type, party):
|
||||||
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
|
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
|
||||||
|
|
||||||
@@ -2137,8 +2138,8 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
|||||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
||||||
if party_type in ["Customer", "Supplier"]:
|
if party_type in ["Customer", "Supplier"]:
|
||||||
party_bank_account = get_party_bank_account(party_type, party)
|
party_bank_account = get_party_bank_account(party_type, party)
|
||||||
|
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||||
|
|
||||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
|
||||||
return {
|
return {
|
||||||
"party_account": party_account,
|
"party_account": party_account,
|
||||||
"party_name": party_name,
|
"party_name": party_name,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, nowdate
|
|||||||
from erpnext.accounts.doctype.account.test_account import create_account
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
get_outstanding_reference_documents,
|
get_outstanding_reference_documents,
|
||||||
|
get_party_details,
|
||||||
get_payment_entry,
|
get_payment_entry,
|
||||||
get_reference_details,
|
get_reference_details,
|
||||||
)
|
)
|
||||||
@@ -1476,6 +1477,68 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.check_gl_entries()
|
self.check_gl_entries()
|
||||||
self.check_pl_entries()
|
self.check_pl_entries()
|
||||||
|
|
||||||
|
def test_advance_as_liability_against_order(self):
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||||
|
make_purchase_invoice as _make_purchase_invoice,
|
||||||
|
)
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
|
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
advance_account = create_account(
|
||||||
|
parent_account="Current Liabilities - _TC",
|
||||||
|
account_name="Advances Paid",
|
||||||
|
company=company,
|
||||||
|
account_type="Liability",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_paid_account": advance_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
po = create_purchase_order(supplier="_Test Supplier")
|
||||||
|
pe = get_payment_entry("Purchase Order", po.name, bank_account="Cash - _TC")
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
pre_reconciliation_gle = [
|
||||||
|
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||||
|
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.voucher_no = pe.name
|
||||||
|
self.expected_gle = pre_reconciliation_gle
|
||||||
|
self.check_gl_entries()
|
||||||
|
|
||||||
|
# Make Purchase Invoice against the order
|
||||||
|
pi = _make_purchase_invoice(po.name)
|
||||||
|
pi.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": pe.doctype,
|
||||||
|
"reference_name": pe.name,
|
||||||
|
"reference_row": pe.references[0].name,
|
||||||
|
"advance_amount": 5000,
|
||||||
|
"allocated_amount": 5000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi.save().submit()
|
||||||
|
|
||||||
|
# # assert General and Payment Ledger entries post partial reconciliation
|
||||||
|
self.expected_gle = [
|
||||||
|
{"account": pi.credit_to, "debit": 5000.0, "credit": 0.0},
|
||||||
|
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||||
|
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||||
|
{"account": advance_account, "debit": 0.0, "credit": 5000.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.voucher_no = pe.name
|
||||||
|
self.check_gl_entries()
|
||||||
|
|
||||||
def check_pl_entries(self):
|
def check_pl_entries(self):
|
||||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||||
pl_entries = (
|
pl_entries = (
|
||||||
@@ -1684,6 +1747,10 @@ def create_payment_entry(**args):
|
|||||||
payment_entry.reference_no = "Test001"
|
payment_entry.reference_no = "Test001"
|
||||||
payment_entry.reference_date = nowdate()
|
payment_entry.reference_date = nowdate()
|
||||||
|
|
||||||
|
get_party_details(
|
||||||
|
payment_entry.company, payment_entry.party_type, payment_entry.party, payment_entry.posting_date
|
||||||
|
)
|
||||||
|
|
||||||
if args.get("save"):
|
if args.get("save"):
|
||||||
payment_entry.save()
|
payment_entry.save()
|
||||||
if args.get("submit"):
|
if args.get("submit"):
|
||||||
|
|||||||
@@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
if (typeof r.message === "string") {
|
||||||
this.frm.set_value("default_advance_account", r.message[1]);
|
this.frm.set_value("receivable_payable_account", r.message);
|
||||||
|
} else if (Array.isArray(r.message)) {
|
||||||
|
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||||
|
this.frm.set_value("default_advance_account", r.message[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.frm.refresh();
|
this.frm.refresh();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -196,6 +196,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.party",
|
"depends_on": "eval:doc.party",
|
||||||
|
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||||
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||||
"fieldname": "default_advance_account",
|
"fieldname": "default_advance_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Default Advance Account",
|
"label": "Default Advance Account",
|
||||||
@@ -230,7 +232,7 @@
|
|||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:10.488007",
|
"modified": "2024-04-23 12:38:29.557315",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation",
|
"name": "Payment Reconciliation",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class PaymentRequest(Document):
|
|||||||
self.status = "Draft"
|
self.status = "Draft"
|
||||||
self.validate_reference_document()
|
self.validate_reference_document()
|
||||||
self.validate_payment_request_amount()
|
self.validate_payment_request_amount()
|
||||||
self.validate_currency()
|
# self.validate_currency()
|
||||||
self.validate_subscription_details()
|
self.validate_subscription_details()
|
||||||
|
|
||||||
def validate_reference_document(self):
|
def validate_reference_document(self):
|
||||||
@@ -335,21 +335,17 @@ class PaymentRequest(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||||
|
amount = payment_entry.base_paid_amount
|
||||||
|
else:
|
||||||
|
amount = self.grand_total
|
||||||
|
|
||||||
|
payment_entry.received_amount = amount
|
||||||
|
payment_entry.get("references")[0].allocated_amount = amount
|
||||||
|
|
||||||
for dimension in get_accounting_dimensions():
|
for dimension in get_accounting_dimensions():
|
||||||
payment_entry.update({dimension: self.get(dimension)})
|
payment_entry.update({dimension: self.get(dimension)})
|
||||||
|
|
||||||
if payment_entry.difference_amount:
|
|
||||||
company_details = get_company_defaults(ref_doc.company)
|
|
||||||
|
|
||||||
payment_entry.append(
|
|
||||||
"deductions",
|
|
||||||
{
|
|
||||||
"account": company_details.exchange_gain_loss_account,
|
|
||||||
"cost_center": company_details.cost_center,
|
|
||||||
"amount": payment_entry.difference_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if submit:
|
if submit:
|
||||||
payment_entry.insert(ignore_permissions=True)
|
payment_entry.insert(ignore_permissions=True)
|
||||||
payment_entry.submit()
|
payment_entry.submit()
|
||||||
@@ -479,6 +475,12 @@ def make_payment_request(**args):
|
|||||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||||
else:
|
else:
|
||||||
pr = frappe.new_doc("Payment Request")
|
pr = frappe.new_doc("Payment Request")
|
||||||
|
|
||||||
|
if not args.get("payment_request_type"):
|
||||||
|
args["payment_request_type"] = (
|
||||||
|
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||||
|
)
|
||||||
|
|
||||||
pr.update(
|
pr.update(
|
||||||
{
|
{
|
||||||
"payment_gateway_account": gateway_account.get("name"),
|
"payment_gateway_account": gateway_account.get("name"),
|
||||||
@@ -538,9 +540,9 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if not ref_doc.get("is_pos"):
|
if not ref_doc.get("is_pos"):
|
||||||
if ref_doc.party_account_currency == ref_doc.currency:
|
if ref_doc.party_account_currency == ref_doc.currency:
|
||||||
grand_total = flt(ref_doc.outstanding_amount)
|
grand_total = flt(ref_doc.grand_total)
|
||||||
else:
|
else:
|
||||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||||
elif dt == "Sales Invoice":
|
elif dt == "Sales Invoice":
|
||||||
for pay in ref_doc.payments:
|
for pay in ref_doc.payments:
|
||||||
if pay.type == "Phone" and pay.account == payment_account:
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
pr = make_payment_request(
|
pr = make_payment_request(
|
||||||
dt="Purchase Invoice",
|
dt="Purchase Invoice",
|
||||||
dn=si_usd.name,
|
dn=si_usd.name,
|
||||||
|
party_type="Supplier",
|
||||||
|
party="_Test Supplier USD",
|
||||||
recipient_id="user@example.com",
|
recipient_id="user@example.com",
|
||||||
mute_email=1,
|
mute_email=1,
|
||||||
payment_gateway_account="_Test Gateway - USD",
|
payment_gateway_account="_Test Gateway - USD",
|
||||||
@@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(pr.status, "Paid")
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
|
||||||
|
def test_multiple_payment_entry_against_purchase_invoice(self):
|
||||||
|
purchase_invoice = make_purchase_invoice(
|
||||||
|
customer="_Test Supplier USD",
|
||||||
|
debit_to="_Test Payable USD - _TC",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
party_type="Supplier",
|
||||||
|
party="_Test Supplier USD",
|
||||||
|
dn=purchase_invoice.name,
|
||||||
|
recipient_id="user@example.com",
|
||||||
|
mute_email=1,
|
||||||
|
payment_gateway_account="_Test Gateway - USD",
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.grand_total = pr.grand_total / 2
|
||||||
|
|
||||||
|
pr.submit()
|
||||||
|
pr.create_payment_entry()
|
||||||
|
|
||||||
|
purchase_invoice.load_from_db()
|
||||||
|
self.assertEqual(purchase_invoice.status, "Partly Paid")
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
party_type="Supplier",
|
||||||
|
party="_Test Supplier USD",
|
||||||
|
dn=purchase_invoice.name,
|
||||||
|
recipient_id="user@example.com",
|
||||||
|
mute_email=1,
|
||||||
|
payment_gateway_account="_Test Gateway - USD",
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.save()
|
||||||
|
pr.submit()
|
||||||
|
pr.create_payment_entry()
|
||||||
|
|
||||||
|
purchase_invoice.load_from_db()
|
||||||
|
self.assertEqual(purchase_invoice.status, "Paid")
|
||||||
|
|
||||||
def test_payment_entry(self):
|
def test_payment_entry(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||||
|
|||||||
@@ -573,6 +573,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
|||||||
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
||||||
# Apply discount on discounted rate
|
# Apply discount on discounted rate
|
||||||
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
||||||
|
elif args.price_list_rate:
|
||||||
|
value = pricing_rule.get(field, 0)
|
||||||
|
calculate_discount_percentage = False
|
||||||
|
if field == "discount_percentage":
|
||||||
|
field = "discount_amount"
|
||||||
|
value = args.price_list_rate * (value / 100)
|
||||||
|
calculate_discount_percentage = True
|
||||||
|
|
||||||
|
if field not in item_details:
|
||||||
|
item_details.setdefault(field, 0)
|
||||||
|
|
||||||
|
item_details[field] += value if pricing_rule else args.get(field, 0)
|
||||||
|
if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount:
|
||||||
|
item_details.discount_percentage = flt(
|
||||||
|
(flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if field not in item_details:
|
if field not in item_details:
|
||||||
item_details.setdefault(field, 0)
|
item_details.setdefault(field, 0)
|
||||||
|
|||||||
@@ -1102,7 +1102,60 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
so.load_from_db()
|
so.load_from_db()
|
||||||
self.assertEqual(so.items[1].is_free_item, 1)
|
self.assertEqual(so.items[1].is_free_item, 1)
|
||||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||||
self.assertEqual(so.items[1].qty, 4)
|
self.assertEqual(so.items[1].qty, 3)
|
||||||
|
|
||||||
|
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||||
|
test_record = {
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": "_Test Pricing Rule 1",
|
||||||
|
"name": "_Test Pricing Rule 1",
|
||||||
|
"apply_on": "Item Code",
|
||||||
|
"currency": "USD",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selling": 1,
|
||||||
|
"price_or_product_discount": "Price",
|
||||||
|
"rate_or_discount": "Discount Percentage",
|
||||||
|
"discount_percentage": 10,
|
||||||
|
"apply_multiple_pricing_rules": 1,
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.get_doc(test_record.copy()).insert()
|
||||||
|
|
||||||
|
test_record = {
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": "_Test Pricing Rule 2",
|
||||||
|
"name": "_Test Pricing Rule 2",
|
||||||
|
"apply_on": "Item Code",
|
||||||
|
"currency": "USD",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selling": 1,
|
||||||
|
"price_or_product_discount": "Price",
|
||||||
|
"rate_or_discount": "Discount Amount",
|
||||||
|
"discount_amount": 100,
|
||||||
|
"apply_multiple_pricing_rules": 1,
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.get_doc(test_record.copy()).insert()
|
||||||
|
|
||||||
|
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||||
|
self.assertEqual(so.items[0].discount_amount, 200)
|
||||||
|
self.assertEqual(so.items[0].rate, 800)
|
||||||
|
|
||||||
|
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||||
|
|
||||||
|
|
||||||
test_dependencies = ["Campaign"]
|
test_dependencies = ["Campaign"]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
@@ -653,7 +654,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
|||||||
if transaction_qty:
|
if transaction_qty:
|
||||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||||
if pricing_rule.round_free_qty:
|
if pricing_rule.round_free_qty:
|
||||||
qty = round(qty)
|
qty = math.floor(qty)
|
||||||
|
|
||||||
free_item_data_args = {
|
free_item_data_args = {
|
||||||
"item_code": free_item,
|
"item_code": free_item,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
|
|||||||
ageing_filters = frappe._dict(
|
ageing_filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"company": doc.company,
|
"company": doc.company,
|
||||||
"report_date": doc.to_date,
|
"report_date": doc.posting_date,
|
||||||
"ageing_based_on": doc.ageing_based_on,
|
"ageing_based_on": doc.ageing_based_on,
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
"range2": 60,
|
"range2": 60,
|
||||||
|
|||||||
@@ -340,10 +340,11 @@
|
|||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 25%">30 Days</th>
|
<th style="width: 25%">0 - 30 Days</th>
|
||||||
<th style="width: 25%">60 Days</th>
|
<th style="width: 25%">30 - 60 Days</th>
|
||||||
<th style="width: 25%">90 Days</th>
|
<th style="width: 25%">60 - 90 Days</th>
|
||||||
<th style="width: 25%">120 Days</th>
|
<th style="width: 25%">90 - 120 Days</th>
|
||||||
|
<th style="width: 20%">Above 120 Days</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -352,6 +353,7 @@
|
|||||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||||
|
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -11,13 +11,15 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "cost_center_name",
|
"fieldname": "cost_center_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:23.244686",
|
"modified": "2024-05-03 17:16:51.666461",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "PSOA Cost Center",
|
"name": "PSOA Cost Center",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
cost_center_name: DF.Link | None
|
cost_center_name: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -299,6 +299,7 @@
|
|||||||
"remember_last_selected_value": 1
|
"remember_last_selected_value": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
@@ -1368,6 +1369,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
@@ -1638,7 +1640,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:23.476658",
|
"modified": "2024-04-11 11:28:42.802211",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -68,15 +68,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
|
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
|
||||||
PurchaseInvoiceAdvance,
|
PurchaseInvoiceAdvance,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import (
|
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import PurchaseInvoiceItem
|
||||||
PurchaseInvoiceItem,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
|
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
|
||||||
PurchaseTaxesandCharges,
|
PurchaseTaxesandCharges,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import (
|
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
|
||||||
TaxWithheldVouchers,
|
|
||||||
)
|
|
||||||
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
|
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
|
||||||
PurchaseReceiptItemSupplied,
|
PurchaseReceiptItemSupplied,
|
||||||
)
|
)
|
||||||
@@ -1067,7 +1063,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check if the exchange rate has changed
|
# check if the exchange rate has changed
|
||||||
if item.get("purchase_receipt"):
|
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||||
if (
|
if (
|
||||||
exchange_rate_map[item.purchase_receipt]
|
exchange_rate_map[item.purchase_receipt]
|
||||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||||
@@ -1211,7 +1207,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
asset.name,
|
asset.name,
|
||||||
{
|
{
|
||||||
"gross_purchase_amount": purchase_amount,
|
"gross_purchase_amount": purchase_amount,
|
||||||
"purchase_receipt_amount": purchase_amount,
|
"purchase_amount": purchase_amount,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -356,6 +357,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -2040,7 +2042,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contact_and_address_tab",
|
"fieldname": "contact_and_address_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact & Address"
|
"label": "Address & Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "payments_tab",
|
"fieldname": "payments_tab",
|
||||||
@@ -2201,7 +2203,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:10:35.407256",
|
"modified": "2024-05-08 18:02:28.549041",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -58,13 +58,9 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||||
SalesInvoiceAdvance,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem
|
from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem
|
||||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||||
SalesInvoicePayment,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||||
SalesInvoiceTimesheet,
|
SalesInvoiceTimesheet,
|
||||||
)
|
)
|
||||||
@@ -397,6 +393,9 @@ class SalesInvoice(SellingController):
|
|||||||
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
def set_tax_withholding(self):
|
||||||
|
if self.get("is_opening") == "Yes":
|
||||||
|
return
|
||||||
|
|
||||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||||
|
|
||||||
if not tax_withholding_details:
|
if not tax_withholding_details:
|
||||||
|
|||||||
@@ -1783,6 +1783,49 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertTrue(gle)
|
self.assertTrue(gle)
|
||||||
|
|
||||||
|
def test_gle_in_transaction_currency(self):
|
||||||
|
# create multi currency sales invoice with 2 items with same income account
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer USD",
|
||||||
|
debit_to="_Test Receivable USD - _TC",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
# add 2nd item with same income account
|
||||||
|
si.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 80,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
gl_entries = frappe.db.sql(
|
||||||
|
"""select transaction_currency, transaction_exchange_rate,
|
||||||
|
debit_in_transaction_currency, credit_in_transaction_currency
|
||||||
|
from `tabGL Entry`
|
||||||
|
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
|
||||||
|
order by account asc""",
|
||||||
|
si.name,
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_gle = {
|
||||||
|
"transaction_currency": "USD",
|
||||||
|
"transaction_exchange_rate": 50,
|
||||||
|
"debit_in_transaction_currency": 0,
|
||||||
|
"credit_in_transaction_currency": 180,
|
||||||
|
}
|
||||||
|
|
||||||
|
for gle in gl_entries:
|
||||||
|
for field in expected_gle:
|
||||||
|
self.assertEqual(expected_gle[field], gle[field])
|
||||||
|
|
||||||
def test_invoice_exchange_rate(self):
|
def test_invoice_exchange_rate(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
customer="_Test Customer USD",
|
customer="_Test Customer USD",
|
||||||
|
|||||||
@@ -112,11 +112,7 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
_current_invoice_start = None
|
_current_invoice_start = None
|
||||||
|
|
||||||
if (
|
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||||
self.is_new_subscription()
|
|
||||||
and self.trial_period_end
|
|
||||||
and getdate(self.trial_period_end) > getdate(self.start_date)
|
|
||||||
):
|
|
||||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||||
elif self.trial_period_start and self.is_trialling():
|
elif self.trial_period_start and self.is_trialling():
|
||||||
_current_invoice_start = self.trial_period_start
|
_current_invoice_start = self.trial_period_start
|
||||||
@@ -143,7 +139,7 @@ class Subscription(Document):
|
|||||||
else:
|
else:
|
||||||
billing_cycle_info = self.get_billing_cycle_data()
|
billing_cycle_info = self.get_billing_cycle_data()
|
||||||
if billing_cycle_info:
|
if billing_cycle_info:
|
||||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
|
if getdate(self.start_date) < getdate(date):
|
||||||
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||||
|
|
||||||
# For cases where trial period is for an entire billing interval
|
# For cases where trial period is for an entire billing interval
|
||||||
@@ -234,14 +230,14 @@ class Subscription(Document):
|
|||||||
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||||
self.status = "Past Due Date"
|
self.status = "Past Due Date"
|
||||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
elif not self.has_outstanding_invoice():
|
||||||
self.status = "Active"
|
self.status = "Active"
|
||||||
|
|
||||||
def is_trialling(self) -> bool:
|
def is_trialling(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns `True` if the `Subscription` is in trial period.
|
Returns `True` if the `Subscription` is in trial period.
|
||||||
"""
|
"""
|
||||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
return not self.period_has_passed(self.trial_period_end)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def period_has_passed(
|
def period_has_passed(
|
||||||
@@ -288,14 +284,6 @@ class Subscription(Document):
|
|||||||
def invoice_document_type(self) -> str:
|
def invoice_document_type(self) -> str:
|
||||||
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||||
|
|
||||||
def is_new_subscription(self) -> bool:
|
|
||||||
"""
|
|
||||||
Returns `True` if `Subscription` has never generated an invoice
|
|
||||||
"""
|
|
||||||
return self.is_new() or not frappe.db.exists(
|
|
||||||
{"doctype": self.invoice_document_type, "subscription": self.name}
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
def validate(self) -> None:
|
||||||
self.validate_trial_period()
|
self.validate_trial_period()
|
||||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||||
@@ -604,7 +592,7 @@ class Subscription(Document):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
||||||
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
|
getdate(posting_date) == getdate(self.current_invoice_start)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
||||||
|
|||||||
@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
|
|||||||
|
|
||||||
# Process subscription and create first invoice
|
# Process subscription and create first invoice
|
||||||
# Subscription status will be unpaid since due date has already passed
|
# Subscription status will be unpaid since due date has already passed
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-04-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
def test_multi_currency_subscription(self):
|
def test_multi_currency_subscription(self):
|
||||||
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
|
|||||||
party=party,
|
party=party,
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:52.419915",
|
"modified": "2024-04-30 10:26:48.218294",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withholding Account",
|
"name": "Tax Withholding Account",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
|
|||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
from frappe.utils import cint, flt, getdate
|
from frappe.utils import cint, flt, getdate
|
||||||
|
|
||||||
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
|
|
||||||
|
|
||||||
class TaxWithholdingCategory(Document):
|
class TaxWithholdingCategory(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
|
|||||||
if d.get("account") in existing_accounts:
|
if d.get("account") in existing_accounts:
|
||||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||||
|
|
||||||
|
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||||
existing_accounts.append(d.get("account"))
|
existing_accounts.append(d.get("account"))
|
||||||
|
|
||||||
def validate_thresholds(self):
|
def validate_thresholds(self):
|
||||||
@@ -282,6 +285,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
if taxable_vouchers:
|
if taxable_vouchers:
|
||||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||||
|
|
||||||
|
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
|
||||||
|
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
|
||||||
|
# rest of the below logic to function properly
|
||||||
|
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
|
||||||
|
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
|
||||||
|
if tax_deducted_on_advances:
|
||||||
|
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
|
||||||
|
|
||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
|
|
||||||
if party_type == "Supplier":
|
if party_type == "Supplier":
|
||||||
@@ -418,7 +429,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
|||||||
frappe.qb.from_(at)
|
frappe.qb.from_(at)
|
||||||
.inner_join(pe)
|
.inner_join(pe)
|
||||||
.on(pe.name == at.parent)
|
.on(pe.name == at.parent)
|
||||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||||
.where(at.parent.isin(advances))
|
.where(at.parent.isin(advances))
|
||||||
.where(at.account_head == tax_details.account_head)
|
.where(at.account_head == tax_details.account_head)
|
||||||
@@ -443,6 +454,16 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
|||||||
return sum(entries)
|
return sum(entries)
|
||||||
|
|
||||||
|
|
||||||
|
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||||
|
"""
|
||||||
|
Only applies for Taxes deducted on Advance Payments
|
||||||
|
"""
|
||||||
|
advance_tax_from_across_fiscal_year = sum(
|
||||||
|
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
|
||||||
|
)
|
||||||
|
return advance_tax_from_across_fiscal_year
|
||||||
|
|
||||||
|
|
||||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
|
import datetime
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
from frappe.utils import today
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||||
|
|
||||||
test_dependencies = ["Supplier Group", "Customer Group"]
|
test_dependencies = ["Supplier Group", "Customer Group"]
|
||||||
|
|
||||||
|
|
||||||
class TestTaxWithholdingCategory(unittest.TestCase):
|
class TestTaxWithholdingCategory(FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
# create relevant supplier, etc
|
# create relevant supplier, etc
|
||||||
@@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
make_pan_no_field()
|
make_pan_no_field()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
cancel_invoices()
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_cumulative_threshold_tds(self):
|
def test_cumulative_threshold_tds(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
|
||||||
|
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||||
)
|
)
|
||||||
@@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
pi2.cancel()
|
pi2.cancel()
|
||||||
pi3.cancel()
|
pi3.cancel()
|
||||||
|
|
||||||
|
def set_previous_fy_and_tax_category(self):
|
||||||
|
test_company = "_Test Company"
|
||||||
|
category = "Cumulative Threshold TDS"
|
||||||
|
|
||||||
|
def add_company_to_fy(fy, company):
|
||||||
|
if not [x.company for x in fy.companies if x.company == company]:
|
||||||
|
fy.append("companies", {"company": company})
|
||||||
|
fy.save()
|
||||||
|
|
||||||
|
# setup previous fiscal year
|
||||||
|
fiscal_year = get_fiscal_year(today(), company=test_company)
|
||||||
|
if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)):
|
||||||
|
self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0])
|
||||||
|
add_company_to_fy(self.prev_fy, test_company)
|
||||||
|
else:
|
||||||
|
# make previous fiscal year
|
||||||
|
start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day)
|
||||||
|
end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day)
|
||||||
|
self.prev_fy = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Fiscal Year",
|
||||||
|
"year_start_date": start,
|
||||||
|
"year_end_date": end,
|
||||||
|
"companies": [{"company": test_company}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.prev_fy.save()
|
||||||
|
|
||||||
|
# setup tax withholding category for previous fiscal year
|
||||||
|
cat = frappe.get_doc("Tax Withholding Category", category)
|
||||||
|
cat.append(
|
||||||
|
"rates",
|
||||||
|
{
|
||||||
|
"from_date": self.prev_fy.year_start_date,
|
||||||
|
"to_date": self.prev_fy.year_end_date,
|
||||||
|
"tax_withholding_rate": 10,
|
||||||
|
"single_threshold": 0,
|
||||||
|
"cumulative_threshold": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cat.save()
|
||||||
|
|
||||||
|
def test_tds_across_fiscal_year(self):
|
||||||
|
"""
|
||||||
|
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
|
||||||
|
--||-----FY 2023-----||-----FY 2024-----||--
|
||||||
|
--||-----Advance-----||---Inv1---Inv2---||--
|
||||||
|
"""
|
||||||
|
self.set_previous_fy_and_tax_category()
|
||||||
|
supplier = "Test TDS Supplier"
|
||||||
|
# Cumulative threshold 30000 and tax rate 10%
|
||||||
|
category = "Cumulative Threshold TDS"
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Supplier",
|
||||||
|
supplier,
|
||||||
|
{
|
||||||
|
"tax_withholding_category": category,
|
||||||
|
"pan": "ABCTY1234D",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10)
|
||||||
|
po = create_purchase_order(supplier=supplier, qty=10, rate=10000)
|
||||||
|
po.transaction_date = po_and_advance_posting_date
|
||||||
|
po.taxes = []
|
||||||
|
po.apply_tds = False
|
||||||
|
po.tax_withholding_category = None
|
||||||
|
po.save().submit()
|
||||||
|
|
||||||
|
# Partial advance
|
||||||
|
payment = get_payment_entry(po.doctype, po.name)
|
||||||
|
payment.posting_date = po_and_advance_posting_date
|
||||||
|
payment.paid_amount = 60000
|
||||||
|
payment.apply_tax_withholding_amount = 1
|
||||||
|
payment.tax_withholding_category = category
|
||||||
|
payment.references = []
|
||||||
|
payment.taxes = []
|
||||||
|
payment.save().submit()
|
||||||
|
|
||||||
|
self.assertEqual(len(payment.taxes), 1)
|
||||||
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
|
|
||||||
|
# Multiple partial invoices
|
||||||
|
payment.reload()
|
||||||
|
pi1 = make_purchase_invoice(source_name=po.name)
|
||||||
|
pi1.apply_tds = True
|
||||||
|
pi1.tax_withholding_category = category
|
||||||
|
pi1.items[0].qty = 3
|
||||||
|
pi1.items[0].rate = 10000
|
||||||
|
advances = pi1.get_advance_entries()
|
||||||
|
pi1.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"advance_amount": advances[0].amount,
|
||||||
|
"allocated_amount": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi1.save().submit()
|
||||||
|
pi1.reload()
|
||||||
|
payment.reload()
|
||||||
|
self.assertEqual(pi1.taxes, [])
|
||||||
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
|
self.assertEqual(payment.taxes[0].allocated_amount, 3000)
|
||||||
|
|
||||||
|
pi2 = make_purchase_invoice(source_name=po.name)
|
||||||
|
pi2.apply_tds = True
|
||||||
|
pi2.tax_withholding_category = category
|
||||||
|
pi2.items[0].qty = 3
|
||||||
|
pi2.items[0].rate = 10000
|
||||||
|
advances = pi2.get_advance_entries()
|
||||||
|
pi2.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"advance_amount": advances[0].amount,
|
||||||
|
"allocated_amount": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi2.save().submit()
|
||||||
|
pi2.reload()
|
||||||
|
payment.reload()
|
||||||
|
self.assertEqual(pi2.taxes, [])
|
||||||
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
|
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||||
|
|
||||||
|
|
||||||
def cancel_invoices():
|
def cancel_invoices():
|
||||||
purchase_invoices = frappe.get_all(
|
purchase_invoices = frappe.get_all(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||||
|
from frappe.utils.dashboard import cache_source
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -240,10 +241,16 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||||
entry.debit_in_account_currency
|
entry.debit_in_account_currency
|
||||||
)
|
)
|
||||||
|
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
||||||
|
entry.debit_in_transaction_currency
|
||||||
|
)
|
||||||
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
||||||
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
||||||
entry.credit_in_account_currency
|
entry.credit_in_account_currency
|
||||||
)
|
)
|
||||||
|
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
||||||
|
entry.credit_in_transaction_currency
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
merged_gl_map.append(entry)
|
merged_gl_map.append(entry)
|
||||||
|
|
||||||
@@ -574,6 +581,8 @@ def make_reverse_gl_entries(
|
|||||||
and make reverse gl entries by swapping debit and credit
|
and make reverse gl entries by swapping debit and credit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
immutable_ledger_enabled = is_immutable_ledger_enabled()
|
||||||
|
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
gl_entry = frappe.qb.DocType("GL Entry")
|
gl_entry = frappe.qb.DocType("GL Entry")
|
||||||
gl_entries = (
|
gl_entries = (
|
||||||
@@ -605,7 +614,6 @@ def make_reverse_gl_entries(
|
|||||||
for x in gl_entries:
|
for x in gl_entries:
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.update(gle)
|
frappe.qb.update(gle)
|
||||||
.set(gle.is_cancelled, True)
|
|
||||||
.set(gle.modified, now())
|
.set(gle.modified, now())
|
||||||
.set(gle.modified_by, frappe.session.user)
|
.set(gle.modified_by, frappe.session.user)
|
||||||
.where(
|
.where(
|
||||||
@@ -620,9 +628,14 @@ def make_reverse_gl_entries(
|
|||||||
& (gle.voucher_detail_no == x.voucher_detail_no)
|
& (gle.voucher_detail_no == x.voucher_detail_no)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not immutable_ledger_enabled:
|
||||||
|
query = query.set(gle.is_cancelled, True)
|
||||||
|
|
||||||
query.run()
|
query.run()
|
||||||
else:
|
else:
|
||||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
if not immutable_ledger_enabled:
|
||||||
|
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
new_gle = copy.deepcopy(entry)
|
new_gle = copy.deepcopy(entry)
|
||||||
@@ -641,6 +654,10 @@ def make_reverse_gl_entries(
|
|||||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||||
new_gle["is_cancelled"] = 1
|
new_gle["is_cancelled"] = 1
|
||||||
|
|
||||||
|
if immutable_ledger_enabled:
|
||||||
|
new_gle["is_cancelled"] = 0
|
||||||
|
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
|
||||||
|
|
||||||
if new_gle["debit"] or new_gle["credit"]:
|
if new_gle["debit"] or new_gle["credit"]:
|
||||||
make_entry(new_gle, adv_adj, "Yes")
|
make_entry(new_gle, adv_adj, "Yes")
|
||||||
|
|
||||||
@@ -733,3 +750,7 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
|||||||
),
|
),
|
||||||
InvalidAccountDimensionError,
|
InvalidAccountDimensionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_immutable_ledger_enabled():
|
||||||
|
return frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ def set_address_details(
|
|||||||
*,
|
*,
|
||||||
ignore_permissions=False,
|
ignore_permissions=False,
|
||||||
):
|
):
|
||||||
billing_address_field = "customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
billing_address_field = (
|
||||||
|
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
|
||||||
|
)
|
||||||
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
||||||
if doctype:
|
if doctype:
|
||||||
party_details.update(
|
party_details.update(
|
||||||
@@ -754,52 +756,6 @@ def validate_party_frozen_disabled(party_type, party_name):
|
|||||||
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
||||||
|
|
||||||
|
|
||||||
def get_timeline_data(doctype, name):
|
|
||||||
"""returns timeline data for the past one year"""
|
|
||||||
from frappe.desk.form.load import get_communication_data
|
|
||||||
|
|
||||||
out = {}
|
|
||||||
after = add_years(None, -1).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
data = get_communication_data(
|
|
||||||
doctype,
|
|
||||||
name,
|
|
||||||
after=after,
|
|
||||||
group_by="group by communication_date",
|
|
||||||
fields="C.communication_date as communication_date, count(C.name)",
|
|
||||||
as_dict=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# fetch and append data from Activity Log
|
|
||||||
activity_log = frappe.qb.DocType("Activity Log")
|
|
||||||
data += (
|
|
||||||
frappe.qb.from_(activity_log)
|
|
||||||
.select(activity_log.communication_date, Count(activity_log.name))
|
|
||||||
.where(
|
|
||||||
(
|
|
||||||
((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
|
|
||||||
| ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
|
|
||||||
| (
|
|
||||||
(activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
|
|
||||||
& (activity_log.timeline_name == name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
& (activity_log.status != "Success")
|
|
||||||
& (activity_log.creation > after)
|
|
||||||
)
|
|
||||||
.groupby(activity_log.communication_date)
|
|
||||||
.orderby(activity_log.communication_date, order=frappe.qb.desc)
|
|
||||||
).run()
|
|
||||||
|
|
||||||
timeline_items = dict(data)
|
|
||||||
|
|
||||||
for date, count in timeline_items.items():
|
|
||||||
timestamp = get_timestamp(date)
|
|
||||||
out.update({timestamp: count})
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard_info(party_type, party, loyalty_program=None):
|
def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||||
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
||||||
|
|
||||||
|
|||||||
@@ -501,8 +501,9 @@ class ReceivablePayableReport:
|
|||||||
# Deduct that from paid amount pre allocation
|
# Deduct that from paid amount pre allocation
|
||||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||||
|
|
||||||
# If no or single payment terms, no need to split the row
|
# If single payment terms, no need to split the row
|
||||||
if len(payment_terms_details) <= 1:
|
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||||
|
self.append_payment_term(row, payment_terms_details[0], original_row)
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in payment_terms_details:
|
for d in payment_terms_details:
|
||||||
@@ -1027,20 +1028,6 @@ class ReceivablePayableReport:
|
|||||||
fieldtype="Link",
|
fieldtype="Link",
|
||||||
options="Contact",
|
options="Contact",
|
||||||
)
|
)
|
||||||
if self.filters.party_type == "Customer":
|
|
||||||
self.add_column(
|
|
||||||
_("Customer Name"),
|
|
||||||
fieldname="customer_name",
|
|
||||||
fieldtype="Link",
|
|
||||||
options="Customer",
|
|
||||||
)
|
|
||||||
elif self.filters.party_type == "Supplier":
|
|
||||||
self.add_column(
|
|
||||||
_("Supplier Name"),
|
|
||||||
fieldname="supplier_name",
|
|
||||||
fieldtype="Link",
|
|
||||||
options="Supplier",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
|||||||
filters.end_date = fiscal_year.year_end_date
|
filters.end_date = fiscal_year.year_end_date
|
||||||
|
|
||||||
for company in companies:
|
for company in companies:
|
||||||
|
filters.company = company
|
||||||
amount = get_account_type_based_gl_data(company, filters)
|
amount = get_account_type_based_gl_data(company, filters)
|
||||||
|
|
||||||
if amount and account_type == "Depreciation":
|
if amount and account_type == "Depreciation":
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ class Deferred_Item:
|
|||||||
For a given GL/Journal posting, get balance based on item type
|
For a given GL/Journal posting, get balance based on item type
|
||||||
"""
|
"""
|
||||||
if self.type == "Deferred Sale Item":
|
if self.type == "Deferred Sale Item":
|
||||||
return entry.debit - entry.credit
|
return flt(entry.debit) - flt(entry.credit)
|
||||||
elif self.type == "Deferred Purchase Item":
|
elif self.type == "Deferred Purchase Item":
|
||||||
return -(entry.credit - entry.debit)
|
return -(flt(entry.credit) - flt(entry.debit))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_item_total(self):
|
def get_item_total(self):
|
||||||
@@ -147,7 +147,7 @@ class Deferred_Item:
|
|||||||
actual = 0
|
actual = 0
|
||||||
for posting in self.gle_entries:
|
for posting in self.gle_entries:
|
||||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date:
|
||||||
period_sum += self.get_amount(posting)
|
period_sum += self.get_amount(posting)
|
||||||
if posting.posted == "posted":
|
if posting.posted == "posted":
|
||||||
actual += self.get_amount(posting)
|
actual += self.get_amount(posting)
|
||||||
@@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report:
|
|||||||
qb.from_(inv_item)
|
qb.from_(inv_item)
|
||||||
.join(inv)
|
.join(inv)
|
||||||
.on(inv.name == inv_item.parent)
|
.on(inv.name == inv_item.parent)
|
||||||
.join(gle)
|
.left_join(gle)
|
||||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||||
.select(
|
.select(
|
||||||
inv.name.as_("doc"),
|
inv.name.as_("doc"),
|
||||||
|
|||||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
|||||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||||
]
|
]
|
||||||
self.assertEqual(report.period_total, expected)
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||||
|
)
|
||||||
|
def test_zero_amount(self):
|
||||||
|
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||||
|
item = frappe.get_doc("Item", self.item)
|
||||||
|
item.enable_deferred_expense = 1
|
||||||
|
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||||
|
item.no_of_months_exp = 12
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
supplier=self.supplier,
|
||||||
|
is_return=False,
|
||||||
|
update_stock=False,
|
||||||
|
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
do_not_save=True,
|
||||||
|
rate=3910,
|
||||||
|
price_list_rate=3910,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
qty=1,
|
||||||
|
)
|
||||||
|
pi.set_posting_time = True
|
||||||
|
pi.items[0].enable_deferred_expense = 1
|
||||||
|
pi.items[0].service_start_date = "2021-12-30"
|
||||||
|
pi.items[0].service_end_date = "2022-12-30"
|
||||||
|
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||||
|
pi.items[0].expense_account = self.expense_account
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pda = frappe.get_doc(
|
||||||
|
doctype="Process Deferred Accounting",
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2022-01-01",
|
||||||
|
end_date="2022-01-31",
|
||||||
|
type="Expense",
|
||||||
|
company=self.company,
|
||||||
|
)
|
||||||
|
pda.insert()
|
||||||
|
pda.submit()
|
||||||
|
|
||||||
|
# execute report
|
||||||
|
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"period_start_date": "2022-01-01",
|
||||||
|
"period_end_date": "2022-01-31",
|
||||||
|
"from_fiscal_year": fiscal_year.year,
|
||||||
|
"to_fiscal_year": fiscal_year.year,
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"type": "Expense",
|
||||||
|
"with_upcoming_postings": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||||
|
report.run()
|
||||||
|
|
||||||
|
# fetch the invoice from deferred invoices list
|
||||||
|
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||||
|
# make sure the list isn't empty
|
||||||
|
self.assertTrue(inv)
|
||||||
|
# calculate the total deferred expense for the period
|
||||||
|
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||||
|
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||||
|
# make sure the total deferred expense is greater than 0
|
||||||
|
self.assertLess(deferred_exp, 0)
|
||||||
|
|||||||
@@ -460,7 +460,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
|
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
group_by_value = gle.get(group_by)
|
group_by_value = gle.get(group_by)
|
||||||
gle.voucher_type = gle.voucher_type
|
|
||||||
gle.voucher_subtype = _(gle.voucher_subtype)
|
gle.voucher_subtype = _(gle.voucher_subtype)
|
||||||
gle.against_voucher_type = _(gle.against_voucher_type)
|
gle.against_voucher_type = _(gle.against_voucher_type)
|
||||||
gle.remarks = _(gle.remarks)
|
gle.remarks = _(gle.remarks)
|
||||||
|
|||||||
@@ -655,13 +655,13 @@ class GrossProfitGenerator:
|
|||||||
elif self.delivery_notes.get((row.parent, row.item_code), None):
|
elif self.delivery_notes.get((row.parent, row.item_code), None):
|
||||||
# check if Invoice has delivery notes
|
# check if Invoice has delivery notes
|
||||||
dn = self.delivery_notes.get((row.parent, row.item_code))
|
dn = self.delivery_notes.get((row.parent, row.item_code))
|
||||||
parenttype, parent, item_row, _warehouse = (
|
parenttype, parent, item_row, dn_warehouse = (
|
||||||
"Delivery Note",
|
"Delivery Note",
|
||||||
dn["delivery_note"],
|
dn["delivery_note"],
|
||||||
dn["item_row"],
|
dn["item_row"],
|
||||||
dn["warehouse"],
|
dn["warehouse"],
|
||||||
)
|
)
|
||||||
my_sle = self.get_stock_ledger_entries(item_code, _warehouse)
|
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
|
||||||
return self.calculate_buying_amount_from_sle(
|
return self.calculate_buying_amount_from_sle(
|
||||||
row, my_sle, parenttype, parent, item_row, item_code
|
row, my_sle, parenttype, parent, item_row, item_code
|
||||||
)
|
)
|
||||||
@@ -720,20 +720,22 @@ class GrossProfitGenerator:
|
|||||||
frappe.qb.from_(purchase_invoice_item)
|
frappe.qb.from_(purchase_invoice_item)
|
||||||
.inner_join(purchase_invoice)
|
.inner_join(purchase_invoice)
|
||||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||||
.select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor)
|
.select(
|
||||||
|
purchase_invoice.name,
|
||||||
|
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||||
|
)
|
||||||
.where(purchase_invoice.docstatus == 1)
|
.where(purchase_invoice.docstatus == 1)
|
||||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||||
.where(purchase_invoice_item.item_code == item_code)
|
.where(purchase_invoice_item.item_code == item_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
if row.project:
|
if row.project:
|
||||||
query.where(purchase_invoice_item.project == row.project)
|
query = query.where(purchase_invoice_item.project == row.project)
|
||||||
|
|
||||||
if row.cost_center:
|
if row.cost_center:
|
||||||
query.where(purchase_invoice_item.cost_center == row.cost_center)
|
query = query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||||
|
|
||||||
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
|
query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1)
|
||||||
query.limit(1)
|
|
||||||
last_purchase_rate = query.run()
|
last_purchase_rate = query.run()
|
||||||
|
|
||||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||||
|
|||||||
@@ -516,6 +516,10 @@ def reconcile_against_document(
|
|||||||
doc.make_advance_gl_entries()
|
doc.make_advance_gl_entries()
|
||||||
else:
|
else:
|
||||||
gl_map = doc.build_gl_map()
|
gl_map = doc.build_gl_map()
|
||||||
|
# Make sure there is no overallocation
|
||||||
|
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||||
|
|
||||||
|
process_debit_credit_difference(gl_map)
|
||||||
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
|
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
|
||||||
|
|
||||||
# Only update outstanding for newly linked vouchers
|
# Only update outstanding for newly linked vouchers
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
frm.set_value("gross_purchase_amount", purchase_amount);
|
frm.set_value("gross_purchase_amount", purchase_amount);
|
||||||
frm.set_value("purchase_receipt_amount", purchase_amount);
|
frm.set_value("purchase_amount", purchase_amount);
|
||||||
frm.set_value("asset_quantity", asset_quantity);
|
frm.set_value("asset_quantity", asset_quantity);
|
||||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||||
if (item.asset_location) {
|
if (item.asset_location) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"status",
|
"status",
|
||||||
"booked_fixed_asset",
|
"booked_fixed_asset",
|
||||||
"column_break_51",
|
"column_break_51",
|
||||||
"purchase_receipt_amount",
|
"purchase_amount",
|
||||||
"default_finance_book",
|
"default_finance_book",
|
||||||
"depr_entry_posting_status",
|
"depr_entry_posting_status",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
@@ -408,15 +408,6 @@
|
|||||||
"options": "Purchase Receipt",
|
"options": "Purchase Receipt",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "purchase_receipt_amount",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Purchase Receipt Amount",
|
|
||||||
"no_copy": 1,
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
||||||
"fieldname": "purchase_invoice",
|
"fieldname": "purchase_invoice",
|
||||||
@@ -546,6 +537,15 @@
|
|||||||
"label": "Additional Asset Cost",
|
"label": "Additional Asset Cost",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "purchase_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Purchase Amount",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 72,
|
"idx": 72,
|
||||||
@@ -589,7 +589,7 @@
|
|||||||
"link_fieldname": "target_asset"
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:06:32.494326",
|
"modified": "2024-04-18 16:45:47.306032",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
@@ -628,7 +628,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "asset_name",
|
"title_field": "asset_name",
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ class Asset(AccountsController):
|
|||||||
number_of_depreciations_booked: DF.Int
|
number_of_depreciations_booked: DF.Int
|
||||||
opening_accumulated_depreciation: DF.Currency
|
opening_accumulated_depreciation: DF.Currency
|
||||||
policy_number: DF.Data | None
|
policy_number: DF.Data | None
|
||||||
|
purchase_amount: DF.Currency
|
||||||
purchase_date: DF.Date | None
|
purchase_date: DF.Date | None
|
||||||
purchase_invoice: DF.Link | None
|
purchase_invoice: DF.Link | None
|
||||||
purchase_receipt: DF.Link | None
|
purchase_receipt: DF.Link | None
|
||||||
purchase_receipt_amount: DF.Currency
|
|
||||||
split_from: DF.Link | None
|
split_from: DF.Link | None
|
||||||
status: DF.Literal[
|
status: DF.Literal[
|
||||||
"Draft",
|
"Draft",
|
||||||
@@ -356,7 +356,7 @@ class Asset(AccountsController):
|
|||||||
if self.is_existing_asset:
|
if self.is_existing_asset:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
|
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
|
||||||
error_message = _(
|
error_message = _(
|
||||||
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||||
)
|
)
|
||||||
@@ -695,11 +695,7 @@ class Asset(AccountsController):
|
|||||||
purchase_document = self.get_purchase_document()
|
purchase_document = self.get_purchase_document()
|
||||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||||
|
|
||||||
if (
|
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||||
purchase_document
|
|
||||||
and self.purchase_receipt_amount
|
|
||||||
and getdate(self.available_for_use_date) <= getdate()
|
|
||||||
):
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
@@ -707,8 +703,8 @@ class Asset(AccountsController):
|
|||||||
"against": fixed_asset_account,
|
"against": fixed_asset_account,
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||||
"posting_date": self.available_for_use_date,
|
"posting_date": self.available_for_use_date,
|
||||||
"credit": self.purchase_receipt_amount,
|
"credit": self.purchase_amount,
|
||||||
"credit_in_account_currency": self.purchase_receipt_amount,
|
"credit_in_account_currency": self.purchase_amount,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
},
|
},
|
||||||
item=self,
|
item=self,
|
||||||
@@ -722,8 +718,8 @@ class Asset(AccountsController):
|
|||||||
"against": cwip_account,
|
"against": cwip_account,
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||||
"posting_date": self.available_for_use_date,
|
"posting_date": self.available_for_use_date,
|
||||||
"debit": self.purchase_receipt_amount,
|
"debit": self.purchase_amount,
|
||||||
"debit_in_account_currency": self.purchase_receipt_amount,
|
"debit_in_account_currency": self.purchase_amount,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
},
|
},
|
||||||
item=self,
|
item=self,
|
||||||
@@ -1119,8 +1115,8 @@ def create_new_asset_after_split(asset, split_qty):
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
||||||
if asset.purchase_receipt_amount:
|
if asset.purchase_amount:
|
||||||
new_asset.purchase_receipt_amount = new_gross_purchase_amount
|
new_asset.purchase_amount = new_gross_purchase_amount
|
||||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||||
new_asset.asset_quantity = split_qty
|
new_asset.asset_quantity = split_qty
|
||||||
new_asset.split_from = asset.name
|
new_asset.split_from = asset.name
|
||||||
|
|||||||
@@ -1000,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
|
|
||||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||||
|
|
||||||
depreciation_amount = get_depreciation_amount(
|
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||||
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
|
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
|
||||||
)
|
)
|
||||||
self.assertEqual(depreciation_amount, 30000)
|
self.assertEqual(depreciation_amount, 30000)
|
||||||
@@ -1698,7 +1698,7 @@ def create_asset(**args):
|
|||||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||||
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
|
"purchase_amount": args.purchase_amount or 100000,
|
||||||
"maintenance_required": args.maintenance_required or 0,
|
"maintenance_required": args.maintenance_required or 0,
|
||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||||
@@ -1723,6 +1723,7 @@ def create_asset(**args):
|
|||||||
"depreciation_start_date": args.depreciation_start_date,
|
"depreciation_start_date": args.depreciation_start_date,
|
||||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||||
"shift_based": args.shift_based or 0,
|
"shift_based": args.shift_based or 0,
|
||||||
|
"rate_of_depreciation": args.rate_of_depreciation or 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1738,12 +1739,12 @@ def create_asset(**args):
|
|||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
def create_asset_category():
|
def create_asset_category(enable_cwip=1):
|
||||||
asset_category = frappe.new_doc("Asset Category")
|
asset_category = frappe.new_doc("Asset Category")
|
||||||
asset_category.asset_category_name = "Computers"
|
asset_category.asset_category_name = "Computers"
|
||||||
asset_category.total_number_of_depreciations = 3
|
asset_category.total_number_of_depreciations = 3
|
||||||
asset_category.frequency_of_depreciation = 3
|
asset_category.frequency_of_depreciation = 3
|
||||||
asset_category.enable_cwip_accounting = 1
|
asset_category.enable_cwip_accounting = enable_cwip
|
||||||
asset_category.append(
|
asset_category.append(
|
||||||
"accounts",
|
"accounts",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -616,8 +616,7 @@ class AssetCapitalization(StockController):
|
|||||||
asset_doc.available_for_use_date = self.posting_date
|
asset_doc.available_for_use_date = self.posting_date
|
||||||
asset_doc.purchase_date = self.posting_date
|
asset_doc.purchase_date = self.posting_date
|
||||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
asset_doc.purchase_amount = total_target_asset_value
|
||||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
|
||||||
asset_doc.capitalized_in = self.name
|
asset_doc.capitalized_in = self.name
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||||
@@ -653,7 +652,7 @@ class AssetCapitalization(StockController):
|
|||||||
|
|
||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
asset_doc.purchase_amount = total_target_asset_value
|
||||||
asset_doc.capitalized_in = self.name
|
asset_doc.capitalized_in = self.name
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.save()
|
asset_doc.save()
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
# Test Target Asset values
|
# Test Target Asset values
|
||||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||||
|
|
||||||
# Test Consumed Asset values
|
# Test Consumed Asset values
|
||||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||||
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
# Test Target Asset values
|
# Test Target Asset values
|
||||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||||
|
|
||||||
# Test Consumed Asset values
|
# Test Consumed Asset values
|
||||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||||
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
# Test Target Asset values
|
# Test Target Asset values
|
||||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||||
|
|
||||||
# Test General Ledger Entries
|
# Test General Ledger Entries
|
||||||
expected_gle = {
|
expected_gle = {
|
||||||
@@ -526,7 +526,7 @@ def create_depreciation_asset(**args):
|
|||||||
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||||
|
|
||||||
asset.gross_purchase_amount = args.asset_value or 100000
|
asset.gross_purchase_amount = args.asset_value or 100000
|
||||||
asset.purchase_receipt_amount = asset.gross_purchase_amount
|
asset.purchase_amount = asset.gross_purchase_amount
|
||||||
|
|
||||||
finance_book = asset.append("finance_books")
|
finance_book = asset.append("finance_books")
|
||||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
number_of_pending_depreciations = final_number_of_depreciations - start
|
number_of_pending_depreciations = final_number_of_depreciations - start
|
||||||
yearly_opening_wdv = value_after_depreciation
|
yearly_opening_wdv = value_after_depreciation
|
||||||
current_fiscal_year_end_date = None
|
current_fiscal_year_end_date = None
|
||||||
|
prev_per_day_depr = True
|
||||||
for n in range(start, final_number_of_depreciations):
|
for n in range(start, final_number_of_depreciations):
|
||||||
# If depreciation is already completed (for double declining balance)
|
# If depreciation is already completed (for double declining balance)
|
||||||
if skip_row:
|
if skip_row:
|
||||||
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
||||||
else:
|
else:
|
||||||
prev_depreciation_amount = 0
|
prev_depreciation_amount = 0
|
||||||
|
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||||
depreciation_amount = get_depreciation_amount(
|
|
||||||
self,
|
self,
|
||||||
asset_doc,
|
asset_doc,
|
||||||
value_after_depreciation,
|
value_after_depreciation,
|
||||||
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
number_of_pending_depreciations,
|
number_of_pending_depreciations,
|
||||||
|
prev_per_day_depr,
|
||||||
)
|
)
|
||||||
if not has_pro_rata or (
|
if not has_pro_rata or (
|
||||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||||
@@ -599,11 +600,12 @@ def get_depreciation_amount(
|
|||||||
prev_depreciation_amount=0,
|
prev_depreciation_amount=0,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||||
number_of_pending_depreciations=0,
|
number_of_pending_depreciations=0,
|
||||||
|
prev_per_day_depr=0,
|
||||||
):
|
):
|
||||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||||
return get_straight_line_or_manual_depr_amount(
|
return get_straight_line_or_manual_depr_amount(
|
||||||
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||||
)
|
), None
|
||||||
else:
|
else:
|
||||||
return get_wdv_or_dd_depr_amount(
|
return get_wdv_or_dd_depr_amount(
|
||||||
asset,
|
asset,
|
||||||
@@ -614,6 +616,7 @@ def get_depreciation_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -637,49 +640,14 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||||
if row.daily_prorata_based:
|
if row.daily_prorata_based:
|
||||||
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||||
total_days = (
|
|
||||||
date_diff(
|
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date,
|
|
||||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
|
||||||
* row.frequency_of_depreciation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
add_days(
|
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date,
|
|
||||||
flt(
|
|
||||||
row.total_number_of_depreciations
|
|
||||||
- asset.number_of_depreciations_booked
|
|
||||||
- number_of_pending_depreciations
|
|
||||||
- 1
|
|
||||||
)
|
|
||||||
* row.frequency_of_depreciation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
+ 1
|
|
||||||
)
|
|
||||||
|
|
||||||
daily_depr_amount = amount / total_days
|
return get_daily_prorata_based_straight_line_depr(
|
||||||
|
asset,
|
||||||
to_date = get_last_day(
|
row,
|
||||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
schedule_idx,
|
||||||
|
number_of_pending_depreciations,
|
||||||
|
amount,
|
||||||
)
|
)
|
||||||
from_date = add_days(
|
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
|
||||||
)
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||||
@@ -692,40 +660,9 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
- flt(asset.opening_accumulated_depreciation)
|
- flt(asset.opening_accumulated_depreciation)
|
||||||
- flt(row.expected_value_after_useful_life)
|
- flt(row.expected_value_after_useful_life)
|
||||||
)
|
)
|
||||||
|
return get_daily_prorata_based_straight_line_depr(
|
||||||
total_days = (
|
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||||
date_diff(
|
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date,
|
|
||||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
|
||||||
* row.frequency_of_depreciation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
add_days(
|
|
||||||
get_last_day(
|
|
||||||
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
+ 1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
daily_depr_amount = amount / total_days
|
|
||||||
|
|
||||||
to_date = get_last_day(
|
|
||||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
|
||||||
)
|
|
||||||
from_date = add_days(
|
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
|
||||||
)
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
flt(asset.gross_purchase_amount)
|
flt(asset.gross_purchase_amount)
|
||||||
@@ -734,6 +671,23 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||||
|
|
||||||
|
|
||||||
|
def get_daily_prorata_based_straight_line_depr(
|
||||||
|
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||||
|
):
|
||||||
|
total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12
|
||||||
|
every_year_depr = amount / total_years
|
||||||
|
|
||||||
|
year_start_date = add_years(
|
||||||
|
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||||
|
)
|
||||||
|
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||||
|
daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
|
||||||
|
from_date, total_depreciable_days = _get_total_days(
|
||||||
|
row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
|
||||||
|
)
|
||||||
|
return daily_depr_amount * total_depreciable_days
|
||||||
|
|
||||||
|
|
||||||
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||||
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
||||||
return (
|
return (
|
||||||
@@ -779,6 +733,7 @@ def get_wdv_or_dd_depr_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
):
|
):
|
||||||
return get_default_wdv_or_dd_depr_amount(
|
return get_default_wdv_or_dd_depr_amount(
|
||||||
asset,
|
asset,
|
||||||
@@ -788,6 +743,7 @@ def get_wdv_or_dd_depr_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -799,6 +755,39 @@ def get_default_wdv_or_dd_depr_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
|
):
|
||||||
|
if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12:
|
||||||
|
return _get_default_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
schedule_idx,
|
||||||
|
prev_depreciation_amount,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
|
), None
|
||||||
|
else:
|
||||||
|
return _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
schedule_idx,
|
||||||
|
prev_depreciation_amount,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
schedule_idx,
|
||||||
|
prev_depreciation_amount,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
):
|
):
|
||||||
if cint(fb_row.frequency_of_depreciation) == 12:
|
if cint(fb_row.frequency_of_depreciation) == 12:
|
||||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
||||||
@@ -825,6 +814,75 @@ def get_default_wdv_or_dd_depr_amount(
|
|||||||
return prev_depreciation_amount
|
return prev_depreciation_amount
|
||||||
|
|
||||||
|
|
||||||
|
def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
schedule_idx,
|
||||||
|
prev_depreciation_amount,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
|
):
|
||||||
|
if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month
|
||||||
|
if schedule_idx == 0:
|
||||||
|
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100), None
|
||||||
|
|
||||||
|
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes
|
||||||
|
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
|
||||||
|
else:
|
||||||
|
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
|
||||||
|
else:
|
||||||
|
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes
|
||||||
|
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
|
||||||
|
else:
|
||||||
|
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
|
||||||
|
|
||||||
|
|
||||||
|
def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
|
||||||
|
""" "
|
||||||
|
Returns monthly depreciation amount when year changes
|
||||||
|
1. Calculate per day depr based on new year
|
||||||
|
2. Calculate monthly amount based on new per day amount
|
||||||
|
"""
|
||||||
|
from_date, days_in_month = _get_total_days(
|
||||||
|
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
|
||||||
|
)
|
||||||
|
per_day_depr = get_per_day_depr(fb_row, depreciable_value, from_date)
|
||||||
|
return (per_day_depr * days_in_month), per_day_depr
|
||||||
|
|
||||||
|
|
||||||
|
def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr):
|
||||||
|
""" "
|
||||||
|
Returns monthly depreciation amount based on prev per day depr
|
||||||
|
Calculate per day depr only for the first month
|
||||||
|
"""
|
||||||
|
from_date, days_in_month = _get_total_days(
|
||||||
|
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
|
||||||
|
)
|
||||||
|
return (prev_per_day_depr * days_in_month), prev_per_day_depr
|
||||||
|
|
||||||
|
|
||||||
|
def get_per_day_depr(
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
from_date,
|
||||||
|
):
|
||||||
|
to_date = add_days(add_years(from_date, 1), -1)
|
||||||
|
total_days = date_diff(to_date, from_date) + 1
|
||||||
|
per_day_depr = (flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days
|
||||||
|
return per_day_depr
|
||||||
|
|
||||||
|
|
||||||
|
def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation):
|
||||||
|
from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation)
|
||||||
|
to_date = add_months(from_date, frequency_of_depreciation)
|
||||||
|
if is_last_day_of_the_month(depreciation_start_date):
|
||||||
|
to_date = get_last_day(to_date)
|
||||||
|
from_date = add_days(get_last_day(from_date), 1)
|
||||||
|
return from_date, date_diff(to_date, from_date) + 1
|
||||||
|
|
||||||
|
|
||||||
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
||||||
asset_depr_schedules_names = []
|
asset_depr_schedules_names = []
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import cstr
|
||||||
|
|
||||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
get_asset_depr_schedule_doc,
|
get_asset_depr_schedule_doc,
|
||||||
|
get_depr_schedule,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,3 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
||||||
|
|
||||||
|
def test_daily_prorata_based_depr_on_sl_methond(self):
|
||||||
|
asset = create_asset(
|
||||||
|
calculate_depreciation=1,
|
||||||
|
depreciation_method="Straight Line",
|
||||||
|
daily_prorata_based=1,
|
||||||
|
available_for_use_date="2020-01-01",
|
||||||
|
depreciation_start_date="2020-01-31",
|
||||||
|
frequency_of_depreciation=1,
|
||||||
|
total_number_of_depreciations=24,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_schedules = [
|
||||||
|
["2020-01-31", 4234.97, 4234.97],
|
||||||
|
["2020-02-29", 3961.75, 8196.72],
|
||||||
|
["2020-03-31", 4234.97, 12431.69],
|
||||||
|
["2020-04-30", 4098.36, 16530.05],
|
||||||
|
["2020-05-31", 4234.97, 20765.02],
|
||||||
|
["2020-06-30", 4098.36, 24863.38],
|
||||||
|
["2020-07-31", 4234.97, 29098.35],
|
||||||
|
["2020-08-31", 4234.97, 33333.32],
|
||||||
|
["2020-09-30", 4098.36, 37431.68],
|
||||||
|
["2020-10-31", 4234.97, 41666.65],
|
||||||
|
["2020-11-30", 4098.36, 45765.01],
|
||||||
|
["2020-12-31", 4234.97, 49999.98],
|
||||||
|
["2021-01-31", 4246.58, 54246.56],
|
||||||
|
["2021-02-28", 3835.62, 58082.18],
|
||||||
|
["2021-03-31", 4246.58, 62328.76],
|
||||||
|
["2021-04-30", 4109.59, 66438.35],
|
||||||
|
["2021-05-31", 4246.58, 70684.93],
|
||||||
|
["2021-06-30", 4109.59, 74794.52],
|
||||||
|
["2021-07-31", 4246.58, 79041.1],
|
||||||
|
["2021-08-31", 4246.58, 83287.68],
|
||||||
|
["2021-09-30", 4109.59, 87397.27],
|
||||||
|
["2021-10-31", 4246.58, 91643.85],
|
||||||
|
["2021-11-30", 4109.59, 95753.44],
|
||||||
|
["2021-12-31", 4246.56, 100000.0],
|
||||||
|
]
|
||||||
|
|
||||||
|
schedules = [
|
||||||
|
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||||
|
for d in get_depr_schedule(asset.name, "Draft")
|
||||||
|
]
|
||||||
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
|
# Test for Written Down Value Method
|
||||||
|
# Frequency of deprciation = 3
|
||||||
|
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self):
|
||||||
|
asset = create_asset(
|
||||||
|
item_code="Macbook Pro",
|
||||||
|
calculate_depreciation=1,
|
||||||
|
depreciation_method="Written Down Value",
|
||||||
|
daily_prorata_based=1,
|
||||||
|
available_for_use_date="2021-02-20",
|
||||||
|
depreciation_start_date="2021-03-31",
|
||||||
|
frequency_of_depreciation=3,
|
||||||
|
total_number_of_depreciations=6,
|
||||||
|
rate_of_depreciation=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_schedules = [
|
||||||
|
["2021-03-31", 4383.56, 4383.56],
|
||||||
|
["2021-06-30", 9535.45, 13919.01],
|
||||||
|
["2021-09-30", 9640.23, 23559.24],
|
||||||
|
["2021-12-31", 9640.23, 33199.47],
|
||||||
|
["2022-03-31", 9430.66, 42630.13],
|
||||||
|
["2022-06-30", 5721.27, 48351.4],
|
||||||
|
["2022-08-20", 51648.6, 100000.0],
|
||||||
|
]
|
||||||
|
|
||||||
|
schedules = [
|
||||||
|
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||||
|
for d in get_depr_schedule(asset.name, "Draft")
|
||||||
|
]
|
||||||
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
|
# Frequency of deprciation = 6
|
||||||
|
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_6_months(self):
|
||||||
|
asset = create_asset(
|
||||||
|
item_code="Macbook Pro",
|
||||||
|
calculate_depreciation=1,
|
||||||
|
depreciation_method="Written Down Value",
|
||||||
|
daily_prorata_based=1,
|
||||||
|
available_for_use_date="2020-02-20",
|
||||||
|
depreciation_start_date="2020-02-29",
|
||||||
|
frequency_of_depreciation=6,
|
||||||
|
total_number_of_depreciations=6,
|
||||||
|
rate_of_depreciation=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_schedules = [
|
||||||
|
["2020-02-29", 1092.90, 1092.90],
|
||||||
|
["2020-08-31", 19944.01, 21036.91],
|
||||||
|
["2021-02-28", 19618.83, 40655.74],
|
||||||
|
["2021-08-31", 11966.4, 52622.14],
|
||||||
|
["2022-02-28", 11771.3, 64393.44],
|
||||||
|
["2022-08-31", 7179.84, 71573.28],
|
||||||
|
["2023-02-20", 28426.72, 100000.0],
|
||||||
|
]
|
||||||
|
|
||||||
|
schedules = [
|
||||||
|
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||||
|
for d in get_depr_schedule(asset.name, "Draft")
|
||||||
|
]
|
||||||
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
|
# Frequency of deprciation = 12
|
||||||
|
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_12_months(self):
|
||||||
|
asset = create_asset(
|
||||||
|
item_code="Macbook Pro",
|
||||||
|
calculate_depreciation=1,
|
||||||
|
depreciation_method="Written Down Value",
|
||||||
|
daily_prorata_based=1,
|
||||||
|
available_for_use_date="2020-02-20",
|
||||||
|
depreciation_start_date="2020-03-31",
|
||||||
|
frequency_of_depreciation=12,
|
||||||
|
total_number_of_depreciations=4,
|
||||||
|
rate_of_depreciation=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_schedules = [
|
||||||
|
["2020-03-31", 4480.87, 4480.87],
|
||||||
|
["2021-03-31", 38207.65, 42688.52],
|
||||||
|
["2022-03-31", 22924.59, 65613.11],
|
||||||
|
["2023-03-31", 13754.76, 79367.87],
|
||||||
|
["2024-02-20", 20632.13, 100000],
|
||||||
|
]
|
||||||
|
|
||||||
|
schedules = [
|
||||||
|
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||||
|
for d in get_depr_schedule(asset.name, "Draft")
|
||||||
|
]
|
||||||
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ def create_asset_repair(**args):
|
|||||||
"serial_nos": args.serial_no,
|
"serial_nos": args.serial_no,
|
||||||
"posting_date": today(),
|
"posting_date": today(),
|
||||||
"posting_time": nowtime(),
|
"posting_time": nowtime(),
|
||||||
|
"do_not_submit": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
).name
|
).name
|
||||||
|
|||||||
@@ -159,8 +159,9 @@ def prepare_chart_data(data, filters):
|
|||||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||||
filters_filter_based_on = "Date Range"
|
filters_filter_based_on = "Date Range"
|
||||||
date_field = "purchase_date"
|
date_field = "purchase_date"
|
||||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
filtered_data = [d for d in data if not d.get(date_field)]
|
||||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||||
|
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||||
else:
|
else:
|
||||||
filters_filter_based_on = filters.filter_based_on
|
filters_filter_based_on = filters.filter_based_on
|
||||||
date_field = frappe.scrub(filters.date_based_on)
|
date_field = frappe.scrub(filters.date_based_on)
|
||||||
|
|||||||
@@ -772,12 +772,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
else:
|
else:
|
||||||
account = frappe.db.get_value(
|
account = frappe.get_doc("Account", {"account_name": account_name, "company": company})
|
||||||
"Account",
|
|
||||||
filters={"account_name": account_name, "company": company},
|
|
||||||
fieldname="name",
|
|
||||||
pluck=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
@@ -808,22 +803,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
|
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||||
|
|
||||||
pi = make_purchase_invoice(po_doc.name)
|
|
||||||
pi.append(
|
|
||||||
"advances",
|
|
||||||
{
|
|
||||||
"reference_type": pe.doctype,
|
|
||||||
"reference_name": pe.name,
|
|
||||||
"reference_row": pe.references[0].name,
|
|
||||||
"advance_amount": 5000,
|
|
||||||
"allocated_amount": 5000,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
pi.save().submit()
|
|
||||||
pe.reload()
|
|
||||||
po_doc.reload()
|
|
||||||
self.assertEqual(po_doc.advance_paid, 0)
|
|
||||||
|
|
||||||
company_doc.book_advance_payments_in_separate_party_account = False
|
company_doc.book_advance_payments_in_separate_party_account = False
|
||||||
company_doc.save()
|
company_doc.save()
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contact_and_address_tab",
|
"fieldname": "contact_and_address_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact & Address"
|
"label": "Address & Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "accounting_tab",
|
"fieldname": "accounting_tab",
|
||||||
@@ -485,7 +485,7 @@
|
|||||||
"link_fieldname": "party"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:10:48.412732",
|
"modified": "2024-05-08 18:02:57.342931",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier",
|
"name": "Supplier",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ from frappe import _
|
|||||||
|
|
||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
"heatmap": True,
|
|
||||||
"heatmap_message": _(
|
|
||||||
"This is based on transactions against this Supplier. See timeline below for details"
|
|
||||||
),
|
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
|||||||
return row.supplier_name;
|
return row.supplier_name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
report.data.forEach((d) => {
|
||||||
|
if (!items.includes(d.item_code)) {
|
||||||
|
items.push(d.item_code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create a dialog window for the user to pick their supplier
|
// Create a dialog window for the user to pick their supplier
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Default Supplier"),
|
title: __("Select Default Supplier"),
|
||||||
@@ -151,20 +158,34 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
reqd: 1,
|
||||||
|
label: "Item",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
fieldname: "item_code",
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["in", items],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.set_primary_action(__("Set Default Supplier"), () => {
|
dialog.set_primary_action(__("Set Default Supplier"), () => {
|
||||||
let values = dialog.get_values();
|
let values = dialog.get_values();
|
||||||
|
|
||||||
if (values) {
|
if (values) {
|
||||||
// Set the default_supplier field of the appropriate Item to the selected supplier
|
// Set the default_supplier field of the appropriate Item to the selected supplier
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "frappe.client.set_value",
|
method: "erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison.set_default_supplier",
|
||||||
args: {
|
args: {
|
||||||
doctype: "Item",
|
item_code: values.item_code,
|
||||||
name: item_code,
|
supplier: values.supplier,
|
||||||
fieldname: "default_supplier",
|
company: filters.company,
|
||||||
value: values.supplier,
|
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
|
|||||||
@@ -292,3 +292,13 @@ def get_message():
|
|||||||
<span class="indicator red">
|
<span class="indicator red">
|
||||||
Expires today / Already Expired
|
Expires today / Already Expired
|
||||||
</span>"""
|
</span>"""
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def set_default_supplier(item_code, supplier, company):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Item Default",
|
||||||
|
{"parent": item_code, "company": company},
|
||||||
|
"default_supplier",
|
||||||
|
supplier,
|
||||||
|
)
|
||||||
|
|||||||
@@ -778,6 +778,9 @@ class AccountsController(TransactionBase):
|
|||||||
# reset pricing rule fields if pricing_rule_removed
|
# reset pricing rule fields if pricing_rule_removed
|
||||||
item.set(fieldname, value)
|
item.set(fieldname, value)
|
||||||
|
|
||||||
|
elif fieldname == "expense_account" and not item.get("expense_account"):
|
||||||
|
item.expense_account = value
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
||||||
"is_fixed_asset"
|
"is_fixed_asset"
|
||||||
):
|
):
|
||||||
@@ -1439,7 +1442,8 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||||
|
|
||||||
if d.reference_doctype == "Purchase Invoice":
|
# Inverse debit/credit for payable accounts
|
||||||
|
if self.is_payable_account(d.reference_doctype, party_account):
|
||||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
@@ -1473,6 +1477,14 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_payable_account(self, reference_doctype, account):
|
||||||
|
if reference_doctype == "Purchase Invoice" or (
|
||||||
|
reference_doctype == "Journal Entry"
|
||||||
|
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def update_against_document_in_jv(self):
|
def update_against_document_in_jv(self):
|
||||||
"""
|
"""
|
||||||
Links invoice and advance voucher:
|
Links invoice and advance voucher:
|
||||||
|
|||||||
@@ -712,6 +712,7 @@ class BuyingController(SubcontractingController):
|
|||||||
def auto_make_assets(self, asset_items):
|
def auto_make_assets(self, asset_items):
|
||||||
items_data = get_asset_item_details(asset_items)
|
items_data = get_asset_item_details(asset_items)
|
||||||
messages = []
|
messages = []
|
||||||
|
alert = False
|
||||||
|
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.is_fixed_asset:
|
if d.is_fixed_asset:
|
||||||
@@ -761,9 +762,10 @@ class BuyingController(SubcontractingController):
|
|||||||
frappe.bold(d.item_code)
|
frappe.bold(d.item_code)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
alert = True
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
frappe.msgprint(message, title="Success", indicator="green")
|
frappe.msgprint(message, title="Success", indicator="green", alert=alert)
|
||||||
|
|
||||||
def make_asset(self, row, is_grouped_asset=False):
|
def make_asset(self, row, is_grouped_asset=False):
|
||||||
if not row.asset_location:
|
if not row.asset_location:
|
||||||
@@ -787,7 +789,7 @@ class BuyingController(SubcontractingController):
|
|||||||
"supplier": self.supplier,
|
"supplier": self.supplier,
|
||||||
"purchase_date": self.posting_date,
|
"purchase_date": self.posting_date,
|
||||||
"calculate_depreciation": 0,
|
"calculate_depreciation": 0,
|
||||||
"purchase_receipt_amount": purchase_amount,
|
"purchase_amount": purchase_amount,
|
||||||
"gross_purchase_amount": purchase_amount,
|
"gross_purchase_amount": purchase_amount,
|
||||||
"asset_quantity": asset_quantity,
|
"asset_quantity": asset_quantity,
|
||||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ class SellingController(StockController):
|
|||||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||||
):
|
):
|
||||||
# Get incoming rate based on original item cost based on valuation method
|
# Get incoming rate based on original item cost based on valuation method
|
||||||
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not d.incoming_rate
|
not d.incoming_rate
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class StockController(AccountsController):
|
|||||||
# remove extra whitespace and store one serial no on each line
|
# remove extra whitespace and store one serial no on each line
|
||||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||||
|
|
||||||
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
|
def make_bundle_using_old_serial_batch_fields(self, table_name=None, via_landed_cost_voucher=False):
|
||||||
if self.get("_action") == "update_after_submit":
|
if self.get("_action") == "update_after_submit":
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ class StockController(AccountsController):
|
|||||||
"company": self.company,
|
"company": self.company,
|
||||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||||
"do_not_submit": True,
|
"do_not_submit": True if not via_landed_cost_voucher else False,
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.get("qty") or row.get("consumed_qty"):
|
if row.get("qty") or row.get("consumed_qty"):
|
||||||
@@ -1119,7 +1119,7 @@ class StockController(AccountsController):
|
|||||||
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def repost_future_sle_and_gle(self, force=False):
|
def repost_future_sle_and_gle(self, force=False, via_landed_cost_voucher=False):
|
||||||
args = frappe._dict(
|
args = frappe._dict(
|
||||||
{
|
{
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
@@ -1127,6 +1127,7 @@ class StockController(AccountsController):
|
|||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
"voucher_no": self.name,
|
"voucher_no": self.name,
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
|
"via_landed_cost_voucher": via_landed_cost_voucher,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1138,7 +1139,11 @@ class StockController(AccountsController):
|
|||||||
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
||||||
)
|
)
|
||||||
if item_based_reposting:
|
if item_based_reposting:
|
||||||
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
create_item_wise_repost_entries(
|
||||||
|
voucher_type=self.doctype,
|
||||||
|
voucher_no=self.name,
|
||||||
|
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
create_repost_item_valuation_entry(args)
|
create_repost_item_valuation_entry(args)
|
||||||
|
|
||||||
@@ -1222,8 +1227,8 @@ def get_accounting_ledger_preview(doc, filters):
|
|||||||
"debit",
|
"debit",
|
||||||
"credit",
|
"credit",
|
||||||
"against",
|
"against",
|
||||||
"party",
|
|
||||||
"party_type",
|
"party_type",
|
||||||
|
"party",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"against_voucher_type",
|
"against_voucher_type",
|
||||||
"against_voucher",
|
"against_voucher",
|
||||||
@@ -1356,7 +1361,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_quality_inspections(doctype, docname, items):
|
def make_quality_inspections(doctype, docname, items, inspection_type):
|
||||||
if isinstance(items, str):
|
if isinstance(items, str):
|
||||||
items = json.loads(items)
|
items = json.loads(items)
|
||||||
|
|
||||||
@@ -1376,7 +1381,7 @@ def make_quality_inspections(doctype, docname, items):
|
|||||||
quality_inspection = frappe.get_doc(
|
quality_inspection = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Quality Inspection",
|
"doctype": "Quality Inspection",
|
||||||
"inspection_type": "Incoming",
|
"inspection_type": inspection_type,
|
||||||
"inspected_by": frappe.session.user,
|
"inspected_by": frappe.session.user,
|
||||||
"reference_type": doctype,
|
"reference_type": doctype,
|
||||||
"reference_name": docname,
|
"reference_name": docname,
|
||||||
@@ -1399,7 +1404,12 @@ def is_reposting_pending():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def future_sle_exists(args, sl_entries=None):
|
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||||
|
if allow_force_reposting and frappe.db.get_single_value(
|
||||||
|
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
key = (args.voucher_type, args.voucher_no)
|
key = (args.voucher_type, args.voucher_no)
|
||||||
if not hasattr(frappe.local, "future_sle"):
|
if not hasattr(frappe.local, "future_sle"):
|
||||||
frappe.local.future_sle = {}
|
frappe.local.future_sle = {}
|
||||||
@@ -1510,11 +1520,14 @@ def create_repost_item_valuation_entry(args):
|
|||||||
repost_entry.allow_zero_rate = args.allow_zero_rate
|
repost_entry.allow_zero_rate = args.allow_zero_rate
|
||||||
repost_entry.flags.ignore_links = True
|
repost_entry.flags.ignore_links = True
|
||||||
repost_entry.flags.ignore_permissions = True
|
repost_entry.flags.ignore_permissions = True
|
||||||
|
repost_entry.via_landed_cost_voucher = args.via_landed_cost_voucher
|
||||||
repost_entry.save()
|
repost_entry.save()
|
||||||
repost_entry.submit()
|
repost_entry.submit()
|
||||||
|
|
||||||
|
|
||||||
def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False):
|
def create_item_wise_repost_entries(
|
||||||
|
voucher_type, voucher_no, allow_zero_rate=False, via_landed_cost_voucher=False
|
||||||
|
):
|
||||||
"""Using a voucher create repost item valuation records for all item-warehouse pairs."""
|
"""Using a voucher create repost item valuation records for all item-warehouse pairs."""
|
||||||
|
|
||||||
stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
|
stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
|
||||||
@@ -1538,6 +1551,7 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
|
|||||||
repost_entry.allow_zero_rate = allow_zero_rate
|
repost_entry.allow_zero_rate = allow_zero_rate
|
||||||
repost_entry.flags.ignore_links = True
|
repost_entry.flags.ignore_links = True
|
||||||
repost_entry.flags.ignore_permissions = True
|
repost_entry.flags.ignore_permissions = True
|
||||||
|
repost_entry.via_landed_cost_voucher = via_landed_cost_voucher
|
||||||
repost_entry.submit()
|
repost_entry.submit()
|
||||||
repost_entries.append(repost_entry)
|
repost_entries.append(repost_entry)
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,27 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
acc = frappe.get_doc("Account", name)
|
acc = frappe.get_doc("Account", name)
|
||||||
self.debtors_usd = acc.name
|
self.debtors_usd = acc.name
|
||||||
|
|
||||||
|
account_name = "Creditors 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 Payable - " + self.company_abbr
|
||||||
|
acc.company = self.company
|
||||||
|
acc.account_currency = "USD"
|
||||||
|
acc.account_type = "Payable"
|
||||||
|
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.creditors_usd = acc.name
|
||||||
|
|
||||||
def create_sales_invoice(
|
def create_sales_invoice(
|
||||||
self,
|
self,
|
||||||
qty=1,
|
qty=1,
|
||||||
@@ -174,7 +195,9 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
return sinv
|
return sinv
|
||||||
|
|
||||||
def create_payment_entry(self, amount=1, source_exc_rate=75, posting_date=None, customer=None):
|
def create_payment_entry(
|
||||||
|
self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Helper function to populate default values in payment entry
|
Helper function to populate default values in payment entry
|
||||||
"""
|
"""
|
||||||
@@ -1606,3 +1629,72 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
||||||
self.assertEqual(exc_je_for_je1, [])
|
self.assertEqual(exc_je_for_je1, [])
|
||||||
self.assertEqual(exc_je_for_je2, [])
|
self.assertEqual(exc_je_for_je2, [])
|
||||||
|
|
||||||
|
def test_61_payment_entry_against_journal_for_payable_accounts(self):
|
||||||
|
# Invoices
|
||||||
|
exc_rate1 = 75
|
||||||
|
exc_rate2 = 77
|
||||||
|
amount = 1
|
||||||
|
je1 = self.create_journal_entry(
|
||||||
|
acc1=self.creditors_usd,
|
||||||
|
acc1_exc_rate=exc_rate1,
|
||||||
|
acc2=self.cash,
|
||||||
|
acc1_amount=-amount,
|
||||||
|
acc2_amount=(-amount * 75),
|
||||||
|
acc2_exc_rate=1,
|
||||||
|
)
|
||||||
|
je1.accounts[0].party_type = "Supplier"
|
||||||
|
je1.accounts[0].party = self.supplier
|
||||||
|
je1 = je1.save().submit()
|
||||||
|
|
||||||
|
# Payment
|
||||||
|
pe = create_payment_entry(
|
||||||
|
company=self.company,
|
||||||
|
payment_type="Pay",
|
||||||
|
party_type="Supplier",
|
||||||
|
party=self.supplier,
|
||||||
|
paid_from=self.cash,
|
||||||
|
paid_to=self.creditors_usd,
|
||||||
|
paid_amount=amount,
|
||||||
|
)
|
||||||
|
pe.target_exchange_rate = exc_rate2
|
||||||
|
pe.received_amount = amount
|
||||||
|
pe.paid_amount = amount * exc_rate2
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
pr = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Payment Reconciliation",
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Supplier",
|
||||||
|
"party": self.supplier,
|
||||||
|
"receivable_payable_account": get_party_account("Supplier", self.supplier, self.company),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
self.assertEqual(len(pr.invoices), 0)
|
||||||
|
self.assertEqual(len(pr.payments), 0)
|
||||||
|
|
||||||
|
# There should be no outstanding in both currencies
|
||||||
|
self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been created
|
||||||
|
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||||
|
self.assertEqual(len(exc_je_for_je1), 1)
|
||||||
|
|
||||||
|
# Cancel Payment
|
||||||
|
pe.reload()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been cancelled
|
||||||
|
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||||
|
self.assertEqual(exc_je_for_je1, [])
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ def send_mail(entry, email_campaign):
|
|||||||
doctype="Email Campaign",
|
doctype="Email Campaign",
|
||||||
name=email_campaign.name,
|
name=email_campaign.name,
|
||||||
subject=frappe.render_template(email_template.get("subject"), context),
|
subject=frappe.render_template(email_template.get("subject"), context),
|
||||||
content=frappe.render_template(email_template.get("response"), context),
|
content=frappe.render_template(email_template.response_, context),
|
||||||
sender=sender,
|
sender=sender,
|
||||||
recipients=recipient_list,
|
recipients=recipient_list,
|
||||||
communication_medium="Email",
|
communication_medium="Email",
|
||||||
|
|||||||
@@ -534,6 +534,7 @@ accounting_dimension_doctypes = [
|
|||||||
"Supplier Quotation Item",
|
"Supplier Quotation Item",
|
||||||
"Payment Reconciliation",
|
"Payment Reconciliation",
|
||||||
"Payment Reconciliation Allocation",
|
"Payment Reconciliation Allocation",
|
||||||
|
"Payment Request",
|
||||||
]
|
]
|
||||||
|
|
||||||
get_matching_queries = (
|
get_matching_queries = (
|
||||||
|
|||||||
2103
erpnext/locale/ar.po
2103
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
4774
erpnext/locale/bs.po
4774
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2525
erpnext/locale/de.po
2525
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2103
erpnext/locale/eo.po
2103
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2611
erpnext/locale/es.po
2611
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3419
erpnext/locale/fa.po
3419
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2103
erpnext/locale/fr.po
2103
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
8768
erpnext/locale/tr.po
8768
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -1896,7 +1896,7 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
|
|||||||
query = query.where(so_table.name.isin(filters.get("sales_orders")))
|
query = query.where(so_table.name.isin(filters.get("sales_orders")))
|
||||||
|
|
||||||
if txt:
|
if txt:
|
||||||
query = query.where(table.item_code.like(f"{txt}%"))
|
query = query.where(table.parent.like(f"%{txt}%"))
|
||||||
|
|
||||||
if page_len:
|
if page_len:
|
||||||
query = query.limit(page_len)
|
query = query.limit(page_len)
|
||||||
|
|||||||
@@ -948,6 +948,21 @@ class WorkOrder(Document):
|
|||||||
if self.qty <= 0:
|
if self.qty <= 0:
|
||||||
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.stock_uom
|
||||||
|
and frappe.get_cached_value("UOM", self.stock_uom, "must_be_whole_number")
|
||||||
|
and abs(cint(self.qty) - flt(self.qty, self.precision("qty"))) > 0.0000001
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}."
|
||||||
|
).format(
|
||||||
|
flt(self.qty, self.precision("qty")),
|
||||||
|
frappe.bold(_("Must be Whole Number")),
|
||||||
|
frappe.bold(self.stock_uom),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
|
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
|
||||||
qty_dict = frappe.db.get_value(
|
qty_dict = frappe.db.get_value(
|
||||||
"Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1
|
"Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1
|
||||||
|
|||||||
@@ -93,4 +93,11 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
formatter: function (value, row, column, data, default_formatter) {
|
||||||
|
value = default_formatter(value, row, column, data);
|
||||||
|
if (column.fieldname === "item_code" && value.includes("Total Quantity")) {
|
||||||
|
value = "<strong>" + value + "</strong>";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
|||||||
if not self.data:
|
if not self.data:
|
||||||
return
|
return
|
||||||
|
|
||||||
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
|
total_row = {"item_code": _("Total Quantity")}
|
||||||
|
|
||||||
for value in self.data:
|
for value in self.data:
|
||||||
for period in self.period_list:
|
for period in self.period_list:
|
||||||
|
|||||||
@@ -357,9 +357,12 @@ erpnext.patches.v15_0.create_advance_payment_status
|
|||||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||||
|
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||||
# below migration patch should always run last
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||||
|
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||||
|
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
|
create_accounting_dimensions_for_doctype,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
create_accounting_dimensions_for_doctype(doctype="Payment Request")
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
# update debit and credit in transaction currency:
|
||||||
|
# if transaction currency is same as account currency,
|
||||||
|
# then debit and credit in transaction currency is same as debit and credit in account currency
|
||||||
|
# else debit and credit divided by exchange rate
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
|
frappe.db.sql(
|
||||||
|
"""
|
||||||
|
UPDATE `tabGL Entry`
|
||||||
|
SET
|
||||||
|
debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate),
|
||||||
|
credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate)
|
||||||
|
WHERE
|
||||||
|
transaction_exchange_rate > 0
|
||||||
|
and transaction_currency is not null
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("assets", "doctype", "asset")
|
||||||
|
if frappe.db.has_column("Asset", "purchase_receipt_amount"):
|
||||||
|
rename_field("Asset", "purchase_receipt_amount", "purchase_amount")
|
||||||
@@ -454,7 +454,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 4,
|
"max_attachments": 4,
|
||||||
"modified": "2024-03-27 13:10:21.057163",
|
"modified": "2024-04-24 10:56:16.001032",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Project",
|
"name": "Project",
|
||||||
@@ -489,6 +489,15 @@
|
|||||||
"role": "Projects Manager",
|
"role": "Projects Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Employee",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
const me = this;
|
const me = this;
|
||||||
if (!this.frm.is_new() && this.frm.doc.docstatus === 0) {
|
if (!this.frm.is_new() && this.frm.doc.docstatus === 0 && frappe.model.can_create("Quality Inspection")) {
|
||||||
this.frm.add_custom_button(__("Quality Inspection(s)"), () => {
|
this.frm.add_custom_button(__("Quality Inspection(s)"), () => {
|
||||||
me.make_quality_inspection();
|
me.make_quality_inspection();
|
||||||
}, __("Create"));
|
}, __("Create"));
|
||||||
@@ -2203,6 +2203,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
];
|
];
|
||||||
|
|
||||||
const me = this;
|
const me = this;
|
||||||
|
const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)
|
||||||
|
? "Incoming" : "Outgoing";
|
||||||
const dialog = new frappe.ui.Dialog({
|
const dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Items for Quality Inspection"),
|
title: __("Select Items for Quality Inspection"),
|
||||||
size: "extra-large",
|
size: "extra-large",
|
||||||
@@ -2214,7 +2216,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
args: {
|
args: {
|
||||||
doctype: me.frm.doc.doctype,
|
doctype: me.frm.doc.doctype,
|
||||||
docname: me.frm.doc.name,
|
docname: me.frm.doc.name,
|
||||||
items: data.items
|
items: data.items,
|
||||||
|
inspection_type: inspection_type
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
|
|||||||
@@ -51,38 +51,7 @@ $.extend(erpnext, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setup_serial_or_batch_no: function () {
|
setup_serial_or_batch_no: function () {
|
||||||
let grid_row = cur_frm.open_grid_row();
|
// Deprecated in v15
|
||||||
if (
|
|
||||||
!grid_row ||
|
|
||||||
!grid_row.grid_form.fields_dict.serial_no ||
|
|
||||||
grid_row.grid_form.fields_dict.serial_no.get_status() !== "Write"
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
frappe.model.get_value(
|
|
||||||
"Item",
|
|
||||||
{ name: grid_row.doc.item_code },
|
|
||||||
["has_serial_no", "has_batch_no"],
|
|
||||||
({ has_serial_no, has_batch_no }) => {
|
|
||||||
Object.assign(grid_row.doc, { has_serial_no, has_batch_no });
|
|
||||||
|
|
||||||
if (has_serial_no) {
|
|
||||||
attach_selector_button(
|
|
||||||
__("Add Serial No"),
|
|
||||||
grid_row.grid_form.fields_dict.serial_no.$wrapper,
|
|
||||||
this,
|
|
||||||
grid_row
|
|
||||||
);
|
|
||||||
} else if (has_batch_no) {
|
|
||||||
attach_selector_button(
|
|
||||||
__("Pick Batch No"),
|
|
||||||
grid_row.grid_form.fields_dict.batch_no.$wrapper,
|
|
||||||
this,
|
|
||||||
grid_row
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
route_to_adjustment_jv: (args) => {
|
route_to_adjustment_jv: (args) => {
|
||||||
@@ -938,11 +907,14 @@ erpnext.utils.map_current_doc = function (opts) {
|
|||||||
if (opts.source_doctype) {
|
if (opts.source_doctype) {
|
||||||
let data_fields = [];
|
let data_fields = [];
|
||||||
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
||||||
data_fields.push({
|
let target_meta = frappe.get_meta(cur_frm.doc.doctype);
|
||||||
fieldname: "merge_taxes",
|
if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
|
||||||
fieldtype: "Check",
|
data_fields.push({
|
||||||
label: __("Merge taxes from multiple documents"),
|
fieldname: "merge_taxes",
|
||||||
});
|
fieldtype: "Check",
|
||||||
|
label: __("Merge taxes from multiple documents"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const d = new frappe.ui.form.MultiSelectDialog({
|
const d = new frappe.ui.form.MultiSelectDialog({
|
||||||
doctype: opts.source_doctype,
|
doctype: opts.source_doctype,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ erpnext.accounts.dimensions = {
|
|||||||
});
|
});
|
||||||
me.default_dimensions = r.message[1];
|
me.default_dimensions = r.message[1];
|
||||||
me.setup_filters(frm, doctype);
|
me.setup_filters(frm, doctype);
|
||||||
|
me.update_dimension(frm, doctype);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ erpnext.sales_common = {
|
|||||||
frappe.model.set_value(item.doctype, item.name, {
|
frappe.model.set_value(item.doctype, item.name, {
|
||||||
serial_and_batch_bundle: r.name,
|
serial_and_batch_bundle: r.name,
|
||||||
use_serial_batch_fields: 0,
|
use_serial_batch_fields: 0,
|
||||||
|
incoming_rate: r.avg_rate,
|
||||||
qty:
|
qty:
|
||||||
qty /
|
qty /
|
||||||
flt(
|
flt(
|
||||||
|
|||||||
@@ -135,13 +135,50 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:02.865812",
|
"modified": "2024-04-18 15:25:25.808355",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Regional",
|
"module": "Regional",
|
||||||
"name": "Lower Deduction Certificate",
|
"name": "Lower Deduction Certificate",
|
||||||
"naming_rule": "By fieldname",
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -482,7 +482,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contact_and_address_tab",
|
"fieldname": "contact_and_address_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact & Address"
|
"label": "Address & Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "defaults_tab",
|
"fieldname": "defaults_tab",
|
||||||
@@ -583,7 +583,7 @@
|
|||||||
"link_fieldname": "party"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:06:48.056107",
|
"modified": "2024-05-08 18:03:20.716169",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Customer",
|
"name": "Customer",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ from frappe import _
|
|||||||
|
|
||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
"heatmap": True,
|
|
||||||
"heatmap_message": _(
|
|
||||||
"This is based on transactions against this Customer. See timeline below for details"
|
|
||||||
),
|
|
||||||
"fieldname": "customer",
|
"fieldname": "customer",
|
||||||
"non_standard_fieldnames": {
|
"non_standard_fieldnames": {
|
||||||
"Payment Entry": "party",
|
"Payment Entry": "party",
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ frappe.ui.form.on("Quotation", {
|
|||||||
frm.trigger("set_label");
|
frm.trigger("set_label");
|
||||||
frm.trigger("toggle_reqd_lead_customer");
|
frm.trigger("toggle_reqd_lead_customer");
|
||||||
frm.trigger("set_dynamic_field_label");
|
frm.trigger("set_dynamic_field_label");
|
||||||
|
frm.set_value("party_name", "");
|
||||||
|
frm.set_value("customer_name", "");
|
||||||
},
|
},
|
||||||
|
|
||||||
set_label: function (frm) {
|
set_label: function (frm) {
|
||||||
@@ -97,7 +99,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
frappe.dynamic_link = {
|
frappe.dynamic_link = {
|
||||||
doc: this.frm.doc,
|
doc: this.frm.doc,
|
||||||
fieldname: "party_name",
|
fieldname: "party_name",
|
||||||
doctype: doc.quotation_to == "Customer" ? "Customer" : "Lead",
|
doctype: doc.quotation_to,
|
||||||
};
|
};
|
||||||
|
|
||||||
var me = this;
|
var me = this;
|
||||||
@@ -197,6 +199,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
};
|
};
|
||||||
} else if (this.frm.doc.quotation_to == "Prospect") {
|
} else if (this.frm.doc.quotation_to == "Prospect") {
|
||||||
this.frm.set_df_property("party_name", "label", "Prospect");
|
this.frm.set_df_property("party_name", "label", "Prospect");
|
||||||
|
this.frm.fields_dict.party_name.get_query = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
target.flags.ignore_permissions = ignore_permissions
|
target.flags.ignore_permissions = ignore_permissions
|
||||||
target.delivery_date = nowdate()
|
|
||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class TestQuotation(FrappeTestCase):
|
|||||||
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
|
||||||
def test_make_sales_order_with_terms(self):
|
def test_make_sales_order_with_terms(self):
|
||||||
@@ -164,6 +165,7 @@ class TestQuotation(FrappeTestCase):
|
|||||||
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
|
||||||
# Remove any unknown taxes if applied
|
# Remove any unknown taxes if applied
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
if (
|
if (
|
||||||
frm.doc.status !== "Closed" &&
|
frm.doc.status !== "Closed" &&
|
||||||
flt(frm.doc.per_delivered, 2) < 100 &&
|
flt(frm.doc.per_delivered, 2) < 100 &&
|
||||||
flt(frm.doc.per_billed, 2) < 100
|
flt(frm.doc.per_billed, 2) < 100 &&
|
||||||
|
frm.has_perm("write")
|
||||||
) {
|
) {
|
||||||
frm.add_custom_button(__("Update Items"), () => {
|
frm.add_custom_button(__("Update Items"), () => {
|
||||||
erpnext.utils.update_child_items({
|
erpnext.utils.update_child_items({
|
||||||
@@ -74,7 +75,8 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
if (
|
if (
|
||||||
frm.doc.__onload &&
|
frm.doc.__onload &&
|
||||||
frm.doc.__onload.has_unreserved_stock &&
|
frm.doc.__onload.has_unreserved_stock &&
|
||||||
flt(frm.doc.per_picked) === 0
|
flt(frm.doc.per_picked) === 0 &&
|
||||||
|
frappe.model.can_create("Stock Reservation Entry")
|
||||||
) {
|
) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Reserve"),
|
__("Reserve"),
|
||||||
@@ -85,7 +87,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stock Reservation > Unreserve button will be only visible if the SO has un-delivered reserved stock.
|
// Stock Reservation > Unreserve button will be only visible if the SO has un-delivered reserved stock.
|
||||||
if (frm.doc.__onload && frm.doc.__onload.has_reserved_stock) {
|
if (
|
||||||
|
frm.doc.__onload &&
|
||||||
|
frm.doc.__onload.has_reserved_stock &&
|
||||||
|
frappe.model.can_cancel("Stock Reservation Entry")
|
||||||
|
) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Unreserve"),
|
__("Unreserve"),
|
||||||
() => frm.events.cancel_stock_reservation_entries(frm),
|
() => frm.events.cancel_stock_reservation_entries(frm),
|
||||||
@@ -94,7 +100,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frm.doc.items.forEach((item) => {
|
frm.doc.items.forEach((item) => {
|
||||||
if (flt(item.stock_reserved_qty) > 0) {
|
if (flt(item.stock_reserved_qty) > 0 && frappe.model.can_read("Stock Reservation Entry")) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Reserved Stock"),
|
__("Reserved Stock"),
|
||||||
() => frm.events.show_reserved_stock(frm),
|
() => frm.events.show_reserved_stock(frm),
|
||||||
@@ -142,6 +148,10 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_items_from_internal_purchase_order(frm) {
|
get_items_from_internal_purchase_order(frm) {
|
||||||
|
if (!frappe.model.can_read("Purchase Order")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Purchase Order"),
|
__("Purchase Order"),
|
||||||
() => {
|
() => {
|
||||||
@@ -169,6 +179,27 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// When multiple companies are set up. in case company name is changed set default company address
|
||||||
|
company: function (frm) {
|
||||||
|
if (frm.doc.company) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||||
|
args: {
|
||||||
|
name: frm.doc.company,
|
||||||
|
existing_address: frm.doc.company_address || "",
|
||||||
|
},
|
||||||
|
debounce: 2000,
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value("company_address", r.message);
|
||||||
|
} else {
|
||||||
|
frm.set_value("company_address", "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
if (!frm.doc.transaction_date) {
|
if (!frm.doc.transaction_date) {
|
||||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||||
@@ -288,6 +319,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
label: __("Items to Reserve"),
|
label: __("Items to Reserve"),
|
||||||
allow_bulk_edit: false,
|
allow_bulk_edit: false,
|
||||||
cannot_add_rows: true,
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
data: [],
|
data: [],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -356,7 +388,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
],
|
],
|
||||||
primary_action_label: __("Reserve Stock"),
|
primary_action_label: __("Reserve Stock"),
|
||||||
primary_action: () => {
|
primary_action: () => {
|
||||||
var data = { items: dialog.fields_dict.items.grid.data };
|
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
|
||||||
|
|
||||||
if (data.items && data.items.length > 0) {
|
if (data.items && data.items.length > 0) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -373,9 +405,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Please select items to reserve."));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,6 +424,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
|
|
||||||
if (unreserved_qty > 0) {
|
if (unreserved_qty > 0) {
|
||||||
dialog.fields_dict.items.df.data.push({
|
dialog.fields_dict.items.df.data.push({
|
||||||
|
__checked: 1,
|
||||||
sales_order_item: item.name,
|
sales_order_item: item.name,
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
warehouse: item.warehouse,
|
warehouse: item.warehouse,
|
||||||
@@ -414,6 +449,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
label: __("Reserved Stock"),
|
label: __("Reserved Stock"),
|
||||||
allow_bulk_edit: false,
|
allow_bulk_edit: false,
|
||||||
cannot_add_rows: true,
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
in_place_edit: true,
|
in_place_edit: true,
|
||||||
data: [],
|
data: [],
|
||||||
fields: [
|
fields: [
|
||||||
@@ -457,7 +493,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
],
|
],
|
||||||
primary_action_label: __("Unreserve Stock"),
|
primary_action_label: __("Unreserve Stock"),
|
||||||
primary_action: () => {
|
primary_action: () => {
|
||||||
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.data };
|
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
|
||||||
|
|
||||||
if (data.sr_entries && data.sr_entries.length > 0) {
|
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -473,9 +509,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Please select items to unreserve."));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -602,15 +640,17 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doc.__onload || !doc.__onload.has_reserved_stock) {
|
if (
|
||||||
// Don't show the `Reserve` button if the Sales Order has Picked Items.
|
(!doc.__onload || !doc.__onload.has_reserved_stock) &&
|
||||||
if (flt(doc.per_picked, 2) < 100 && flt(doc.per_delivered, 2) < 100) {
|
flt(doc.per_picked, 2) < 100 &&
|
||||||
this.frm.add_custom_button(
|
flt(doc.per_delivered, 2) < 100 &&
|
||||||
__("Pick List"),
|
frappe.model.can_create("Pick List")
|
||||||
() => this.create_pick_list(),
|
) {
|
||||||
__("Create")
|
this.frm.add_custom_button(
|
||||||
);
|
__("Pick List"),
|
||||||
}
|
() => this.create_pick_list(),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
|
const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
|
||||||
@@ -625,20 +665,25 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
(order_is_a_sale || order_is_a_custom_sale) &&
|
(order_is_a_sale || order_is_a_custom_sale) &&
|
||||||
allow_delivery
|
allow_delivery
|
||||||
) {
|
) {
|
||||||
this.frm.add_custom_button(
|
if (frappe.model.can_create("Delivery Note")) {
|
||||||
__("Delivery Note"),
|
this.frm.add_custom_button(
|
||||||
() => this.make_delivery_note_based_on_delivery_date(true),
|
__("Delivery Note"),
|
||||||
__("Create")
|
() => this.make_delivery_note_based_on_delivery_date(true),
|
||||||
);
|
__("Create")
|
||||||
this.frm.add_custom_button(
|
);
|
||||||
__("Work Order"),
|
}
|
||||||
() => this.make_work_order(),
|
|
||||||
__("Create")
|
if (frappe.model.can_create("Work Order")) {
|
||||||
);
|
this.frm.add_custom_button(
|
||||||
|
__("Work Order"),
|
||||||
|
() => this.make_work_order(),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sales invoice
|
// sales invoice
|
||||||
if (flt(doc.per_billed, 2) < 100) {
|
if (flt(doc.per_billed, 2) < 100 && frappe.model.can_create("Sales Invoice")) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Sales Invoice"),
|
__("Sales Invoice"),
|
||||||
() => me.make_sales_invoice(),
|
() => me.make_sales_invoice(),
|
||||||
@@ -648,8 +693,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
|
|
||||||
// material request
|
// material request
|
||||||
if (
|
if (
|
||||||
!doc.order_type ||
|
(!doc.order_type ||
|
||||||
((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 2) < 100)
|
((order_is_a_sale || order_is_a_custom_sale) &&
|
||||||
|
flt(doc.per_delivered, 2) < 100)) &&
|
||||||
|
frappe.model.can_create("Material Request")
|
||||||
) {
|
) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Material Request"),
|
__("Material Request"),
|
||||||
@@ -664,7 +711,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make Purchase Order
|
// Make Purchase Order
|
||||||
if (!this.frm.doc.is_internal_customer) {
|
if (!this.frm.doc.is_internal_customer && frappe.model.can_create("Purchase Order")) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Purchase Order"),
|
__("Purchase Order"),
|
||||||
() => this.make_purchase_order(),
|
() => this.make_purchase_order(),
|
||||||
@@ -674,24 +721,32 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
|
|
||||||
// maintenance
|
// maintenance
|
||||||
if (flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
|
if (flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
|
||||||
this.frm.add_custom_button(
|
if (frappe.model.can_create("Maintenance Visit")) {
|
||||||
__("Maintenance Visit"),
|
this.frm.add_custom_button(
|
||||||
() => this.make_maintenance_visit(),
|
__("Maintenance Visit"),
|
||||||
__("Create")
|
() => this.make_maintenance_visit(),
|
||||||
);
|
__("Create")
|
||||||
this.frm.add_custom_button(
|
);
|
||||||
__("Maintenance Schedule"),
|
}
|
||||||
() => this.make_maintenance_schedule(),
|
if (frappe.model.can_create("Maintenance Schedule")) {
|
||||||
__("Create")
|
this.frm.add_custom_button(
|
||||||
);
|
__("Maintenance Schedule"),
|
||||||
|
() => this.make_maintenance_schedule(),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// project
|
// project
|
||||||
if (flt(doc.per_delivered, 2) < 100) {
|
if (flt(doc.per_delivered, 2) < 100 && frappe.model.can_create("Project")) {
|
||||||
this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create"));
|
this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
|
if (
|
||||||
|
doc.docstatus === 1 &&
|
||||||
|
!doc.inter_company_order_reference &&
|
||||||
|
frappe.model.can_create("Purchase Order")
|
||||||
|
) {
|
||||||
let me = this;
|
let me = this;
|
||||||
let internal = me.frm.doc.is_internal_customer;
|
let internal = me.frm.doc.is_internal_customer;
|
||||||
if (internal) {
|
if (internal) {
|
||||||
@@ -715,18 +770,27 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
flt(doc.per_billed, precision("per_billed", doc)) <
|
flt(doc.per_billed, precision("per_billed", doc)) <
|
||||||
100 + frappe.boot.sysdefaults.over_billing_allowance
|
100 + frappe.boot.sysdefaults.over_billing_allowance
|
||||||
) {
|
) {
|
||||||
this.frm.add_custom_button(
|
if (frappe.model.can_create("Payment Request")) {
|
||||||
__("Payment Request"),
|
this.frm.add_custom_button(
|
||||||
() => this.make_payment_request(),
|
__("Payment Request"),
|
||||||
__("Create")
|
() => this.make_payment_request(),
|
||||||
);
|
__("Create")
|
||||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frappe.model.can_create("Payment Entry")) {
|
||||||
|
this.frm.add_custom_button(
|
||||||
|
__("Payment"),
|
||||||
|
() => this.make_payment_entry(),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.frm.doc.docstatus === 0) {
|
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Quotation")) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Quotation"),
|
__("Quotation"),
|
||||||
function () {
|
function () {
|
||||||
|
|||||||
@@ -691,6 +691,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
|
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
|
||||||
|
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||||
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||||
"fieldname": "default_advance_received_account",
|
"fieldname": "default_advance_received_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Default Advance Received Account",
|
"label": "Default Advance Received Account",
|
||||||
@@ -699,6 +701,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
|
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
|
||||||
|
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||||
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||||
"fieldname": "default_advance_paid_account",
|
"fieldname": "default_advance_paid_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Default Advance Paid Account",
|
"label": "Default Advance Paid Account",
|
||||||
@@ -766,7 +770,7 @@
|
|||||||
"image_field": "company_logo",
|
"image_field": "company_logo",
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:45.374715",
|
"modified": "2024-04-23 12:38:33.173938",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Company",
|
"name": "Company",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Company(NestedSet):
|
|||||||
auto_exchange_rate_revaluation: DF.Check
|
auto_exchange_rate_revaluation: DF.Check
|
||||||
book_advance_payments_in_separate_party_account: DF.Check
|
book_advance_payments_in_separate_party_account: DF.Check
|
||||||
capital_work_in_progress_account: DF.Link | None
|
capital_work_in_progress_account: DF.Link | None
|
||||||
chart_of_accounts: DF.Literal
|
chart_of_accounts: DF.Literal[None]
|
||||||
company_description: DF.TextEditor | None
|
company_description: DF.TextEditor | None
|
||||||
company_logo: DF.AttachImage | None
|
company_logo: DF.AttachImage | None
|
||||||
company_name: DF.Data
|
company_name: DF.Data
|
||||||
@@ -139,6 +139,7 @@ class Company(NestedSet):
|
|||||||
self.validate_abbr()
|
self.validate_abbr()
|
||||||
self.validate_default_accounts()
|
self.validate_default_accounts()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
self.validate_advance_account_currency()
|
||||||
self.validate_coa_input()
|
self.validate_coa_input()
|
||||||
self.validate_perpetual_inventory()
|
self.validate_perpetual_inventory()
|
||||||
self.validate_provisional_account_for_non_stock_items()
|
self.validate_provisional_account_for_non_stock_items()
|
||||||
@@ -191,6 +192,29 @@ class Company(NestedSet):
|
|||||||
).format(frappe.bold(account[0]))
|
).format(frappe.bold(account[0]))
|
||||||
frappe.throw(error_message)
|
frappe.throw(error_message)
|
||||||
|
|
||||||
|
def validate_advance_account_currency(self):
|
||||||
|
if (
|
||||||
|
self.default_advance_received_account
|
||||||
|
and frappe.get_cached_value("Account", self.default_advance_received_account, "account_currency")
|
||||||
|
!= self.default_currency
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("'{0}' should be in company currency {1}.").format(
|
||||||
|
frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.default_advance_paid_account
|
||||||
|
and frappe.get_cached_value("Account", self.default_advance_paid_account, "account_currency")
|
||||||
|
!= self.default_currency
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("'{0}' should be in company currency {1}.").format(
|
||||||
|
frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_currency(self):
|
def validate_currency(self):
|
||||||
if self.is_new():
|
if self.is_new():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -40,14 +40,9 @@ class CustomerGroup(NestedSet):
|
|||||||
self.parent_customer_group = get_root_of("Customer Group")
|
self.parent_customer_group = get_root_of("Customer Group")
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.validate_name_with_customer()
|
|
||||||
super().on_update()
|
super().on_update()
|
||||||
self.validate_one_root()
|
self.validate_one_root()
|
||||||
|
|
||||||
def validate_name_with_customer(self):
|
|
||||||
if frappe.db.exists("Customer", self.name):
|
|
||||||
frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_parent_customer_groups(customer_group):
|
def get_parent_customer_groups(customer_group):
|
||||||
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user