Merge branch 'frappe:develop' into patch-1

This commit is contained in:
Markus Lobedann
2024-05-31 14:39:41 +02:00
committed by GitHub
210 changed files with 26907 additions and 19517 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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",
]) { ]) {

View File

@@ -1525,7 +1525,8 @@
"41-Clients et comptes rattach\u00e9s (PASSIF)": { "41-Clients et comptes rattach\u00e9s (PASSIF)": {
"Clients cr\u00e9diteurs": { "Clients cr\u00e9diteurs": {
"Clients - Avances et acomptes re\u00e7us sur commandes": { "Clients - Avances et acomptes re\u00e7us sur commandes": {
"account_number": "4191" "account_number": "4191",
"account_type": "Income Account"
}, },
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": { "Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
"account_number": "4196" "account_number": "4196"
@@ -3141,4 +3142,4 @@
"account_number": "7" "account_number": "7"
} }
} }
} }

View File

@@ -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);
}
);
},
}); });

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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: {

View File

@@ -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",

View File

@@ -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,

View File

@@ -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")

View File

@@ -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",

View File

@@ -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):

View File

@@ -82,7 +82,7 @@
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2024-01-30 12:35:38.645968", "modified": "2024-05-27 17:29:55.560840",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Fiscal Year", "name": "Fiscal Year",
@@ -127,6 +127,10 @@
{ {
"read": 1, "read": 1,
"role": "Stock Manager" "role": "Stock Manager"
},
{
"read": 1,
"role": "Auditor"
} }
], ],
"show_name_in_global_search": 1, "show_name_in_global_search": 1,

View File

@@ -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], [])
@@ -179,6 +194,7 @@ class JournalEntry(AccountsController):
self.update_asset_value() self.update_asset_value()
self.update_inter_company_jv() self.update_inter_company_jv()
self.update_invoice_discounting() self.update_invoice_discounting()
self.update_booked_depreciation()
def on_update_after_submit(self): def on_update_after_submit(self):
if hasattr(self, "repost_required"): if hasattr(self, "repost_required"):
@@ -210,6 +226,7 @@ class JournalEntry(AccountsController):
self.unlink_inter_company_jv() self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry() self.unlink_asset_adjustment_entry()
self.update_invoice_discounting() self.update_invoice_discounting()
self.update_booked_depreciation()
def get_title(self): def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account return self.pay_to_recd_from or self.accounts[0].account
@@ -427,6 +444,28 @@ class JournalEntry(AccountsController):
if status: if status:
inv_disc_doc.set_status(status=status) inv_disc_doc.set_status(status=status)
def update_booked_depreciation(self):
for d in self.get("accounts"):
if (
self.voucher_type == "Depreciation Entry"
and d.reference_type == "Asset"
and d.reference_name
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
and d.debit
):
asset = frappe.get_doc("Asset", d.reference_name)
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations
for je in depr_schedule:
if je.journal_entry:
total_number_of_booked_depreciations += 1
fb_row.db_set(
"total_number_of_booked_depreciations", total_number_of_booked_depreciations
)
break
def unlink_advance_entry_reference(self): def unlink_advance_entry_reference(self):
for d in self.get("accounts"): for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"): if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
@@ -442,7 +481,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)

View File

@@ -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);
} }

View File

@@ -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",

View File

@@ -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,

View File

@@ -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"):

View File

@@ -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();
}, },

View File

@@ -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",

View File

@@ -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):

View File

