Merge branch 'develop' into stock-reservation

This commit is contained in:
s-aga-r 2023-04-22 10:04:46 +05:30
commit 0cd10a50d2
55 changed files with 920 additions and 427 deletions

View File

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

View File

@ -1,3 +1,4 @@
import functools
import inspect import inspect
import frappe import frappe
@ -120,12 +121,14 @@ def get_region(company=None):
You can also set global company flag in `frappe.flags.company` You can also set global company flag in `frappe.flags.company`
""" """
if company or frappe.flags.company:
return frappe.get_cached_value("Company", company or frappe.flags.company, "country") if not company:
elif frappe.flags.country: company = frappe.local.flags.company
return frappe.flags.country
else: if company:
return frappe.get_system_settings("country") return frappe.get_cached_value("Company", company, "country")
return frappe.flags.country or frappe.get_system_settings("country")
def allow_regional(fn): def allow_regional(fn):
@ -136,6 +139,7 @@ def allow_regional(fn):
def myfunction(): def myfunction():
pass""" pass"""
@functools.wraps(fn)
def caller(*args, **kwargs): def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region()) overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}" function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"

View File

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

View File

@ -19,6 +19,8 @@
"column_break_17", "column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"journals_section",
"merge_similar_account_heads",
"report_setting_section", "report_setting_section",
"use_custom_cash_flow", "use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@ -184,6 +186,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms", "fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order" "label": "Automatically Fetch Payment Terms from Order"
@ -368,6 +371,18 @@
"fieldname": "book_tax_discount_loss", "fieldname": "book_tax_discount_loss",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount" "label": "Book Tax Loss on Early Payment Discount"
},
{
"fieldname": "journals_section",
"fieldtype": "Section Break",
"label": "Journals"
},
{
"default": "0",
"description": "Rows with Same Account heads will be merged on Ledger",
"fieldname": "merge_similar_account_heads",
"fieldtype": "Check",
"label": "Merge Similar Account Heads"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -375,7 +390,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-03-28 09:50:20.375233", "modified": "2023-04-17 11:45:42.049247",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -885,6 +885,8 @@ class JournalEntry(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
gl_map = self.build_gl_map() gl_map = self.build_gl_map()
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"): if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
update_outstanding = "No" update_outstanding = "No"
@ -892,7 +894,13 @@ class JournalEntry(AccountsController):
update_outstanding = "Yes" update_outstanding = "Yes"
if gl_map: if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) make_gl_entries(
gl_map,
cancel=cancel,
adv_adj=adv_adj,
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
@frappe.whitelist() @frappe.whitelist()
def get_balance(self, difference_account=None): def get_balance(self, difference_account=None):

View File

@ -287,10 +287,6 @@ class TestJournalEntry(unittest.TestCase):
jv.submit() jv.submit()
def test_inter_company_jv(self): 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( jv = make_journal_entry(
"Sales Expenses - _TC", "Sales Expenses - _TC",
"Buildings - _TC", "Buildings - _TC",

View File

@ -970,29 +970,48 @@ frappe.ui.form.on('Payment Entry', {
}, },
callback: function(r, rt) { callback: function(r, rt) {
if(r.message) { 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; }); return t.account==r.message[account] ? t : null; });
var row = []; const difference_amount = flt(frm.doc.difference_amount,
var difference_amount = flt(frm.doc.difference_amount,
precision("difference_amount")); precision("difference_amount"));
if (!write_off_row.length && difference_amount) { const add_deductions = (details) => {
row = frm.add_child("deductions"); let row = null;
row.account = r.message[account]; if (!write_off_row.length && difference_amount) {
row.cost_center = r.message["cost_center"]; row = frm.add_child("deductions");
} else { row.account = details[account];
row = write_off_row[0]; row.cost_center = details["cost_center"];
} } else {
row = write_off_row[0];
}
if (row) { if (row) {
row.amount = flt(row.amount) + difference_amount; row.amount = flt(row.amount) + difference_amount;
} else { } else {
frappe.msgprint(__("No gain or loss in the exchange rate")) 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); frm.events.set_unallocated_amount(frm);
} }

View File

@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
def validate(self): def validate(self):
self.setup_party_account_field() self.setup_party_account_field()
self.set_missing_values() self.set_missing_values()
self.set_missing_ref_details()
self.validate_payment_type() self.validate_payment_type()
self.validate_party_details() self.validate_party_details()
self.set_exchange_rate() self.set_exchange_rate()
@ -219,11 +220,16 @@ class PaymentEntry(AccountsController):
else self.paid_to_account_currency else self.paid_to_account_currency
) )
self.set_missing_ref_details() def set_missing_ref_details(
self, force: bool = False, update_ref_details_only_for: list | None = None
def set_missing_ref_details(self, force=False): ) -> None:
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount: if d.allocated_amount:
if update_ref_details_only_for and (
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
):
continue
ref_details = get_reference_details( ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency d.reference_doctype, d.reference_name, self.party_account_currency
) )
@ -1594,17 +1600,7 @@ def get_account_details(account, date, cost_center=None):
@frappe.whitelist() @frappe.whitelist()
def get_company_defaults(company): def get_company_defaults(company):
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"] fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
ret = frappe.get_cached_value("Company", company, fields, as_dict=1) return 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
def get_outstanding_on_journal_entry(name): def get_outstanding_on_journal_entry(name):
@ -1764,7 +1760,12 @@ def get_payment_entry(
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date)) frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
else: 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", "Payment Terms Template",
doc.payment_terms_template, doc.payment_terms_template,
"allocate_payment_based_on_payment_terms", "allocate_payment_based_on_payment_terms",
@ -1816,6 +1817,7 @@ def get_payment_entry(
pe.setup_party_account_field() pe.setup_party_account_field()
pe.set_missing_values() pe.set_missing_values()
pe.set_missing_ref_details()
update_accounting_dimensions(pe, doc) update_accounting_dimensions(pe, doc)

View File

@ -334,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
} }
make_inter_company_invoice() { make_inter_company_invoice() {
let me = this;
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice", method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
frm: me.frm frm: me.frm

View File

@ -259,6 +259,8 @@ def set_address_details(
) )
if doctype in TRANSACTION_TYPES: if doctype in TRANSACTION_TYPES:
# required to set correct region
frappe.flags.company = company
get_regional_address_details(party_details, doctype, company) get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address return party_address, shipping_address

View File

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

View File

@ -646,6 +646,7 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field() payment_entry.setup_party_account_field()
payment_entry.set_missing_values() payment_entry.set_missing_values()
payment_entry.set_missing_ref_details()
payment_entry.set_amounts() payment_entry.set_amounts()
if not do_not_save: if not do_not_save:

View File

@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("Period Based On"), "label": __("Period Based On"),
"fieldtype": "Select", "fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"], "options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"], "default": "Fiscal Year",
"reqd": 1 "reqd": 1
}, },
{ {
@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link", fieldtype: "Link",
options: "Asset Category" options: "Asset Category"
}, },
{
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book"
},
{ {
fieldname:"cost_center", fieldname:"cost_center",
label: __("Cost Center"), label: __("Cost Center"),
@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
reqd: 1 reqd: 1
}, },
{ {
fieldname:"is_existing_asset", fieldname:"finance_book",
label: __("Is Existing Asset"), 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" fieldtype: "Check"
}, },
] ]

