Merge pull request #45566 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-01-29 16:57:29 +05:30
committed by GitHub
74 changed files with 1653 additions and 2130 deletions

View File

@@ -57,7 +57,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -66,7 +66,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -81,7 +81,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -76,7 +76,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -85,7 +85,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -100,7 +100,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -66,7 +66,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -75,7 +75,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -90,7 +90,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -0,0 +1,532 @@
{
"country_code": "ch",
"name": "240812 Schulkontenrahmen VEB - DE",
"tree": {
"Aktiven": {
"account_number": "1",
"is_group": 1,
"root_type": "Asset",
"Umlaufvermögen": {
"account_number": "10",
"is_group": 1,
"Flüssige Mittel": {
"account_number": "100",
"is_group": 1,
"Kasse": {
"account_number": "1000",
"account_type": "Cash"
},
"Bankguthaben": {
"account_number": "1020",
"account_type": "Bank"
}
},
"Kurzfristig gehaltene Aktiven mit Börsenkurs": {
"account_number": "106",
"is_group": 1,
"Wertschriften": {
"account_number": "1060"
},
"Wertberichtigungen Wertschriften": {
"account_number": "1069"
}
},
"Forderungen aus Lieferungen und Leistungen": {
"account_number": "110",
"is_group": 1,
"Forderungen aus Lieferungen und Leistungen (Debitoren)": {
"account_number": "1100"
},
"Delkredere": {
"account_number": "1109"
}
},
"Übrige kurzfristige Forderungen": {
"account_number": "114",
"is_group": 1,
"Vorschüsse und Darlehen": {
"account_number": "1140"
},
"Wertberichtigungen Vorschüsse und Darlehen": {
"account_number": "1149"
},
"Vorsteuer MWST Material, Waren, Dienstleistungen, Energie": {
"account_number": "1170"
},
"Vorsteuer MWST Investitionen, übriger Betriebsaufwand": {
"account_number": "1171"
},
"Verrechnungssteuer": {
"account_number": "1176"
},
"Forderungen gegenüber Sozialversicherungen und Vorsorgeeinrichtungen": {
"account_number": "1180"
},
"Quellensteuer": {
"account_number": "1189"
},
"Sonstige kurzfristige Forderungen": {
"account_number": "1190"
},
"Wertberichtigungen sonstige kurzfristige Forderungen": {
"account_number": "1199"
}
},
"Vorräte und nicht fakturierte Dienstleistungen": {
"account_number": "120",
"is_group": 1,
"Handelswaren": {
"account_number": "1200"
},
"Rohstoffe": {
"account_number": "1210"
},
"Werkstoffe": {
"account_number": "1220"
},
"Hilfs- und Verbrauchsmaterial": {
"account_number": "1230"
},
"Handelswaren in Konsignation": {
"account_number": "1250"
},
"Fertige Erzeugnisse": {
"account_number": "1260"
},
"Unfertige Erzeugnisse": {
"account_number": "1270"
},
"Nicht fakturierte Dienstleistungen": {
"account_number": "1280"
}
},
"Aktive Rechnungsabgrenzungen": {
"account_number": "130",
"is_group": 1,
"Bezahlter Aufwand des Folgejahres": {
"account_number": "1300"
},
"Noch nicht erhaltener Ertrag": {
"account_number": "1301"
}
}
},
"Anlagevermögen": {
"account_number": "14",
"is_group": 1,
"Finanzanlagen": {
"account_number": "140",
"is_group": 1,
"Wertschriften": {
"account_number": "1400"
},
"Wertberichtigungen Wertschriften": {
"account_number": "1409"
},
"Darlehen": {
"account_number": "1440"
},
"Hypotheken": {
"account_number": "1441"
},
"Wertberichtigungen langfristige Forderungen": {
"account_number": "1449"
}
},
"Beteiligungen": {
"account_number": "148",
"is_group": 1,
"Beteiligungen": {
"account_number": "1480"
},
"Wertberichtigungen Beteiligungen": {
"account_number": "1489"
}
},
"Mobile Sachanlagen": {
"account_number": "150",
"is_group": 1,
"Maschinen und Apparate": {
"account_number": "1500"
},
"Wertberichtigungen Maschinen und Apparate": {
"account_number": "1509"
},
"Mobiliar und Einrichtungen": {
"account_number": "1510"
},
"Wertberichtigungen Mobiliar und Einrichtungen": {
"account_number": "1519"
},
"Büromaschinen, Informatik, Kommunikationstechnologie": {
"account_number": "1520"
},
"Wertberichtigungen Büromaschinen, Informatik, Kommunikationstechnologie": {
"account_number": "1529"
},
"Fahrzeuge": {
"account_number": "1530"
},
"Wertberichtigungen Fahrzeuge": {
"account_number": "1539"
},
"Werkzeuge und Geräte": {
"account_number": "1540"
},
"Wertberichtigungen Werkzeuge und Geräte": {
"account_number": "1549"
}
},
"Immobile Sachanlagen": {
"account_number": "160",
"is_group": 1,
"Geschäftsliegenschaften": {
"account_number": "1600"
},
"Wertberichtigungen Geschäftsliegenschaften": {
"account_number": "1609"
}
},
"Immaterielle Werte": {
"account_number": "170",
"is_group": 1,
"Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
"account_number": "1700"
},
"Wertberichtigungen Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
"account_number": "1709"
},
"Goodwill": {
"account_number": "1770"
},
"Wertberichtigungen Goodwill": {
"account_number": "1779"
}
},
"Nicht einbezahltes Grund-, Gesellschafter- oder Stiftungskapital": {
"account_number": "180",
"is_group": 1,
"Nicht einbezahltes Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
"account_number": "1850"
}
}
}
},
"Passiven": {
"account_number": "2",
"is_group": 1,
"root_type": "Liability",
"Kurzfristiges Fremdkapital": {
"account_number": "20",
"is_group": 1,
"Verbindlichkeiten aus Lieferungen und Leistungen": {
"account_number": "200",
"is_group": 1,
"Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)": {
"account_number": "2000"
},
"Erhaltene Anzahlungen": {
"account_number": "2030"
}
},
"Kurzfristige verzinsliche Verbindlichkeiten": {
"account_number": "210",
"is_group": 1,
"Bankverbindlichkeiten": {
"account_number": "2100"
},
"Verbindlichkeiten aus Finanzierungsleasing": {
"account_number": "2120"
},
"Übrige verzinsliche Verbindlichkeiten": {
"account_number": "2140"
}
},
"Übrige kurzfristige Verbindlichkeiten": {
"account_number": "220",
"is_group": 1,
"Geschuldete MWST (Umsatzsteuer)": {
"account_number": "2200"
},
"Abrechnungskonto MWST": {
"account_number": "2201"
},
"Verrechnungssteuer": {
"account_number": "2206"
},
"Direkte Steuern": {
"account_number": "2208"
},
"Sonstige kurzfristige Verbindlichkeiten": {
"account_number": "2210"
},
"Beschlossene Ausschüttungen": {
"account_number": "2261"
},
"Sozialversicherungen und Vorsorgeeinrichtungen": {
"account_number": "2270"
},
"Quellensteuer": {
"account_number": "2279"
}
},
"Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen": {
"account_number": "230",
"is_group": 1,
"Noch nicht bezahlter Aufwand": {
"account_number": "2300"
},
"Erhaltener Ertrag des Folgejahres": {
"account_number": "2301"
},
"Kurzfristige Rückstellungen": {
"account_number": "2330"
}
}
},
"Langfristiges Fremdkapital": {
"account_number": "24",
"is_group": 1,
"Langfristige verzinsliche Verbindlichkeiten": {
"account_number": "240",
"is_group": 1,
"Bankverbindlichkeiten": {
"account_number": "2400"
},
"Verbindlichkeiten aus Finanzierungsleasing": {
"account_number": "2420"
},
"Obligationenanleihen": {
"account_number": "2430"
},
"Darlehen": {
"account_number": "2450"
},
"Hypotheken": {
"account_number": "2451"
}
},
"Übrige langfristige Verbindlichkeiten": {
"account_number": "250",
"is_group": 1,
"Übrige langfristige Verbindlichkeiten (unverzinslich)": {
"account_number": "2500"
}
},
"Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen": {
"account_number": "260",
"is_group": 1,
"Rückstellungen": {
"account_number": "2600"
}
}
},
"Eigenkapital (juristische Personen)": {
"account_number": "28",
"is_group": 1,
"Grund-, Gesellschafter- oder Stiftungskapital": {
"account_number": "280",
"is_group": 1,
"Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
"account_number": "2800"
}
},
"Reserven und Jahresgewinn oder Jahresverlust": {
"account_number": "290",
"is_group": 1,
"Gesetzliche Kapitalreserve": {
"account_number": "2900"
},
"Reserve für eigene Kapitalanteile": {
"account_number": "2930"
},
"Aufwertungsreserve": {
"account_number": "2940"
},
"Gesetzliche Gewinnreserve": {
"account_number": "2950"
},
"Freiwillige Gewinnreserven": {
"account_number": "2960"
},
"Gewinnvortrag oder Verlustvortrag": {
"account_number": "2970"
},
"Jahresgewinn oder Jahresverlust": {
"account_number": "2979"
},
"Eigene Aktien, Stammanteile oder Anteilscheine (Minusposten)": {
"account_number": "2980"
}
}
}
},
"Betrieblicher Ertrag aus Lieferungen und Leistungen": {
"account_number": "3",
"is_group": 1,
"root_type": "Income",
"Produktionserlöse": {
"account_number": "3000"
},
"Handelserlöse": {
"account_number": "3200"
},
"Dienstleistungserlöse": {
"account_number": "3400"
},
"Übrige Erlöse aus Lieferungen und Leistungen": {
"account_number": "3600"
},
"Eigenleistungen": {
"account_number": "3700"
},
"Eigenverbrauch": {
"account_number": "3710"
},
"Erlösminderungen": {
"account_number": "3800"
},
"Verluste Forderungen (Debitoren), Veränderung Delkredere": {
"account_number": "3805"
},
"Bestandesänderungen unfertige Erzeugnisse": {
"account_number": "3900"
},
"Bestandesänderungen fertige Erzeugnisse": {
"account_number": "3901"
},
"Bestandesänderungen nicht fakturierte Dienstleistungen": {
"account_number": "3940"
}
},
"Aufwand für Material, Handelswaren, Dienstleistungen und Energie": {
"account_number": "4",
"is_group": 1,
"root_type": "Expense",
"Materialaufwand Produktion": {
"account_number": "4000"
},
"Handelswarenaufwand": {
"account_number": "4200"
},
"Aufwand für bezogene Dienstleistungen": {
"account_number": "4400"
},
"Energieaufwand zur Leistungserstellung": {
"account_number": "4500"
},
"Aufwandminderungen": {
"account_number": "4900"
}
},
"Personalaufwand": {
"account_number": "5",
"is_group": 1,
"root_type": "Expense",
"Lohnaufwand": {
"account_number": "5000"
},
"Sozialversicherungsaufwand": {
"account_number": "5700"
},
"Übriger Personalaufwand": {
"account_number": "5800"
},
"Leistungen Dritter": {
"account_number": "5900"
}
},
"Übriger betrieblicher Aufwand, Abschreibungen und Wertberichtigungen sowie Finanzergebnis": {
"account_number": "6",
"is_group": 1,
"root_type": "Expense",
"Raumaufwand": {
"account_number": "6000"
},
"Unterhalt, Reparaturen, Ersatz mobile Sachanlagen": {
"account_number": "6100"
},
"Leasingaufwand mobile Sachanlagen": {
"account_number": "6105"
},
"Fahrzeug- und Transportaufwand": {
"account_number": "6200"
},
"Fahrzeugleasing und -mieten": {
"account_number": "6260"
},
"Sachversicherungen, Abgaben, Gebühren, Bewilligungen": {
"account_number": "6300"
},
"Energie- und Entsorgungsaufwand": {
"account_number": "6400"
},
"Verwaltungsaufwand": {
"account_number": "6500"
},
"Informatikaufwand inkl. Leasing": {
"account_number": "6570"
},
"Werbeaufwand": {
"account_number": "6600"
},
"Sonstiger betrieblicher Aufwand": {
"account_number": "6700"
},
"Abschreibungen und Wertberichtigungen auf Positionen des Anlagevermögens": {
"account_number": "6800"
},
"Finanzaufwand": {
"account_number": "6900"
},
"Finanzertrag": {
"account_number": "6950"
}
},
"Betrieblicher Nebenerfolg": {
"account_number": "7",
"is_group": 1,
"root_type": "Income",
"Ertrag Nebenbetrieb": {
"account_number": "7000"
},
"Aufwand Nebenbetrieb": {
"account_number": "7010"
},
"Ertrag betriebliche Liegenschaft": {
"account_number": "7500"
},
"Aufwand betriebliche Liegenschaft": {
"account_number": "7510"
}
},
"Betriebsfremder, ausserordentlicher, einmaliger oder periodenfremder Aufwand und Ertrag": {
"account_number": "8",
"is_group": 1,
"root_type": "Expense",
"Betriebsfremder Aufwand": {
"account_number": "8000"
},
"Betriebsfremder Ertrag": {
"account_number": "8100"
},
"Ausserordentlicher, einmaliger oder periodenfremder Aufwand": {
"account_number": "8500"
},
"Ausserordentlicher, einmaliger oder periodenfremder Ertrag": {
"account_number": "8510"
},
"Direkte Steuern": {
"account_number": "8900"
}
},
"Abschluss": {
"account_number": "9",
"is_group": 1,
"root_type": "Equity",
"Jahresgewinn oder Jahresverlust": {
"account_number": "9200"
}
}
}
}