@@ -2,6 +2,7 @@
# See license.txt # See license.txt
import unittest import unittest
from unittest.mock import patch
import frappe import frappe
@@ -13,7 +14,12 @@ from erpnext.setup.utils import get_exchange_rate
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"} PAYMENT_URL = "https://example.com/payment"
payment_gateways = [
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
{"doctype": "Payment Gateway", "gateway": "_Test Gateway Phone"},
]
payment_method = [ payment_method = [
{ {
@@ -29,13 +35,21 @@ payment_method = [
"payment_account": "_Test Bank USD - _TC", "payment_account": "_Test Bank USD - _TC",
"currency": "USD", "currency": "USD",
}, },
{
"doctype": "Payment Gateway Account",
"payment_gateway": "_Test Gateway Phone",
"payment_account": "_Test Bank USD - _TC",
"payment_channel": "Phone",
"currency": "USD",
},
] ]
class TestPaymentRequest(unittest.TestCase): class TestPaymentRequest(unittest.TestCase):
def setUp(self): def setUp(self):
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"): for payment_gateway in payment_gateways:
frappe.get_doc(payment_gateway).insert(ignore_permissions=True) if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
for method in payment_method: for method in payment_method:
if not frappe.db.get_value( if not frappe.db.get_value(
@@ -45,6 +59,25 @@ class TestPaymentRequest(unittest.TestCase):
): ):
frappe.get_doc(method).insert(ignore_permissions=True) frappe.get_doc(method).insert(ignore_permissions=True)
send_email = patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
return_value=None,
)
self.send_email = send_email.start()
self.addCleanup(send_email.stop)
get_payment_url = patch(
# this also shadows one (1) call to _get_payment_gateway_controller
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
return_value=PAYMENT_URL,
)
self.get_payment_url = get_payment_url.start()
self.addCleanup(get_payment_url.stop)
_get_payment_gateway_controller = patch(
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
)
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
self.addCleanup(_get_payment_gateway_controller.stop)
def test_payment_request_linkings(self): def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR", do_not_save=True) so_inr = make_sales_order(currency="INR", do_not_save=True)
so_inr.disable_rounded_total = 1 so_inr.disable_rounded_total = 1
@@ -75,6 +108,83 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD") self.assertEqual(pr.currency, "USD")
def test_payment_channels(self):
so = make_sales_order(currency="USD")
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD", # email channel
submit_doc=False,
return_doc=True,
)
pr.flags.mute_email = True # but temporarily prohibit sending
pr.submit()
pr.reload()
self.assertEqual(pr.payment_channel, "Email")
self.assertEqual(pr.mute_email, False)
self.assertIsNone(pr.payment_url)
self.assertEqual(self.send_email.call_count, 0) # hence: no increment
self.assertEqual(self._get_payment_gateway_controller.call_count, 1)
pr.cancel()
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway Phone - USD",
submit_doc=True,
return_doc=True,
)
pr.reload()
self.assertEqual(pr.payment_channel, "Phone")
self.assertEqual(pr.mute_email, False)
self.assertIsNone(pr.payment_url)
self.assertEqual(self.send_email.call_count, 0) # no increment on phone channel
self.assertEqual(self._get_payment_gateway_controller.call_count, 3)
pr.cancel()
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD", # email channel
submit_doc=True,
return_doc=True,
)
pr.reload()
self.assertEqual(pr.payment_channel, "Email")
self.assertEqual(pr.mute_email, False)
self.assertEqual(pr.payment_url, PAYMENT_URL)
self.assertEqual(self.send_email.call_count, 1) # increment on normal email channel
self.assertEqual(self._get_payment_gateway_controller.call_count, 4)
pr.cancel()
so = make_sales_order(currency="USD", do_not_save=True)
# no-op; for optical consistency with how a webshop SO would look like
so.order_type = "Shopping Cart"
so.save()
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD", # email channel
order_type="Shopping Cart",
submit_doc=True,
return_doc=True,
)
pr.reload()
self.assertEqual(pr.payment_channel, "Email")
self.assertEqual(pr.mute_email, False)
self.assertIsNone(pr.payment_url)
self.assertEqual(self.send_email.call_count, 1) # no increment on shopping cart
self.assertEqual(self._get_payment_gateway_controller.call_count, 5)
pr.cancel()
def test_payment_entry_against_purchase_invoice(self): def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice( si_usd = make_purchase_invoice(
customer="_Test Supplier USD", customer="_Test Supplier USD",

View File

@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points() self.apply_loyalty_points()
self.check_phone_payments() self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
self.make_bundle_for_sales_purchase_return()
self.submit_serial_batch_bundle() self.submit_serial_batch_bundle()
if self.coupon_code: if self.coupon_code:

View File

@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
pos.insert() pos.insert()
pos.submit() pos.submit()
pos.reload()
pos_return1 = make_sales_return(pos.name) pos_return1 = make_sales_return(pos.name)
# partial return 1 # partial return 1
pos_return1.get("items")[0].qty = -1 pos_return1.get("items")[0].qty = -1
pos_return1.submit()
pos_return1.reload()
bundle_id = frappe.get_doc( bundle_id = frappe.get_doc(
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle "Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
) )
bundle_id.remove(bundle_id.entries[1])
bundle_id.save()
bundle_id.load_from_db() bundle_id.load_from_db()
serial_no = bundle_id.entries[0].serial_no serial_no = bundle_id.entries[0].serial_no
self.assertEqual(serial_no, serial_nos[0]) self.assertEqual(serial_no, serial_nos[0])
pos_return1.insert()
pos_return1.submit()
# partial return 2 # partial return 2
pos_return2 = make_sales_return(pos.name) pos_return2 = make_sales_return(pos.name)
pos_return2.submit()
self.assertEqual(pos_return2.get("items")[0].qty, -1) self.assertEqual(pos_return2.get("items")[0].qty, -1)
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0] serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
self.assertEqual(serial_no, serial_nos[1]) self.assertEqual(serial_no, serial_nos[1])

View File

@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry: if closing_entry:
closing_entry.set_status(update=True, status="Failed") closing_entry.set_status(update=True, status="Failed")
if isinstance(error_message, list): if isinstance(error_message, list):
error_message = frappe.json.dumps(error_message) error_message = json.dumps(error_message)
closing_entry.db_set("error_message", error_message) closing_entry.db_set("error_message", error_message)
raise raise

View File

@@ -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",

View File

@@ -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)

View File

@@ -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,
} }
) )

View File

@@ -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,

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _, qb from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
@@ -502,7 +504,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None running_doc = None
if for_filter: if for_filter:
if isinstance(for_filter, str): if isinstance(for_filter, str):
for_filter = frappe.json.loads(for_filter) for_filter = json.loads(for_filter)
running_doc = frappe.db.get_value( running_doc = frappe.db.get_value(
"Process Payment Reconciliation", "Process Payment Reconciliation",

View File

@@ -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,

View File

@@ -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>

View File

@@ -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",

View File

@@ -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

View File

@@ -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();
} }

View File

@@ -708,6 +708,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status, # Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO # because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1: if self.update_stock == 1:
self.make_bundle_for_sales_purchase_return()
self.make_bundle_using_old_serial_batch_fields() self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger() self.update_stock_ledger()
@@ -1063,7 +1064,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 +1148,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 +1176,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 +1193,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 +1212,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,
}, },
) )

View File