View File

@ -45,8 +45,10 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date) filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("is_existing_asset"): if filters.get("only_depreciable_assets"):
conditions["is_existing_asset"] = filters.get("is_existing_asset") 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"): if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category") conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"): 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_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
assets_linked_to_fb = frappe.db.get_all( assets_linked_to_fb = None
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")}, if filters.only_depreciable_assets:
pluck="parent", 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: for asset in assets_record:
if filters.finance_book: if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
if asset.asset_id not in assets_linked_to_fb: continue
continue
else:
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
continue
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
row = { row = {
@ -172,11 +173,11 @@ def prepare_chart_data(data, filters):
"datasets": [ "datasets": [
{ {
"name": _("Asset Value"), "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"), "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 [ return [
{ {
"label": _("Asset Id"), "label": _("Asset ID"),
"fieldtype": "Link", "fieldtype": "Link",
"fieldname": "asset_id", "fieldname": "asset_id",
"options": "Asset", "options": "Asset",

View File

@ -495,6 +495,7 @@
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items",
"oldfieldname": "po_details", "oldfieldname": "po_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Order Item", "options": "Purchase Order Item",
@ -1100,8 +1101,7 @@
{ {
"fieldname": "before_items_section", "fieldname": "before_items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1, "hide_border": 1
"label": "Items"
}, },
{ {
"fieldname": "items_col_break", "fieldname": "items_col_break",
@ -1271,7 +1271,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-28 18:59:16.322824", "modified": "2023-04-14 16:42:29.448464",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -310,7 +310,6 @@
"fieldname": "items_section", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1, "hide_border": 1,
"label": "Items",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart" "options": "fa fa-shopping-cart"
}, },
@ -318,6 +317,7 @@
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items",
"oldfieldname": "po_details", "oldfieldname": "po_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Supplier Quotation Item", "options": "Supplier Quotation Item",
@ -844,7 +844,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:35:39.740974", "modified": "2023-04-14 16:43:41.714832",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@ -5,7 +5,7 @@
import json import json
import frappe import frappe
from frappe import _, throw from frappe import _, bold, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Abs, Sum from frappe.query_builder.functions import Abs, Sum
from frappe.utils import ( from frappe.utils import (
@ -273,8 +273,8 @@ class AccountsController(TransactionBase):
self.validate_payment_schedule_dates() self.validate_payment_schedule_dates()
self.set_due_date() self.set_due_date()
self.set_payment_schedule() self.set_payment_schedule()
self.validate_payment_schedule_amount()
if not self.get("ignore_default_payment_terms_template"): if not self.get("ignore_default_payment_terms_template"):
self.validate_payment_schedule_amount()
self.validate_due_date() self.validate_due_date()
self.validate_advance_entries() self.validate_advance_entries()
@ -405,6 +405,15 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself") msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing")) frappe.throw(msg, title=_("Internal Sales Reference Missing"))
label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
field = frappe.scrub(label)
for row in self.get("items"):
if not row.get(field):
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
def disable_pricing_rule_on_internal_transfer(self): def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer(): if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1 self.ignore_pricing_rule = 1
@ -515,7 +524,6 @@ class AccountsController(TransactionBase):
parent_dict.update({"customer": parent_dict.get("party_name")}) parent_dict.update({"customer": parent_dict.get("party_name")})
self.pricing_rules = [] self.pricing_rules = []
basic_item_details_map = {}
for item in self.get("items"): for item in self.get("items"):
if item.get("item_code"): if item.get("item_code"):
@ -535,17 +543,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"): if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted args["is_subcontracted"] = self.is_subcontracted
basic_details = basic_item_details_map.get(item.item_code) ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
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)
for fieldname, value in ret.items(): for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None: if item.meta.get_field(fieldname) and value is not None:
@ -1618,6 +1616,7 @@ class AccountsController(TransactionBase):
base_grand_total = self.get("base_rounded_total") or self.base_grand_total base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.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"): if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount) base_grand_total = base_grand_total - flt(self.base_write_off_amount)
@ -1663,19 +1662,20 @@ class AccountsController(TransactionBase):
) )
self.append("payment_schedule", data) self.append("payment_schedule", data)
for d in self.get("payment_schedule"): if not automatically_fetch_payment_terms:
if d.invoice_portion: for d in self.get("payment_schedule"):
d.payment_amount = flt( if d.invoice_portion:
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount") 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.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.outstanding = d.payment_amount
d.base_payment_amount = flt( elif not d.invoice_portion:
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") d.base_payment_amount = flt(
) d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
def get_order_details(self): def get_order_details(self):
if self.doctype == "Sales Invoice": if self.doctype == "Sales Invoice":
@ -1728,6 +1728,10 @@ class AccountsController(TransactionBase):
"invoice_portion": schedule.invoice_portion, "invoice_portion": schedule.invoice_portion,
"mode_of_payment": schedule.mode_of_payment, "mode_of_payment": schedule.mode_of_payment,
"description": schedule.description, "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": if schedule.discount_type == "Percentage":

View File

@ -859,6 +859,8 @@ def is_reposting_pending():
def future_sle_exists(args, sl_entries=None): def future_sle_exists(args, sl_entries=None):
key = (args.voucher_type, args.voucher_no) 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): if validate_future_sle_not_exists(args, key, sl_entries):
return False return False
@ -892,6 +894,9 @@ def future_sle_exists(args, sl_entries=None):
) )
for d in data: 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 frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data) 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")) item_key = (args.get("item_code"), args.get("warehouse"))
if not sl_entries and hasattr(frappe.local, "future_sle"): 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 ( if not frappe.local.future_sle.get(key) or (
item_key and item_key not in frappe.local.future_sle.get(key) 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): 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: if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({}) return False
if args.get("item_code"): if args.get("item_code"):
item_key = (args.get("item_code"), args.get("warehouse")) item_key = (args.get("item_code"), args.get("warehouse"))

View File

@ -976,6 +976,8 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist() @frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list): def get_round_off_applicable_accounts(company, account_list):
# required to set correct region
frappe.flags.company = company
account_list = get_regional_round_off_accounts(company, account_list) account_list = get_regional_round_off_accounts(company, account_list)
return account_list return account_list

View File

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

View File

@ -315,6 +315,7 @@ class WebsiteItem(WebsiteGenerator):
self.item_code, skip_quotation_creation=True self.item_code, skip_quotation_creation=True
) )
@frappe.whitelist()
def copy_specification_from_item_group(self): def copy_specification_from_item_group(self):
self.set("website_specifications", []) self.set("website_specifications", [])
if self.item_group: if self.item_group:

