mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-24 15:39:20 +00:00
Merge branch 'develop' into print-language
This commit is contained in:
6
.github/workflows/release_notes.yml
vendored
6
.github/workflows/release_notes.yml
vendored
@@ -29,7 +29,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Update notes
|
- name: Update notes
|
||||||
run: |
|
run: |
|
||||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
|
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG \
|
||||||
|
| jq -r '.body' \
|
||||||
|
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
|
||||||
|
| sed -E 's/by @mergify //'
|
||||||
|
)
|
||||||
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
|
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
|
||||||
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
||||||
|
|
||||||
|
|||||||
@@ -59,12 +59,14 @@ repos:
|
|||||||
rev: v0.2.0
|
rev: v0.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: "Run ruff linter and apply fixes"
|
name: "Run ruff import sorter"
|
||||||
args: ["--fix"]
|
args: ["--select=I", "--fix"]
|
||||||
|
|
||||||
|
- id: ruff
|
||||||
|
name: "Run ruff linter"
|
||||||
|
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
name: "Format Python code"
|
name: "Run ruff formatter"
|
||||||
|
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
autoupdate_schedule: weekly
|
autoupdate_schedule: weekly
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
|
|||||||
"General Ledger",
|
"General Ledger",
|
||||||
"Balance Sheet",
|
"Balance Sheet",
|
||||||
"Profit and Loss Statement",
|
"Profit and Loss Statement",
|
||||||
"Cash Flow Statement",
|
"Cash Flow",
|
||||||
"Accounts Payable",
|
"Accounts Payable",
|
||||||
"Accounts Receivable",
|
"Accounts Receivable",
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -490,4 +498,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
frm.add_custom_button(__("Auto Reconcile"), function () {
|
frm.add_custom_button(__("Auto Reconcile"), function () {
|
||||||
|
if (!frm.doc.bank_account) {
|
||||||
|
frappe.msgprint(__("Please select Bank Account"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -495,12 +495,12 @@ def check_matching(
|
|||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types=None,
|
||||||
from_date,
|
from_date=None,
|
||||||
to_date,
|
to_date=None,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date=None,
|
||||||
from_reference_date,
|
from_reference_date=None,
|
||||||
to_reference_date,
|
to_reference_date=None,
|
||||||
):
|
):
|
||||||
exact_match = True if "exact_match" in document_types else False
|
exact_match = True if "exact_match" in document_types else False
|
||||||
|
|
||||||
@@ -540,14 +540,14 @@ def get_queries(
|
|||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types=None,
|
||||||
from_date,
|
from_date=None,
|
||||||
to_date,
|
to_date=None,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date=None,
|
||||||
from_reference_date,
|
from_reference_date=None,
|
||||||
to_reference_date,
|
to_reference_date=None,
|
||||||
exact_match,
|
exact_match=None,
|
||||||
common_filters,
|
common_filters=None,
|
||||||
):
|
):
|
||||||
# get queries to get matching vouchers
|
# get queries to get matching vouchers
|
||||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||||
@@ -580,15 +580,15 @@ def get_matching_queries(
|
|||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types=None,
|
||||||
exact_match,
|
exact_match=None,
|
||||||
account_from_to,
|
account_from_to=None,
|
||||||
from_date,
|
from_date=None,
|
||||||
to_date,
|
to_date=None,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date=None,
|
||||||
from_reference_date,
|
from_reference_date=None,
|
||||||
to_reference_date,
|
to_reference_date=None,
|
||||||
common_filters,
|
common_filters=None,
|
||||||
):
|
):
|
||||||
queries = []
|
queries = []
|
||||||
currency = get_account_currency(bank_account)
|
currency = get_account_currency(bank_account)
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -148,7 +148,19 @@ class Dunning(AccountsController):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = [
|
||||||
|
"GL Entry",
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
"Repost Item Valuation",
|
||||||
|
"Repost Payment Ledger",
|
||||||
|
"Repost Payment Ledger Items",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Repost Accounting Ledger Items",
|
||||||
|
"Unreconcile Payment",
|
||||||
|
"Unreconcile Payment Entries",
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"party",
|
"party",
|
||||||
"party_name",
|
"party_name",
|
||||||
"book_advance_payments_in_separate_party_account",
|
"book_advance_payments_in_separate_party_account",
|
||||||
|
"reconcile_on_advance_payment_date",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"bank_account",
|
"bank_account",
|
||||||
"party_bank_account",
|
"party_bank_account",
|
||||||
@@ -751,6 +752,7 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Book Advance Payments in Separate Party Account",
|
"label": "Book Advance Payments in Separate Party Account",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -766,6 +768,16 @@
|
|||||||
"label": "In Words",
|
"label": "In Words",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "company.reconcile_on_advance_payment_date",
|
||||||
|
"fieldname": "reconcile_on_advance_payment_date",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Reconcile on Advance Payment Date",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -779,7 +791,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-04-11 11:25:07.366347",
|
"modified": "2024-05-17 10:21:11.199445",
|
||||||
"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)
|
||||||
@@ -1345,13 +1344,16 @@ class PaymentEntry(AccountsController):
|
|||||||
"voucher_detail_no": invoice.name,
|
"voucher_detail_no": invoice.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
date_field = "posting_date"
|
if self.reconcile_on_advance_payment_date:
|
||||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
|
||||||
date_field = "transaction_date"
|
|
||||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
|
||||||
|
|
||||||
if getdate(posting_date) < getdate(self.posting_date):
|
|
||||||
posting_date = self.posting_date
|
posting_date = self.posting_date
|
||||||
|
else:
|
||||||
|
date_field = "posting_date"
|
||||||
|
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||||
|
date_field = "transaction_date"
|
||||||
|
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||||
|
|
||||||
|
if getdate(posting_date) < getdate(self.posting_date):
|
||||||
|
posting_date = self.posting_date
|
||||||
|
|
||||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||||
args_dict["account"] = account
|
args_dict["account"] = account
|
||||||
@@ -2126,6 +2128,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 +2141,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",
|
||||||
|
|||||||
@@ -1525,6 +1525,55 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(pl_entries, expected_ple)
|
self.assertEqual(pl_entries, expected_ple)
|
||||||
|
|
||||||
|
def test_advance_payment_reconciliation_date(self):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
self.company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_paid_account": self.advance_payable_account,
|
||||||
|
"reconcile_on_advance_payment_date": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
amount = 1500
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount)
|
||||||
|
pe.posting_date = add_days(nowdate(), -1)
|
||||||
|
pe.party_type = "Supplier"
|
||||||
|
pe.party = self.supplier
|
||||||
|
pe.payment_type = "Pay"
|
||||||
|
pe.paid_from = self.cash
|
||||||
|
pe.paid_to = self.advance_payable_account
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
pi = self.create_purchase_invoice(qty=10, rate=100)
|
||||||
|
self.assertNotEqual(pe.posting_date, pi.posting_date)
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.default_advance_account = self.advance_payable_account
|
||||||
|
pr.from_payment_date = pe.posting_date
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# Assert Ledger Entries
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name, "is_cancelled": 0, "posting_date": pe.posting_date},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(gl_entries), 4)
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name, "delinked": 0, "posting_date": pe.posting_date},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(pl_entries), 3)
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
|||||||
@@ -74,15 +74,21 @@
|
|||||||
"discount_amount",
|
"discount_amount",
|
||||||
"discount_percentage",
|
"discount_percentage",
|
||||||
"for_price_list",
|
"for_price_list",
|
||||||
"section_break_13",
|
"dynamic_condition_tab",
|
||||||
"threshold_percentage",
|
|
||||||
"priority",
|
|
||||||
"condition",
|
"condition",
|
||||||
"column_break_66",
|
"section_break_13",
|
||||||
"apply_multiple_pricing_rules",
|
"apply_multiple_pricing_rules",
|
||||||
"apply_discount_on_rate",
|
"apply_discount_on_rate",
|
||||||
|
"column_break_66",
|
||||||
|
"threshold_percentage",
|
||||||
|
"validate_pricing_rule_section",
|
||||||
"validate_applied_rule",
|
"validate_applied_rule",
|
||||||
|
"column_break_texp",
|
||||||
"rule_description",
|
"rule_description",
|
||||||
|
"priority_section",
|
||||||
|
"has_priority",
|
||||||
|
"column_break_sayg",
|
||||||
|
"priority",
|
||||||
"help_section",
|
"help_section",
|
||||||
"pricing_rule_help",
|
"pricing_rule_help",
|
||||||
"reference_section",
|
"reference_section",
|
||||||
@@ -477,7 +483,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "section_break_13",
|
"fieldname": "section_break_13",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Advanced Settings"
|
"label": "Advanced Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -487,6 +493,7 @@
|
|||||||
"label": "Threshold for Suggestion (In Percentage)"
|
"label": "Threshold for Suggestion (In Percentage)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "has_priority",
|
||||||
"description": "Higher the number, higher the priority",
|
"description": "Higher the number, higher the priority",
|
||||||
"fieldname": "priority",
|
"fieldname": "priority",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -513,6 +520,7 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
||||||
|
"description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
|
||||||
"fieldname": "validate_applied_rule",
|
"fieldname": "validate_applied_rule",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Validate Applied Rule"
|
"label": "Validate Applied Rule"
|
||||||
@@ -525,7 +533,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "help_section",
|
"fieldname": "help_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Help Article",
|
||||||
"options": "Simple"
|
"options": "Simple"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -603,12 +612,42 @@
|
|||||||
"fieldname": "apply_recursion_over",
|
"fieldname": "apply_recursion_over",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "priority_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Priority"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dynamic_condition_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Dynamic Condition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "validate_pricing_rule_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Validate Pricing Rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_texp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_sayg",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Enable this checkbox even if you want to set the zero priority",
|
||||||
|
"fieldname": "has_priority",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Has Priority"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-gift",
|
"icon": "fa fa-gift",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:17.521896",
|
"modified": "2024-05-17 13:16:34.496704",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule",
|
"name": "Pricing Rule",
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ class PricingRule(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
||||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import (
|
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
|
||||||
PricingRuleItemCode,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
||||||
PricingRuleItemGroup,
|
PricingRuleItemGroup,
|
||||||
)
|
)
|
||||||
@@ -67,6 +65,7 @@ class PricingRule(Document):
|
|||||||
free_item_rate: DF.Currency
|
free_item_rate: DF.Currency
|
||||||
free_item_uom: DF.Link | None
|
free_item_uom: DF.Link | None
|
||||||
free_qty: DF.Float
|
free_qty: DF.Float
|
||||||
|
has_priority: DF.Check
|
||||||
is_cumulative: DF.Check
|
is_cumulative: DF.Check
|
||||||
is_recursive: DF.Check
|
is_recursive: DF.Check
|
||||||
item_groups: DF.Table[PricingRuleItemGroup]
|
item_groups: DF.Table[PricingRuleItemGroup]
|
||||||
@@ -156,6 +155,12 @@ class PricingRule(Document):
|
|||||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||||
|
|
||||||
def validate_mandatory(self):
|
def validate_mandatory(self):
|
||||||
|
if self.has_priority and not self.priority:
|
||||||
|
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
|
||||||
|
|
||||||
|
if self.priority and not self.has_priority:
|
||||||
|
self.has_priority = 1
|
||||||
|
|
||||||
for apply_on, field in apply_on_dict.items():
|
for apply_on, field in apply_on_dict.items():
|
||||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||||
@@ -573,6 +578,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,116 @@ 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")
|
||||||
|
|
||||||
|
def test_priority_of_multiple_pricing_rules(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,
|
||||||
|
"has_priority": 1,
|
||||||
|
"priority": 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 Percentage",
|
||||||
|
"discount_percentage": 20,
|
||||||
|
"has_priority": 1,
|
||||||
|
"priority": 3,
|
||||||
|
"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_percentage, 20)
|
||||||
|
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"]
|
||||||
@@ -1132,6 +1241,7 @@ def make_pricing_rule(**args):
|
|||||||
"priority": args.priority or 1,
|
"priority": args.priority or 1,
|
||||||
"discount_amount": args.discount_amount or 0.0,
|
"discount_amount": args.discount_amount or 0.0,
|
||||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||||
|
"has_priority": args.has_priority or 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -32,6 +33,9 @@ def get_pricing_rules(args, doc=None):
|
|||||||
|
|
||||||
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||||
|
if pricing_rules and pricing_rules[0].has_priority:
|
||||||
|
continue
|
||||||
|
|
||||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -653,7 +657,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
|
||||||
|
|||||||
@@ -489,10 +489,12 @@ function hide_fields(doc) {
|
|||||||
|
|
||||||
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
||||||
|
|
||||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
if (cur_frm.fields_dict["items"]) {
|
||||||
item_fields_stock,
|
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
item_fields_stock,
|
||||||
);
|
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cur_frm.refresh_fields();
|
cur_frm.refresh_fields();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1063,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]
|
||||||
@@ -1147,7 +1147,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
pr_items = frappe.get_all(
|
pr_items = frappe.get_all(
|
||||||
"Purchase Receipt Item",
|
"Purchase Receipt Item",
|
||||||
filters={"parent": ("in", linked_purchase_receipts)},
|
filters={"parent": ("in", linked_purchase_receipts)},
|
||||||
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||||
)
|
)
|
||||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||||
provisional_accounts = set(
|
provisional_accounts = set(
|
||||||
@@ -1175,6 +1175,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||||
"qty": item.qty,
|
"qty": item.qty,
|
||||||
"base_rate": item.base_rate,
|
"base_rate": item.base_rate,
|
||||||
|
"rate": item.rate,
|
||||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,7 +1192,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.posting_date,
|
self.posting_date,
|
||||||
pr_item.get("provisional_account"),
|
pr_item.get("provisional_account"),
|
||||||
reverse=1,
|
reverse=1,
|
||||||
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
|
item_amount=(
|
||||||
|
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||||
|
* purchase_receipt_doc.get("conversion_rate")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||||
@@ -1207,7 +1211,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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
|
||||||
"autoname": "format:ACC-REPOST-{#####}",
|
|
||||||
"creation": "2023-07-04 13:07:32.923675",
|
"creation": "2023-07-04 13:07:32.923675",
|
||||||
"default_view": "List",
|
"default_view": "List",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -55,11 +53,10 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:32.013542",
|
"modified": "2024-05-23 17:00:42.984798",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger",
|
"name": "Repost Accounting Ledger",
|
||||||
"naming_rule": "Expression",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
|
||||||
"creation": "2022-10-19 21:59:33.553852",
|
"creation": "2022-10-19 21:59:33.553852",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -99,7 +98,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:32.740806",
|
"modified": "2024-05-23 17:00:31.540640",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Payment Ledger",
|
"name": "Repost Payment Ledger",
|
||||||
|
|||||||
@@ -2044,7 +2044,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",
|
||||||
@@ -2205,7 +2205,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-04-20 01:21:44.055127",
|
"modified": "2024-05-23 14:02:28.549041",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -393,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)
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
if filters.get("account"):
|
if filters.get("account"):
|
||||||
filters.account = get_accounts_with_children(filters.account)
|
filters.account = get_accounts_with_children(filters.account)
|
||||||
conditions.append("account in %(account)s")
|
if filters.account:
|
||||||
|
conditions.append("account in %(account)s")
|
||||||
|
|
||||||
if filters.get("cost_center"):
|
if filters.get("cost_center"):
|
||||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||||
@@ -329,7 +330,7 @@ def get_accounts_with_children(accounts):
|
|||||||
else:
|
else:
|
||||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||||
|
|
||||||
return list(set(all_accounts))
|
return list(set(all_accounts)) if all_accounts else None
|
||||||
|
|
||||||
|
|
||||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||||
@@ -460,7 +461,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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, qb
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
|
from frappe.query_builder.functions import Abs
|
||||||
from frappe.utils import flt, getdate
|
from frappe.utils import flt, getdate
|
||||||
|
|
||||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for d in entries:
|
for d in entries:
|
||||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||||
|
payment_amount = d.amount
|
||||||
if d.reference_type == "Purchase Invoice":
|
|
||||||
payment_amount = flt(d.debit) or -1 * flt(d.credit)
|
|
||||||
else:
|
|
||||||
payment_amount = flt(d.credit) or -1 * flt(d.debit)
|
|
||||||
|
|
||||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
||||||
|
|
||||||
if d.against_voucher:
|
if d.against_voucher_no:
|
||||||
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||||
|
|
||||||
row = [
|
row = [
|
||||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
|||||||
d.party_type,
|
d.party_type,
|
||||||
d.party,
|
d.party,
|
||||||
d.posting_date,
|
d.posting_date,
|
||||||
d.against_voucher,
|
d.against_voucher_no,
|
||||||
invoice.posting_date,
|
invoice.posting_date,
|
||||||
invoice.due_date,
|
invoice.due_date,
|
||||||
d.debit,
|
d.amount,
|
||||||
d.credit,
|
|
||||||
d.remarks,
|
d.remarks,
|
||||||
d.age,
|
d.age,
|
||||||
d.range1,
|
d.range1,
|
||||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
|||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
|
||||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
if not filters.party_type:
|
conditions.append(ple.delinked.eq(0))
|
||||||
if filters.payment_type == _("Outgoing"):
|
if filters.payment_type == _("Outgoing"):
|
||||||
filters.party_type = "Supplier"
|
conditions.append(ple.party_type.eq("Supplier"))
|
||||||
else:
|
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||||
filters.party_type = "Customer"
|
else:
|
||||||
|
conditions.append(ple.party_type.eq("Customer"))
|
||||||
if filters.party_type:
|
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||||
conditions.append("party_type=%(party_type)s")
|
|
||||||
|
|
||||||
if filters.party:
|
if filters.party:
|
||||||
conditions.append("party=%(party)s")
|
conditions.append(ple.party.eq(filters.party))
|
||||||
|
|
||||||
if filters.party_type:
|
|
||||||
conditions.append("against_voucher_type=%(reference_type)s")
|
|
||||||
filters["reference_type"] = (
|
|
||||||
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
|
|
||||||
)
|
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("from_date"):
|
||||||
conditions.append("posting_date >= %(from_date)s")
|
conditions.append(ple.posting_date.gte(filters.get("from_date")))
|
||||||
|
|
||||||
if filters.get("to_date"):
|
if filters.get("to_date"):
|
||||||
conditions.append("posting_date <= %(to_date)s")
|
conditions.append(ple.posting_date.lte(filters.get("to_date")))
|
||||||
|
|
||||||
return "and " + " and ".join(conditions) if conditions else ""
|
if filters.get("company"):
|
||||||
|
conditions.append(ple.company.eq(filters.get("company")))
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
def get_entries(filters):
|
def get_entries(filters):
|
||||||
return frappe.db.sql(
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
"""select
|
conditions = get_conditions(filters)
|
||||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
|
||||||
from `tabGL Entry`
|
query = (
|
||||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
qb.from_(ple)
|
||||||
""".format(get_conditions(filters)),
|
.select(
|
||||||
filters,
|
ple.voucher_type,
|
||||||
as_dict=1,
|
ple.voucher_no,
|
||||||
|
ple.party_type,
|
||||||
|
ple.party,
|
||||||
|
ple.posting_date,
|
||||||
|
Abs(ple.amount).as_("amount"),
|
||||||
|
ple.remarks,
|
||||||
|
ple.against_voucher_no,
|
||||||
|
)
|
||||||
|
.where(Criterion.all(conditions))
|
||||||
)
|
)
|
||||||
|
res = query.run(as_dict=True)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_posting_date_map(filters):
|
def get_invoice_posting_date_map(filters):
|
||||||
invoice_details = {}
|
invoice_details = {}
|
||||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
dt = (
|
||||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
qb.DocType("Sales Invoice")
|
||||||
|
if filters.get("payment_type") == _("Incoming")
|
||||||
|
else qb.DocType("Purchase Invoice")
|
||||||
|
)
|
||||||
|
res = (
|
||||||
|
qb.from_(dt)
|
||||||
|
.select(dt.name, dt.posting_date, dt.due_date)
|
||||||
|
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
|
||||||
|
.run(as_dict=1)
|
||||||
|
)
|
||||||
|
for t in res:
|
||||||
invoice_details[t.name] = t
|
invoice_details[t.name] = t
|
||||||
|
|
||||||
return invoice_details
|
return invoice_details
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -362,6 +363,16 @@ class AssetDepreciationSchedule(Document):
|
|||||||
row.depreciation_start_date,
|
row.depreciation_start_date,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
)
|
)
|
||||||
|
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations."
|
||||||
|
).format(
|
||||||
|
frappe.bold(asset_doc.gross_purchase_amount),
|
||||||
|
frappe.bold(row.total_number_of_depreciations),
|
||||||
|
frappe.bold(row.frequency_of_depreciation),
|
||||||
|
)
|
||||||
|
)
|
||||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||||
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||||
from_date = get_last_day(
|
from_date = get_last_day(
|
||||||
@@ -599,11 +610,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 +626,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 +650,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 +670,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 +681,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 +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,
|
||||||
):
|
):
|
||||||
return get_default_wdv_or_dd_depr_amount(
|
return get_default_wdv_or_dd_depr_amount(
|
||||||
asset,
|
asset,
|
||||||
@@ -788,6 +753,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 +765,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 +824,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)
|
||||||
|
|||||||
@@ -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"
|
||||||
):
|
):
|
||||||
@@ -2208,10 +2211,10 @@ class AccountsController(TransactionBase):
|
|||||||
for d in self.get("payment_schedule"):
|
for d in self.get("payment_schedule"):
|
||||||
if d.invoice_portion:
|
if d.invoice_portion:
|
||||||
d.payment_amount = flt(
|
d.payment_amount = flt(
|
||||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
||||||
)
|
)
|
||||||
d.base_payment_amount = flt(
|
d.base_payment_amount = flt(
|
||||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||||
)
|
)
|
||||||
d.outstanding = d.payment_amount
|
d.outstanding = d.payment_amount
|
||||||
elif not d.invoice_portion:
|
elif not d.invoice_portion:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -205,6 +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,
|
||||||
|
"via_landed_cost_voucher": via_landed_cost_voucher,
|
||||||
"do_not_submit": True if not via_landed_cost_voucher else False,
|
"do_not_submit": True if not via_landed_cost_voucher else False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,8 +1228,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",
|
||||||
@@ -1361,7 +1362,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)
|
||||||
|
|
||||||
@@ -1381,7 +1382,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,
|
||||||
@@ -1404,7 +1405,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 = {}
|
||||||
|
|||||||
@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
|
|||||||
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
||||||
|
|
||||||
# Will be deprecated in v16
|
# Will be deprecated in v16
|
||||||
if row.serial_no:
|
if row.serial_no and not consumed_bundles.serial_nos:
|
||||||
self.available_materials[key]["serial_no"] = list(
|
self.available_materials[key]["serial_no"] = list(
|
||||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Will be deprecated in v16
|
# Will be deprecated in v16
|
||||||
if row.batch_no:
|
if row.batch_no and not consumed_bundles.batch_nos:
|
||||||
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
||||||
|
|
||||||
def get_available_materials(self):
|
def get_available_materials(self):
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
2453
erpnext/locale/ar.po
2453
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
5122
erpnext/locale/bs.po
5122
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2887
erpnext/locale/de.po
2887
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2453
erpnext/locale/eo.po
2453
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2963
erpnext/locale/es.po
2963
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3977
erpnext/locale/fa.po
3977
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2453
erpnext/locale/fr.po
2453
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
8914
erpnext/locale/tr.po
8914
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,18 @@ class BOMCreator(Document):
|
|||||||
if row.is_expandable and row.item_code == self.item_code:
|
if row.is_expandable and row.item_code == self.item_code:
|
||||||
frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
|
frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
|
||||||
|
|
||||||
|
if not row.parent_row_no and row.fg_item and row.fg_item != self.item_code:
|
||||||
|
frappe.throw(
|
||||||
|
_("At row {0}: set Parent Row No for item {1}").format(row.idx, row.item_code),
|
||||||
|
title=_("Set Parent Row No in Items Table"),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif row.parent_row_no and row.fg_item == self.item_code:
|
||||||
|
frappe.throw(
|
||||||
|
_("At row {0}: Parent Row No cannot be set for item {1}").format(row.idx, row.item_code),
|
||||||
|
title=_("Remove Parent Row No in Items Table"),
|
||||||
|
)
|
||||||
|
|
||||||
def set_status(self, save=False):
|
def set_status(self, save=False):
|
||||||
self.status = {
|
self.status = {
|
||||||
0: "Draft",
|
0: "Draft",
|
||||||
@@ -410,6 +422,10 @@ def add_sub_assembly(**kwargs):
|
|||||||
|
|
||||||
parent_row_no = item_row.idx
|
parent_row_no = item_row.idx
|
||||||
name = ""
|
name = ""
|
||||||
|
else:
|
||||||
|
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
||||||
|
if parent_row_no:
|
||||||
|
parent_row_no = parent_row_no[0]
|
||||||
|
|
||||||
for row in bom_item.get("items"):
|
for row in bom_item.get("items"):
|
||||||
row = frappe._dict(row)
|
row = frappe._dict(row)
|
||||||
|
|||||||
@@ -214,7 +214,11 @@ class JobCard(Document):
|
|||||||
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
|
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
|
||||||
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
|
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
|
||||||
|
|
||||||
data = self.get_overlap_for(d)
|
open_job_cards = []
|
||||||
|
if d.get("employee"):
|
||||||
|
open_job_cards = self.get_open_job_cards(d.get("employee"))
|
||||||
|
|
||||||
|
data = self.get_overlap_for(d, open_job_cards=open_job_cards)
|
||||||
if data:
|
if data:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
|
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
|
||||||
@@ -235,12 +239,12 @@ class JobCard(Document):
|
|||||||
for row in self.sub_operations:
|
for row in self.sub_operations:
|
||||||
self.total_completed_qty += row.completed_qty
|
self.total_completed_qty += row.completed_qty
|
||||||
|
|
||||||
def get_overlap_for(self, args):
|
def get_overlap_for(self, args, open_job_cards=None):
|
||||||
time_logs = []
|
time_logs = []
|
||||||
|
|
||||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
||||||
|
|
||||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
|
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", open_job_cards=open_job_cards))
|
||||||
|
|
||||||
if not time_logs:
|
if not time_logs:
|
||||||
return {}
|
return {}
|
||||||
@@ -304,7 +308,7 @@ class JobCard(Document):
|
|||||||
return True
|
return True
|
||||||
return overlap
|
return overlap
|
||||||
|
|
||||||
def get_time_logs(self, args, doctype):
|
def get_time_logs(self, args, doctype, open_job_cards=None):
|
||||||
jc = frappe.qb.DocType("Job Card")
|
jc = frappe.qb.DocType("Job Card")
|
||||||
jctl = frappe.qb.DocType(doctype)
|
jctl = frappe.qb.DocType(doctype)
|
||||||
|
|
||||||
@@ -341,8 +345,14 @@ class JobCard(Document):
|
|||||||
if self.workstation:
|
if self.workstation:
|
||||||
query = query.where(jc.workstation == self.workstation)
|
query = query.where(jc.workstation == self.workstation)
|
||||||
|
|
||||||
if args.get("employee") and doctype == "Job Card Time Log":
|
if args.get("employee"):
|
||||||
query = query.where(jctl.employee == args.get("employee"))
|
if not open_job_cards and doctype == "Job Card Scheduled Time":
|
||||||
|
return []
|
||||||
|
|
||||||
|
if doctype == "Job Card Time Log":
|
||||||
|
query = query.where(jctl.employee == args.get("employee"))
|
||||||
|
else:
|
||||||
|
query = query.where(jc.name.isin(open_job_cards))
|
||||||
|
|
||||||
if doctype != "Job Card Time Log":
|
if doctype != "Job Card Time Log":
|
||||||
query = query.where(jc.total_time_in_mins == 0)
|
query = query.where(jc.total_time_in_mins == 0)
|
||||||
@@ -351,6 +361,27 @@ class JobCard(Document):
|
|||||||
|
|
||||||
return time_logs
|
return time_logs
|
||||||
|
|
||||||
|
def get_open_job_cards(self, employee):
|
||||||
|
jc = frappe.qb.DocType("Job Card")
|
||||||
|
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(jc)
|
||||||
|
.left_join(jctl)
|
||||||
|
.on(jc.name == jctl.parent)
|
||||||
|
.select(jc.name)
|
||||||
|
.where(
|
||||||
|
(jctl.parent == jc.name)
|
||||||
|
& (jc.workstation == self.workstation)
|
||||||
|
& (jctl.employee == employee)
|
||||||
|
& (jc.docstatus < 1)
|
||||||
|
& (jc.name != self.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
jobs = query.run(as_dict=True)
|
||||||
|
return [job.get("name") for job in jobs] if jobs else []
|
||||||
|
|
||||||
def get_workstation_based_on_available_slot(self, existing_time_logs) -> dict:
|
def get_workstation_based_on_available_slot(self, existing_time_logs) -> dict:
|
||||||
workstations = get_workstations(self.workstation_type)
|
workstations = get_workstations(self.workstation_type)
|
||||||
if workstations:
|
if workstations:
|
||||||
|
|||||||
@@ -42,8 +42,7 @@
|
|||||||
"fieldname": "completed_qty",
|
"fieldname": "completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Completed Qty",
|
"label": "Completed Qty"
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "employee",
|
"fieldname": "employee",
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:09:57.448800",
|
"modified": "2024-05-21 12:40:55.765860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card Time Log",
|
"name": "Job Card Time Log",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
|||||||
exploded_items = frappe.get_all(
|
exploded_items = frappe.get_all(
|
||||||
"BOM Item",
|
"BOM Item",
|
||||||
filters={"parent": bom},
|
filters={"parent": bom},
|
||||||
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
|
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom", "idx"],
|
||||||
|
order_by="idx ASC",
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in exploded_items:
|
for item in exploded_items:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -363,4 +363,7 @@ 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
|
||||||
|
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ def execute():
|
|||||||
for d in accounting_dimensions:
|
for d in accounting_dimensions:
|
||||||
doctype = "Asset Repair"
|
doctype = "Asset Repair"
|
||||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||||
|
docfield = frappe.db.get_value("DocField", {"parent": doctype, "fieldname": d.fieldname})
|
||||||
|
|
||||||
if field:
|
if field or docfield:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
df = {
|
df = {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
pr_table = frappe.qb.DocType("Pricing Rule")
|
||||||
|
(
|
||||||
|
frappe.qb.update(pr_table)
|
||||||
|
.set(pr_table.has_priority, 1)
|
||||||
|
.where((pr_table.priority.isnotnull()) & (pr_table.priority != ""))
|
||||||
|
).run()
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.refresh_fields();
|
this.frm.refresh_field("taxes");
|
||||||
}
|
}
|
||||||
|
|
||||||
calculate_discount_amount() {
|
calculate_discount_amount() {
|
||||||
@@ -853,7 +853,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.refresh_fields();
|
this.frm.refresh_field("taxes");
|
||||||
}
|
}
|
||||||
|
|
||||||
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -907,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,
|
||||||
@@ -1181,4 +1184,39 @@ $.extend(erpnext.stock.utils, {
|
|||||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
||||||
barcode_scanner.scan_api_call(child_row.barcode, callback);
|
barcode_scanner.scan_api_call(child_row.barcode, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_serial_range(range_string, separator) {
|
||||||
|
/* Return an array of serial numbers generated from a range string.
|
||||||
|
|
||||||
|
Examples (using separator "::"):
|
||||||
|
- "1::5" => ["1", "2", "3", "4", "5"]
|
||||||
|
- "SN0009::12" => ["SN0009", "SN0010", "SN0011", "SN0012"]
|
||||||
|
- "ABC//05::8" => ["ABC//05", "ABC//06", "ABC//07", "ABC//08"]
|
||||||
|
*/
|
||||||
|
if (!range_string) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [start_str, end_str] = range_string.trim().split(separator);
|
||||||
|
|
||||||
|
if (!start_str || !end_str) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const end_int = parseInt(end_str);
|
||||||
|
const length_difference = start_str.length - end_str.length;
|
||||||
|
const start_int = parseInt(start_str.substring(length_difference));
|
||||||
|
|
||||||
|
if (isNaN(start_int) || isNaN(end_int)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serial_numbers = Array(end_int - start_int + 1)
|
||||||
|
.fill(1)
|
||||||
|
.map((x, y) => x + y)
|
||||||
|
.map((x) => x + start_int - 1);
|
||||||
|
return serial_numbers.map((val) => {
|
||||||
|
return start_str.substring(0, length_difference) + val.toString().padStart(end_str.length, "0");
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -206,6 +206,16 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
label: __("{0} {1} Manually", [primary_label, label]),
|
label: __("{0} {1} Manually", [primary_label, label]),
|
||||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Enter Serial No Range"),
|
||||||
|
fieldname: "serial_no_range",
|
||||||
|
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||||
|
description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'),
|
||||||
|
onchange: () => {
|
||||||
|
this.set_serial_nos_from_range();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Small Text",
|
fieldtype: "Small Text",
|
||||||
label: __("Enter Serial Nos"),
|
label: __("Enter Serial Nos"),
|
||||||
@@ -255,6 +265,20 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_serial_nos_from_range() {
|
||||||
|
const serial_no_range = this.dialog.get_value("serial_no_range");
|
||||||
|
|
||||||
|
if (!serial_no_range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serial_nos = erpnext.stock.utils.get_serial_range(serial_no_range, "::");
|
||||||
|
|
||||||
|
if (serial_nos) {
|
||||||
|
this.dialog.set_value("upload_serial_nos", serial_nos.join("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
create_serial_nos() {
|
create_serial_nos() {
|
||||||
let { upload_serial_nos } = this.dialog.get_values();
|
let { upload_serial_nos } = this.dialog.get_values();
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user