@@ -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": [
{ {

View File

@@ -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",

View File

@@ -2042,7 +2042,7 @@
{ {
"fieldname": "contact_and_address_tab", "fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Contact & Address" "label": "Address & Contact"
}, },
{ {
"fieldname": "payments_tab", "fieldname": "payments_tab",
@@ -2203,7 +2203,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2024-04-11 11:30:26.272441", "modified": "2024-05-08 18:02:28.549041",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -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:
@@ -452,6 +455,7 @@ class SalesInvoice(SellingController):
if not self.get(table_name): if not self.get(table_name):
continue continue
self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name) self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_ledger() self.update_stock_ledger()

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy import copy
import json
import frappe import frappe
from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.dynamic_links import get_dynamic_link_map
@@ -1783,6 +1784,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",
@@ -3690,9 +3734,9 @@ class TestSalesInvoice(FrappeTestCase):
map_docs( map_docs(
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_names=frappe.json.dumps([dn1.name, dn2.name]), source_names=json.dumps([dn1.name, dn2.name]),
target_doc=si, target_doc=si,
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}), args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
) )
si.save().submit() si.save().submit()

View File

@@ -870,7 +870,8 @@
"label": "Purchase Order", "label": "Purchase Order",
"options": "Purchase Order", "options": "Purchase Order",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "column_break_92", "fieldname": "column_break_92",
@@ -926,7 +927,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:36.139679", "modified": "2024-05-23 16:36:18.970862",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -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 (

View File

@@ -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")

View File

@@ -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",

View File

@@ -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}

View File

@@ -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(

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _, qb from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
@@ -163,7 +165,7 @@ def get_linked_payments_for_doc(
@frappe.whitelist() @frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None): def create_unreconcile_doc_for_selection(selections=None):
if selections: if selections:
selections = frappe.json.loads(selections) selections = json.loads(selections)
# assuming each row is a unique voucher # assuming each row is a unique voucher
for row in selections: for row in selections:
unrecon = frappe.new_doc("Unreconcile Payment") unrecon = frappe.new_doc("Unreconcile Payment")

View File

@@ -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")

View File

@@ -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)

View File

@@ -49,7 +49,6 @@ def get_conditions(filters):
if filters.account_type: if filters.account_type:
conditions["account_type"] = filters.account_type conditions["account_type"] = filters.account_type
return conditions
if filters.company: if filters.company:
conditions["company"] = filters.company conditions["company"] = filters.company

View File

@@ -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")

View File

@@ -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":

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -56,7 +56,7 @@ def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
): ):
if isinstance(boolean, str): if isinstance(boolean, str):
boolean = frappe.json.loads(boolean) boolean = loads(boolean)
fiscal_years = get_fiscal_years( fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
@@ -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
@@ -1105,7 +1109,7 @@ def get_companies():
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent, company, is_root=False, include_disabled=False): def get_children(doctype, parent, company, is_root=False, include_disabled=False):
if isinstance(include_disabled, str): if isinstance(include_disabled, str):
include_disabled = frappe.json.loads(include_disabled) include_disabled = loads(include_disabled)
from erpnext.accounts.report.financial_statements import sort_accounts from erpnext.accounts.report.financial_statements import sort_accounts
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")

View File

@@ -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) {

View File

@@ -45,7 +45,7 @@
"calculate_depreciation", "calculate_depreciation",
"column_break_33", "column_break_33",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
"number_of_depreciations_booked", "opening_number_of_booked_depreciations",
"is_fully_depreciated", "is_fully_depreciated",
"section_break_36", "section_break_36",
"finance_books", "finance_books",
@@ -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",
@@ -257,12 +257,6 @@
"label": "Opening Accumulated Depreciation", "label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
}, },
{
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "number_of_depreciations_booked",
"fieldtype": "Int",
"label": "Number of Depreciations Booked"
},
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset", "collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
@@ -408,15 +402,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 +531,21 @@
"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
},
{
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "opening_number_of_booked_depreciations",
"fieldtype": "Int",
"label": "Opening Number of Booked Depreciations"
} }
], ],
"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-05-21 13:46:21.066483",
"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",

View File

@@ -89,13 +89,13 @@ class Asset(AccountsController):
maintenance_required: DF.Check maintenance_required: DF.Check
naming_series: DF.Literal["ACC-ASS-.YYYY.-"] naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
next_depreciation_date: DF.Date | None next_depreciation_date: DF.Date | None
number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency opening_accumulated_depreciation: DF.Currency
opening_number_of_booked_depreciations: DF.Int
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",
@@ -145,7 +145,7 @@ class Asset(AccountsController):
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset." "Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links) ).format(asset_depr_schedules_links)
) )
self.set_total_booked_depreciations()
self.total_asset_cost = self.gross_purchase_amount self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status() self.status = self.get_status()
@@ -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."
) )
@@ -419,7 +419,7 @@ class Asset(AccountsController):
if not self.is_existing_asset: if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0 self.opening_accumulated_depreciation = 0
self.number_of_depreciations_booked = 0 self.opening_number_of_booked_depreciations = 0
else: else:
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
if flt(self.opening_accumulated_depreciation) > depreciable_amount: if flt(self.opening_accumulated_depreciation) > depreciable_amount:
@@ -430,15 +430,15 @@ class Asset(AccountsController):
) )
if self.opening_accumulated_depreciation: if self.opening_accumulated_depreciation:
if not self.number_of_depreciations_booked: if not self.opening_number_of_booked_depreciations:
frappe.throw(_("Please set Number of Depreciations Booked")) frappe.throw(_("Please set Opening Number of Booked Depreciations"))
else: else:
self.number_of_depreciations_booked = 0 self.opening_number_of_booked_depreciations = 0
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked): if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
frappe.throw( frappe.throw(
_( _(
"Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked" "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
).format(row.idx), ).format(row.idx),
title=_("Invalid Schedule"), title=_("Invalid Schedule"),
) )
@@ -459,6 +459,17 @@ class Asset(AccountsController):
).format(row.idx) ).format(row.idx)
) )
def set_total_booked_depreciations(self):
# set value of total number of booked depreciations field
for fb_row in self.get("finance_books"):
total_number_of_booked_depreciations = self.opening_number_of_booked_depreciations
depr_schedule = get_depr_schedule(self.name, "Active", fb_row.finance_book)
if depr_schedule:
for je in depr_schedule:
if je.journal_entry:
total_number_of_booked_depreciations += 1
fb_row.db_set("total_number_of_booked_depreciations", total_number_of_booked_depreciations)
def validate_expected_value_after_useful_life(self): def validate_expected_value_after_useful_life(self):
for row in self.get("finance_books"): for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book) depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
@@ -695,11 +706,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 +714,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 +729,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 +1126,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