View File

@ -411,7 +411,6 @@ frappe.ui.form.on("BOM", {
} }
frm.set_value("process_loss_qty", qty); frm.set_value("process_loss_qty", qty);
frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
} }
}); });

View File

@ -74,6 +74,37 @@ class JobCard(Document):
self.update_sub_operation_status() self.update_sub_operation_status()
self.validate_work_order() self.validate_work_order()
def on_update(self):
self.validate_job_card_qty()
def validate_job_card_qty(self):
if not (self.operation_id and self.work_order):
return
wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty"))
completed_qty = flt(
frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")
)
job_card_qty = frappe.get_all(
"Job Card",
fields=["sum(for_quantity)"],
filters={
"work_order": self.work_order,
"operation_id": self.operation_id,
"docstatus": ["!=", 2],
},
as_list=1,
)
job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
msg = f"""Job Card quantity cannot be greater than
Work Order quantity for the operation {self.operation}"""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
def set_sub_operations(self): def set_sub_operations(self):
if not self.sub_operations and self.operation: if not self.sub_operations and self.operation:
self.sub_operations = [] self.sub_operations = []

View File

@ -1598,6 +1598,57 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30)) self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
self.assertEqual(row.workstation, workstations_to_check[index]) self.assertEqual(row.workstation, workstations_to_check[index])
def test_job_card_extra_qty(self):
items = [
"Test FG Item for Scrap Item Test 1",
"Test RM Item 1 for Scrap Item Test 1",
"Test RM Item 2 for Scrap Item Test 1",
]
company = "_Test Company with perpetual inventory"
for item_code in items:
create_item(
item_code=item_code,
is_stock_item=1,
is_purchase_item=1,
opening_stock=100,
valuation_rate=10,
company=company,
warehouse="Stores - TCP1",
)
item = "Test FG Item for Scrap Item Test 1"
raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"]
if not frappe.db.get_value("BOM", {"item": item}):
bom = make_bom(
item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True
)
bom.with_operations = 1
bom.append(
"operations",
{
"operation": "_Test Operation 1",
"workstation": "_Test Workstation 1",
"hour_rate": 20,
"time_in_mins": 60,
},
)
bom.submit()
wo_order = make_wo_order_test_record(
item=item,
company=company,
planned_start_date=now(),
qty=20,
)
job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name")
job_card_doc = frappe.get_doc("Job Card", job_card)
# Make another Job Card for the same Work Order
job_card2 = frappe.copy_doc(job_card_doc)
self.assertRaises(frappe.ValidationError, job_card2.save)
def prepare_data_for_workstation_type_check(): def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.operation.test_operation import make_operation

