Merge branch 'develop' into bank-trans-party-automatch

This commit is contained in:
Marica 2023-04-17 16:34:46 +05:30 committed by GitHub
commit fd38e8e0af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 359 additions and 240 deletions

View File

@ -3,13 +3,13 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/loan_management/ @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
@ -18,12 +18,8 @@ erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/projects/ @ruchamahabal
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
.github/ @deepeshgarg007
pyproject.toml @ankush

View File

@ -18,7 +18,6 @@
"root_type",
"report_type",
"account_currency",
"inter_company_account",
"column_break1",
"parent_account",
"account_type",
@ -34,15 +33,11 @@
{
"fieldname": "properties",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
"oldfieldtype": "Section Break"
},
{
"fieldname": "column_break0",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%"
},
{
@ -53,9 +48,7 @@
"no_copy": 1,
"oldfieldname": "account_name",
"oldfieldtype": "Data",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "account_number",
@ -63,17 +56,13 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account Number",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"default": "0",
"fieldname": "is_group",
"fieldtype": "Check",
"label": "Is Group",
"show_days": 1,
"show_seconds": 1
"label": "Is Group"
},
{
"fieldname": "company",
@ -85,9 +74,7 @@
"options": "Company",
"read_only": 1,
"remember_last_selected_value": 1,
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "root_type",
@ -95,9 +82,7 @@
"in_standard_filter": 1,
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "report_type",
@ -105,32 +90,18 @@
"in_standard_filter": 1,
"label": "Report Type",
"options": "\nBalance Sheet\nProfit and Loss",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"depends_on": "eval:doc.is_group==0",
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "inter_company_account",
"fieldtype": "Check",
"label": "Inter Company Account",
"show_days": 1,
"show_seconds": 1
"options": "Currency"
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%"
},
{
@ -142,9 +113,7 @@
"oldfieldtype": "Link",
"options": "Account",
"reqd": 1,
"search_index": 1,
"show_days": 1,
"show_seconds": 1
"search_index": 1
},
{
"description": "Setting Account Type helps in selecting this Account in transactions.",
@ -154,9 +123,7 @@
"label": "Account Type",
"oldfieldname": "account_type",
"oldfieldtype": "Select",
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"show_days": 1,
"show_seconds": 1
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
},
{
"description": "Rate at which this tax is applied",
@ -164,9 +131,7 @@
"fieldtype": "Float",
"label": "Rate",
"oldfieldname": "tax_rate",
"oldfieldtype": "Currency",
"show_days": 1,
"show_seconds": 1
"oldfieldtype": "Currency"
},
{
"description": "If the account is frozen, entries are allowed to restricted users.",
@ -175,17 +140,13 @@
"label": "Frozen",
"oldfieldname": "freeze_account",
"oldfieldtype": "Select",
"options": "No\nYes",
"show_days": 1,
"show_seconds": 1
"options": "No\nYes"
},
{
"fieldname": "balance_must_be",
"fieldtype": "Select",
"label": "Balance must be",
"options": "\nDebit\nCredit",
"show_days": 1,
"show_seconds": 1
"options": "\nDebit\nCredit"
},
{
"fieldname": "lft",
@ -194,9 +155,7 @@
"label": "Lft",
"print_hide": 1,
"read_only": 1,
"search_index": 1,
"show_days": 1,
"show_seconds": 1
"search_index": 1
},
{
"fieldname": "rgt",
@ -205,9 +164,7 @@
"label": "Rgt",
"print_hide": 1,
"read_only": 1,
"search_index": 1,
"show_days": 1,
"show_seconds": 1
"search_index": 1
},
{
"fieldname": "old_parent",
@ -215,33 +172,27 @@
"hidden": 1,
"label": "Old Parent",
"print_hide": 1,
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
"fieldname": "include_in_gross",
"fieldtype": "Check",
"label": "Include in gross",
"show_days": 1,
"show_seconds": 1
"label": "Include in gross"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disable",
"show_days": 1,
"show_seconds": 1
"label": "Disable"
}
],
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2020-06-11 15:15:54.338622",
"modified": "2023-04-11 16:08:46.983677",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@ -301,5 +252,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@ -186,6 +186,7 @@
},
{
"default": "0",
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order"
@ -389,7 +390,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-04-04 16:20:41.330039",
"modified": "2023-04-14 17:22:03.680886",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -287,10 +287,6 @@ class TestJournalEntry(unittest.TestCase):
jv.submit()
def test_inter_company_jv(self):
frappe.db.set_value("Account", "Sales Expenses - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
jv = make_journal_entry(
"Sales Expenses - _TC",
"Buildings - _TC",

View File

@ -970,29 +970,47 @@ frappe.ui.form.on('Payment Entry', {
},
callback: function(r, rt) {
if(r.message) {
var write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
const write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
return t.account==r.message[account] ? t : null; });
var row = [];
var difference_amount = flt(frm.doc.difference_amount,
const difference_amount = flt(frm.doc.difference_amount,
precision("difference_amount"));
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = r.message[account];
row.cost_center = r.message["cost_center"];
} else {
row = write_off_row[0];
}
const add_deductions = (details) => {
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = details[account];
row.cost_center = details["cost_center"];
} else {
row = write_off_row[0];
}
if (row) {
row.amount = flt(row.amount) + difference_amount;
} else {
frappe.msgprint(__("No gain or loss in the exchange rate"))
}
if (row) {
row.amount = flt(row.amount) + difference_amount;
} else {
frappe.msgprint(__("No gain or loss in the exchange rate"))
}
refresh_field("deductions");
};
refresh_field("deductions");
if (!r.message[account]) {
frappe.prompt({
label: __("Please Specify Account"),
fieldname: account,
fieldtype: "Link",
options: "Account",
get_query: () => ({
filters: {
company: frm.doc.company,
}
})
}, (values) => {
const details = Object.assign({}, r.message, values);
add_deductions(details);
}, __(frappe.unscrub(account)));
} else {
add_deductions(r.message);
}
frm.events.set_unallocated_amount(frm);
}

View File

@ -1594,17 +1594,7 @@ def get_account_details(account, date, cost_center=None):
@frappe.whitelist()
def get_company_defaults(company):
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
ret = frappe.get_cached_value("Company", company, fields, as_dict=1)
for fieldname in fields:
if not ret[fieldname]:
frappe.throw(
_("Please set default {0} in Company {1}").format(
frappe.get_meta("Company").get_label(fieldname), company
)
)
return ret
return frappe.get_cached_value("Company", company, fields, as_dict=1)
def get_outstanding_on_journal_entry(name):
@ -1764,7 +1754,12 @@ def get_payment_entry(
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
else:
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_cached_value(
if doc.doctype in (
"Sales Invoice",
"Purchase Invoice",
"Purchase Order",
"Sales Order",
) and frappe.get_cached_value(
"Payment Terms Template",
doc.payment_terms_template,
"allocate_payment_based_on_payment_terms",

View File

@ -24,7 +24,6 @@ class TestGeneralLedger(FrappeTestCase):
"root_type": "Asset",
"report_type": "Balance Sheet",
"account_currency": "USD",
"inter_company_account": 0,
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
"doctype": "Account",

View File

@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("Period Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"default": "Fiscal Year",
"reqd": 1
},
{
@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link",
options: "Asset Category"
},
{
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book"
},
{
fieldname:"cost_center",
label: __("Cost Center"),
@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
reqd: 1
},
{
fieldname:"is_existing_asset",
label: __("Is Existing Asset"),
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
depends_on: "eval: doc.only_depreciable_assets == 1",
},
{
fieldname:"only_depreciable_assets",
label: __("Only depreciable assets"),
fieldtype: "Check"
},
{
fieldname:"only_existing_assets",
label: __("Only existing assets"),
fieldtype: "Check"
},
]

View File

@ -45,8 +45,10 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("is_existing_asset"):
conditions["is_existing_asset"] = filters.get("is_existing_asset")
if filters.get("only_depreciable_assets"):
conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"):
@ -102,19 +104,18 @@ def get_data(filters):
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
assets_linked_to_fb = None
if filters.only_depreciable_assets:
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
for asset in assets_record:
if filters.finance_book:
if asset.asset_id not in assets_linked_to_fb:
continue
else:
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
continue
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
continue
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
row = {
@ -172,11 +173,11 @@ def prepare_chart_data(data, filters):
"datasets": [
{
"name": _("Asset Value"),
"values": [d.get("asset_value") for d in labels_values_map.values()],
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
},
{
"name": _("Depreciatied Amount"),
"values": [d.get("depreciated_amount") for d in labels_values_map.values()],
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
},
],
},
@ -312,7 +313,7 @@ def get_columns(filters):
return [
{
"label": _("Asset Id"),
"label": _("Asset ID"),
"fieldtype": "Link",
"fieldname": "asset_id",
"options": "Asset",

View File

@ -273,8 +273,8 @@ class AccountsController(TransactionBase):
self.validate_payment_schedule_dates()
self.set_due_date()
self.set_payment_schedule()
self.validate_payment_schedule_amount()
if not self.get("ignore_default_payment_terms_template"):
self.validate_payment_schedule_amount()
self.validate_due_date()
self.validate_advance_entries()
@ -515,7 +515,6 @@ class AccountsController(TransactionBase):
parent_dict.update({"customer": parent_dict.get("party_name")})
self.pricing_rules = []
basic_item_details_map = {}
for item in self.get("items"):
if item.get("item_code"):
@ -535,17 +534,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
basic_details = basic_item_details_map.get(item.item_code)
ret, basic_item_details = get_item_details(
args,
self,
for_validate=True,
overwrite_warehouse=False,
return_basic_details=True,
basic_details=basic_details,
)
basic_item_details_map.setdefault(item.item_code, basic_item_details)
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
@ -1618,6 +1607,7 @@ class AccountsController(TransactionBase):
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
automatically_fetch_payment_terms = 0
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
@ -1663,19 +1653,20 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
)
d.base_payment_amount = flt(
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
)
d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
if not automatically_fetch_payment_terms:
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
)
d.base_payment_amount = flt(
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
)
d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
def get_order_details(self):
if self.doctype == "Sales Invoice":
@ -1728,6 +1719,10 @@ class AccountsController(TransactionBase):
"invoice_portion": schedule.invoice_portion,
"mode_of_payment": schedule.mode_of_payment,
"description": schedule.description,
"payment_amount": schedule.payment_amount,
"base_payment_amount": schedule.base_payment_amount,
"outstanding": schedule.outstanding,
"paid_amount": schedule.paid_amount,
}
if schedule.discount_type == "Percentage":

View File

@ -859,6 +859,8 @@ def is_reposting_pending():
def future_sle_exists(args, sl_entries=None):
key = (args.voucher_type, args.voucher_no)
if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {}
if validate_future_sle_not_exists(args, key, sl_entries):
return False
@ -892,6 +894,9 @@ def future_sle_exists(args, sl_entries=None):
)
for d in data:
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
@ -903,6 +908,9 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
item_key = (args.get("item_code"), args.get("warehouse"))
if not sl_entries and hasattr(frappe.local, "future_sle"):
if key not in frappe.local.future_sle:
return False
if not frappe.local.future_sle.get(key) or (
item_key and item_key not in frappe.local.future_sle.get(key)
):
@ -910,11 +918,8 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
def get_cached_data(args, key):
if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {}
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
return False
if args.get("item_code"):
item_key = (args.get("item_code"), args.get("warehouse"))

View File

@ -2,6 +2,7 @@
"actions": [],
"allow_events_in_timeline": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2022-02-08 13:14:41.083327",
"doctype": "DocType",
@ -515,7 +516,7 @@
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2023-01-24 18:20:05.044791",
"modified": "2023-04-14 18:20:05.044791",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@ -582,4 +583,4 @@
"states": [],
"subject_field": "title",
"title_field": "title"
}
}

