Merge branch 'develop' of github.com:rahib-hassan/erpnext into separate-discount-account
This commit is contained in:
commit
52fd804aed
@ -18,7 +18,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
check_if_stock_and_account_balance_synced,
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
@ -88,9 +87,6 @@ class JournalEntry(AccountsController):
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
self.update_status_for_full_and_final_statement()
|
||||
check_if_stock_and_account_balance_synced(
|
||||
self.posting_date, self.company, self.doctype, self.name
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
@ -34,8 +34,9 @@ class ProcessStatementOfAccounts(Document):
|
||||
frappe.throw(_("Customers not selected."))
|
||||
|
||||
if self.enable_auto_email:
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
if self.start_date and getdate(self.start_date) >= getdate(today()):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
|
@ -18,10 +18,6 @@ from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
|
||||
|
||||
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class FiscalYearError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
@ -1246,47 +1242,6 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||
return matched
|
||||
|
||||
|
||||
def check_if_stock_and_account_balance_synced(
|
||||
posting_date, company, voucher_type=None, voucher_no=None
|
||||
):
|
||||
if not cint(erpnext.is_perpetual_inventory_enabled(company)):
|
||||
return
|
||||
|
||||
accounts = get_stock_accounts(company, voucher_type, voucher_no)
|
||||
stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account")
|
||||
|
||||
for account in accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, posting_date, company
|
||||
)
|
||||
|
||||
if abs(account_bal - stock_bal) > 0.1:
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"),
|
||||
currency=frappe.get_cached_value("Company", company, "default_currency"),
|
||||
)
|
||||
|
||||
diff = flt(stock_bal - account_bal, precision)
|
||||
|
||||
error_reason = _(
|
||||
"Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}."
|
||||
).format(stock_bal, account_bal, frappe.bold(account), posting_date)
|
||||
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}").format(
|
||||
frappe.bold(diff), frappe.bold(posting_date)
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
|
||||
raise_exception=StockValueAndAccountBalanceOutOfSync,
|
||||
title=_("Values Out Of Sync"),
|
||||
primary_action={
|
||||
"label": _("Make Journal Entry"),
|
||||
"client_action": "erpnext.route_to_adjustment_jv",
|
||||
"args": get_journal_entry(account, stock_adjustment_account, diff),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||
stock_accounts = [
|
||||
d.name
|
||||
|
@ -18,16 +18,16 @@
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"tax_withholding_category",
|
||||
"is_transporter",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"image",
|
||||
"column_break0",
|
||||
"supplier_group",
|
||||
"supplier_type",
|
||||
"allow_purchase_invoice_creation_without_purchase_order",
|
||||
"allow_purchase_invoice_creation_without_purchase_receipt",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"disabled",
|
||||
"is_transporter",
|
||||
"warn_rfqs",
|
||||
"warn_pos",
|
||||
"prevent_rfqs",
|
||||
@ -38,12 +38,6 @@
|
||||
"default_currency",
|
||||
"column_break_10",
|
||||
"default_price_list",
|
||||
"section_credit_limit",
|
||||
"payment_terms",
|
||||
"cb_21",
|
||||
"on_hold",
|
||||
"hold_type",
|
||||
"release_date",
|
||||
"address_contacts",
|
||||
"address_html",
|
||||
"column_break1",
|
||||
@ -57,6 +51,12 @@
|
||||
"primary_address",
|
||||
"default_payable_accounts",
|
||||
"accounts",
|
||||
"section_credit_limit",
|
||||
"payment_terms",
|
||||
"cb_21",
|
||||
"on_hold",
|
||||
"hold_type",
|
||||
"release_date",
|
||||
"default_tax_withholding_config",
|
||||
"column_break2",
|
||||
"website",
|
||||
@ -258,7 +258,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_credit_limit",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Credit Limit"
|
||||
"label": "Payment Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
@ -432,7 +432,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-20 22:03:33.147249",
|
||||
"modified": "2022-04-16 18:02:27.838623",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
@ -497,6 +497,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "supplier_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -59,6 +59,7 @@ treeviews = [
|
||||
"Warehouse",
|
||||
"Item Group",
|
||||
"Customer Group",
|
||||
"Supplier Group",
|
||||
"Sales Person",
|
||||
"Territory",
|
||||
"Assessment Group",
|
||||
|
@ -15,23 +15,23 @@
|
||||
"salutation",
|
||||
"customer_name",
|
||||
"gender",
|
||||
"customer_type",
|
||||
"tax_withholding_category",
|
||||
"default_bank_account",
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"tax_withholding_category",
|
||||
"lead_name",
|
||||
"opportunity_name",
|
||||
"image",
|
||||
"column_break0",
|
||||
"account_manager",
|
||||
"customer_group",
|
||||
"customer_type",
|
||||
"territory",
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"account_manager",
|
||||
"so_required",
|
||||
"dn_required",
|
||||
"disabled",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"disabled",
|
||||
"allowed_to_transact_section",
|
||||
"companies",
|
||||
"currency_and_price_list",
|
||||
@ -40,7 +40,6 @@
|
||||
"default_price_list",
|
||||
"address_contacts",
|
||||
"address_html",
|
||||
"website",
|
||||
"column_break1",
|
||||
"contact_html",
|
||||
"primary_address_and_contact_detail",
|
||||
@ -60,6 +59,7 @@
|
||||
"column_break_45",
|
||||
"market_segment",
|
||||
"industry",
|
||||
"website",
|
||||
"language",
|
||||
"is_frozen",
|
||||
"column_break_38",
|
||||
@ -100,7 +100,7 @@
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Full Name",
|
||||
"label": "Customer Name",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "customer_name",
|
||||
"oldfieldtype": "Data",
|
||||
@ -118,7 +118,7 @@
|
||||
"default": "Company",
|
||||
"fieldname": "customer_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"label": "Customer Type",
|
||||
"oldfieldname": "customer_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company\nIndividual",
|
||||
@ -337,7 +337,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "default_receivable_accounts",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting"
|
||||
"label": "Default Receivable Accounts"
|
||||
},
|
||||
{
|
||||
"description": "Mention if non-standard receivable account",
|
||||
@ -511,7 +511,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-20 22:07:52.485809",
|
||||
"modified": "2022-04-16 20:32:34.000304",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
@ -595,6 +595,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -4,15 +4,12 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today
|
||||
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
|
||||
from frappe.utils.user import get_users_with_role
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import (
|
||||
check_if_stock_and_account_balance_synced,
|
||||
update_gl_entries_after,
|
||||
)
|
||||
from erpnext.accounts.utils import update_gl_entries_after
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle
|
||||
|
||||
|
||||
@ -224,6 +221,10 @@ def notify_error_to_stock_managers(doc, traceback):
|
||||
|
||||
|
||||
def repost_entries():
|
||||
"""
|
||||
Reposts 'Repost Item Valuation' entries in queue.
|
||||
Called hourly via hooks.py.
|
||||
"""
|
||||
if not in_configured_timeslot():
|
||||
return
|
||||
|
||||
@ -239,9 +240,6 @@ def repost_entries():
|
||||
if riv_entries:
|
||||
return
|
||||
|
||||
for d in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}):
|
||||
check_if_stock_and_account_balance_synced(today(), d.name)
|
||||
|
||||
|
||||
def get_repost_item_valuation_entries():
|
||||
return frappe.db.sql(
|
||||
|
@ -3,24 +3,41 @@
|
||||
|
||||
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CombineDatetime
|
||||
from frappe.utils import cint, date_diff, flt, getdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
class StockBalanceFilter(TypedDict):
|
||||
company: Optional[str]
|
||||
from_date: str
|
||||
to_date: str
|
||||
item_group: Optional[str]
|
||||
item: Optional[str]
|
||||
warehouse: Optional[str]
|
||||
warehouse_type: Optional[str]
|
||||
include_uom: Optional[str] # include extra info in converted UOM
|
||||
show_stock_ageing_data: bool
|
||||
show_variant_attributes: bool
|
||||
|
||||
|
||||
SLEntry = Dict[str, Any]
|
||||
|
||||
|
||||
def execute(filters: Optional[StockBalanceFilter] = None):
|
||||
is_reposting_item_valuation_in_progress()
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
to_date = filters.get("to_date")
|
||||
|
||||
if filters.get("company"):
|
||||
company_currency = erpnext.get_company_currency(filters.get("company"))
|
||||
else:
|
||||
@ -48,6 +65,7 @@ def execute(filters=None):
|
||||
|
||||
_func = itemgetter(1)
|
||||
|
||||
to_date = filters.get("to_date")
|
||||
for (company, item, warehouse) in sorted(iwb_map):
|
||||
if item_map.get(item):
|
||||
qty_dict = iwb_map[(company, item, warehouse)]
|
||||
@ -92,7 +110,7 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
def get_columns(filters: StockBalanceFilter):
|
||||
"""return columns"""
|
||||
columns = [
|
||||
{
|
||||
@ -215,66 +233,77 @@ def get_columns(filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
def apply_conditions(query, filters):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
warehouse_table = frappe.qb.DocType("Warehouse")
|
||||
|
||||
if not filters.get("from_date"):
|
||||
frappe.throw(_("'From Date' is required"))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions += " and sle.posting_date <= %s" % frappe.db.escape(filters.get("to_date"))
|
||||
if to_date := filters.get("to_date"):
|
||||
query = query.where(sle.posting_date <= to_date)
|
||||
else:
|
||||
frappe.throw(_("'To Date' is required"))
|
||||
|
||||
if filters.get("company"):
|
||||
conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company"))
|
||||
if company := filters.get("company"):
|
||||
query = query.where(sle.company == company)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
if warehouse_details:
|
||||
conditions += (
|
||||
" and exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"
|
||||
% (warehouse_details.lft, warehouse_details.rgt)
|
||||
if warehouse := filters.get("warehouse"):
|
||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
chilren_subquery = (
|
||||
frappe.qb.from_(warehouse_table)
|
||||
.select(warehouse_table.name)
|
||||
.where(
|
||||
(warehouse_table.lft >= lft)
|
||||
& (warehouse_table.rgt <= rgt)
|
||||
& (warehouse_table.name == sle.warehouse)
|
||||
)
|
||||
|
||||
if filters.get("warehouse_type") and not filters.get("warehouse"):
|
||||
conditions += (
|
||||
" and exists (select name from `tabWarehouse` wh \
|
||||
where wh.warehouse_type = '%s' and sle.warehouse = wh.name)"
|
||||
% (filters.get("warehouse_type"))
|
||||
)
|
||||
query = query.where(ExistsCriterion(chilren_subquery))
|
||||
elif warehouse_type := filters.get("warehouse_type"):
|
||||
query = (
|
||||
query.join(warehouse_table)
|
||||
.on(warehouse_table.name == sle.warehouse)
|
||||
.where(warehouse_table.warehouse_type == warehouse_type)
|
||||
)
|
||||
|
||||
return conditions
|
||||
return query
|
||||
|
||||
|
||||
def get_stock_ledger_entries(filters, items):
|
||||
item_conditions_sql = ""
|
||||
if items:
|
||||
item_conditions_sql = " and sle.item_code in ({})".format(
|
||||
", ".join(frappe.db.escape(i, percent=False) for i in items)
|
||||
def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> List[SLEntry]:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(
|
||||
sle.item_code,
|
||||
sle.warehouse,
|
||||
sle.posting_date,
|
||||
sle.actual_qty,
|
||||
sle.valuation_rate,
|
||||
sle.company,
|
||||
sle.voucher_type,
|
||||
sle.qty_after_transaction,
|
||||
sle.stock_value_difference,
|
||||
sle.item_code.as_("name"),
|
||||
sle.voucher_no,
|
||||
sle.stock_value,
|
||||
sle.batch_no,
|
||||
)
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate,
|
||||
sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
|
||||
sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no
|
||||
from
|
||||
`tabStock Ledger Entry` sle
|
||||
where sle.docstatus < 2 %s %s
|
||||
and is_cancelled = 0
|
||||
order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty"""
|
||||
% (item_conditions_sql, conditions), # nosec
|
||||
as_dict=1,
|
||||
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
|
||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
||||
.orderby(sle.creation)
|
||||
.orderby(sle.actual_qty)
|
||||
)
|
||||
|
||||
if items:
|
||||
query = query.where(sle.item_code.isin(items))
|
||||
|
||||
def get_item_warehouse_map(filters, sle):
|
||||
query = apply_conditions(query, filters)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]):
|
||||
iwb_map = {}
|
||||
from_date = getdate(filters.get("from_date"))
|
||||
to_date = getdate(filters.get("to_date"))
|
||||
@ -332,7 +361,7 @@ def get_item_warehouse_map(filters, sle):
|
||||
return iwb_map
|
||||
|
||||
|
||||
def filter_items_with_no_transactions(iwb_map, float_precision):
|
||||
def filter_items_with_no_transactions(iwb_map, float_precision: float):
|
||||
for (company, item, warehouse) in sorted(iwb_map):
|
||||
qty_dict = iwb_map[(company, item, warehouse)]
|
||||
|
||||
@ -349,26 +378,22 @@ def filter_items_with_no_transactions(iwb_map, float_precision):
|
||||
return iwb_map
|
||||
|
||||
|
||||
def get_items(filters):
|
||||
def get_items(filters: StockBalanceFilter) -> List[str]:
|
||||
"Get items based on item code, item group or brand."
|
||||
conditions = []
|
||||
if filters.get("item_code"):
|
||||
conditions.append("item.name=%(item_code)s")
|
||||
if item_code := filters.get("item_code"):
|
||||
return [item_code]
|
||||
else:
|
||||
if filters.get("item_group"):
|
||||
conditions.append(get_item_group_condition(filters.get("item_group")))
|
||||
if filters.get("brand"): # used in stock analytics report
|
||||
conditions.append("item.brand=%(brand)s")
|
||||
item_filters = {}
|
||||
if item_group := filters.get("item_group"):
|
||||
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
|
||||
item_filters["item_group"] = ("in", children + [item_group])
|
||||
if brand := filters.get("brand"):
|
||||
item_filters["brand"] = brand
|
||||
|
||||
items = []
|
||||
if conditions:
|
||||
items = frappe.db.sql_list(
|
||||
"""select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters
|
||||
)
|
||||
return items
|
||||
return frappe.get_all("Item", filters=item_filters, pluck="name", order_by=None)
|
||||
|
||||
|
||||
def get_item_details(items, sle, filters):
|
||||
def get_item_details(items: List[str], sle: List[SLEntry], filters: StockBalanceFilter):
|
||||
item_details = {}
|
||||
if not items:
|
||||
items = list(set(d.item_code for d in sle))
|
||||
@ -376,33 +401,35 @@ def get_item_details(items, sle, filters):
|
||||
if not items:
|
||||
return item_details
|
||||
|
||||
cf_field = cf_join = ""
|
||||
if filters.get("include_uom"):
|
||||
cf_field = ", ucd.conversion_factor"
|
||||
cf_join = (
|
||||
"left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s"
|
||||
% frappe.db.escape(filters.get("include_uom"))
|
||||
)
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
|
||||
res = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom %s
|
||||
from
|
||||
`tabItem` item
|
||||
%s
|
||||
where
|
||||
item.name in (%s)
|
||||
"""
|
||||
% (cf_field, cf_join, ",".join(["%s"] * len(items))),
|
||||
items,
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(item_table)
|
||||
.select(
|
||||
item_table.name,
|
||||
item_table.item_name,
|
||||
item_table.description,
|
||||
item_table.item_group,
|
||||
item_table.brand,
|
||||
item_table.stock_uom,
|
||||
)
|
||||
.where(item_table.name.isin(items))
|
||||
)
|
||||
|
||||
for item in res:
|
||||
item_details.setdefault(item.name, item)
|
||||
if uom := filters.get("include_uom"):
|
||||
uom_conv_detail = frappe.qb.DocType("UOM Conversion Detail")
|
||||
query = (
|
||||
query.left_join(uom_conv_detail)
|
||||
.on((uom_conv_detail.parent == item_table.name) & (uom_conv_detail.uom == uom))
|
||||
.select(uom_conv_detail.conversion_factor)
|
||||
)
|
||||
|
||||
if filters.get("show_variant_attributes", 0) == 1:
|
||||
result = query.run(as_dict=1)
|
||||
|
||||
for item_table in result:
|
||||
item_details.setdefault(item_table.name, item_table)
|
||||
|
||||
if filters.get("show_variant_attributes"):
|
||||
variant_values = get_variant_values_for(list(item_details))
|
||||
item_details = {k: v.update(variant_values.get(k, {})) for k, v in item_details.items()}
|
||||
|
||||
@ -413,36 +440,33 @@ def get_item_reorder_details(items):
|
||||
item_reorder_details = frappe._dict()
|
||||
|
||||
if items:
|
||||
item_reorder_details = frappe.db.sql(
|
||||
"""
|
||||
select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level
|
||||
from `tabItem Reorder`
|
||||
where parent in ({0})
|
||||
""".format(
|
||||
", ".join(frappe.db.escape(i, percent=False) for i in items)
|
||||
),
|
||||
as_dict=1,
|
||||
item_reorder_details = frappe.get_all(
|
||||
"Item Reorder",
|
||||
["parent", "warehouse", "warehouse_reorder_qty", "warehouse_reorder_level"],
|
||||
filters={"parent": ("in", items)},
|
||||
)
|
||||
|
||||
return dict((d.parent + d.warehouse, d) for d in item_reorder_details)
|
||||
|
||||
|
||||
def get_variants_attributes():
|
||||
def get_variants_attributes() -> List[str]:
|
||||
"""Return all item variant attributes."""
|
||||
return [i.name for i in frappe.get_all("Item Attribute")]
|
||||
return frappe.get_all("Item Attribute", pluck="name")
|
||||
|
||||
|
||||
def get_variant_values_for(items):
|
||||
"""Returns variant values for items."""
|
||||
attribute_map = {}
|
||||
for attr in frappe.db.sql(
|
||||
"""select parent, attribute, attribute_value
|
||||
from `tabItem Variant Attribute` where parent in (%s)
|
||||
"""
|
||||
% ", ".join(["%s"] * len(items)),
|
||||
tuple(items),
|
||||
as_dict=1,
|
||||
):
|
||||
|
||||
attribute_info = frappe.get_all(
|
||||
"Item Variant Attribute",
|
||||
["parent", "attribute", "attribute_value"],
|
||||
{
|
||||
"parent": ("in", items),
|
||||
},
|
||||
)
|
||||
|
||||
for attr in attribute_info:
|
||||
attribute_map.setdefault(attr["parent"], {})
|
||||
attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]})
|
||||
|
||||
|
174
erpnext/stock/report/stock_balance/test_stock_balance.py
Normal file
174
erpnext/stock/report/stock_balance/test_stock_balance.py
Normal file
@ -0,0 +1,174 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
import frappe
|
||||
from frappe import _dict
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.report.stock_balance.stock_balance import execute
|
||||
|
||||
|
||||
def stock_balance(filters):
|
||||
"""Get rows from stock balance report"""
|
||||
return [_dict(row) for row in execute(filters)[1]]
|
||||
|
||||
|
||||
class TestStockBalance(FrappeTestCase):
|
||||
# ----------- utils
|
||||
|
||||
def setUp(self):
|
||||
self.item = make_item()
|
||||
self.filters = _dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"item_code": self.item.name,
|
||||
"from_date": "2020-01-01",
|
||||
"to_date": str(today()),
|
||||
}
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def assertPartialDictEq(self, expected: Dict[str, Any], actual: Dict[str, Any]):
|
||||
for k, v in expected.items():
|
||||
self.assertEqual(v, actual[k], msg=f"{expected=}\n{actual=}")
|
||||
|
||||
def generate_stock_ledger(self, item_code: str, movements):
|
||||
|
||||
for movement in map(_dict, movements):
|
||||
if "to_warehouse" not in movement:
|
||||
movement.to_warehouse = "_Test Warehouse - _TC"
|
||||
make_stock_entry(item_code=item_code, **movement)
|
||||
|
||||
def assertInvariants(self, rows):
|
||||
last_balance = frappe.db.sql(
|
||||
"""
|
||||
WITH last_balances AS (
|
||||
SELECT item_code, warehouse,
|
||||
stock_value, qty_after_transaction,
|
||||
ROW_NUMBER() OVER (PARTITION BY item_code, warehouse
|
||||
ORDER BY timestamp(posting_date, posting_time) desc, creation desc)
|
||||
AS rn
|
||||
FROM `tabStock Ledger Entry`
|
||||
where is_cancelled=0
|
||||
)
|
||||
SELECT * FROM last_balances WHERE rn = 1""",
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
item_wh_stock = _dict()
|
||||
|
||||
for line in last_balance:
|
||||
item_wh_stock.setdefault((line.item_code, line.warehouse), line)
|
||||
|
||||
for row in rows:
|
||||
msg = f"Invariants not met for {rows=}"
|
||||
# qty invariant
|
||||
self.assertAlmostEqual(row.bal_qty, row.opening_qty + row.in_qty - row.out_qty, msg)
|
||||
|
||||
# value invariant
|
||||
self.assertAlmostEqual(row.bal_val, row.opening_val + row.in_val - row.out_val, msg)
|
||||
|
||||
# check against SLE
|
||||
last_sle = item_wh_stock[(row.item_code, row.warehouse)]
|
||||
self.assertAlmostEqual(row.bal_qty, last_sle.qty_after_transaction, 3)
|
||||
self.assertAlmostEqual(row.bal_val, last_sle.stock_value, 3)
|
||||
|
||||
# valuation rate
|
||||
if not row.bal_qty:
|
||||
continue
|
||||
self.assertAlmostEqual(row.val_rate, row.bal_val / row.bal_qty, 3, msg)
|
||||
|
||||
# ----------- tests
|
||||
|
||||
def test_basic_stock_balance(self):
|
||||
"""Check very basic functionality and item info"""
|
||||
rows = stock_balance(self.filters)
|
||||
self.assertEqual(rows, [])
|
||||
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=5, rate=10)])
|
||||
|
||||
# check item info
|
||||
rows = stock_balance(self.filters)
|
||||
self.assertPartialDictEq(
|
||||
{
|
||||
"item_code": self.item.name,
|
||||
"item_name": self.item.item_name,
|
||||
"item_group": self.item.item_group,
|
||||
"stock_uom": self.item.stock_uom,
|
||||
"in_qty": 5,
|
||||
"in_val": 50,
|
||||
"val_rate": 10,
|
||||
},
|
||||
rows[0],
|
||||
)
|
||||
self.assertInvariants(rows)
|
||||
|
||||
def test_opening_balance(self):
|
||||
self.generate_stock_ledger(
|
||||
self.item.name,
|
||||
[
|
||||
_dict(qty=1, rate=1, posting_date="2021-01-01"),
|
||||
_dict(qty=2, rate=2, posting_date="2021-01-02"),
|
||||
_dict(qty=3, rate=3, posting_date="2021-01-03"),
|
||||
],
|
||||
)
|
||||
rows = stock_balance(self.filters)
|
||||
self.assertInvariants(rows)
|
||||
|
||||
rows = stock_balance(self.filters.update({"from_date": "2021-01-02"}))
|
||||
self.assertInvariants(rows)
|
||||
self.assertPartialDictEq({"opening_qty": 1, "in_qty": 5}, rows[0])
|
||||
|
||||
rows = stock_balance(self.filters.update({"from_date": "2022-01-01"}))
|
||||
self.assertInvariants(rows)
|
||||
self.assertPartialDictEq({"opening_qty": 6, "in_qty": 0}, rows[0])
|
||||
|
||||
def test_uom_converted_info(self):
|
||||
|
||||
self.item.append("uoms", {"conversion_factor": 5, "uom": "Box"})
|
||||
self.item.save()
|
||||
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=5, rate=10)])
|
||||
|
||||
rows = stock_balance(self.filters.update({"include_uom": "Box"}))
|
||||
self.assertEqual(rows[0].bal_qty_alt, 1)
|
||||
self.assertInvariants(rows)
|
||||
|
||||
def test_item_group(self):
|
||||
self.filters.pop("item_code", None)
|
||||
rows = stock_balance(self.filters.update({"item_group": self.item.item_group}))
|
||||
self.assertTrue(all(r.item_group == self.item.item_group for r in rows))
|
||||
|
||||
def test_child_warehouse_balances(self):
|
||||
# This is default
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=5, rate=10, to_warehouse="Stores - _TC")])
|
||||
|
||||
self.filters.pop("item_code", None)
|
||||
rows = stock_balance(self.filters.update({"warehouse": "All Warehouses - _TC"}))
|
||||
|
||||
self.assertTrue(
|
||||
any(r.item_code == self.item.name and r.warehouse == "Stores - _TC" for r in rows),
|
||||
msg=f"Expected child warehouse balances \n{rows}",
|
||||
)
|
||||
|
||||
def test_show_item_attr(self):
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
|
||||
self.item.has_variants = True
|
||||
self.item.append("attributes", {"attribute": "Test Size"})
|
||||
self.item.save()
|
||||
|
||||
attributes = {"Test Size": "Large"}
|
||||
variant = create_variant(self.item.name, attributes)
|
||||
variant.save()
|
||||
|
||||
self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)])
|
||||
rows = stock_balance(
|
||||
self.filters.update({"show_variant_attributes": 1, "item_code": variant.name})
|
||||
)
|
||||
self.assertPartialDictEq(attributes, rows[0])
|
||||
self.assertInvariants(rows)
|
@ -271,7 +271,7 @@ Assessment Report,Rapport d'Évaluation,
|
||||
Assessment Reports,Rapports d'évaluation,
|
||||
Assessment Result,Résultat de l'Évaluation,
|
||||
Assessment Result record {0} already exists.,Le Résultat d'Évaluation {0} existe déjà.,
|
||||
Asset,Atout,
|
||||
Asset,Actif - Immo.,
|
||||
Asset Category,Catégorie d'Actif,
|
||||
Asset Category is mandatory for Fixed Asset item,Catégorie d'Actif est obligatoire pour l'article Immobilisé,
|
||||
Asset Maintenance,Maintenance des actifs,
|
||||
@ -3037,6 +3037,7 @@ To Date must be greater than From Date,La date de fin doit être supérieure à
|
||||
To Date should be within the Fiscal Year. Assuming To Date = {0},La Date Finale doit être dans l'exercice. En supposant Date Finale = {0},
|
||||
To Datetime,À la Date,
|
||||
To Deliver,À Livrer,
|
||||
{} To Deliver,{} à livrer
|
||||
To Deliver and Bill,À Livrer et Facturer,
|
||||
To Fiscal Year,À l'année fiscale,
|
||||
To GSTIN,GSTIN (Destination),
|
||||
@ -9871,3 +9872,4 @@ Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les t
|
||||
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
|
||||
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries
|
||||
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
|
||||
Unit Of Measure (UOM),Unité de mesure (UDM),
|
||||
|
Can't render this file because it is too large.
|
Loading…
x
Reference in New Issue
Block a user