View File

@ -625,20 +625,18 @@ erpnext.work_order = {
// all materials transferred for manufacturing, make this primary // all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary'); finish_btn.addClass('btn-primary');
} }
} else { } else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) {
frappe.db.get_doc("Manufacturing Settings").then((doc) => { let allowance_percentage = frm.doc.__onload.overproduction_percentage;
let allowance_percentage = doc.overproduction_percentage_for_work_order;
if (allowance_percentage > 0) { if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty); let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if ((flt(doc.produced_qty) < allowed_qty)) { if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() { frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture'); erpnext.work_order.make_se(frm, 'Manufacture');
}); });
}
} }
}); }
} }
} }
} else { } else {

View File

@ -327,6 +327,7 @@ erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances erpnext.patches.v14_0.update_closing_balances
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last # below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
execute:frappe.delete_doc_if_exists("Report", "Tax Detail") execute:frappe.delete_doc_if_exists("Report", "Tax Detail")

View File

@ -96,7 +96,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.work_order || doc.docstatus == 1",
"fieldname": "employee_detail", "fieldname": "employee_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Employee Detail" "label": "Employee Detail"
@ -311,7 +310,7 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-02-14 04:55:41.735991", "modified": "2023-04-20 15:59:11.107831",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Timesheet", "name": "Timesheet",

View File

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

View File

@ -1,31 +1,135 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import re import re
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import format_datetime from frappe.utils import format_datetime
COLUMNS = [
{
"label": "JournalCode",
"fieldname": "JournalCode",
"fieldtype": "Data",
"width": 90,
},
{
"label": "JournalLib",
"fieldname": "JournalLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureNum",
"fieldname": "EcritureNum",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureDate",
"fieldname": "EcritureDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "CompteNum",
"fieldname": "CompteNum",
"fieldtype": "Link",
"options": "Account",
"width": 100,
},
{
"label": "CompteLib",
"fieldname": "CompteLib",
"fieldtype": "Link",
"options": "Account",
"width": 200,
},
{
"label": "CompAuxNum",
"fieldname": "CompAuxNum",
"fieldtype": "Data",
"width": 90,
},
{
"label": "CompAuxLib",
"fieldname": "CompAuxLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "PieceRef",
"fieldname": "PieceRef",
"fieldtype": "Data",
"width": 90,
},
{
"label": "PieceDate",
"fieldname": "PieceDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureLib",
"fieldname": "EcritureLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Debit",
"fieldname": "Debit",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Credit",
"fieldname": "Credit",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureLet",
"fieldname": "EcritureLet",
"fieldtype": "Data",
"width": 90,
},
{
"label": "DateLet",
"fieldname": "DateLet",
"fieldtype": "Data",
"width": 90,
},
{
"label": "ValidDate",
"fieldname": "ValidDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Montantdevise",
"fieldname": "Montantdevise",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Idevise",
"fieldname": "Idevise",
"fieldtype": "Data",
"width": 90,
},
]
def execute(filters=None): def execute(filters=None):
account_details = {} validate_filters(filters)
for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): return COLUMNS, get_result(
account_details.setdefault(acc.name, acc) company=filters["company"],
fiscal_year=filters["fiscal_year"],
validate_filters(filters, account_details) )
filters = set_account_currency(filters)
columns = get_columns(filters)
res = get_result(filters)
return columns, res
def validate_filters(filters, account_details): def validate_filters(filters):
if not filters.get("company"): if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company"))) frappe.throw(_("{0} is mandatory").format(_("Company")))
@ -33,107 +137,96 @@ def validate_filters(filters, account_details):
frappe.throw(_("{0} is mandatory").format(_("Fiscal Year"))) frappe.throw(_("{0} is mandatory").format(_("Fiscal Year")))
def set_account_currency(filters): def get_gl_entries(company, fiscal_year):
gle = frappe.qb.DocType("GL Entry")
sales_invoice = frappe.qb.DocType("Sales Invoice")
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
journal_entry = frappe.qb.DocType("Journal Entry")
payment_entry = frappe.qb.DocType("Payment Entry")
customer = frappe.qb.DocType("Customer")
supplier = frappe.qb.DocType("Supplier")
employee = frappe.qb.DocType("Employee")
filters["company_currency"] = frappe.get_cached_value( debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit")
"Company", filters.company, "default_currency" credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit")
debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_(
"debitCurr"
)
credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_(
"creditCurr"
) )
return filters query = (
frappe.qb.from_(gle)
.left_join(sales_invoice)
def get_columns(filters): .on(gle.voucher_no == sales_invoice.name)
columns = [ .left_join(purchase_invoice)
"JournalCode" + "::90", .on(gle.voucher_no == purchase_invoice.name)
"JournalLib" + "::90", .left_join(journal_entry)
"EcritureNum" + ":Dynamic Link:90", .on(gle.voucher_no == journal_entry.name)
"EcritureDate" + "::90", .left_join(payment_entry)
"CompteNum" + ":Link/Account:100", .on(gle.voucher_no == payment_entry.name)
"CompteLib" + ":Link/Account:200", .left_join(customer)
"CompAuxNum" + "::90", .on(gle.party == customer.name)
"CompAuxLib" + "::90", .left_join(supplier)
"PieceRef" + "::90", .on(gle.party == supplier.name)
"PieceDate" + "::90", .left_join(employee)
"EcritureLib" + "::90", .on(gle.party == employee.name)
"Debit" + "::90", .select(
"Credit" + "::90", gle.posting_date.as_("GlPostDate"),
"EcritureLet" + "::90", gle.name.as_("GlName"),
"DateLet" + "::90", gle.account,
"ValidDate" + "::90", gle.transaction_date,
"Montantdevise" + "::90", debit,
"Idevise" + "::90", credit,
] debit_currency,
credit_currency,
return columns gle.voucher_type,
gle.voucher_no,
gle.against_voucher_type,
def get_result(filters): gle.against_voucher,
gl_entries = get_gl_entries(filters) gle.account_currency,
gle.against,
result = get_result_as_list(gl_entries, filters) gle.party_type,
gle.party,
return result sales_invoice.name.as_("InvName"),
sales_invoice.title.as_("InvTitle"),
sales_invoice.posting_date.as_("InvPostDate"),
def get_gl_entries(filters): purchase_invoice.name.as_("PurName"),
purchase_invoice.title.as_("PurTitle"),
group_by_condition = ( purchase_invoice.posting_date.as_("PurPostDate"),
"group by voucher_type, voucher_no, account" journal_entry.cheque_no.as_("JnlRef"),
if filters.get("group_by_voucher") journal_entry.posting_date.as_("JnlPostDate"),
else "group by gl.name" journal_entry.title.as_("JnlTitle"),
payment_entry.name.as_("PayName"),
payment_entry.posting_date.as_("PayPostDate"),
payment_entry.title.as_("PayTitle"),
customer.customer_name,
customer.name.as_("cusName"),
supplier.supplier_name,
supplier.name.as_("supName"),
employee.employee_name,
employee.name.as_("empName"),
)
.where((gle.company == company) & (gle.fiscal_year == fiscal_year))
.groupby(gle.voucher_type, gle.voucher_no, gle.account)
.orderby(gle.posting_date, gle.voucher_no)
) )
gl_entries = frappe.db.sql( return query.run(as_dict=True)
"""
select
gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date,
sum(gl.debit) as debit, sum(gl.credit) as credit,
sum(gl.debit_in_account_currency) as debitCurr, sum(gl.credit_in_account_currency) as creditCurr,
gl.voucher_type, gl.voucher_no, gl.against_voucher_type,
gl.against_voucher, gl.account_currency, gl.against,
gl.party_type, gl.party,
inv.name as InvName, inv.title as InvTitle, inv.posting_date as InvPostDate,
pur.name as PurName, pur.title as PurTitle, pur.posting_date as PurPostDate,
jnl.cheque_no as JnlRef, jnl.posting_date as JnlPostDate, jnl.title as JnlTitle,
pay.name as PayName, pay.posting_date as PayPostDate, pay.title as PayTitle,
cus.customer_name, cus.name as cusName,
sup.supplier_name, sup.name as supName,
emp.employee_name, emp.name as empName,
stu.title as student_name, stu.name as stuName,
member_name, mem.name as memName
from `tabGL Entry` gl
left join `tabSales Invoice` inv on gl.voucher_no = inv.name
left join `tabPurchase Invoice` pur on gl.voucher_no = pur.name
left join `tabJournal Entry` jnl on gl.voucher_no = jnl.name
left join `tabPayment Entry` pay on gl.voucher_no = pay.name
left join `tabCustomer` cus on gl.party = cus.name
left join `tabSupplier` sup on gl.party = sup.name
left join `tabEmployee` emp on gl.party = emp.name
left join `tabStudent` stu on gl.party = stu.name
left join `tabMember` mem on gl.party = mem.name
where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s
{group_by_condition}
order by GlPostDate, voucher_no""".format(
group_by_condition=group_by_condition
),
filters,
as_dict=1,
)
return gl_entries
def get_result_as_list(data, filters): def get_result(company, fiscal_year):
data = get_gl_entries(company, fiscal_year)
result = [] result = []
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") company_currency = frappe.get_cached_value("Company", company, "default_currency")
accounts = frappe.get_all( accounts = frappe.get_all(
"Account", filters={"Company": filters.company}, fields=["name", "account_number"] "Account", filters={"Company": company}, fields=["name", "account_number"]
) )
for d in data: for d in data:
JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0] JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0]
if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith( if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith(
@ -141,9 +234,7 @@ def get_result_as_list(data, filters):
): ):
EcritureNum = re.split("-|/", d.get("voucher_no"))[1] EcritureNum = re.split("-|/", d.get("voucher_no"))[1]
else: else:
EcritureNum = re.search( EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1]
r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE
).group(1)
EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
@ -185,7 +276,7 @@ def get_result_as_list(data, filters):
ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
PieceRef = d.get("voucher_no") if d.get("voucher_no") else "Sans Reference" PieceRef = d.get("voucher_no") or "Sans Reference"
# EcritureLib is the reference title unless it is an opening entry # EcritureLib is the reference title unless it is an opening entry
if d.get("is_opening") == "Yes": if d.get("is_opening") == "Yes":