View File

@@ -323,6 +323,7 @@ def _make_journal_entry_for_depreciation(
if not je.meta.get_workflow(): if not je.meta.get_workflow():
je.submit() je.submit()
asset.reload()
idx = cint(asset_depr_schedule_doc.finance_book_id) idx = cint(asset_depr_schedule_doc.finance_book_id)
row = asset.get("finance_books")[idx - 1] row = asset.get("finance_books")[idx - 1]
row.value_after_depreciation -= depr_schedule.depreciation_amount row.value_after_depreciation -= depr_schedule.depreciation_amount

View File

@@ -355,7 +355,7 @@ class TestAsset(AssetSetup):
purchase_date="2020-04-01", purchase_date="2020-04-01",
expected_value_after_useful_life=0, expected_value_after_useful_life=0,
total_number_of_depreciations=5, total_number_of_depreciations=5,
number_of_depreciations_booked=2, opening_number_of_booked_depreciations=2,
frequency_of_depreciation=12, frequency_of_depreciation=12,
depreciation_start_date="2023-03-31", depreciation_start_date="2023-03-31",
opening_accumulated_depreciation=24000, opening_accumulated_depreciation=24000,
@@ -453,7 +453,7 @@ class TestAsset(AssetSetup):
purchase_date="2020-01-01", purchase_date="2020-01-01",
expected_value_after_useful_life=0, expected_value_after_useful_life=0,
total_number_of_depreciations=6, total_number_of_depreciations=6,
number_of_depreciations_booked=1, opening_number_of_booked_depreciations=1,
frequency_of_depreciation=10, frequency_of_depreciation=10,
depreciation_start_date="2021-01-01", depreciation_start_date="2021-01-01",
opening_accumulated_depreciation=20000, opening_accumulated_depreciation=20000,
@@ -739,7 +739,7 @@ class TestDepreciationMethods(AssetSetup):
calculate_depreciation=1, calculate_depreciation=1,
available_for_use_date="2030-06-06", available_for_use_date="2030-06-06",
is_existing_asset=1, is_existing_asset=1,
number_of_depreciations_booked=2, opening_number_of_booked_depreciations=2,
opening_accumulated_depreciation=47095.89, opening_accumulated_depreciation=47095.89,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2032-12-31", depreciation_start_date="2032-12-31",
@@ -789,7 +789,7 @@ class TestDepreciationMethods(AssetSetup):
available_for_use_date="2030-01-01", available_for_use_date="2030-01-01",
is_existing_asset=1, is_existing_asset=1,
depreciation_method="Double Declining Balance", depreciation_method="Double Declining Balance",
number_of_depreciations_booked=1, opening_number_of_booked_depreciations=1,
opening_accumulated_depreciation=50000, opening_accumulated_depreciation=50000,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2031-12-31", depreciation_start_date="2031-12-31",
@@ -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)
@@ -1123,8 +1123,8 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations_booked(self): def test_opening_booked_depreciations(self):
"""Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is.""" """Tests if an error is raised when opening_number_of_booked_depreciations is not specified when opening_accumulated_depreciation is."""
asset = create_asset( asset = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
@@ -1140,9 +1140,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self): def test_number_of_depreciations(self):
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations.""" """Tests if an error is raised when opening_number_of_booked_depreciations >= total_number_of_depreciations."""
# number_of_depreciations_booked > total_number_of_depreciations # opening_number_of_booked_depreciations > total_number_of_depreciations
asset = create_asset( asset = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
calculate_depreciation=1, calculate_depreciation=1,
@@ -1151,13 +1151,13 @@ class TestDepreciationBasics(AssetSetup):
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2020-07-01", depreciation_start_date="2020-07-01",
opening_accumulated_depreciation=10000, opening_accumulated_depreciation=10000,
number_of_depreciations_booked=5, opening_number_of_booked_depreciations=5,
do_not_save=1, do_not_save=1,
) )
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
# number_of_depreciations_booked = total_number_of_depreciations # opening_number_of_booked_depreciations = total_number_of_depreciations
asset_2 = create_asset( asset_2 = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
calculate_depreciation=1, calculate_depreciation=1,
@@ -1166,7 +1166,7 @@ class TestDepreciationBasics(AssetSetup):
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2020-07-01", depreciation_start_date="2020-07-01",
opening_accumulated_depreciation=10000, opening_accumulated_depreciation=10000,
number_of_depreciations_booked=5, opening_number_of_booked_depreciations=5,
do_not_save=1, do_not_save=1,
) )
@@ -1502,7 +1502,7 @@ class TestDepreciationBasics(AssetSetup):
asset = create_asset(calculate_depreciation=1) asset = create_asset(calculate_depreciation=1)
asset.opening_accumulated_depreciation = 2000 asset.opening_accumulated_depreciation = 2000
asset.number_of_depreciations_booked = 1 asset.opening_number_of_booked_depreciations = 1
asset.finance_books[0].expected_value_after_useful_life = 100 asset.finance_books[0].expected_value_after_useful_life = 100
asset.save() asset.save()
@@ -1696,9 +1696,9 @@ def create_asset(**args):
"purchase_date": args.purchase_date or "2015-01-01", "purchase_date": args.purchase_date or "2015-01-01",
"calculate_depreciation": args.calculate_depreciation or 0, "calculate_depreciation": args.calculate_depreciation or 0,
"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, "opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations 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",
{ {

View File

@@ -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()

View File

@@ -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"

View File

@@ -13,7 +13,7 @@
"column_break_2", "column_break_2",
"gross_purchase_amount", "gross_purchase_amount",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
"number_of_depreciations_booked", "opening_number_of_booked_depreciations",
"finance_book", "finance_book",
"finance_book_id", "finance_book_id",
"depreciation_details_section", "depreciation_details_section",
@@ -171,10 +171,10 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "number_of_depreciations_booked", "fieldname": "opening_number_of_booked_depreciations",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 1, "hidden": 1,
"label": "Number of Depreciations Booked", "label": "Opening Number of Booked Depreciations",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },

View File

@@ -50,7 +50,7 @@ class AssetDepreciationSchedule(Document):
gross_purchase_amount: DF.Currency gross_purchase_amount: DF.Currency
naming_series: DF.Literal["ACC-ADS-.YYYY.-"] naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
notes: DF.SmallText | None notes: DF.SmallText | None
number_of_depreciations_booked: DF.Int opening_number_of_booked_depreciations: DF.Int
opening_accumulated_depreciation: DF.Currency opening_accumulated_depreciation: DF.Currency
rate_of_depreciation: DF.Percent rate_of_depreciation: DF.Percent
shift_based: DF.Check shift_based: DF.Check
@@ -161,7 +161,7 @@ class AssetDepreciationSchedule(Document):
return ( return (
asset_doc.gross_purchase_amount != self.gross_purchase_amount asset_doc.gross_purchase_amount != self.gross_purchase_amount
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked or asset_doc.opening_number_of_booked_depreciations != self.opening_number_of_booked_depreciations
) )
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row): def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
@@ -194,7 +194,7 @@ class AssetDepreciationSchedule(Document):
self.finance_book = row.finance_book self.finance_book = row.finance_book
self.finance_book_id = row.idx self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0 self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0 self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0
self.gross_purchase_amount = asset_doc.gross_purchase_amount self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations self.total_number_of_depreciations = row.total_number_of_depreciations
@@ -263,7 +263,7 @@ class AssetDepreciationSchedule(Document):
row.db_update() row.db_update()
final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint( final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked self.opening_number_of_booked_depreciations
) )
has_pro_rata = _check_is_pro_rata(asset_doc, row) has_pro_rata = _check_is_pro_rata(asset_doc, row)
@@ -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
@@ -327,7 +328,7 @@ class AssetDepreciationSchedule(Document):
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal): if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
from_date = add_months( from_date = add_months(
getdate(asset_doc.available_for_use_date), getdate(asset_doc.available_for_use_date),
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation), (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
) )
if self.depreciation_schedule: if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date from_date = self.depreciation_schedule[-1].schedule_date
@@ -362,18 +363,31 @@ 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(
add_months( add_months(
getdate(asset_doc.available_for_use_date), getdate(asset_doc.available_for_use_date),
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation), (
(self.opening_number_of_booked_depreciations - 1)
* row.frequency_of_depreciation
),
) )
) )
else: else:
from_date = add_months( from_date = add_months(
getdate(add_days(asset_doc.available_for_use_date, -1)), getdate(add_days(asset_doc.available_for_use_date, -1)),
(self.number_of_depreciations_booked * row.frequency_of_depreciation), (self.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
) )
depreciation_amount, days, months = _get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, row,
@@ -389,7 +403,8 @@ class AssetDepreciationSchedule(Document):
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months( asset_doc.to_date = add_months(
asset_doc.available_for_use_date, asset_doc.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), (n + self.opening_number_of_booked_depreciations)
* cint(row.frequency_of_depreciation),
) )
depreciation_amount_without_pro_rata = depreciation_amount depreciation_amount_without_pro_rata = depreciation_amount
@@ -535,7 +550,7 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False has_pro_rata = False
# if not existing asset, from_date = available_for_use_date # if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022 # from_date = 01/01/2022
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly) from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1 days = date_diff(row.depreciation_start_date, from_date) + 1
@@ -556,12 +571,12 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
if wdv_or_dd_non_yearly: if wdv_or_dd_non_yearly:
return add_months( return add_months(
asset_doc.available_for_use_date, asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * 12), (asset_doc.opening_number_of_booked_depreciations * 12),
) )
else: else:
return add_months( return add_months(
asset_doc.available_for_use_date, asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation), (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
) )
@@ -599,11 +614,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 +630,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 +654,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,46 +674,32 @@ 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)
- flt(asset.opening_accumulated_depreciation) - flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) ) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
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):
@@ -740,7 +708,7 @@ def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
flt(asset.gross_purchase_amount) flt(asset.gross_purchase_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)
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) ) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
asset_shift_factors_map = get_asset_shift_factors_map() asset_shift_factors_map = get_asset_shift_factors_map()
shift = ( shift = (
@@ -779,6 +747,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 +757,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 +769,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 +828,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 = []

View File

@@ -3,10 +3,15 @@
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.depreciation import (
post_depreciation_entries,
)
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 +30,168 @@ 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_method(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)
def test_update_total_number_of_booked_depreciations(self):
# check if updates total number of booked depreciations when depreciation gets booked
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
opening_accumulated_depreciation=2000,
opening_number_of_booked_depreciations=2,
depreciation_method="Straight Line",
available_for_use_date="2020-03-01",
depreciation_start_date="2020-03-31",
frequency_of_depreciation=1,
total_number_of_depreciations=24,
submit=1,
)
post_depreciation_entries(date="2021-03-31")
asset.reload()
"""
opening_number_of_booked_depreciations = 2
number_of_booked_depreciations till 2021-03-31 = 13
total_number_of_booked_depreciations = 15
"""
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 15)
# cancel depreciation entry
depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
frappe.get_doc("Journal Entry", depr_entry).cancel()
asset.reload()
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)

View File

@@ -8,6 +8,7 @@
"finance_book", "finance_book",
"depreciation_method", "depreciation_method",
"total_number_of_depreciations", "total_number_of_depreciations",
"total_number_of_booked_depreciations",
"daily_prorata_based", "daily_prorata_based",
"shift_based", "shift_based",
"column_break_5", "column_break_5",
@@ -104,12 +105,19 @@
"fieldname": "shift_based", "fieldname": "shift_based",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Depreciate based on shifts" "label": "Depreciate based on shifts"
},
{
"default": "0",
"fieldname": "total_number_of_booked_depreciations",
"fieldtype": "Int",
"label": "Total Number of Booked Depreciations ",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:06:34.342264", "modified": "2024-05-21 15:48:20.907250",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",

View File

@@ -28,6 +28,7 @@ class AssetFinanceBook(Document):
rate_of_depreciation: DF.Percent rate_of_depreciation: DF.Percent
salvage_value_percentage: DF.Percent salvage_value_percentage: DF.Percent
shift_based: DF.Check shift_based: DF.Check
total_number_of_booked_depreciations: DF.Int
total_number_of_depreciations: DF.Int total_number_of_depreciations: DF.Int
value_after_depreciation: DF.Currency value_after_depreciation: DF.Currency
# end: auto-generated types # end: auto-generated types

View File

@@ -377,7 +377,7 @@ class AssetRepair(AccountsController):
def calculate_last_schedule_date(self, asset, row, extra_months): def calculate_last_schedule_date(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset.number_of_depreciations_booked asset.opening_number_of_booked_depreciations
) )
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
@@ -410,7 +410,7 @@ class AssetRepair(AccountsController):
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset.number_of_depreciations_booked asset.opening_number_of_booked_depreciations
) )
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)

View File

@@ -305,6 +305,7 @@ def create_asset_repair(**args):
"serial_nos": args.serial_no, "serial_nos": args.serial_no,
"posting_date": today(), "posting_date": today(),
"posting_time": nowtime(), "posting_time": nowtime(),
"do_not_submit": 1,
} }
) )
).name ).name

View File

@@ -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)

View File

@@ -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()

View File

@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
method: "frappe.desk.doctype.tag.tag.get_tagged_docs", method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
args: { args: {
doctype: "Supplier", doctype: "Supplier",
tag: args.tag, tag: "%" + args.tag + "%",
}, },
callback: load_suppliers, callback: load_suppliers,
}); });

View File

@@ -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",

View File

@@ -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": [

View File

@@ -2,3 +2,10 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters); frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
frappe.query_reports["Purchase Order Trends"]["filters"].push({
fieldname: "include_closed_orders",
label: __("Include Closed Orders"),
fieldtype: "Check",
default: 0,
});

View File

@@ -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) => {

View File

@@ -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,
)

View File

@@ -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"
): ):
@@ -1939,7 +1942,7 @@ class AccountsController(TransactionBase):
def set_advance_payment_status(self): def set_advance_payment_status(self):
new_status = None new_status = None
stati = frappe.get_list( stati = frappe.get_all(
"Payment Request", "Payment Request",
{ {
"reference_doctype": self.doctype, "reference_doctype": self.doctype,
@@ -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:

View File

@@ -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,

View File

@@ -427,6 +427,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
& (stock_ledger_entry.batch_no.isnotnull()) & (stock_ledger_entry.batch_no.isnotnull())
) )
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse) .groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
.having(Sum(stock_ledger_entry.actual_qty) > 0)
.offset(start) .offset(start)
.limit(page_len) .limit(page_len)
) )
@@ -477,6 +478,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull()) & (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
) )
.groupby(bundle.batch_no, bundle.warehouse) .groupby(bundle.batch_no, bundle.warehouse)
.having(Sum(bundle.qty) > 0)
.offset(start) .offset(start)
.limit(page_len) .limit(page_len)
) )

View File

@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections import defaultdict
import frappe 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 flt, format_datetime, get_datetime from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.rejected_warehouse = "" target_doc.rejected_warehouse = ""
target_doc.warehouse = source_doc.rejected_warehouse target_doc.warehouse = source_doc.rejected_warehouse
target_doc.received_qty = target_doc.qty target_doc.received_qty = target_doc.qty
target_doc.return_qty_from_rejected_warehouse = 1
elif doctype == "Purchase Invoice": elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return
if source_doc.item_code: if (
(source_doc.serial_no or source_doc.batch_no)
and not source_doc.serial_and_batch_bundle
and not source_doc.use_serial_batch_fields
):
target_doc.set("use_serial_batch_fields", 1)
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
item_details = frappe.get_cached_value( item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
) )
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if not item_details.has_batch_no and not item_details.has_serial_no: if not item_details.has_batch_no and not item_details.has_serial_no:
return return
if not target_doc.get("use_serial_batch_fields"): update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
for qty_field in ["stock_qty", "rejected_qty"]:
if not target_doc.get(qty_field):
continue
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
elif target_doc.get("use_serial_batch_fields"):
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent): def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -839,3 +841,229 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
batches.update(get_batches_from_bundle(ids)) batches.update(get_batches_from_bundle(ids))
return batches return batches
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
if not available_dict:
frappe.throw(_("No Serial / Batches are available for return"))
return available_dict
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
if not _bundle_ids:
return frappe._dict({})
return get_serial_batches_based_on_bundle(field, _bundle_ids)
def get_serial_batches_based_on_bundle(field, _bundle_ids):
available_dict = frappe._dict({})
batch_serial_nos = frappe.get_all(
"Serial and Batch Bundle",
fields=[
"`tabSerial and Batch Entry`.`serial_no`",
"`tabSerial and Batch Entry`.`batch_no`",
"`tabSerial and Batch Entry`.`qty`",
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
"`tabSerial and Batch Bundle`.`voucher_type`",
"`tabSerial and Batch Bundle`.`voucher_no`",
],
filters=[
["Serial and Batch Bundle", "name", "in", _bundle_ids],
["Serial and Batch Entry", "docstatus", "=", 1],
],
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
)
for row in batch_serial_nos:
key = row.voucher_detail_no
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
if key not in available_dict:
available_dict[key] = frappe._dict(
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
)
available_dict[key]["qty"] += row.qty
if row.serial_no:
available_dict[key]["serial_nos"][row.serial_no] += row.qty
elif row.batch_no:
available_dict[key]["batches"][row.batch_no] += row.qty
return available_dict
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
pluck_field = "serial_and_batch_bundle"
if is_rejected:
del filters["serial_and_batch_bundle"]
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
pluck_field = "rejected_serial_and_batch_bundle"
_bundle_ids = frappe.get_all(
doctype,
filters=filters,
pluck=pluck_field,
)
if not _bundle_ids:
return {}
del filters["name"]
filters[field] = ("in", reference_ids)
if not is_rejected:
_bundle_ids.extend(
frappe.get_all(
doctype,
filters=filters,
pluck="serial_and_batch_bundle",
)
)
else:
fields = [
"serial_and_batch_bundle",
]
if is_rejected:
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
data = frappe.get_all(
doctype,
fields=fields,
filters=filters,
)
for d in data:
if is_rejected:
if d.get("return_qty_from_rejected_warehouse"):
_bundle_ids.append(d.get("serial_and_batch_bundle"))
else:
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
else:
_bundle_ids.append(d.get("serial_and_batch_bundle"))
return _bundle_ids
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
if not qty_field:
qty_field = "qty"
if not warehouse_field:
warehouse_field = "warehouse"
warehouse = row.get(warehouse_field)
qty = abs(row.get(qty_field))
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
if data.serial_nos:
available_serial_nos = []
for serial_no, sn_qty in data.serial_nos.items():
if sn_qty != 0:
available_serial_nos.append(serial_no)
if available_serial_nos:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
available_serial_nos = get_available_serial_nos(available_serial_nos)
if len(available_serial_nos) > qty:
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
else:
filterd_serial_batch["serial_nos"] = available_serial_nos
elif data.batches:
for batch_no, batch_qty in data.batches.items():
if parent_doc.get("is_internal_customer"):
batch_qty = batch_qty * -1
if batch_qty <= 0:
continue
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
batch_qty = get_available_batch_qty(
parent_doc,
batch_no,
warehouse,
)
if batch_qty <= 0:
frappe.throw(
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
title=_("Batch Not Available for Return"),
)
if qty <= 0:
break
if batch_qty > qty:
filterd_serial_batch["batches"][batch_no] = qty
qty = 0
else:
filterd_serial_batch["batches"][batch_no] += batch_qty
qty -= batch_qty
return filterd_serial_batch
def get_available_batch_qty(parent_doc, batch_no, warehouse):
from erpnext.stock.doctype.batch.batch import get_batch_qty
return get_batch_qty(
batch_no,
warehouse,
posting_date=parent_doc.posting_date,
posting_time=parent_doc.posting_time,
for_stock_levels=True,
)
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
type_of_transaction = "Outward"
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
type_of_transaction = "Inward"
if not warehouse_field:
warehouse_field = "warehouse"
warehouse = child_doc.get(warehouse_field)
if parent_doc.get("is_internal_customer"):
warehouse = child_doc.get("target_warehouse")
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"item_code": child_doc.item_code,
"warehouse": warehouse,
"serial_nos": data.get("serial_nos"),
"batches": data.get("batches"),
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
"voucher_type": parent_doc.doctype,
"voucher_no": parent_doc.name,
"voucher_detail_no": child_doc.name,
"qty": child_doc.qty,
"company": parent_doc.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
return cls_obj.name
def get_available_serial_nos(serial_nos, warehouse):
return frappe.get_all(
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
)

View File

@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
) )
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.controllers.sales_and_purchase_return import (
available_serial_batch_for_return,
filter_serial_batches,
make_serial_batch_bundle_for_return,
)
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension, get_evaluated_inventory_dimension,
@@ -205,6 +210,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,
} }
@@ -216,6 +222,125 @@ class StockController(AccountsController):
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True) self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
self.create_serial_batch_bundle(bundle_details, row) self.create_serial_batch_bundle(bundle_details, row)
def make_bundle_for_sales_purchase_return(self, table_name=None):
if not self.get("is_return"):
return
if not table_name:
table_name = "items"
self.make_bundle_for_non_rejected_qty(table_name)
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
self.make_bundle_for_rejected_qty(table_name)
def make_bundle_for_rejected_qty(self, table_name=None):
field, reference_ids = self.get_reference_ids(
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
)
if not reference_ids:
return
child_doctype = self.doctype + " Item"
available_dict = available_serial_batch_for_return(
field, child_doctype, reference_ids, is_rejected=True
)
for row in self.get(table_name):
if data := available_dict.get(row.get(field)):
qty_field = "rejected_qty"
warehouse_field = "rejected_warehouse"
if row.get("return_qty_from_rejected_warehouse"):
qty_field = "qty"
warehouse_field = "warehouse"
data = filter_serial_batches(
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
)
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
if row.get("return_qty_from_rejected_warehouse"):
row.db_set(
{
"serial_and_batch_bundle": bundle,
"batch_no": "",
"serial_no": "",
}
)
else:
row.db_set(
{
"rejected_serial_and_batch_bundle": bundle,
"batch_no": "",
"rejected_serial_no": "",
}
)
def make_bundle_for_non_rejected_qty(self, table_name):
field, reference_ids = self.get_reference_ids(table_name)
if not reference_ids:
return
child_doctype = self.doctype + " Item"
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
for row in self.get(table_name):
if data := available_dict.get(row.get(field)):
data = filter_serial_batches(self, data, row)
bundle = make_serial_batch_bundle_for_return(data, row, self)
row.db_set(
{
"serial_and_batch_bundle": bundle,
"batch_no": "",
"serial_no": "",
}
)
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
field = {
"Sales Invoice": "sales_invoice_item",
"Delivery Note": "dn_detail",
"Purchase Receipt": "purchase_receipt_item",
"Purchase Invoice": "purchase_invoice_item",
"POS Invoice": "pos_invoice_item",
}.get(self.doctype)
if not bundle_field:
bundle_field = "serial_and_batch_bundle"
if not qty_field:
qty_field = "qty"
reference_ids = []
for row in self.get(table_name):
if not self.is_serial_batch_item(row.item_code):
continue
if (
row.get(field)
and (
qty_field == "qty"
and not row.get("return_qty_from_rejected_warehouse")
or qty_field == "rejected_qty"
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
)
and not row.get("use_serial_batch_fields")
and not row.get(bundle_field)
):
reference_ids.append(row.get(field))
return field, reference_ids
@frappe.request_cache
def is_serial_batch_item(self, item_code) -> bool:
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
if item_details.has_serial_no or item_details.has_batch_no:
return True
return False
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False): def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -610,35 +735,16 @@ class StockController(AccountsController):
def make_package_for_transfer( def make_package_for_transfer(
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
): ):
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle) return make_bundle_for_material_transfer(
is_new=self.is_new(),
if not type_of_transaction: docstatus=self.docstatus,
type_of_transaction = "Inward" voucher_type=self.doctype,
voucher_no=self.name,
bundle_doc = frappe.copy_doc(bundle_doc) serial_and_batch_bundle=serial_and_batch_bundle,
bundle_doc.warehouse = warehouse warehouse=warehouse,
bundle_doc.type_of_transaction = type_of_transaction type_of_transaction=type_of_transaction,
bundle_doc.voucher_type = self.doctype do_not_submit=do_not_submit,
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name )
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
row.is_outward = 0
row.qty = abs(row.qty)
row.stock_value_difference = abs(row.stock_value_difference)
if type_of_transaction == "Outward":
row.qty *= -1
row.stock_value_difference *= row.stock_value_difference
row.is_outward = 1
row.warehouse = warehouse
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name
def get_sl_entries(self, d, args): def get_sl_entries(self, d, args):
sl_dict = frappe._dict( sl_dict = frappe._dict(
@@ -1227,8 +1333,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 +1467,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 +1487,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 +1510,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 = {}
@@ -1551,3 +1662,38 @@ def create_item_wise_repost_entries(
repost_entries.append(repost_entry) repost_entries.append(repost_entry)
return repost_entries return repost_entries
def make_bundle_for_material_transfer(**kwargs):
if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs)
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
if not kwargs.type_of_transaction:
kwargs.type_of_transaction = "Inward"
bundle_doc = frappe.copy_doc(bundle_doc)
bundle_doc.warehouse = kwargs.warehouse
bundle_doc.type_of_transaction = kwargs.type_of_transaction
bundle_doc.voucher_type = kwargs.voucher_type
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
row.is_outward = 0
row.qty = abs(row.qty)
row.stock_value_difference = abs(row.stock_value_difference)
if kwargs.type_of_transaction == "Outward":
row.qty *= -1
row.stock_value_difference *= row.stock_value_difference
row.is_outward = 1
row.warehouse = kwargs.warehouse
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name

View File

@@ -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):

View File

@@ -74,8 +74,10 @@ def get_data(filters, conditions):
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]: if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL" cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
cond += " and t1.status != 'Closed'" if not filters.get("include_closed_orders"):
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
cond += " and t1.status != 'Closed'"
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer": if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
cond += " and t1.quotation_to = 'Customer'" cond += " and t1.quotation_to = 'Customer'"

View File

@@ -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",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More