View File

@@ -31,7 +31,8 @@
"label": "Reference Document Type",
"options": "DocType",
"read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"default": "0",

View File

@@ -802,7 +802,6 @@ def get_je_matching_query(
.where(je.clearance_date.isnull())
.where(jea.account == common_filters.bank_account)
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
.where(je.docstatus == 1)
.where(filter_by_date)
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
)

View File

@@ -45,45 +45,41 @@ class AutoMatchbyAccountIBAN:
if not (self.bank_party_account_number or self.bank_party_iban):
return None
result = self.match_account_in_party()
return result
return self.match_account_in_party()
def match_account_in_party(self) -> tuple | None:
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
result = None
parties = get_parties_in_order(self.deposit)
or_filters = self.get_or_filters()
"""
Returns (Party Type, Party) if a matching account is found in Bank Account or Employee:
1. Get party from a matching (iban/account no) Bank Account
2. If not found, get party from Employee with matching bank account details (iban/account no)
"""
if not (self.bank_party_account_number or self.bank_party_iban):
# Nothing to match
return None
for party in parties:
party_result = frappe.db.get_all(
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
)
# Search for a matching Bank Account that has party set
party_result = frappe.db.get_all(
"Bank Account",
or_filters=self.get_or_filters(),
filters={"party_type": ("is", "set"), "party": ("is", "set")},
fields=["party", "party_type"],
limit_page_length=1,
)
if result := party_result[0] if party_result else None:
return (result["party_type"], result["party"])
if party == "Employee" and not party_result:
# Search in Bank Accounts first for Employee, and then Employee record
if "bank_account_no" in or_filters:
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
# If no party is found, search in Employee (since it has bank account details)
if employee_result := frappe.db.get_all(
"Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1
):
return ("Employee", employee_result[0])
party_result = frappe.db.get_all(
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
if "bank_ac_no" in or_filters:
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
if party_result:
result = (
party,
party_result[0],
)
break
return result
def get_or_filters(self) -> dict:
def get_or_filters(self, party: str | None = None) -> dict:
"""Return OR filters for Bank Account and IBAN"""
or_filters = {}
if self.bank_party_account_number:
or_filters["bank_account_no"] = self.bank_party_account_number
bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no"
or_filters[bank_ac_field] = self.bank_party_account_number
if self.bank_party_iban:
or_filters["iban"] = self.bank_party_iban
@@ -103,8 +99,7 @@ class AutoMatchbyPartyNameDescription:
if not (self.bank_party_name or self.description):
return None
result = self.match_party_name_desc_in_party()
return result
return self.match_party_name_desc_in_party()
def match_party_name_desc_in_party(self) -> tuple | None:
"""Fuzzy search party name and/or description against parties in the system"""
@@ -113,7 +108,8 @@ class AutoMatchbyPartyNameDescription:
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
field = f"{party.lower()}_name"
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
for field in ["bank_party_name", "description"]:
if not self.get(field):
@@ -132,16 +128,14 @@ class AutoMatchbyPartyNameDescription:
def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None:
skip = False
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
result = process.extract(
query=self.get(field),
choices={row.get("name"): row.get("party_name") for row in names},
scorer=fuzz.token_set_ratio,
)
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
return None, skip
return (
party,
party_name,
), skip
return ((party, party_name), skip) if party_name else (None, skip)
def process_fuzzy_result(self, result: list | None):
"""
@@ -150,30 +144,30 @@ class AutoMatchbyPartyNameDescription:
Returns: Result, Skip (whether or not to discontinue matching)
"""
PARTY, SCORE, CUTOFF = 0, 1, 80
SCORE, PARTY_ID, CUTOFF = 1, 2, 80
if not result or not len(result):
return None, False
first_result = result[0]
if len(result) == 1:
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
second_result = result[1]
# If multiple matches with the same score, return None but discontinue matching
# Matches were found but were too close to distinguish between
if first_result[SCORE] == second_result[SCORE]:
return None, True
return first_result[PARTY], True
return first_result[PARTY_ID], True
else:
return None, False
def get_parties_in_order(deposit: float) -> list:
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
if flt(deposit) > 0:
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
return parties
return (
["Customer", "Supplier", "Employee"] # most -> least likely to pay us
if flt(deposit) > 0
else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us
)

View File

@@ -430,12 +430,6 @@ frappe.ui.form.on("Journal Entry Account", {
});
}
},
cost_center: function (frm, dt, dn) {
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
erpnext.journal_entry.set_account_details(frm, dt, dn);
}
},
account: function (frm, dt, dn) {
erpnext.journal_entry.set_account_details(frm, dt, dn);

View File

@@ -146,10 +146,9 @@ class TestJournalEntry(unittest.TestCase):
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
},
)
jv.insert()
if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
self.assertRaises(StockAccountInvalidTransaction, jv.save)
frappe.db.rollback()
else:
jv.submit()

View File

@@ -1576,6 +1576,14 @@ class PaymentEntry(AccountsController):
elif self.payment_type in ("Pay", "Internal Transfer"):
return self.paid_from
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
company_currency = erpnext.get_company_currency(self.company)
conversion_rate = self.target_exchange_rate
if self.paid_from_account_currency != company_currency:
conversion_rate = self.source_exchange_rate
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
@@ -2826,7 +2834,7 @@ def get_payment_entry(
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
pe.set("party_bank_account", bank_account)
pe.set_bank_account_data()
# only Purchase Invoice can be blocked individually

View File

@@ -83,8 +83,7 @@ class TestPaymentRequest(FrappeTestCase):
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
supplier="_Test Supplier USD",
currency="USD",
conversion_rate=50,
)
@@ -108,8 +107,7 @@ class TestPaymentRequest(FrappeTestCase):
def test_multiple_payment_entry_against_purchase_invoice(self):
purchase_invoice = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
supplier="_Test Supplier USD",
currency="USD",
conversion_rate=50,
)

View File