View File

@ -416,7 +416,6 @@
"fieldname": "items_section", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1, "hide_border": 1,
"label": "Items",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart" "options": "fa fa-shopping-cart"
}, },
@ -424,6 +423,7 @@
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items",
"oldfieldname": "quotation_details", "oldfieldname": "quotation_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Quotation Item", "options": "Quotation Item",
@ -1072,7 +1072,7 @@
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:32:28.671332", "modified": "2023-04-14 16:50:44.550098",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@ -30,10 +30,6 @@
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "project",
"column_break_77",
"source",
"campaign",
"custom_dimensions_section",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@ -163,7 +159,9 @@
"is_internal_customer", "is_internal_customer",
"represents_company", "represents_company",
"column_break_152", "column_break_152",
"source",
"inter_company_order_reference", "inter_company_order_reference",
"campaign",
"party_account_currency", "party_account_currency",
"connections_tab" "connections_tab"
], ],
@ -1165,12 +1163,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "column_break_77",
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{ {
"fieldname": "source", "fieldname": "source",
"fieldtype": "Link", "fieldtype": "Link",
@ -1613,10 +1605,6 @@
"fieldname": "column_break_92", "fieldname": "column_break_92",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "custom_dimensions_section",
"fieldtype": "Section Break"
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "additional_info_section", "fieldname": "additional_info_section",

View File

@ -559,8 +559,10 @@ erpnext.PointOfSale.Controller = class {
item_row = this.frm.add_child('items', new_item); item_row = this.frm.add_child('items', new_item);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock) if (field === 'qty' && value !== 0 && !this.allow_negative_stock) {
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); const qty_needed = value * item_row.conversion_factor;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
}
await this.trigger_new_item_events(item_row); await this.trigger_new_item_events(item_row);

View File

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

View File

@ -161,7 +161,7 @@ def add_standard_navbar_items():
{ {
"item_label": "User Forum", "item_label": "User Forum",
"item_type": "Route", "item_type": "Route",
"route": "https://discuss.erpnext.com", "route": "https://discuss.frappe.io",
"is_standard": 1, "is_standard": 1,
}, },
{ {

View File

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

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CurDate, Sum, Timestamp from frappe.query_builder.functions import CombineDatetime, CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.utils.data import add_days from frappe.utils.data import add_days
from frappe.utils.jinja import render_template from frappe.utils.jinja import render_template
@ -192,7 +192,8 @@ def get_batch_qty(
posting_time = nowtime() posting_time = nowtime()
query = query.where( query = query.where(
Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time) CombineDatetime(sle.posting_date, sle.posting_time)
<= CombineDatetime(posting_date, posting_time)
) )
out = query.run(as_list=True)[0][0] or 0 out = query.run(as_list=True)[0][0] or 0
@ -376,7 +377,7 @@ def get_pos_reserved_batch_qty(filters):
p = frappe.qb.DocType("POS Invoice").as_("p") p = frappe.qb.DocType("POS Invoice").as_("p")
item = frappe.qb.DocType("POS Invoice Item").as_("item") item = frappe.qb.DocType("POS Invoice Item").as_("item")
sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") sum_qty = frappe.query_builder.functions.Sum(item.stock_qty).as_("qty")
reserved_batch_qty = ( reserved_batch_qty = (
frappe.qb.from_(p) frappe.qb.from_(p)

View File

@ -28,8 +28,6 @@
"column_break_18", "column_break_18",
"project", "project",
"dimension_col_break", "dimension_col_break",
"campaign",
"source",
"custom_dimensions_section", "custom_dimensions_section",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
@ -161,11 +159,12 @@
"inter_company_reference", "inter_company_reference",
"customer_group", "customer_group",
"territory", "territory",
"source",
"campaign",
"column_break5", "column_break5",
"excise_page", "excise_page",
"instructions", "instructions",
"connections_tab", "connections_tab"
"column_break_25"
], ],
"fields": [ "fields": [
{ {
@ -1339,10 +1338,6 @@
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{ {
"fieldname": "section_break_30", "fieldname": "section_break_30",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -1403,7 +1398,7 @@
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-02-14 04:45:44.179670", "modified": "2023-04-21 11:15:23.931084",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@ -616,7 +616,7 @@ def make_stock_entry(source_name, target_doc=None):
target.set_transfer_qty() target.set_transfer_qty()
target.set_actual_qty() target.set_actual_qty()
target.calculate_rate_and_amount(raise_error_if_no_rate=False) target.calculate_rate_and_amount(raise_error_if_no_rate=False)
target.set_stock_entry_type() target.stock_entry_type = target.purpose
target.set_job_card_data() target.set_job_card_data()
doclist = get_mapped_doc( doclist = get_mapped_doc(

View File

@ -54,6 +54,8 @@ class TestMaterialRequest(FrappeTestCase):
mr.submit() mr.submit()
se = make_stock_entry(mr.name) se = make_stock_entry(mr.name)
self.assertEqual(se.stock_entry_type, "Material Transfer")
self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items"))) self.assertEqual(len(se.get("items")), len(mr.get("items")))
@ -69,6 +71,8 @@ class TestMaterialRequest(FrappeTestCase):
in_transit_warehouse = get_in_transit_warehouse(mr.company) in_transit_warehouse = get_in_transit_warehouse(mr.company)
se = make_in_transit_stock_entry(mr.name, in_transit_warehouse) se = make_in_transit_stock_entry(mr.name, in_transit_warehouse)
self.assertEqual(se.stock_entry_type, "Material Transfer")
self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(se.doctype, "Stock Entry")
for row in se.get("items"): for row in se.get("items"):
self.assertEqual(row.t_warehouse, in_transit_warehouse) self.assertEqual(row.t_warehouse, in_transit_warehouse)

View File

@ -380,7 +380,19 @@ class PurchaseReceipt(BuyingController):
outgoing_amount = d.base_net_amount outgoing_amount = d.base_net_amount
if self.is_internal_supplier and d.valuation_rate: if self.is_internal_supplier and d.valuation_rate:
outgoing_amount = d.valuation_rate * d.stock_qty outgoing_amount = abs(
frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": self.name,
"voucher_detail_no": d.name,
"warehouse": d.from_warehouse,
"is_cancelled": 0,
},
"stock_value_difference",
)
)
credit_amount = outgoing_amount credit_amount = outgoing_amount
if credit_amount: if credit_amount:

View File

@ -1610,6 +1610,147 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
def test_internal_pr_gl_entries(self):
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
make_stock_entry(
purpose="Material Receipt",
item_code=item.name,
qty=10,
company=company,
to_warehouse=from_warehouse,
posting_date=add_days(today(), -3),
)
# Step - 1: Create Delivery Note with Internal Customer
dn = create_delivery_note(
item_code=item.name,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=100,
warehouse=from_warehouse,
target_warehouse=target_warehouse,
posting_date=add_days(today(), -2),
)
# Step - 2: Create Internal Purchase Receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.items[0].qty = 10
pr.items[0].from_warehouse = target_warehouse
pr.items[0].warehouse = to_warehouse
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
pr.submit()
# Step - 3: Create back-date Stock Reconciliation [After DN and Before PR]
create_stock_reconciliation(
item_code=item,
warehouse=target_warehouse,
qty=10,
rate=50,
company=company,
posting_date=add_days(today(), -1),
)
warehouse_account = get_warehouse_account_map(company)
stock_account_value = frappe.db.get_value(
"GL Entry",
{
"account": warehouse_account[target_warehouse]["account"],
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"is_cancelled": 0,
},
fieldname=["credit"],
)
stock_diff = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"is_cancelled": 0,
},
fieldname=["sum(stock_value_difference)"],
)
# Value of Stock Account should be equal to the sum of Stock Value Difference
self.assertEqual(stock_account_value, stock_diff)
def test_internal_pr_reference(self):
item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
from_warehouse = create_warehouse("_Test Internal From Warehouse New 1", company=company)
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New 1", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New 1", company=company)
# Step 2: Create Stock Entry (Material Receipt)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
make_stock_entry(
purpose="Material Receipt",
item_code=item.name,
qty=15,
company=company,
to_warehouse=from_warehouse,
)
# Step 3: Create Delivery Note with Internal Customer
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
dn = create_delivery_note(
item_code=item.name,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=100,
warehouse=from_warehouse,
target_warehouse=target_warehouse,
)
# Step 4: Create Internal Purchase Receipt
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.inter_company_reference = ""
self.assertRaises(frappe.ValidationError, pr.save)
pr.inter_company_reference = dn.name
pr.items[0].qty = 10
pr.items[0].from_warehouse = target_warehouse
pr.items[0].warehouse = to_warehouse
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
delivery_note_item = pr.items[0].delivery_note_item
pr.items[0].delivery_note_item = ""
self.assertRaises(frappe.ValidationError, pr.save)
pr.load_from_db()
pr.items[0].delivery_note_item = delivery_note_item
pr.save()
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

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

View File

@ -2346,7 +2346,7 @@ def move_sample_to_retention_warehouse(company, items):
@frappe.whitelist() @frappe.whitelist()
def make_stock_in_entry(source_name, target_doc=None): def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.set_stock_entry_type() target.stock_entry_type = "Material Transfer"
target.set_missing_values() target.set_missing_values()
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):

View File

@ -202,6 +202,9 @@ class TestStockEntry(FrappeTestCase):
) )
end_transit_entry = make_stock_in_entry(transit_entry.name) end_transit_entry = make_stock_in_entry(transit_entry.name)
self.assertEqual(end_transit_entry.stock_entry_type, "Material Transfer")
self.assertEqual(end_transit_entry.purpose, "Material Transfer")
self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry) self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry)
self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry) self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry)
self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail) self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail)

