mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 11:55:11 +00:00
Merge pull request #34442 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
|
||||
# TODO: Set new balance in Base/Account currency
|
||||
if d.balance > 0:
|
||||
if d.balance != 0:
|
||||
current_exchange_rate = new_exchange_rate = 0
|
||||
|
||||
new_balance_in_account_currency = 0 # this will be '0'
|
||||
@@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in accounts:
|
||||
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
|
||||
continue
|
||||
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d.get("balance_in_account_currency") > 0
|
||||
@@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_amounts_in_company_currency()
|
||||
journal_entry.set_total_debit_credit()
|
||||
|
||||
self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
|
||||
journal_entry.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
@@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_amounts_in_company_currency()
|
||||
journal_entry.set_total_debit_credit()
|
||||
journal_entry.save()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
|
||||
@@ -23,7 +23,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
|
||||
class ProcessStatementOfAccounts(Document):
|
||||
def validate(self):
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.name }}"
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
|
||||
|
||||
@@ -86,6 +86,7 @@ def get_report_pdf(doc, consolidated=True):
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"currency": doc.currency,
|
||||
@@ -153,7 +154,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
|
||||
]
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[[fields_dict[customer_collection], "IN", selected]],
|
||||
)
|
||||
|
||||
@@ -176,7 +177,7 @@ def get_customers_based_on_sales_person(sales_person):
|
||||
if sales_person_records.get("Customer"):
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["name", "in", list(sales_person_records["Customer"])]],
|
||||
)
|
||||
else:
|
||||
@@ -225,7 +226,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
if customer_collection == "Sales Partner":
|
||||
customers = frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["default_sales_partner", "=", collection_name]],
|
||||
)
|
||||
else:
|
||||
@@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
continue
|
||||
|
||||
customer_list.append(
|
||||
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
|
||||
{
|
||||
"name": customer.name,
|
||||
"customer_name": customer.customer_name,
|
||||
"primary_email": primary_email,
|
||||
"billing_email": billing_email,
|
||||
}
|
||||
)
|
||||
return customer_list
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_workflow": 1,
|
||||
"creation": "2020-08-03 16:35:21.852178",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer",
|
||||
"customer_name",
|
||||
"billing_email",
|
||||
"primary_email"
|
||||
],
|
||||
@@ -30,11 +30,18 @@
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Email"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 22:55:38.875601",
|
||||
"modified": "2023-03-13 00:12:34.508086",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts Customer",
|
||||
@@ -43,5 +50,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -32,9 +32,6 @@
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"column_break_27",
|
||||
"campaign",
|
||||
"source",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -203,7 +200,9 @@
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"campaign",
|
||||
"represents_company",
|
||||
"source",
|
||||
"customer_group",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
@@ -2083,10 +2082,6 @@
|
||||
"fieldname": "company_addr_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_27",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_52",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -2143,11 +2138,10 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-11-07 16:02:07.972258",
|
||||
"modified": "2023-03-13 11:43:15.883055",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
||||
@@ -32,6 +32,16 @@ from erpnext import get_company_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
|
||||
SALES_TRANSACTION_TYPES = {
|
||||
"Quotation",
|
||||
"Sales Order",
|
||||
"Delivery Note",
|
||||
"Sales Invoice",
|
||||
"POS Invoice",
|
||||
}
|
||||
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
|
||||
|
||||
|
||||
class DuplicatePartyAccountError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -124,12 +134,6 @@ def _get_party_details(
|
||||
set_other_values(party_details, party, party_type)
|
||||
set_price_list(party_details, party, party_type, price_list, pos_profile)
|
||||
|
||||
party_details["tax_category"] = get_address_tax_category(
|
||||
party.get("tax_category"),
|
||||
party_address,
|
||||
shipping_address if party_type != "Supplier" else party_address,
|
||||
)
|
||||
|
||||
tax_template = set_taxes(
|
||||
party.name,
|
||||
party_type,
|
||||
@@ -211,20 +215,10 @@ def set_address_details(
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
if doctype and doctype in [
|
||||
"Delivery Note",
|
||||
"Sales Invoice",
|
||||
"Sales Order",
|
||||
"Quotation",
|
||||
"POS Invoice",
|
||||
]:
|
||||
if party_details.company_address:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "company_address", party_details.company_address)
|
||||
)
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
|
||||
party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
|
||||
|
||||
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
|
||||
if doctype in PURCHASE_TRANSACTION_TYPES:
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=shipping_address,
|
||||
@@ -250,9 +244,21 @@ def set_address_details(
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
|
||||
)
|
||||
|
||||
party_address, shipping_address = (
|
||||
party_details.get(billing_address_field),
|
||||
party_details.shipping_address_name,
|
||||
)
|
||||
|
||||
party_details["tax_category"] = get_address_tax_category(
|
||||
party.get("tax_category"),
|
||||
party_address,
|
||||
shipping_address if party_type != "Supplier" else party_address,
|
||||
)
|
||||
|
||||
if doctype in TRANSACTION_TYPES:
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_details.get(billing_address_field), party_details.shipping_address_name
|
||||
return party_address, shipping_address
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
|
||||
@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
|
||||
for data in [asset_data, liability_data, equity_data]:
|
||||
if data:
|
||||
account_name = get_root_account_name(data[0].root_type, company)
|
||||
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
||||
if account_name:
|
||||
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
||||
|
||||
opening_balance[company] = opening_value
|
||||
|
||||
@@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company):
|
||||
|
||||
|
||||
def get_root_account_name(root_type, company):
|
||||
return frappe.get_all(
|
||||
root_account = frappe.get_all(
|
||||
"Account",
|
||||
fields=["account_name"],
|
||||
filters={
|
||||
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
|
||||
"parent_account": ("is", "not set"),
|
||||
},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
if root_account:
|
||||
return root_account[0][0]
|
||||
|
||||
|
||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
@@ -78,7 +78,6 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
||||
|
||||
@@ -118,12 +117,10 @@ def get_data(filters):
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||
)
|
||||
|
||||
total_row = calculate_values(
|
||||
accounts, gl_entries_by_account, opening_balances, filters, company_currency
|
||||
)
|
||||
calculate_values(accounts, gl_entries_by_account, opening_balances)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
|
||||
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||
data = filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
@@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
return opening
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
"closing_credit": 0.0,
|
||||
}
|
||||
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
d.update(init.copy())
|
||||
|
||||
@@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
|
||||
prepare_opening_closing(d)
|
||||
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
def calculate_total_row(accounts, company_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
if not d.parent_account:
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
return total_row
|
||||
|
||||
@@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
|
||||
accounts_by_name[d.parent_account][key] += d[key]
|
||||
|
||||
|
||||
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
|
||||
def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
@@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
|
||||
@@ -305,7 +305,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
||||
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
|
||||
|
||||
# Used retrun against and supplier and is_retrun because there is an index added for it
|
||||
data = frappe.db.get_list(
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=[
|
||||
|
||||
@@ -76,12 +76,9 @@ def get_transaction_list(
|
||||
ignore_permissions = False
|
||||
|
||||
if not filters:
|
||||
filters = []
|
||||
filters = {}
|
||||
|
||||
if doctype in ["Supplier Quotation", "Purchase Invoice"]:
|
||||
filters.append((doctype, "docstatus", "<", 2))
|
||||
else:
|
||||
filters.append((doctype, "docstatus", "=", 1))
|
||||
filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1
|
||||
|
||||
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
|
||||
parties_doctype = (
|
||||
@@ -92,12 +89,12 @@ def get_transaction_list(
|
||||
|
||||
if customers:
|
||||
if doctype == "Quotation":
|
||||
filters.append(("quotation_to", "=", "Customer"))
|
||||
filters.append(("party_name", "in", customers))
|
||||
filters["quotation_to"] = "Customer"
|
||||
filters["party_name"] = ["in", customers]
|
||||
else:
|
||||
filters.append(("customer", "in", customers))
|
||||
filters["customer"] = ["in", customers]
|
||||
elif suppliers:
|
||||
filters.append(("supplier", "in", suppliers))
|
||||
filters["supplier"] = ["in", suppliers]
|
||||
elif not custom:
|
||||
return []
|
||||
|
||||
@@ -110,7 +107,7 @@ def get_transaction_list(
|
||||
|
||||
if not customers and not suppliers and custom:
|
||||
ignore_permissions = False
|
||||
filters = []
|
||||
filters = {}
|
||||
|
||||
transactions = get_list_for_transactions(
|
||||
doctype,
|
||||
|
||||
@@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.opportunity_from && frm.doc.party_name){
|
||||
frm.trigger('set_contact_link');
|
||||
}
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
@@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", {
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
|
||||
if (frm.doc.opportunity_from && frm.doc.party_name) {
|
||||
frm.trigger('set_contact_link');
|
||||
}
|
||||
},
|
||||
|
||||
set_contact_link: function(frm) {
|
||||
@@ -137,6 +137,8 @@ frappe.ui.form.on("Opportunity", {
|
||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
|
||||
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
|
||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
|
||||
} else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) {
|
||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class BOMTree:
|
||||
|
||||
# specifying the attributes to save resources
|
||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"]
|
||||
|
||||
def __init__(
|
||||
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
|
||||
@@ -50,9 +50,10 @@ class BOMTree:
|
||||
def __create_tree(self):
|
||||
bom = frappe.get_cached_doc("BOM", self.name)
|
||||
self.item_code = bom.item
|
||||
self.bom_qty = bom.quantity
|
||||
|
||||
for item in bom.get("items", []):
|
||||
qty = item.qty / bom.quantity # quantity per unit
|
||||
qty = item.stock_qty / bom.quantity # quantity per unit
|
||||
exploded_qty = self.exploded_qty * qty
|
||||
if item.bom_no:
|
||||
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
||||
|
||||
@@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", {
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
"required_qty": 1,
|
||||
"required_qty": row.required_qty || 1,
|
||||
"item_name": r.message.item_name,
|
||||
"description": r.message.description,
|
||||
"source_warehouse": r.message.default_warehouse,
|
||||
|
||||
@@ -682,7 +682,7 @@ class WorkOrder(Document):
|
||||
|
||||
for node in bom_traversal:
|
||||
if node.is_bom:
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty))
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
|
||||
|
||||
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
||||
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from frappe.utils import cint
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
@@ -34,57 +35,55 @@ def get_columns():
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
qty_to_produce = filters.get("qty_to_produce") or 1
|
||||
if int(qty_to_produce) < 0:
|
||||
frappe.throw(_("Quantity to Produce can not be less than Zero"))
|
||||
qty_to_produce = filters.get("qty_to_produce")
|
||||
if cint(qty_to_produce) <= 0:
|
||||
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||
|
||||
if filters.get("show_exploded_view"):
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType(bom_item_table)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bom)
|
||||
.inner_join(bom_item)
|
||||
.on(bom.name == bom_item.parent)
|
||||
.left_join(bin)
|
||||
.on(bom_item.item_code == bin.item_code)
|
||||
.select(
|
||||
bom_item.item_code,
|
||||
bom_item.description,
|
||||
bom_item.stock_qty,
|
||||
bom_item.stock_uom,
|
||||
(bom_item.stock_qty / bom.quantity) * qty_to_produce,
|
||||
Sum(bin.actual_qty),
|
||||
Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
|
||||
)
|
||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
||||
.groupby(bom_item.item_code)
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
BOM = frappe.qb.DocType("BOM")
|
||||
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||
BIN = frappe.qb.DocType("Bin")
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
CONDITIONS = ()
|
||||
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where(
|
||||
(wh.lft >= warehouse_details.lft)
|
||||
& (wh.rgt <= warehouse_details.rgt)
|
||||
& (bin.warehouse == wh.name)
|
||||
)
|
||||
)
|
||||
if warehouse_details:
|
||||
CONDITIONS = ExistsCriterion(
|
||||
frappe.qb.from_(WH)
|
||||
.select(WH.name)
|
||||
.where(
|
||||
(WH.lft >= warehouse_details.lft)
|
||||
& (WH.rgt <= warehouse_details.rgt)
|
||||
& (BIN.warehouse == WH.name)
|
||||
)
|
||||
else:
|
||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
||||
)
|
||||
else:
|
||||
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||
|
||||
return query.run()
|
||||
QUERY = (
|
||||
frappe.qb.from_(BOM)
|
||||
.inner_join(BOM_ITEM)
|
||||
.on(BOM.name == BOM_ITEM.parent)
|
||||
.left_join(BIN)
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
.groupby(BOM_ITEM.item_code)
|
||||
)
|
||||
|
||||
return QUERY.run()
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.exceptions import ValidationError
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import floor
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
|
||||
get_bom_stock as bom_stock_report,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestBomStockReport(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.warehouse = "_Test Warehouse - _TC"
|
||||
self.fg_item, self.rm_items = create_items()
|
||||
make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
|
||||
make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
|
||||
self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
|
||||
|
||||
def test_bom_stock_report(self):
|
||||
# Test 1: When `qty_to_produce` is 0.
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"bom": self.bom.name,
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty_to_produce": 0,
|
||||
}
|
||||
)
|
||||
self.assertRaises(ValidationError, bom_stock_report, filters)
|
||||
|
||||
# Test 2: When stock is not available.
|
||||
data = bom_stock_report(
|
||||
frappe._dict(
|
||||
{
|
||||
"bom": self.bom.name,
|
||||
"warehouse": "Stores - _TC",
|
||||
"qty_to_produce": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
# Test 3: When stock is available.
|
||||
data = bom_stock_report(
|
||||
frappe._dict(
|
||||
{
|
||||
"bom": self.bom.name,
|
||||
"warehouse": self.warehouse,
|
||||
"qty_to_produce": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
expected_data = get_expected_data(self.bom, self.warehouse, 1)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
|
||||
def create_items():
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item1 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"standard_rate": 100,
|
||||
"opening_stock": 100,
|
||||
"last_purchase_rate": 100,
|
||||
}
|
||||
).name
|
||||
rm_item2 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"standard_rate": 200,
|
||||
"opening_stock": 200,
|
||||
"last_purchase_rate": 200,
|
||||
}
|
||||
).name
|
||||
|
||||
return fg_item, [rm_item1, rm_item2]
|
||||
|
||||
|
||||
def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||
expected_data = []
|
||||
|
||||
for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
|
||||
in_stock_qty = frappe.get_cached_value(
|
||||
"Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
|
||||
)
|
||||
|
||||
expected_data.append(
|
||||
[
|
||||
item.item_code,
|
||||
item.description,
|
||||
item.stock_qty,
|
||||
item.stock_uom,
|
||||
item.stock_qty * qty_to_produce / bom.quantity,
|
||||
in_stock_qty,
|
||||
floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
|
||||
if in_stock_qty
|
||||
else None,
|
||||
]
|
||||
)
|
||||
|
||||
return expected_data
|
||||
@@ -325,6 +325,5 @@ erpnext.patches.v14_0.update_entry_type_for_journal_entry
|
||||
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
||||
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
|
||||
erpnext.patches.v14_0.set_pick_list_status
|
||||
# below 2 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_remarks_from_gl_to_payment_ledger
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder import Case, CustomFunction
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Count, IfNull
|
||||
from frappe.utils import flt
|
||||
@@ -18,9 +18,21 @@ def create_accounting_dimension_fields():
|
||||
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
||||
|
||||
|
||||
def generate_name_for_payment_ledger_entries(gl_entries, start):
|
||||
def generate_name_and_calculate_amount(gl_entries, start, receivable_accounts):
|
||||
for index, entry in enumerate(gl_entries, 0):
|
||||
entry.name = start + index
|
||||
if entry.account in receivable_accounts:
|
||||
entry.account_type = "Receivable"
|
||||
entry.amount = entry.debit - entry.credit
|
||||
entry.amount_in_account_currency = (
|
||||
entry.debit_in_account_currency - entry.credit_in_account_currency
|
||||
)
|
||||
else:
|
||||
entry.account_type = "Payable"
|
||||
entry.amount = entry.credit - entry.debit
|
||||
entry.amount_in_account_currency = (
|
||||
entry.credit_in_account_currency - entry.debit_in_account_currency
|
||||
)
|
||||
|
||||
|
||||
def get_columns():
|
||||
@@ -49,6 +61,9 @@ def get_columns():
|
||||
"finance_book",
|
||||
]
|
||||
|
||||
if frappe.db.has_column("Payment Ledger Entry", "remarks"):
|
||||
columns.append("remarks")
|
||||
|
||||
dimensions_and_defaults = get_dimensions()
|
||||
if dimensions_and_defaults:
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
@@ -99,12 +114,17 @@ def execute():
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
# Get Records Count
|
||||
accounts = (
|
||||
relavant_accounts = (
|
||||
qb.from_(account)
|
||||
.select(account.name)
|
||||
.select(account.name, account.account_type)
|
||||
.where((account.account_type == "Receivable") | (account.account_type == "Payable"))
|
||||
.orderby(account.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
receivable_accounts = [x.name for x in relavant_accounts if x.account_type == "Receivable"]
|
||||
accounts = [x.name for x in relavant_accounts]
|
||||
|
||||
un_processed = (
|
||||
qb.from_(gl)
|
||||
.select(Count(gl.name))
|
||||
@@ -122,37 +142,21 @@ def execute():
|
||||
|
||||
while True:
|
||||
if last_name:
|
||||
where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0)
|
||||
where_clause = gl.name.gt(last_name) & gl.account.isin(accounts) & gl.is_cancelled == 0
|
||||
else:
|
||||
where_clause = gl.is_cancelled == 0
|
||||
where_clause = gl.account.isin(accounts) & gl.is_cancelled == 0
|
||||
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.inner_join(account)
|
||||
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
|
||||
.select(
|
||||
gl.star,
|
||||
ConstantColumn(1).as_("docstatus"),
|
||||
account.account_type.as_("account_type"),
|
||||
IfNull(
|
||||
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
|
||||
).as_("against_voucher_type"),
|
||||
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
|
||||
"against_voucher_no"
|
||||
),
|
||||
# convert debit/credit to amount
|
||||
Case()
|
||||
.when(account.account_type == "Receivable", gl.debit - gl.credit)
|
||||
.else_(gl.credit - gl.debit)
|
||||
.as_("amount"),
|
||||
# convert debit/credit in account currency to amount in account currency
|
||||
Case()
|
||||
.when(
|
||||
account.account_type == "Receivable",
|
||||
gl.debit_in_account_currency - gl.credit_in_account_currency,
|
||||
)
|
||||
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
|
||||
.as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(where_clause)
|
||||
.orderby(gl.name)
|
||||
@@ -163,8 +167,8 @@ def execute():
|
||||
if gl_entries:
|
||||
last_name = gl_entries[-1].name
|
||||
|
||||
# primary key(name) for payment ledger records
|
||||
generate_name_for_payment_ledger_entries(gl_entries, processed)
|
||||
# add primary key(name) and calculate based on debit and credit
|
||||
generate_name_and_calculate_amount(gl_entries, processed, receivable_accounts)
|
||||
|
||||
try:
|
||||
insert_query = build_insert_query()
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.functions import Count, IfNull
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry'
|
||||
"""
|
||||
|
||||
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
# Get empty PLE records
|
||||
un_processed = (
|
||||
qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run()
|
||||
)[0][0]
|
||||
|
||||
if un_processed:
|
||||
print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry")
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
processed = 0
|
||||
last_percent_update = 0
|
||||
batch_size = 1000
|
||||
last_name = None
|
||||
|
||||
while True:
|
||||
if last_name:
|
||||
where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0)
|
||||
else:
|
||||
where_clause = (ple.remarks.isnull()) & (ple.delinked == 0)
|
||||
|
||||
# results are deterministic
|
||||
names = (
|
||||
qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run()
|
||||
)
|
||||
|
||||
if names:
|
||||
last_name = names[-1][0]
|
||||
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.left_join(gle)
|
||||
.on(
|
||||
(ple.account == gle.account)
|
||||
& (ple.party_type == gle.party_type)
|
||||
& (ple.party == gle.party)
|
||||
& (ple.voucher_type == gle.voucher_type)
|
||||
& (ple.voucher_no == gle.voucher_no)
|
||||
& (
|
||||
ple.against_voucher_type
|
||||
== IfNull(
|
||||
ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type
|
||||
)
|
||||
)
|
||||
& (
|
||||
ple.against_voucher_no
|
||||
== IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no)
|
||||
)
|
||||
& (ple.company == gle.company)
|
||||
& (
|
||||
((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit)))
|
||||
| (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit))
|
||||
)
|
||||
& (gle.remarks.notnull())
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.select(ple.name)
|
||||
.distinct()
|
||||
.select(
|
||||
gle.remarks.as_("gle_remarks"),
|
||||
)
|
||||
.where(ple.name.isin(names))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if pl_entries:
|
||||
for entry in pl_entries:
|
||||
query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name))
|
||||
query.run()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
processed += len(pl_entries)
|
||||
percentage = flt((processed / un_processed) * 100, 2)
|
||||
if percentage - last_percent_update > 1:
|
||||
print(f"{percentage}% ({processed}) PLE records updated")
|
||||
last_percent_update = percentage
|
||||
|
||||
else:
|
||||
break
|
||||
print("Remarks succesfully migrated")
|
||||
@@ -5,6 +5,8 @@ frappe.ui.form.on("Timesheet", {
|
||||
setup: function(frm) {
|
||||
frappe.require("/assets/erpnext/js/projects/timer.js");
|
||||
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice'];
|
||||
|
||||
frm.fields_dict.employee.get_query = function() {
|
||||
return {
|
||||
filters:{
|
||||
|
||||
@@ -2,7 +2,7 @@ import datetime
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
from frappe.utils import add_days, add_months, nowdate
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
@@ -15,9 +15,16 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp
|
||||
|
||||
|
||||
class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.cleanup_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def cleanup_old_entries(self):
|
||||
frappe.db.delete("Sales Invoice", filters={"company": "_Test Company"})
|
||||
frappe.db.delete("Sales Order", filters={"company": "_Test Company"})
|
||||
|
||||
def create_payment_terms_template(self):
|
||||
# create template for 50-50 payments
|
||||
template = None
|
||||
@@ -348,7 +355,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
|
||||
transaction_date = nowdate()
|
||||
so = make_sales_order(
|
||||
transaction_date=add_days(transaction_date, -30),
|
||||
transaction_date=add_months(transaction_date, -1),
|
||||
delivery_date=add_days(transaction_date, -15),
|
||||
item=item.item_code,
|
||||
qty=10,
|
||||
@@ -369,13 +376,15 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
sinv.items[0].qty = 6
|
||||
sinv.insert()
|
||||
sinv.submit()
|
||||
|
||||
first_due_date = add_days(add_months(transaction_date, -1), 15)
|
||||
columns, data, message, chart = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"item": item.item_code,
|
||||
"from_due_date": add_days(transaction_date, -30),
|
||||
"to_due_date": add_days(transaction_date, -15),
|
||||
"from_due_date": add_months(transaction_date, -1),
|
||||
"to_due_date": first_due_date,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -384,11 +393,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
{
|
||||
"name": so.name,
|
||||
"customer": so.customer,
|
||||
"submitted": datetime.date.fromisoformat(add_days(transaction_date, -30)),
|
||||
"submitted": datetime.date.fromisoformat(add_months(transaction_date, -1)),
|
||||
"status": "Completed",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date.fromisoformat(add_days(transaction_date, -15)),
|
||||
"due_date": datetime.date.fromisoformat(first_due_date),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 500000.0,
|
||||
|
||||
@@ -54,7 +54,7 @@ class ItemAlternative(Document):
|
||||
if not item_data.allow_alternative_item:
|
||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
||||
if self.two_way and not alternative_item_data.allow_alternative_item:
|
||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
||||
frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
|
||||
|
||||
def validate_duplicate(self):
|
||||
if frappe.db.get_value(
|
||||
|
||||
@@ -132,7 +132,7 @@ class TestFIFOValuation(unittest.TestCase):
|
||||
total_qty = 0
|
||||
|
||||
for qty, rate in stock_queue:
|
||||
if qty == 0:
|
||||
if round_off_if_near_zero(qty) == 0:
|
||||
continue
|
||||
if qty > 0:
|
||||
self.queue.add_stock(qty, rate)
|
||||
@@ -154,7 +154,7 @@ class TestFIFOValuation(unittest.TestCase):
|
||||
|
||||
for qty, rate in stock_queue:
|
||||
# don't allow negative stock
|
||||
if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||
if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||
continue
|
||||
if qty > 0:
|
||||
self.queue.add_stock(qty, rate)
|
||||
@@ -179,7 +179,7 @@ class TestFIFOValuation(unittest.TestCase):
|
||||
|
||||
for qty, rate in stock_queue:
|
||||
# don't allow negative stock
|
||||
if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||
if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||
continue
|
||||
if qty > 0:
|
||||
self.queue.add_stock(qty, rate)
|
||||
@@ -282,7 +282,7 @@ class TestLIFOValuation(unittest.TestCase):
|
||||
total_qty = 0
|
||||
|
||||
for qty, rate in stock_stack:
|
||||
if qty == 0:
|
||||
if round_off_if_near_zero(qty) == 0:
|
||||
continue
|
||||
if qty > 0:
|
||||
self.stack.add_stock(qty, rate)
|
||||
@@ -304,7 +304,7 @@ class TestLIFOValuation(unittest.TestCase):
|
||||
|
||||
for qty, rate in stock_stack:
|
||||
# don't allow negative stock
|
||||
if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||
if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||
continue
|
||||
if qty > 0:
|
||||
self.stack.add_stock(qty, rate)
|
||||
|
||||
@@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
|
||||
Stock Levels,Niveaux du Stocks,
|
||||
Stock Liabilities,Passif du Stock,
|
||||
Stock Options,Options du Stock,
|
||||
Stock Qty,Qté en Stock,
|
||||
Stock Qty,Qté en unité de stock,
|
||||
Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
|
||||
Stock Reports,Rapports de stock,
|
||||
Stock Summary,Résumé du Stock,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user