@@ -23,6 +23,7 @@
"hide_unavailable_items",
"auto_add_item_to_cart",
"validate_stock_on_save",
"print_receipt_on_order_complete",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
@@ -375,6 +376,12 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
},
{
"default": "0",
"fieldname": "print_receipt_on_order_complete",
"fieldtype": "Check",
"label": "Print Receipt on Order Complete"
}
],
"icon": "icon-cog",
@@ -402,7 +409,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2022-08-10 12:57:06.241439",
"modified": "2025-01-01 11:07:03.161950",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -47,6 +47,7 @@ class POSProfile(Document):
letter_head: DF.Link | None
payments: DF.Table[POSPaymentMethod]
print_format: DF.Link | None
print_receipt_on_order_complete: DF.Check
select_print_heading: DF.Link | None
selling_price_list: DF.Link | None
tax_category: DF.Link | None

View File

@@ -415,8 +415,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
"parent": args.parent,
"parenttype": args.parenttype,
"child_docname": args.get("child_docname"),
"discount_percentage": 0.0,
"discount_amount": 0,
}
)

View File

@@ -10,7 +10,6 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
@@ -33,7 +32,7 @@ from erpnext.accounts.general_ledger import (
merge_similar_entries,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
@@ -838,12 +837,12 @@ class PurchaseInvoice(BuyingController):
def update_supplier_outstanding(self, update_outstanding):
if update_outstanding == "No":
update_outstanding_amt(
self.credit_to,
"Supplier",
self.supplier,
self.doctype,
self.return_against if cint(self.is_return) and self.return_against else self.name,
update_voucher_outstanding(
voucher_type=self.doctype,
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
account=self.credit_to,
party_type="Supplier",
party=self.supplier,
)
def get_gl_entries(self, warehouse_account=None):
@@ -1126,6 +1125,7 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
and item.net_rate == net_rate_map[item.pr_detail]
and item.item_code in stock_items
):
discrepancy_caused_by_exchange_rate_difference = (
item.qty * item.net_rate
@@ -1796,13 +1796,13 @@ class PurchaseInvoice(BuyingController):
self.remove(d)
## Add pending vouchers on which tax was withheld
for voucher_no, voucher_details in voucher_wise_amount.items():
for row in voucher_wise_amount:
self.append(
"tax_withheld_vouchers",
{
"voucher_name": voucher_no,
"voucher_type": voucher_details.get("voucher_type"),
"taxable_amount": voucher_details.get("amount"),
"voucher_name": row.voucher_name,
"voucher_type": row.voucher_type,
"taxable_amount": row.taxable_amount,
},
)

View File

@@ -45,12 +45,16 @@ frappe.listview_settings["Purchase Invoice"] = {
},
onload: function (listview) {
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
});
if (frappe.model.can_create("Purchase Receipt")) {
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
});
}
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
});
}
},
};

View File

@@ -372,6 +372,53 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice,
)
# Creating Purchase Invoice with USD currency
pr = frappe.new_doc("Purchase Receipt")
pr.currency = "USD"
pr.company = "_Test Company with perpetual inventory"
pr.conversion_rate = (70,)
pr.supplier = "_Test Supplier USD"
pr.append(
"items",
{
"item_code": "_Test Non Stock Item",
"qty": 1,
"rate": 100,
},
)
pr.append(
"items",
{"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"},
)
pr.insert()
pr.submit()
# Createing purchase invoice against Purchase Receipt
pi = create_purchase_invoice(pr.name)
pi.conversion_rate = 80
pi.credit_to = "_Test Payable USD - TCP1"
pi.insert()
pi.submit()
# Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
# fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[1].base_net_amount - pr.items[1].base_net_amount
)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
pi.insert()

View File

@@ -16,6 +16,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
setup(doc) {
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {
Dunning: this.make_dunning.bind(this),
"Invoice Discounting": this.make_invoice_discounting.bind(this),
};
}
company() {
super.company();
@@ -121,12 +125,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
},
__("Create")
);
cur_frm.add_custom_button(
this.frm.add_custom_button(
__("Invoice Discounting"),
function () {
cur_frm.events.create_invoice_discounting(cur_frm);
},
this.make_invoice_discounting.bind(this),
__("Create")
);
@@ -135,22 +136,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
.reduce((prev, current) => prev || current, false);
if (payment_is_overdue) {
this.frm.add_custom_button(
__("Dunning"),
() => {
this.frm.events.create_dunning(this.frm);
},
__("Create")
);
this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create"));
}
}
if (doc.docstatus === 1) {
cur_frm.add_custom_button(
__("Maintenance Schedule"),
function () {
cur_frm.cscript.make_maintenance_schedule();
},
this.make_maintenance_schedule.bind(this),
__("Create")
);
}
@@ -185,6 +178,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
}
make_invoice_discounting() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
frm: this.frm,
});
}
make_dunning() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
frm: this.frm,
});
}
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
@@ -1045,20 +1052,6 @@ frappe.ui.form.on("Sales Invoice", {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}
},
create_invoice_discounting: function (frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
frm: frm,
});
},
create_dunning: function (frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
frm: frm,
});
},
});
frappe.ui.form.on("Sales Invoice Timesheet", {

View File

@@ -24,7 +24,11 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
update_voucher_outstanding,
)
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
@@ -1192,14 +1196,14 @@ class SalesInvoice(SellingController):
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
update_outstanding_amt(
self.debit_to,
"Customer",
self.customer,
self.doctype,
self.return_against if cint(self.is_return) and self.return_against else self.name,
update_voucher_outstanding(
voucher_type=self.doctype,
voucher_no=self.return_against
if cint(self.is_return) and self.return_against
else self.name,
account=self.debit_to,
party_type="Customer",
party=self.customer,
)
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):

View File

@@ -32,12 +32,16 @@ frappe.listview_settings["Sales Invoice"] = {
right_column: "grand_total",
onload: function (listview) {
listview.page.add_action_item(__("Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
});
if (frappe.model.can_create("Delivery Note")) {
listview.page.add_action_item(__("Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
});
}
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
});
}
},
};

View File