View File

@ -5,7 +5,7 @@ from typing import Optional
import frappe import frappe
from frappe import _, bold, msgprint 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 from frappe.utils import cint, cstr, flt
import erpnext import erpnext
@ -613,22 +613,33 @@ class StockReconciliation(StockController):
self._cancel() self._cancel()
def recalculate_current_qty(self, item_code, batch_no): 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: for row in self.items:
if not (row.item_code == item_code and row.batch_no == batch_no): if not (row.item_code == item_code and row.batch_no == batch_no):
continue 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( precesion = row.precision("current_qty")
item_code, if flt(current_qty, precesion) == flt(row.current_qty, precesion):
row.warehouse, continue
self.posting_date,
self.posting_time, val_rate = get_valuation_rate(
with_valuation_rate=True, item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
) )
row.current_valuation_rate = val_rate 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( row.db_set(
{ {
"current_qty": row.current_qty, "current_qty": row.current_qty,
@ -637,8 +648,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") ledger = frappe.qb.DocType("Stock Ledger Entry")
query = ( query = (
@ -652,6 +668,12 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no):
& (ledger.docstatus == 1) & (ledger.docstatus == 1)
& (ledger.is_cancelled == 0) & (ledger.is_cancelled == 0)
& (ledger.batch_no == batch_no) & (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) .groupby(ledger.batch_no)
) )

View File

@ -530,7 +530,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
# check if cancellation of stock reco is blocked # check if cancellation of stock reco is blocked
self.assertRaises(NegativeStockError, sr.cancel) self.assertRaises(NegativeStockError, sr.cancel)
repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name})) repost_exists = bool(
frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name, "status": "Queued"})
)
self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation") self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation")
def test_intermediate_sr_bin_update(self): def test_intermediate_sr_bin_update(self):
@ -676,6 +678,79 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sl_entry.actual_qty), 1.0) self.assertEqual(flt(sl_entry.actual_qty), 1.0)
self.assertEqual(flt(sl_entry.qty_after_transaction), 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): def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1) batch_item_doc = create_item(item_name, is_stock_item=1)