View File

@ -1920,7 +1920,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
prompt_user_for_reference_date(){
var me = this;
let me = this;
frappe.prompt({
label: __("Cheque/Reference Date"),
fieldname: "reference_date",
@ -1947,7 +1947,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
if(!is_eligible || !has_payment_schedule) return false;
let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
let has_discount = this.frm.doc.payment_schedule.some(row => row.discount);
return has_discount;
}

View File

@ -704,7 +704,7 @@
"type": "Link"
}
],
"modified": "2022-04-26 13:29:55.087240",
"modified": "2023-04-16 13:29:55.087240",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",

View File

@ -581,6 +581,11 @@
"title": "Bauleistungen nach § 13b UStG",
"is_default": 0,
"taxes": []
},
{
"title": "Nullsteuersatz nach § 12 Abs. 3 UStG",
"is_default": 0,
"taxes": []
}
],
"purchase_tax_templates": [
@ -1339,6 +1344,11 @@
"title": "Bauleistungen nach § 13b UStG",
"is_default": 0,
"taxes": []
},
{
"title": "Nullsteuersatz nach § 12 Abs. 3 UStG",
"is_default": 0,
"taxes": []
}
],
"purchase_tax_templates": [
@ -2097,6 +2107,11 @@
"title": "Bauleistungen nach § 13b UStG",
"is_default": 0,
"taxes": []
},
{
"title": "Nullsteuersatz nach § 12 Abs. 3 UStG",
"is_default": 0,
"taxes": []
}
],
"purchase_tax_templates": [
@ -2849,6 +2864,11 @@
"title": "Bauleistungen nach § 13b UStG",
"is_default": 0,
"taxes": []
},
{
"title": "Nullsteuersatz nach § 12 Abs. 3 UStG",
"is_default": 0,
"taxes": []
}
],
"purchase_tax_templates": [

View File

@ -410,10 +410,10 @@
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"remember_last_selected_value": 1,
"reqd": 1,
"search_index": 1
"search_index": 1,
"set_only_once": 1
},
{
"fieldname": "status",
@ -433,7 +433,7 @@
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
"modified": "2021-12-23 10:44:30.299450",
"modified": "2023-04-14 15:58:46.139887",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
@ -461,7 +461,6 @@
"read": 1,
"report": 1,
"role": "Stock Manager",
"set_user_permissions": 1,
"write": 1
},
{

View File

@ -5,7 +5,7 @@ from typing import Optional
import frappe
from frappe import _, bold, msgprint
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, cstr, flt
import erpnext
@ -571,22 +571,33 @@ class StockReconciliation(StockController):
self._cancel()
def recalculate_current_qty(self, item_code, batch_no):
from erpnext.stock.stock_ledger import get_valuation_rate
sl_entries = []
for row in self.items:
if not (row.item_code == item_code and row.batch_no == batch_no):
continue
row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no)
current_qty = get_batch_qty_for_stock_reco(
item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name
)
qty, val_rate = get_stock_balance(
item_code,
row.warehouse,
self.posting_date,
self.posting_time,
with_valuation_rate=True,
precesion = row.precision("current_qty")
if flt(current_qty, precesion) == flt(row.current_qty, precesion):
continue
val_rate = get_valuation_rate(
item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
)
row.current_valuation_rate = val_rate
if not row.current_qty and current_qty:
sle = self.get_sle_for_items(row)
sle.actual_qty = current_qty * -1
sle.valuation_rate = val_rate
sl_entries.append(sle)
row.current_qty = current_qty
row.db_set(
{
"current_qty": row.current_qty,
@ -595,8 +606,13 @@ class StockReconciliation(StockController):
}
)
if sl_entries:
self.make_sl_entries(sl_entries)
def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no):
def get_batch_qty_for_stock_reco(
item_code, warehouse, batch_no, posting_date, posting_time, voucher_no
):
ledger = frappe.qb.DocType("Stock Ledger Entry")
query = (
@ -610,6 +626,12 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no):
& (ledger.docstatus == 1)
& (ledger.is_cancelled == 0)
& (ledger.batch_no == batch_no)
& (ledger.posting_date <= posting_date)
& (
CombineDatetime(ledger.posting_date, ledger.posting_time)
<= CombineDatetime(posting_date, posting_time)
)
& (ledger.voucher_no != voucher_no)
)
.groupby(ledger.batch_no)
)