@@ -4235,6 +4235,7 @@ class TestSalesInvoice(FrappeTestCase):
si = create_sales_invoice(do_not_submit=True)
project = frappe.new_doc("Project")
project.company = "_Test Company"
project.project_name = "Test Total Billed Amount"
project.save()
@@ -4245,6 +4246,30 @@ class TestSalesInvoice(FrappeTestCase):
doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total)
def test_pos_returns_with_party_account_currency(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
pos_profile = make_pos_profile()
pos_profile.payments = []
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
pos_profile.save()
pos = create_sales_invoice(
customer="_Test Customer USD",
currency="USD",
conversion_rate=86.595000000,
qty=2,
do_not_save=True,
)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.debit_to = "_Test Receivable USD - _TC"
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35})
pos.save().submit()
pos_return = make_sales_return(pos.name)
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -87,6 +87,7 @@ def get_party_details(inv):
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if inv.doctype == "Payment Entry":
inv.tax_withholding_net_total = inv.net_total
inv.base_tax_withholding_net_total = inv.net_total
pan_no = ""
parties = []
@@ -326,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
else:
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount)
elif party_type == "Customer":
if tax_deducted:
@@ -356,13 +357,16 @@ def is_tax_deducted_on_the_basis_of_inv(vouchers):
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
field = (
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total"
)
voucher_wise_amount = {}
voucher_wise_amount = []
vouchers = []
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
field = [
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
"name",
"grand_total",
]
filters = {
"company": company,
frappe.scrub(party_type): ["in", parties],
@@ -376,15 +380,24 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
)
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field])
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
for d in invoices_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": d.name,
"voucher_type": doctype,
"taxable_amount": d.base_net_total,
"grand_total": d.grand_total,
}
)
)
journal_entries_details = frappe.db.sql(
"""
SELECT j.name, ja.credit - ja.debit AS amount
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
j.name = ja.parent
@@ -403,13 +416,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
tax_details.get("tax_withholding_category"),
company,
),
as_dict=1,
)
if journal_entries_details:
for d in journal_entries_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
for d in journal_entries_details:
vouchers.append(d.name)
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": d.name,
"voucher_type": "Journal Entry",
"taxable_amount": d.amount,
"reference_type": d.reference_type,
}
)
)
return vouchers, voucher_wise_amount
@@ -508,12 +528,24 @@ def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
return advance_tax_from_across_fiscal_year
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
pi_grand_total = 0
pi_base_net_total = 0
jv_credit_amt = 0
pe_credit_amt = 0
for row in voucher_wise_amount:
if row.voucher_type == "Purchase Invoice":
pi_grand_total += row.get("grand_total", 0)
pi_base_net_total += row.get("taxable_amount", 0)
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
jv_credit_amt += row.get("taxable_amount", 0)
## for TDS to be deducted on advances
payment_entry_filters = {
pe_filters = {
"party_type": "Supplier",
"party": ("in", parties),
"docstatus": 1,
@@ -524,70 +556,49 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
"company": inv.company,
}
field = "sum(tax_withholding_net_total)"
consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount)
if cint(tax_details.consider_party_ledger_amount):
invoice_filters.pop("apply_tds", None)
field = "sum(grand_total)"
payment_entry_filters.pop("apply_tax_withholding_amount", None)
payment_entry_filters.pop("tax_withholding_category", None)
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
supp_jv_credit_amt = (
frappe.db.get_value(
"Journal Entry Account",
{
"parent": ("in", vouchers),
"docstatus": 1,
"party": ("in", parties),
"reference_type": ("!=", "Purchase Invoice"),
},
"sum(credit_in_account_currency - debit_in_account_currency)",
)
or 0.0
)
if consider_party_ledger_amt:
pe_filters.pop("apply_tax_withholding_amount", None)
pe_filters.pop("tax_withholding_category", None)
# Get Amount via payment entry
payment_entry_amounts = frappe.db.get_all(
payment_entries = frappe.db.get_all(
"Payment Entry",
filters=payment_entry_filters,
fields=["sum(unallocated_amount) as amount", "payment_type"],
group_by="payment_type",
filters=pe_filters,
fields=["name", "unallocated_amount as taxable_amount", "payment_type"],
)
supp_credit_amt = supp_jv_credit_amt
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
for type in payment_entry_amounts:
if type.payment_type == "Pay":
supp_credit_amt += type.amount
else:
supp_credit_amt -= type.amount
for row in payment_entries:
value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount
pe_credit_amt += value
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": row.name,
"voucher_type": "Payment Entry",
"taxable_amount": value,
}
)
)
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
supp_credit_amt = jv_credit_amt + pe_credit_amt + inv.get("tax_withholding_net_total", 0)
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
if inv.doctype != "Payment Entry":
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
else:
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
# if consider_party_ledger_amount is checked, then threshold will be based on grand total
amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total
has_cumulative_threshold_breached = (
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
cumulative_threshold_breached = (
cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold
)
if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = (
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
)
supp_credit_amt += net_total
if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached):
supp_credit_amt += pi_base_net_total
if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
supp_credit_amt = pi_base_net_total + tax_withholding_net_total - cumulative_threshold
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
tds_amount = get_lower_deduction_amount(

View File

@@ -569,6 +569,15 @@ class TestTaxWithholdingCategory(FrappeTestCase):
pi1.submit()
invoices.append(pi1)
pe = create_payment_entry(
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier6", paid_amount=1000
)
pe.apply_tax_withholding_amount = 1
pe.tax_withholding_category = "Test Multi Invoice Category"
pe.save()
pe.submit()
invoices.append(pe)
pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
pi2.apply_tds = 1
pi2.tax_withholding_category = "Test Multi Invoice Category"
@@ -584,6 +593,8 @@ class TestTaxWithholdingCategory(FrappeTestCase):
self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
self.assertTrue(pi2.tax_withheld_vouchers[2].voucher_name == pe.name)
self.assertTrue(pi2.tax_withheld_vouchers[2].taxable_amount == pe.paid_amount)
# cancel invoices to avoid clashing
for d in reversed(invoices):

View File

@@ -35,7 +35,7 @@ def make_gl_entries(
make_acc_dimensions_offsetting_entry(gl_map)
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost)
if gl_map and len(gl_map) > 1:
if gl_map[0].voucher_type != "Period Closing Voucher":
create_payment_ledger_entry(
@@ -163,12 +163,12 @@ def validate_accounting_period(gl_map):
)
def process_gl_map(gl_map, merge_entries=True, precision=None):
def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False):
if not gl_map:
return []
if gl_map[0].voucher_type != "Period Closing Voucher":
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision, from_repost)
if merge_entries:
gl_map = merge_similar_entries(gl_map, precision)
@@ -178,13 +178,17 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
return gl_map
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False):
new_gl_map = []
for d in gl_map:
cost_center = d.get("cost_center")
# Validate budget against main cost center
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
if not from_repost:
validate_expense_against_budget(
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
)
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)

View File

@@ -1644,7 +1644,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
if wh_details.account == account and not wh_details.is_group
]
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
total_stock_value = get_stock_value_on(related_warehouses, posting_date, company=company)
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses

View File

@@ -34,6 +34,7 @@ frappe.ui.form.on("Depreciation Schedule", {
asset_depr_schedule_name: frm.doc.name,
date: row.schedule_date,
},
debounce: 1000,
callback: function (r) {
frappe.model.sync(r.message);
frm.refresh();

View File

@@ -44,16 +44,22 @@ frappe.listview_settings["Purchase Order"] = {
listview.call_for_selected_items(method, { status: "Submitted" });
});
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
});
if (frappe.model.can_create("Purchase Invoice")) {
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
});
}
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
});
if (frappe.model.can_create("Purchase Receipt")) {
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
});
}
listview.page.add_action_item(__("Advance Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Advance Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
}
},
};

View File

@@ -11,12 +11,20 @@ frappe.listview_settings["Supplier Quotation"] = {
},
onload: function (listview) {
listview.page.add_action_item(__("Purchase Order"), () => {
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
});
if (frappe.model.can_create("Purchase Order")) {
listview.page.add_action_item(__("Purchase Order"), () => {
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
});
}
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
});
if (frappe.model.can_create("Purchase Invoice")) {
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(
listview,
"Supplier Quotation",
"Purchase Invoice"
);
});
}
},
};

View File

@@ -8,7 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold, qb, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder import Criterion
from frappe.query_builder import Criterion, DocType
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import (
@@ -252,6 +252,8 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
self.set_taxes_and_charges()
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
@@ -266,6 +268,7 @@ class AccountsController(TransactionBase):
self.set_total_in_words()
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -403,6 +406,39 @@ class AccountsController(TransactionBase):
for row in batches:
frappe.delete_doc("Batch", row.name)
def validate_company_in_accounting_dimension(self):
doc_field = DocType("DocField")
accounting_dimension = DocType("Accounting Dimension")
dimension_list = (
frappe.qb.from_(accounting_dimension)
.select(accounting_dimension.document_type)
.join(doc_field)
.on(doc_field.parent == accounting_dimension.document_type)
.where(doc_field.fieldname == "company")
).run(as_list=True)
dimension_list = sum(dimension_list, ["Project"])
self.validate_company(dimension_list)
for child in self.get_all_children() or []:
self.validate_company(dimension_list, child)
def validate_company(self, dimension_list, child=None):
for dimension in dimension_list:
if not child:
dimension_value = self.get(frappe.scrub(dimension))
else:
dimension_value = child.get(frappe.scrub(dimension))
if dimension_value:
company = frappe.get_cached_value(dimension, dimension_value, "company")
if company and company != self.company:
frappe.throw(
_("{0}: {1} does not belong to the Company: {2}").format(
dimension, frappe.bold(dimension_value), self.company
)
)
def validate_return_against_account(self):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
@@ -935,6 +971,12 @@ class AccountsController(TransactionBase):
):
return True
def set_taxes_and_charges(self):
if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"):
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
self.append_taxes_from_master(tax_master_doctype)
def append_taxes_from_master(self, tax_master_doctype=None):
if self.get("taxes_and_charges"):
if not tax_master_doctype:
@@ -2413,10 +2455,15 @@ class AccountsController(TransactionBase):
)
if (
flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total"))
abs(
flt(total, self.precision("grand_total"))
- flt(grand_total, self.precision("grand_total"))
)
> 0.1
or flt(base_total, self.precision("base_grand_total"))
- flt(base_grand_total, self.precision("base_grand_total"))
or abs(
flt(base_total, self.precision("base_grand_total"))
- flt(base_grand_total, self.precision("base_grand_total"))
)
> 0.1
):
frappe.throw(
@@ -3686,6 +3733,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
).format(frappe.bold(parent.name))
)
else: # Sales Order
parent.validate_for_duplicate_items()
parent.validate_warehouse()
parent.update_reserved_qty()
parent.update_project()

View File

@@ -174,7 +174,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
)
for column in fields:
returned_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0
returned_qty = (
flt(already_returned_data.get(column, 0), stock_qty_precision)
if len(already_returned_data) > 0
else 0
)
if column == "stock_qty" and not args.get("return_qty_from_rejected_warehouse"):
reference_qty = ref.get(column)
@@ -186,7 +190,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
max_returnable_qty = flt(reference_qty, stock_qty_precision) - returned_qty
max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision)
label = column.replace("_", " ").title()
if reference_qty:
@@ -370,6 +374,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if doc.get("is_return"):
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
doc.consolidated_invoice = ""
# no copy enabled for party_account_currency
doc.party_account_currency = source.party_account_currency
doc.set("payments", [])
doc.update_billed_amount_in_delivery_note = True
for data in source.payments:
@@ -1150,3 +1156,9 @@ def get_available_serial_nos(serial_nos, warehouse):
return frappe.get_all(
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
)
@frappe.whitelist()
def get_payment_data(invoice):
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
return payment

View File

@@ -714,6 +714,16 @@ class SellingController(StockController):
if self.doctype == "POS Invoice":
return
items = [item.item_code for item in self.get("items")]
item_stock_map = frappe._dict(
frappe.get_all(
"Item",
filters={"item_code": ["in", items]},
fields=["item_code", "is_stock_item"],
as_list=True,
)
)
for d in self.get("items"):
if self.doctype == "Sales Invoice":
stock_items = [
@@ -747,7 +757,7 @@ class SellingController(StockController):
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
get_link_to_form("Selling Settings", "Selling Settings"),
)
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
if item_stock_map.get(d.item_code):
if stock_items in check_list:
frappe.throw(duplicate_items_msg)
else:

View File

@@ -17,6 +17,7 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -1532,32 +1533,32 @@ class TestAccountsController(FrappeTestCase):
# Invoices
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si1.department = "Management"
si1.department = "Management - _TC"
si1.save().submit()
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si2.department = "Operations"
si2.department = "Operations - _TC"
si2.save().submit()
# Payments
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note1.department = "Management"
cr_note1.department = "Management - _TC"
cr_note1.is_return = 1
cr_note1.save().submit()
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note2.department = "Legal"
cr_note2.department = "Legal - _TC"
cr_note2.is_return = 1
cr_note2.save().submit()
pe1 = get_payment_entry(si1.doctype, si1.name)
pe1.references = []
pe1.department = "Research & Development"
pe1.department = "Research & Development - _TC"
pe1.save().submit()
pe2 = get_payment_entry(si1.doctype, si1.name)
pe2.references = []
pe2.department = "Management"
pe2.department = "Management - _TC"
pe2.save().submit()
je1 = self.create_journal_entry(
@@ -1570,7 +1571,7 @@ class TestAccountsController(FrappeTestCase):
)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1.accounts[0].department = "Management"
je1.accounts[0].department = "Management - _TC"
je1.save().submit()
# assert dimension filter's result
@@ -1579,17 +1580,17 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(len(pr.invoices), 2)
self.assertEqual(len(pr.payments), 5)
pr.department = "Legal"
pr.department = "Legal - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
pr.department = "Management"
pr.department = "Management - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 3)
pr.department = "Research & Development"
pr.department = "Research & Development - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
@@ -1600,17 +1601,17 @@ class TestAccountsController(FrappeTestCase):
# Invoice
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si.department = "Management"
si.department = "Management - _TC"
si.save().submit()
# Payment
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note.department = "Management"
cr_note.department = "Management - _TC"
cr_note.is_return = 1
cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.department = "Management"
pr.department = "Management - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
@@ -1642,7 +1643,7 @@ class TestAccountsController(FrappeTestCase):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate_in_account_currency = 1
dpt = "Research & Development"
dpt = "Research & Development - _TC"
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
si.department = dpt
@@ -1677,7 +1678,7 @@ class TestAccountsController(FrappeTestCase):
def test_93_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
dpt = "Research & Development - _TC"
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
adv.department = dpt
@@ -2135,3 +2136,13 @@ class TestAccountsController(FrappeTestCase):
journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent)
purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name)
self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date)
def test_company_validation_in_dimension(self):
si = create_sales_invoice(do_not_submit=True)
project = make_project({"project_name": "_Test Demo Project1", "company": "_Test Company 1"})
si.project = project.name
self.assertRaises(frappe.ValidationError, si.save)
si_1 = create_sales_invoice(do_not_submit=True)
si_1.items[0].project = project.name
self.assertRaises(frappe.ValidationError, si_1.save)

View File

@@ -55,12 +55,13 @@ def get_columns():
"options": "Company",
"width": 120,
},
{"fieldname": "address", "label": _("Address"), "fieldtype": "Data", "width": 130},
{"fieldname": "state", "label": _("State"), "fieldtype": "Data", "width": 100},
{"fieldname": "pincode", "label": _("Postal Code"), "fieldtype": "Data", "width": 90},
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
{
"fieldname": "country",
"label": _("Country"),
"fieldname": "country",
"fieldtype": "Link",
"options": "Country",
"width": 100,
@@ -93,8 +94,9 @@ def get_data(filters):
lead.owner,
lead.company,
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
address.state,
address.pincode,
address.city,
address.state,
address.country,
)
.where(lead.company == filters.company)

View File

@@ -69,23 +69,7 @@ class PlaidConnector:
else:
return response["link_token"]
def auth(self):
try:
self.client.Auth.get(self.access_token)
except ItemError as e:
if e.code == "ITEM_LOGIN_REQUIRED":
pass
except APIError as e:
if e.code == "PLANNED_MAINTENANCE":
pass
except requests.Timeout:
pass
except Exception as e:
frappe.log_error("Plaid: Authentication error")
frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None):
self.auth()
kwargs = dict(access_token=self.access_token, start_date=start_date, end_date=end_date)
if account_id:
kwargs.update(dict(account_ids=[account_id]))

View File

@@ -1,77 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("QuickBooks Migrator", {
connect: function (frm) {
// OAuth requires user intervention to provide application access permissionsto requested scope
// Here we open a new window and redirect user to the authorization url.
// After user grants us permission to access. We will set authorization details on this doc which will force refresh.
window.open(frm.doc.authorization_url);
},
fetch_data: function (frm) {
frm.call("migrate");
},
onload: function (frm) {
frm.trigger("set_indicator");
var domain = frappe.urllib.get_base_url();
var redirect_url = `${domain}/api/method/erpnext.erpnext_integrations.doctype.quickbooks_migrator.quickbooks_migrator.callback`;
if (frm.doc.redirect_url != redirect_url) {
frm.set_value("redirect_url", redirect_url);
}
// Instead of changing percentage width and message of single progress bar
// Show a different porgress bar for every action after some time remove the finished progress bar
// Former approach causes the progress bar to dance back and forth.
frm.trigger("set_indicator");
frappe.realtime.on("quickbooks_progress_update", function (data) {
frm.dashboard.show_progress(data.message, (data.count / data.total) * 100, data.message);
if (data.count == data.total) {
window.setTimeout(
function (message) {
frm.dashboard.hide_progress(message);
},
1500,
data.messsage
);
}
});
},
refresh: function (frm) {
frm.trigger("set_indicator");
if (!frm.doc.access_token) {
// Unset access_token signifies that we don't have enough information to connect to quickbooks api and fetch data
if (frm.doc.authorization_url) {
frm.add_custom_button(__("Connect to Quickbooks"), function () {
frm.trigger("connect");
});
}
}
if (frm.doc.access_token) {
// If we have access_token that means we also have refresh_token we don't need user intervention anymore
// All we need now is a Company from erpnext
frm.remove_custom_button(__("Connect to Quickbooks"));
frm.toggle_display("company_settings", 1);
frm.set_df_property("company", "reqd", 1);
if (frm.doc.company) {
frm.add_custom_button(__("Fetch Data"), function () {
frm.trigger("fetch_data");
});
}
}
},
set_indicator: function (frm) {
var indicator_map = {
"Connecting to QuickBooks": [__("Connecting to QuickBooks"), "orange"],
"Connected to QuickBooks": [__("Connected to QuickBooks"), "green"],
"In Progress": [__("In Progress"), "orange"],
Complete: [__("Complete"), "green"],
Failed: [__("Failed"), "red"],
};
if (frm.doc.status) {
var indicator = indicator_map[frm.doc.status];
var label = indicator[0];
var color = indicator[1];
frm.page.set_indicator(label, color);
}
},
});

View File

@@ -1,213 +0,0 @@
{
"beta": 1,
"creation": "2018-07-10 14:48:16.757030",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"application_settings",
"client_id",
"redirect_url",
"token_endpoint",
"application_column_break",
"client_secret",
"scope",
"api_endpoint",
"authorization_settings",
"authorization_endpoint",
"refresh_token",
"code",
"authorization_column_break",
"authorization_url",
"access_token",
"quickbooks_company_id",
"company_settings",
"company",
"default_shipping_account",
"default_warehouse",
"company_column_break",
"default_cost_center",
"undeposited_funds_account"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url",
"fieldname": "application_settings",
"fieldtype": "Section Break",
"label": "Application Settings"
},
{
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID",
"reqd": 1
},
{
"fieldname": "redirect_url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Redirect URL",
"reqd": 1
},
{
"default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
"fieldname": "token_endpoint",
"fieldtype": "Data",
"label": "Token Endpoint",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "application_column_break",
"fieldtype": "Column Break"
},
{
"fieldname": "client_secret",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client Secret",
"reqd": 1
},
{
"default": "com.intuit.quickbooks.accounting",
"fieldname": "scope",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Scope",
"read_only": 1,
"reqd": 1
},
{
"default": "https://quickbooks.api.intuit.com/v3",
"fieldname": "api_endpoint",
"fieldtype": "Data",
"label": "API Endpoint",
"read_only": 1,
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "authorization_settings",
"fieldtype": "Section Break",
"label": "Authorization Settings"
},
{
"default": "https://appcenter.intuit.com/connect/oauth2",
"fieldname": "authorization_endpoint",
"fieldtype": "Data",
"label": "Authorization Endpoint",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "refresh_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Refresh Token"
},
{
"fieldname": "code",
"fieldtype": "Data",
"hidden": 1,
"label": "Code"
},
{
"fieldname": "authorization_column_break",
"fieldtype": "Column Break"
},
{
"fieldname": "authorization_url",
"fieldtype": "Data",
"label": "Authorization URL",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "access_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Access Token"
},
{
"fieldname": "quickbooks_company_id",
"fieldtype": "Data",
"hidden": 1,
"label": "Quickbooks Company ID"
},
{
"fieldname": "company_settings",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Company Settings"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "default_shipping_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Default Shipping Account",
"options": "Account"
},
{
"fieldname": "default_warehouse",
"fieldtype": "Link",
"hidden": 1,
"label": "Default Warehouse",
"options": "Warehouse"
},
{
"fieldname": "company_column_break",
"fieldtype": "Column Break"
},
{
"fieldname": "default_cost_center",
"fieldtype": "Link",
"hidden": 1,
"label": "Default Cost Center",
"options": "Cost Center"
},
{
"fieldname": "undeposited_funds_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Undeposited Funds Account",
"options": "Account"
}
],
"issingle": 1,
"modified": "2019-08-07 15:26:00.653433",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "QuickBooks Migrator",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestQuickBooksMigrator(unittest.TestCase):
pass

View File

@@ -1371,6 +1371,64 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
},
)
def get_max_op_qty():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Job Card")
query = (
frappe.qb.from_(table)
.select(Sum(table.total_completed_qty).as_("qty"))
.where(
(table.docstatus == 1)
& (table.work_order == work_order.name)
& (table.is_corrective_job_card == 0)
)
.groupby(table.operation)
)
return min([d.qty for d in query.run(as_dict=True)], default=0)
def get_utilised_cc():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Stock Entry")
subquery = (
frappe.qb.from_(table)
.select(table.name)
.where(
(table.docstatus == 1)
& (table.work_order == work_order.name)
& (table.purpose == "Manufacture")
)
)
table = frappe.qb.DocType("Landed Cost Taxes and Charges")
query = (
frappe.qb.from_(table)
.select(Sum(table.amount).as_("amount"))
.where(table.parent.isin(subquery) & (table.has_corrective_cost == 1))
)
return query.run(as_dict=True)[0].amount or 0
if (
work_order
and work_order.corrective_operation_cost
and cint(
frappe.db.get_single_value(
"Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation"
)
)
):
max_qty = get_max_op_qty() - work_order.produced_qty
remaining_cc = work_order.corrective_operation_cost - get_utilised_cc()
stock_entry.append(
"additional_costs",
{
"expense_account": expense_account,
"description": "Corrective Operation Cost",
"has_corrective_cost": 1,
"amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty),
},
)
@frappe.whitelist()
def get_bom_diff(bom1, bom2):

View File

@@ -650,7 +650,7 @@ class JobCard(Document):
)
)
if self.get("operation") == d.operation:
if self.get("operation") == d.operation or self.is_corrective_job_card:
self.append(
"items",
{
@@ -791,7 +791,7 @@ class JobCard(Document):
fields=["total_time_in_mins", "hour_rate"],
filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order},
):
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
wo.corrective_operation_cost += (flt(row.total_time_in_mins) / 60) * flt(row.hour_rate)
wo.calculate_operating_cost()
wo.flags.ignore_validate_update_after_submit = True

View File

@@ -424,6 +424,92 @@ class TestJobCard(FrappeTestCase):
cost_after_cancel = self.work_order.total_operating_cost
self.assertEqual(cost_after_cancel, original_cost)
@change_settings(
"Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
)
def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self):
wo = make_wo_order_test_record(
item="_Test FG Item 2",
qty=10,
transfer_material_against=self.transfer_material_against,
source_warehouse=self.source_warehouse,
)
self.generate_required_stock(wo)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name})
job_card.update({"for_quantity": 4})
job_card.append(
"time_logs",
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 4},
)
job_card.submit()
corrective_action = frappe.get_doc(
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
).insert()
corrective_job_card = make_corrective_job_card(
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
)
corrective_job_card.hour_rate = 100
corrective_job_card.insert()
corrective_job_card.append(
"time_logs",
{
"from_time": add_to_date(now(), hours=2),
"to_time": add_to_date(now(), hours=2, minutes=30),
"completed_qty": 4,
},
)
corrective_job_card.submit()
wo.reload()
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_stock_entry_for_wo,
)
stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=3)
self.assertEqual(stock_entry.additional_costs[1].amount, 37.5)
frappe.get_doc(stock_entry).submit()
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
make_job_card(
wo.name,
[{"name": wo.operations[0].name, "operation": "_Test Operation 1", "qty": 3, "pending_qty": 3}],
)
workstation = job_card.workstation
job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name})
job_card.update({"for_quantity": 3})
job_card.workstation = workstation
job_card.append(
"time_logs",
{
"from_time": add_to_date(now(), hours=3),
"to_time": add_to_date(now(), hours=4),
"completed_qty": 3,
},
)
job_card.submit()
corrective_job_card = make_corrective_job_card(
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
)
corrective_job_card.hour_rate = 80
corrective_job_card.insert()
corrective_job_card.append(
"time_logs",
{
"from_time": add_to_date(now(), hours=4),
"to_time": add_to_date(now(), hours=4, minutes=30),
"completed_qty": 3,
},
)
corrective_job_card.submit()
wo.reload()
stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=4)
self.assertEqual(stock_entry.additional_costs[1].amount, 52.5)
def test_job_card_statuses(self):
def assertStatus(status):
jc.set_status()

View File

@@ -348,8 +348,9 @@ class WorkOrder(Document):
if flt(self.material_transferred_for_manufacturing) > 0:
status = "In Process"
total_qty = flt(flt(self.produced_qty) + flt(self.process_loss_qty), self.precision("qty"))
if flt(total_qty) >= flt(self.qty):
precision = frappe.get_precision("Work Order", "produced_qty")
total_qty = flt(self.produced_qty, precision) + flt(self.process_loss_qty, precision)
if flt(total_qty, precision) >= flt(self.qty, precision):
status = "Completed"
else:
status = "Cancelled"

View File

@@ -1,6 +1,7 @@
[
{
"project_name": "_Test Project",
"status": "Open"
"status": "Open",
"company": "_Test Company"
}
]

View File

@@ -822,7 +822,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
}
set_total_amount_to_default_mop() {
async set_total_amount_to_default_mop() {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
@@ -844,6 +844,45 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
);
}
/*
During returns, if an user select mode of payment other than
default mode of payment, it should retain the user selection
instead resetting it to default mode of payment.
*/
let payment_amount = 0;
this.frm.doc.payments.forEach(payment => {
payment_amount += payment.amount
});
if (payment_amount == total_amount_to_pay) {
return;
}
/*
For partial return, if the payment was made using single mode of payment
it should set the return to that mode of payment only.
*/
let return_against_mop = await frappe.call({
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
args: {
invoice: this.frm.doc.return_against
}
});
if (return_against_mop.message.length === 1) {
this.frm.doc.payments.forEach(payment => {
if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) {
payment.amount = total_amount_to_pay;
} else {
payment.amount = 0;
}
});
this.frm.refresh_fields();
return;
}
this.frm.doc.payments.find(payment => {
if (payment.default) {
payment.amount = total_amount_to_pay;

View File

@@ -1658,7 +1658,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if (!r.exc && r.message) {
me.remove_pricing_rule(r.message, removed_pricing_rule);
me.remove_pricing_rule(r.message, removed_pricing_rule, item.name);
me.calculate_taxes_and_totals();
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
}
@@ -1751,7 +1751,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"serial_no": d.serial_no,
"batch_no": d.batch_no,
"price_list_rate": d.price_list_rate,
"conversion_factor": d.conversion_factor || 1.0
"conversion_factor": d.conversion_factor || 1.0,
"discount_percentage" : d.discount_percentage,
"discount_amount" : d.discount_amount,
});
// if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list
@@ -1935,7 +1937,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
});
}
remove_pricing_rule(item, removed_pricing_rule) {
remove_pricing_rule(item, removed_pricing_rule, row_name) {
let me = this;
const fields = ["discount_percentage",
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
@@ -1974,6 +1976,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
me.trigger_price_list_rate();
}
else if(!item.is_free_item && row_name){
me.frm.doc.items.forEach(d => {
if (d.name != row_name) return;
Object.assign(d, item);
});
}
}
trigger_price_list_rate() {

View File

@@ -28,9 +28,13 @@ $.extend(erpnext.queries, {
customer_filter: function (doc) {
if (!doc.customer) {
frappe.throw(
__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "customer", doc.name))])
);
cur_frm.scroll_to_field("customer");
frappe.show_alert({
message: __("Please set {0} first.", [
__(frappe.meta.get_label(doc.doctype, "customer", doc.name)),
]),
indicator: "orange",
});
}
return { filters: { customer: doc.customer } };
@@ -39,11 +43,13 @@ $.extend(erpnext.queries, {
contact_query: function (doc) {
if (frappe.dynamic_link) {
if (!doc[frappe.dynamic_link.fieldname]) {
frappe.throw(
__("Please set {0}", [
cur_frm.scroll_to_field(frappe.dynamic_link.fieldname);
frappe.show_alert({
message: __("Please set {0} first.", [
__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)),
])
);
]),
indicator: "orange",
});
}
return {
@@ -70,11 +76,13 @@ $.extend(erpnext.queries, {
address_query: function (doc) {
if (frappe.dynamic_link) {
if (!doc[frappe.dynamic_link.fieldname]) {
frappe.throw(
__("Please set {0}", [
cur_frm.scroll_to_field(frappe.dynamic_link.fieldname);
frappe.show_alert({
message: __("Please set {0} first.", [
__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)),
])
);
]),
indicator: "orange",
});
}
return {
@@ -89,7 +97,13 @@ $.extend(erpnext.queries, {
company_address_query: function (doc) {
if (!doc.company) {
frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))]));
cur_frm.scroll_to_field("company");
frappe.show_alert({
message: __("Please set {0} first.", [
__(frappe.meta.get_label(doc.doctype, "company", doc.name)),
]),
indicator: "orange",
});
}
return {
@@ -110,9 +124,13 @@ $.extend(erpnext.queries, {
supplier_filter: function (doc) {
if (!doc.supplier) {
frappe.throw(
__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "supplier", doc.name))])
);
cur_frm.scroll_to_field("supplier");
frappe.show_alert({
message: __("Please set {0} first.", [
__(frappe.meta.get_label(doc.doctype, "supplier", doc.name)),
]),
indicator: "orange",
});
}
return { filters: { supplier: doc.supplier } };
@@ -120,9 +138,13 @@ $.extend(erpnext.queries, {
lead_filter: function (doc) {
if (!doc.lead) {
frappe.throw(
__("Please specify a {0}", [__(frappe.meta.get_label(doc.doctype, "lead", doc.name))])
);
cur_frm.scroll_to_field("lead");
frappe.show_alert({
message: __("Please specify a {0} first.", [
__(frappe.meta.get_label(doc.doctype, "lead", doc.name)),
]),
indicator: "orange",
});
}
return { filters: { lead: doc.lead } };

View File

@@ -654,6 +654,62 @@ erpnext.utils.update_child_items = function (opts) {
filters: filters,
};
},
onchange: function () {
const me = this;
frm.call({
method: "erpnext.stock.get_item_details.get_item_details",
args: {
doc: frm.doc,
ctx: {
item_code: this.value,
set_warehouse: frm.doc.set_warehouse,
customer: frm.doc.customer || frm.doc.party_name,
quotation_to: frm.doc.quotation_to,
supplier: frm.doc.supplier,
currency: frm.doc.currency,
is_internal_supplier: frm.doc.is_internal_supplier,
is_internal_customer: frm.doc.is_internal_customer,
conversion_rate: frm.doc.conversion_rate,
price_list: frm.doc.selling_price_list || frm.doc.buying_price_list,
price_list_currency: frm.doc.price_list_currency,
plc_conversion_rate: frm.doc.plc_conversion_rate,
company: frm.doc.company,
order_type: frm.doc.order_type,
is_pos: cint(frm.doc.is_pos),
is_return: cint(frm.doc.is_return),
is_subcontracted: frm.doc.is_subcontracted,
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
doctype: frm.doc.doctype,
name: frm.doc.name,
qty: me.doc.qty || 1,
uom: me.doc.uom,
pos_profile: cint(frm.doc.is_pos) ? frm.doc.pos_profile : "",
tax_category: frm.doc.tax_category,
child_doctype: frm.doc.doctype + " Item",
is_old_subcontracting_flow: frm.doc.is_old_subcontracting_flow,
},
},
callback: function (r) {
if (r.message) {
const { qty, price_list_rate: rate, uom, conversion_factor } = r.message;
const row = dialog.fields_dict.trans_items.df.data.find(
(doc) => doc.idx == me.doc.idx
);
if (row) {
Object.assign(row, {
conversion_factor: me.doc.conversion_factor || conversion_factor,
uom: me.doc.uom || uom,
qty: me.doc.qty || qty,
rate: me.doc.rate || rate,
});
dialog.fields_dict.trans_items.grid.refresh();
}
}
},
});
},
},
{
fieldtype: "Link",

View File

@@ -12,13 +12,17 @@ frappe.listview_settings["Quotation"] = {
};
}
listview.page.add_action_item(__("Sales Order"), () => {
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order");
});
if (frappe.model.can_create("Sales Order")) {
listview.page.add_action_item(__("Sales Order"), () => {
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order");
});
}
listview.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice");
});
if (frappe.model.can_create("Sales Invoice")) {
listview.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice");
});
}
},
get_indicator: function (doc) {

View File

@@ -872,7 +872,11 @@ def make_material_request(source_name, target_doc=None):
},
"Sales Order Item": {
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
"field_map": {
"name": "sales_order_item",
"parent": "sales_order",
"delivery_date": "required_by",
},
"condition": lambda item: not frappe.db.exists(
"Product Bundle", {"name": item.item_code, "disabled": 0}
)
@@ -1357,7 +1361,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
and doc.supplier == supplier
and doc.item_code in items_to_map,
and doc.item_code in items_to_map
and doc.delivered_by_supplier == 1,
},
},
target_doc,
@@ -1501,6 +1506,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
)
set_delivery_date(doc.items, source_name)
doc.set_onload("load_after_mapping", False)
return doc

View File

@@ -60,16 +60,22 @@ frappe.listview_settings["Sales Order"] = {
listview.call_for_selected_items(method, { status: "Submitted" });
});
listview.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice");
});
if (frappe.model.can_create("Sales Invoice")) {
listview.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice");
});
}
listview.page.add_action_item(__("Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
});
if (frappe.model.can_create("Delivery Note")) {
listview.page.add_action_item(__("Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
});
}
listview.page.add_action_item(__("Advance Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Advance Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
});
}
},
};

View File

@@ -155,7 +155,7 @@ erpnext.PointOfSale.Controller = class {
this.page.set_title_sub(
`<span class="indicator orange">
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")}
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
</a>
</span>`
);
@@ -165,6 +165,7 @@ erpnext.PointOfSale.Controller = class {
this.prepare_dom();
this.prepare_components();
this.prepare_menu();
this.prepare_fullscreen_btn();
this.make_new_invoice();
}
@@ -200,6 +201,39 @@ erpnext.PointOfSale.Controller = class {
this.page.add_menu_item(__("Close the POS"), this.close_pos.bind(this), false, "Shift+Ctrl+C");
}
prepare_fullscreen_btn() {
this.page.page_actions.find(".custom-actions").empty();
this.page.add_button(__("Full Screen"), null, { btn_class: "btn-default fullscreen-btn" });
this.bind_fullscreen_events();
}
bind_fullscreen_events() {
this.$fullscreen_btn = this.page.page_actions.find(".fullscreen-btn");
this.$fullscreen_btn.on("click", function () {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else if (document.exitFullscreen) {
document.exitFullscreen();
}
});
$(document).on("fullscreenchange", this.handle_fullscreen_change_event.bind(this));
}
handle_fullscreen_change_event() {
let enable_fullscreen_label = __("Full Screen");
let exit_fullscreen_label = __("Exit Full Screen");
if (document.fullscreenElement) {
this.$fullscreen_btn[0].innerText = exit_fullscreen_label;
} else {
this.$fullscreen_btn[0].innerText = enable_fullscreen_label;
}
}
open_form_view() {
frappe.model.sync(this.frm.doc);
frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name);

View File

@@ -988,8 +988,8 @@ erpnext.PointOfSale.ItemCart = class {
.html(`${__("Last transacted")} ${__(elapsed_time)}`);
res.forEach((invoice) => {
const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format(
"Do MMMM, h:mma"
const posting_datetime = frappe.datetime.str_to_user(
invoice.posting_date + " " + invoice.posting_time
);
let indicator_color = {
Paid: "green",

View File

@@ -96,8 +96,8 @@ erpnext.PointOfSale.PastOrderList = class {
}
get_invoice_html(invoice) {
const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format(
"Do MMMM, h:mma"
const posting_datetime = frappe.datetime.str_to_user(
invoice.posting_date + " " + invoice.posting_time
);
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
<div class="invoice-name-date">
@@ -110,7 +110,7 @@ erpnext.PointOfSale.PastOrderList = class {
</div>
</div>
<div class="invoice-total-status">
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div>
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency) || 0}</div>
<div class="invoice-date">${posting_datetime}</div>
</div>
</div>

View File

@@ -1,7 +1,8 @@
erpnext.PointOfSale.PastOrderSummary = class {
constructor({ wrapper, events }) {
constructor({ wrapper, events, pos_profile }) {
this.wrapper = wrapper;
this.events = events;
this.pos_profile = pos_profile;
this.init_component();
}
@@ -355,6 +356,8 @@ erpnext.PointOfSale.PastOrderSummary = class {
const condition_btns_map = this.get_condition_btn_map(after_submission);
this.add_summary_btns(condition_btns_map);
this.print_receipt_on_order_complete();
}
attach_document_info(doc) {
@@ -421,4 +424,16 @@ erpnext.PointOfSale.PastOrderSummary = class {
toggle_component(show) {
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}
async print_receipt_on_order_complete() {
const res = await frappe.db.get_value(
"POS Profile",
this.pos_profile,
"print_receipt_on_order_complete"
);
if (res.message.print_receipt_on_order_complete) {
this.print_receipt();
}
}
};

View File

@@ -31,9 +31,9 @@ def get_columns(filters):
f"{frappe.unscrub(str(party_type_value))}::150",
"Address Line 1",
"Address Line 2",
"Postal Code",
"City",
"State",
"Postal Code",
"Country",
"Is Primary Address:Check",
"First Name",

View File

@@ -41,6 +41,7 @@ class Employee(NestedSet):
self.validate_email()
self.validate_status()
self.validate_reports_to()
self.set_preferred_email()
self.validate_preferred_email()
if self.user_id:
@@ -160,9 +161,7 @@ class Employee(NestedSet):
def set_preferred_email(self):
preferred_email_field = frappe.scrub(self.prefered_contact_email)
if preferred_email_field:
preferred_email = self.get(preferred_email_field)
self.prefered_email = preferred_email
self.prefered_email = self.get(preferred_email_field) if preferred_email_field else None
def validate_status(self):
if self.status == "Left":

View File

@@ -245,8 +245,9 @@ class DeprecatedBatchNoValuation:
last_sle = self.get_last_sle_for_non_batch()
for d in batch_data:
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
if self.available_qty.get(d.batch_no):
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
def get_last_sle_for_non_batch(self):
from erpnext.stock.utils import get_combine_datetime
@@ -292,6 +293,58 @@ class DeprecatedBatchNoValuation:
data = query.run(as_dict=True)
return data[0] if data else {}
@deprecated
def get_last_sle_for_sabb_no_batchwise_valuation(self):
sabb = frappe.qb.DocType("Serial and Batch Bundle")
sabb_entry = frappe.qb.DocType("Serial and Batch Entry")
batch = frappe.qb.DocType("Batch")
posting_datetime = CombineDatetime(self.sle.posting_date, self.sle.posting_time)
timestamp_condition = CombineDatetime(sabb.posting_date, sabb.posting_time) < posting_datetime
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(sabb.posting_date, sabb.posting_time) == posting_datetime
) & (sabb.creation < self.sle.creation)
query = (
frappe.qb.from_(sabb)
.inner_join(sabb_entry)
.on(sabb.name == sabb_entry.parent)
.inner_join(batch)
.on(sabb_entry.batch_no == batch.name)
.select(sabb.name)
.where(
(sabb.item_code == self.sle.item_code)
& (sabb.warehouse == self.sle.warehouse)
& (sabb_entry.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (sabb.is_cancelled == 0)
& (sabb.docstatus == 1)
)
.where(timestamp_condition)
.orderby(sabb.posting_date, order=Order.desc)
.orderby(sabb.posting_time, order=Order.desc)
.orderby(sabb.creation, order=Order.desc)
.limit(1)
)
if self.sle.voucher_detail_no:
query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no)
data = query.run(as_dict=True)
if not data:
return {}
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"serial_and_batch_bundle": data[0].name},
["stock_value", "qty_after_transaction"],
as_dict=1,
)
return sle if sle else {}
@deprecated
def set_balance_value_from_bundle(self) -> None:
bundle = frappe.qb.DocType("Serial and Batch Bundle")
@@ -338,7 +391,14 @@ class DeprecatedBatchNoValuation:
query = query.where(bundle.voucher_type != "Pick List")
for d in query.run(as_dict=True):
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
last_sle = self.get_last_sle_for_sabb_no_batchwise_valuation()
if not last_sle:
return
for batch_no in self.available_qty:
self.non_batchwise_balance_value[batch_no] = flt(last_sle.stock_value)
self.non_batchwise_balance_qty[batch_no] = flt(last_sle.qty_after_transaction)

View File

@@ -54,7 +54,12 @@ frappe.ui.form.on("Batch", {
frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
args: { batch_no: frm.doc.name, item_code: frm.doc.item, for_stock_levels: for_stock_levels },
args: {
batch_no: frm.doc.name,
item_code: frm.doc.item,
for_stock_levels: for_stock_levels,
consider_negative_batches: 1,
},
callback: (r) => {
if (!r.message) {
return;
@@ -71,7 +76,7 @@ frappe.ui.form.on("Batch", {
// show
(r.message || []).forEach(function (d) {
if (d.qty > 0) {
if (d.qty != 0) {
$(`<div class='row' style='margin-bottom: 10px;'>
<div class='col-sm-3 small' style='padding-top: 3px;'>${d.warehouse}</div>
<div class='col-sm-3 small text-right' style='padding-top: 3px;'>${d.qty}</div>

View File

@@ -218,6 +218,7 @@ def get_batch_qty(
posting_time=None,
ignore_voucher_nos=None,
for_stock_levels=False,
consider_negative_batches=False,
):
"""Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None
@@ -243,6 +244,7 @@ def get_batch_qty(
"batch_no": batch_no,
"ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels,
"consider_negative_batches": consider_negative_batches,
}
)