View File

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

View File

@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import Field from frappe.query_builder import Field
from frappe.query_builder.functions import Min, Timestamp from frappe.query_builder.functions import CombineDatetime, Min
from frappe.utils import add_days, getdate, today from frappe.utils import add_days, getdate, today
import erpnext import erpnext
@ -75,7 +75,7 @@ def get_data(report_filters):
& (sle.company == report_filters.company) & (sle.company == report_filters.company)
& (sle.is_cancelled == 0) & (sle.is_cancelled == 0)
) )
.orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation)
).run(as_dict=True) ).run(as_dict=True)
for d in data: for d in data:

View File

@ -41,7 +41,7 @@ def get_data(report_filters):
key = (d.voucher_type, d.voucher_no) key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {} gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0) d.account_value = gl_data.get("account_value", 0)
d.difference_value = d.stock_value - d.account_value d.difference_value = abs(d.stock_value) - abs(d.account_value)
if abs(d.difference_value) > 0.1: if abs(d.difference_value) > 0.1:
data.append(d) data.append(d)

View File

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

View File

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

@ -548,6 +548,14 @@ class update_entries_after(object):
if not self.args.get("sle_id"): if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle) 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 ( if (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no and sle.voucher_detail_no
@ -609,6 +617,16 @@ class update_entries_after(object):
if not self.args.get("sle_id"): if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle) 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): def validate_negative_stock(self, sle):
""" """
validate negative stock for entries current datetime onwards validate negative stock for entries current datetime onwards
@ -1379,12 +1397,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
def regenerate_sle_for_batch_stock_reco(detail): def regenerate_sle_for_batch_stock_reco(detail):
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) 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.recalculate_current_qty(detail.item_code, detail.batch_no)
doc.docstatus = 1 doc.repost_future_sle_and_gle()
doc.update_stock_ledger()
def get_stock_reco_qty_shift(args): def get_stock_reco_qty_shift(args):
@ -1410,34 +1424,53 @@ def get_stock_reco_qty_shift(args):
return stock_reco_qty_shift return stock_reco_qty_shift
def get_next_stock_reco(args): def get_next_stock_reco(kwargs):
"""Returns next nearest stock reconciliaton's details.""" """Returns next nearest stock reconciliaton's details."""
return frappe.db.sql( sle = frappe.qb.DocType("Stock Ledger Entry")
"""
select query = (
name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty frappe.qb.from_(sle)
from .select(
`tabStock Ledger Entry` sle.name,
where sle.posting_date,
item_code = %(item_code)s sle.posting_time,
and warehouse = %(warehouse)s sle.creation,
and voucher_type = 'Stock Reconciliation' sle.voucher_no,
and voucher_no != %(voucher_no)s sle.item_code,
and is_cancelled = 0 sle.batch_no,
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) sle.actual_qty,
or ( )
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) .where(
and creation > %(creation)s (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 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
""", .orderby(sle.creation)
args, .limit(1)
as_dict=1,
) )
if kwargs.get("batch_no"):
query = query.where(sle.batch_no == kwargs.get("batch_no"))
return query.run(as_dict=True)
def get_datetime_limit_condition(detail): def get_datetime_limit_condition(detail):
return f""" return f"""