View File

@ -676,6 +676,79 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sl_entry.actual_qty), 1.0)
self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0)
def test_backdated_stock_reco_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = self.make_item(
"Test New Batch Item ABCV",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BNS9.####",
"create_new_batch": 1,
},
).name
warehouse = "_Test Warehouse - _TC"
# Added 100 Qty, Balace Qty 100
se1 = make_stock_entry(
item_code=item_code, posting_time="09:00:00", target=warehouse, qty=100, basic_rate=700
)
# Removed 50 Qty, Balace Qty 50
se2 = make_stock_entry(
item_code=item_code,
batch_no=se1.items[0].batch_no,
posting_time="10:00:00",
source=warehouse,
qty=50,
basic_rate=700,
)
# Stock Reco for 100, Balace Qty 100
stock_reco = create_stock_reconciliation(
item_code=item_code,
posting_time="11:00:00",
warehouse=warehouse,
batch_no=se1.items[0].batch_no,
qty=100,
rate=100,
)
# Removed 50 Qty, Balace Qty 50
make_stock_entry(
item_code=item_code,
batch_no=se1.items[0].batch_no,
posting_time="12:00:00",
source=warehouse,
qty=50,
basic_rate=700,
)
self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
# Cancel the backdated Stock Entry se2,
# Since Stock Reco entry in the future the Balace Qty should remain as it's (50)
se2.cancel()
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
self.assertEqual(
frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"),
"Completed",
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
fields=["qty_after_transaction"],
order_by="posting_time desc, creation desc",
)
self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0))
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@ -35,14 +35,7 @@ purchase_doctypes = [
@frappe.whitelist()
def get_item_details(
args,
doc=None,
for_validate=False,
overwrite_warehouse=True,
return_basic_details=False,
basic_details=None,
):
def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
"""
args = {
"item_code": "",
@ -80,12 +73,7 @@ def get_item_details(
if doc.get("doctype") == "Purchase Invoice":
args["bill_date"] = doc.get("bill_date")
if not basic_details:
out = get_basic_details(args, item, overwrite_warehouse)
else:
out = basic_details
basic_details = out.copy()
out = get_basic_details(args, item, overwrite_warehouse)
get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map(
@ -154,11 +142,7 @@ def get_item_details(
out.amount = flt(args.qty) * flt(out.rate)
out = remove_standard_fields(out)
if return_basic_details:
return out, basic_details
else:
return out
return out
def remove_standard_fields(details):

View File

@ -11,6 +11,13 @@ frappe.query_reports["Warehouse Wise Stock Balance"] = {
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"show_disabled_warehouses",
"label": __("Show Disabled Warehouses"),
"fieldtype": "Check",
"default": 0
}
],
"initial_depth": 3,

View File

@ -11,6 +11,7 @@ from frappe.query_builder.functions import Sum
class StockBalanceFilter(TypedDict):
company: Optional[str]
warehouse: Optional[str]
show_disabled_warehouses: Optional[int]
SLEntry = Dict[str, Any]
@ -18,7 +19,7 @@ SLEntry = Dict[str, Any]
def execute(filters=None):
columns, data = [], []
columns = get_columns()
columns = get_columns(filters)
data = get_data(filters)
return columns, data
@ -42,10 +43,14 @@ def get_warehouse_wise_balance(filters: StockBalanceFilter) -> List[SLEntry]:
def get_warehouses(report_filters: StockBalanceFilter):
filters = {"company": report_filters.company, "disabled": 0}
if report_filters.get("show_disabled_warehouses"):
filters["disabled"] = ("in", [0, report_filters.show_disabled_warehouses])
return frappe.get_all(
"Warehouse",
fields=["name", "parent_warehouse", "is_group"],
filters={"company": report_filters.company},
fields=["name", "parent_warehouse", "is_group", "disabled"],
filters=filters,
order_by="lft",
)
@ -90,8 +95,8 @@ def set_balance_in_parent(warehouses):
update_balance(warehouse, warehouse.stock_balance)
def get_columns():
return [
def get_columns(filters: StockBalanceFilter) -> List[Dict]:
columns = [
{
"label": _("Warehouse"),
"fieldname": "name",
@ -101,3 +106,15 @@ def get_columns():
},
{"label": _("Stock Balance"), "fieldname": "stock_balance", "fieldtype": "Float", "width": 150},
]
if filters.get("show_disabled_warehouses"):
columns.append(
{
"label": _("Warehouse Disabled?"),
"fieldname": "disabled",
"fieldtype": "Check",
"width": 200,
}
)
return columns

View File

@ -544,6 +544,14 @@ class update_entries_after(object):
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
if (
sle.voucher_type == "Stock Reconciliation"
and sle.batch_no
and sle.voucher_detail_no
and sle.actual_qty < 0
):
self.reset_actual_qty_for_stock_reco(sle)
if (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no
@ -605,6 +613,16 @@ class update_entries_after(object):
if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle)
def reset_actual_qty_for_stock_reco(self, sle):
current_qty = frappe.get_cached_value(
"Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
)
if current_qty:
sle.actual_qty = current_qty * -1
elif current_qty == 0:
sle.is_cancelled = 1
def validate_negative_stock(self, sle):
"""
validate negative stock for entries current datetime onwards
@ -1369,12 +1387,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
def regenerate_sle_for_batch_stock_reco(detail):
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
doc.docstatus = 2
doc.update_stock_ledger()
doc.recalculate_current_qty(detail.item_code, detail.batch_no)
doc.docstatus = 1
doc.update_stock_ledger()
doc.repost_future_sle_and_gle()
def get_stock_reco_qty_shift(args):
@ -1400,34 +1414,52 @@ def get_stock_reco_qty_shift(args):
return stock_reco_qty_shift
def get_next_stock_reco(args):
def get_next_stock_reco(kwargs):
"""Returns next nearest stock reconciliaton's details."""
return frappe.db.sql(
"""
select
name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty
from
`tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_type = 'Stock Reconciliation'
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
or (
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
and creation > %(creation)s
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(sle)
.select(
sle.name,
sle.posting_date,
sle.posting_time,
sle.creation,
sle.voucher_no,
sle.item_code,
sle.batch_no,
sle.actual_qty,
)
.where(
(sle.item_code == kwargs.get("item_code"))
& (sle.warehouse == kwargs.get("warehouse"))
& (sle.voucher_type == "Stock Reconciliation")
& (sle.voucher_no != kwargs.get("voucher_no"))
& (sle.is_cancelled == 0)
& (
(
CombineDatetime(sle.posting_date, sle.posting_time)
> CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
| (
(
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
& (sle.creation > kwargs.get("creation"))
)
)
)
order by timestamp(posting_date, posting_time) asc, creation asc
limit 1
""",
args,
as_dict=1,
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
)
if kwargs.get("batch_no"):
query.where(sle.batch_no == kwargs.get("batch_no"))
return query.run(as_dict=True)
def get_datetime_limit_condition(detail):
return f"""