View File

@@ -63,16 +63,20 @@ frappe.listview_settings["Delivery Note"] = {
}
};
// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
if (frappe.model.can_create("Delivery Trip")) {
doclist.page.add_action_item(__("Create Delivery Trip"), action);
}
doclist.page.add_action_item(__("Create Delivery Trip"), action);
if (frappe.model.can_create("Sales Invoice")) {
doclist.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
});
}
doclist.page.add_action_item(__("Sales Invoice"), () => {
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
});
doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
});
if (frappe.model.can_create("Packing Slip")) {
doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
});
}
},
};

View File

@@ -11,7 +11,8 @@
"description",
"col_break3",
"amount",
"base_amount"
"base_amount",
"has_corrective_cost"
],
"fields": [
{
@@ -62,12 +63,19 @@
"label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fieldname": "has_corrective_cost",
"fieldtype": "Check",
"label": "Has Corrective Cost",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-17 13:57:10.807980",
"modified": "2025-01-20 12:22:03.455762",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",

View File

@@ -20,6 +20,7 @@ class LandedCostTaxesandCharges(Document):
description: DF.SmallText
exchange_rate: DF.Float
expense_account: DF.Link | None
has_corrective_cost: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -6,6 +6,10 @@ frappe.ui.form.on("Serial and Batch Bundle", {
frm.trigger("set_queries");
},
before_submit(frm) {
frappe.throw(__("User cannot submitted the Serial and Batch Bundle manually"));
},
refresh(frm) {
frm.trigger("toggle_fields");
frm.trigger("prepare_serial_batch_prompt");

View File

@@ -507,18 +507,6 @@ frappe.ui.form.on("Stock Entry", {
});
},
company: function (frm) {
if (frm.doc.company) {
var company_doc = frappe.get_doc(":Company", frm.doc.company);
if (company_doc.default_letter_head) {
frm.set_value("letter_head", company_doc.default_letter_head);
}
frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
},
make_retention_stock_entry: function (frm) {
frappe.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.move_sample_to_retention_warehouse",
@@ -918,7 +906,12 @@ frappe.ui.form.on("Stock Entry Detail", {
var d = locals[cdt][cdn];
$.each(r.message, function (k, v) {
if (v) {
frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered
// set_value trigger barcode function and barcode set qty to 1 in stock_controller.js, to avoid this set value manually instead of set value.
if (k != "barcode") {
frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered
} else {
d.barcode = v;
}
}
});
refresh_field("items");
@@ -1060,11 +1053,9 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
onload_post_render() {
var me = this;
this.set_default_account(function () {
if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) {
me.frm.trigger("company");
}
});
if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) {
me.company();
}
this.frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}
@@ -1143,26 +1134,40 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
this.clean_up();
}
set_default_account(callback) {
company() {
if (this.frm.doc.company) {
var company_doc = frappe.get_doc(":Company", this.frm.doc.company);
if (company_doc.default_letter_head) {
this.frm.set_value("letter_head", company_doc.default_letter_head);
}
this.frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company))
this.set_default_account("stock_adjustment_account", "expense_account");
this.set_default_account("cost_center", "cost_center");
this.frm.refresh_fields("items");
}
}
set_default_account(company_fieldname, fieldname) {
var me = this;
if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) {
return this.frm.call({
method: "erpnext.accounts.utils.get_company_default",
args: {
fieldname: "stock_adjustment_account",
company: this.frm.doc.company,
},
callback: function (r) {
if (!r.exc) {
$.each(me.frm.doc.items || [], function (i, d) {
if (!d.expense_account) d.expense_account = r.message;
});
if (callback) callback();
}
},
});
}
return this.frm.call({
method: "erpnext.accounts.utils.get_company_default",
args: {
fieldname: company_fieldname,
company: this.frm.doc.company,
},
callback: function (r) {
if (!r.exc) {
$.each(me.frm.doc.items || [], function (i, d) {
d[fieldname] = r.message;
});
}
},
});
}
clean_up() {

View File

@@ -2868,17 +2868,6 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
if bom.quantity:
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
if (
work_order
and work_order.produced_qty
and cint(
frappe.db.get_single_value(
"Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation"
)
)
):
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
return operating_cost_per_unit
@@ -3244,12 +3233,13 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
}
)
precision = frappe.get_precision("Stock Entry Detail", "qty")
if row.serial_nos and row.batches_to_be_consume:
doc.has_serial_no = 1
doc.has_batch_no = 1
batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row)
for batch_no, qty in row.batches_to_be_consume.items():
while qty > 0:
while flt(qty, precision) > 0:
qty -= 1
doc.append(
"entries",
@@ -3270,8 +3260,9 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
precision = frappe.get_precision("Serial and Batch Entry", "qty")
doc.has_batch_no = 1
for batch_no, qty in row.batches_to_be_consume.items():
qty = flt(qty, precision)
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
if flt(qty, precision) > 0:
qty = flt(qty, precision)
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
if not doc.entries:
return None

View File

@@ -139,8 +139,8 @@ class StockReconciliation(StockController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": row.current_qty,
"type_of_transaction": "Outward",
"qty": row.current_qty * -1,
"type_of_transaction": "Outward" if row.current_qty > 0 else "Inward",
"company": self.company,
"is_rejected": 0,
"serial_nos": get_serial_nos(row.current_serial_no)
@@ -1367,6 +1367,7 @@ def get_stock_balance_for(
posting_date=posting_date,
posting_time=posting_time,
for_stock_levels=True,
consider_negative_batches=True,
)
or 0
)

View File

@@ -1330,6 +1330,84 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(stock_value_difference, 1500.00 * -1)
def test_stock_reco_for_negative_batch(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = self.make_item(
"Test Item For Negative Batch",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TEST-BATCH-NB-.###",
},
).name
warehouse = "_Test Warehouse - _TC"
se = make_stock_entry(
posting_date="2024-11-01",
posting_time="11:00",
item_code=item_code,
target=warehouse,
qty=10,
basic_rate=100,
)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
se = make_stock_entry(
posting_date="2024-11-01",
posting_time="11:00",
item_code=item_code,
source=warehouse,
qty=10,
basic_rate=100,
use_serial_batch_fields=1,
batch_no=batch_no,
)
sles = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": se.name, "is_cancelled": 0},
)
# intentionally setting negative qty
doc = frappe.get_doc("Stock Ledger Entry", sles[0].name)
doc.db_set(
{
"actual_qty": -20,
"qty_after_transaction": -10,
}
)
sabb_doc = frappe.get_doc("Serial and Batch Bundle", doc.serial_and_batch_bundle)
for row in sabb_doc.entries:
row.db_set("qty", -20)
batch_qty = get_batch_qty(batch_no, warehouse, item_code, consider_negative_batches=True)
self.assertEqual(batch_qty, -10)
sr = create_stock_reconciliation(
posting_date="2024-11-02",
posting_time="11:00",
item_code=item_code,
warehouse=warehouse,
use_serial_batch_fields=1,
batch_no=batch_no,
qty=0,
rate=100,
do_not_submit=True,
)
self.assertEqual(sr.items[0].current_qty, -10)
sr.submit()
sr.reload()
self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
self.assertFalse(sr.items[0].serial_and_batch_bundle)
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@@ -41,8 +41,14 @@ frappe.query_reports["Stock Balance"] = {
width: "80",
options: "Item",
get_query: function () {
let item_group = frappe.query_report.get_filter_value("item_group");
return {
query: "erpnext.controllers.queries.item_query",
filters: {
...(item_group && { item_group }),
is_stock_item: 1,
},
};
},
},