View File

@ -67,6 +67,15 @@ frappe.ui.form.on('Subcontracting Receipt', {
} }
}); });
frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) {
var row = locals[cdt][cdn];
return {
filters: {
item: row.rm_item_code
}
}
});
let batch_no_field = frm.get_docfield("items", "batch_no"); let batch_no_field = frm.get_docfield("items", "batch_no");
if (batch_no_field) { if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = function(row) { batch_no_field.get_route_options_for_new_doc = function(row) {

View File

@ -192,7 +192,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-11-26 15:45:33.289911", "modified": "2023-04-21 17:16:56.192560",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Service Level Agreement", "name": "Service Level Agreement",
@ -212,19 +212,12 @@
"write": 1 "write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1, "read": 1,
"report": 1, "role": "All"
"role": "All",
"share": 1,
"write": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -11,7 +11,10 @@
<div class="product-price"> <div class="product-price">
<!-- Final Price --> <!-- Final Price -->
{{ price_info.formatted_price_sales_uom }} <span itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span itemprop="price" content="{{ price_info.price_list_rate }}">{{ price_info.formatted_price_sales_uom }}</span>
<span style="display:none;" itemprop="priceCurrency" content="{{ price_info.currency }}">{{ price_info.currency }}</span>
</span>
<!-- Striked Price and Discount --> <!-- Striked Price and Discount -->
{% if price_info.formatted_mrp %} {% if price_info.formatted_mrp %}