View File

@@ -418,7 +418,13 @@ class SerialBatchBundle:
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
batches_qty = get_available_batches(
frappe._dict({"item_code": self.item_code, "batch_no": list(batches.keys())})
frappe._dict(
{
"item_code": self.item_code,
"batch_no": list(batches.keys()),
"consider_negative_batches": 1,
}
)
)
for batch_no in batches:

View File

@@ -58,7 +58,10 @@ def get_stock_value_from_bin(warehouse=None, item_code=None):
def get_stock_value_on(
warehouses: list | str | None = None, posting_date: str | None = None, item_code: str | None = None
warehouses: list | str | None = None,
posting_date: str | None = None,
item_code: str | None = None,
company: str | None = None,
) -> float:
if not posting_date:
posting_date = nowdate()
@@ -84,6 +87,9 @@ def get_stock_value_on(
if item_code:
query = query.where(sle.item_code == item_code)
if company:
query = query.where(sle.company == company)
return query.run(as_list=True)[0][0]

View File

@@ -8743,3 +8743,12 @@ WhatsApp,হোয়াটসঅ্যাপ,
Make a call,ফোন করুন,
Approve,অনুমোদন করা,
Reject,প্রত্যাখ্যান,
Signature,স্বাক্ষর,
Signature is mandatory,স্বাক্ষর আবশ্যক,
Signature is not mandatory,স্বাক্ষর আবশ্যক নয়,
Authorised Signature,অনুমোদিত স্বাক্ষর,
Billing Month,বিলিং মাস,
Name of Client,ক্লায়েন্টের নাম,
Client Name,ক্লায়েন্টের নাম,
Client,ক্লায়েন্ট,
BD,বিডি,
Can't render this file because it is too large.

View File

@@ -8,6 +8,9 @@ from frappe.utils import get_link_to_form, today
@frappe.whitelist()
def transaction_processing(data, from_doctype, to_doctype):
frappe.has_permission(from_doctype, "read", throw=True)
frappe.has_permission(to_doctype, "create", throw=True)
if isinstance(data, str):
deserialized_data = json.loads(data)
else:

View File

@@ -84,6 +84,18 @@
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "pincode",
"fieldtype": "Data",
"hidden": 0,
"label": "Postal Code",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "city",
@@ -108,18 +120,7 @@
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "pincode",
"fieldtype": "Data",
"hidden": 0,
"label": "Postal Code",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 1,
"fieldname": "country",